From 91b0e88f4893421a02e8dd845a715844c416bdff Mon Sep 17 00:00:00 2001 From: willGraham01 Date: Wed, 22 Nov 2023 15:58:03 +0000 Subject: [PATCH] jekyll build from Action 11756dc6059c3b5c856e691f9cfc479bdfcd6ccd --- .nojekyll | 0 LICENSE.md | 36 + Makefile | 109 + README.md | 93 + assets/css/jekyll-styles.css | 73 + assets/css/print.css | 1 + assets/css/screen.css | 4269 + assets/css/screen.min.css | 1 + assets/css/ucl_reveal.css | 23 + assets/favicons/favicon-144.png | Bin 0 -> 2576 bytes assets/favicons/favicon-152.png | Bin 0 -> 2706 bytes assets/favicons/favicon.ico | Bin 0 -> 894 bytes assets/images/carousel/carousel-aslash.jpg | Bin 0 -> 57549 bytes assets/images/carousel/carousel-aslms.jpg | Bin 0 -> 26419 bytes assets/images/carousel/carousel-beams.jpg | Bin 0 -> 34687 bytes assets/images/carousel/carousel-ioe.jpg | Bin 0 -> 197634 bytes assets/images/cells.jpg | Bin 0 -> 239259 bytes assets/images/close.png | Bin 0 -> 3300 bytes assets/images/favicon.ico | Bin 0 -> 894 bytes assets/images/hero-bg-ie.png | Bin 0 -> 17658 bytes assets/images/mob-nav.png | Bin 0 -> 959 bytes assets/images/respond.proxy.gif | Bin 0 -> 35 bytes assets/images/search-sprite.png | Bin 0 -> 5210 bytes assets/images/social-buttons/social-icons.png | Bin 0 -> 3355 bytes .../images/social-buttons/social-icons@2x.png | Bin 0 -> 6534 bytes assets/images/ucl-logo-cropped-white.png | Bin 0 -> 3161 bytes assets/images/ucl-logo.png | Bin 0 -> 3999 bytes assets/images/ucl-logo.svg | 301 + assets/images/ucl-menu.png | Bin 0 -> 209 bytes assets/images/ucl-menu.svg | 8 + assets/images/ucl-portico.jpg | Bin 0 -> 213279 bytes assets/js/app/carousel.js | 47 + assets/js/app/demo-site.js | 34 + assets/js/app/general.js | 204 + assets/js/app/lightbox.js | 59 + assets/js/app/map.js | 54 + assets/js/app/searchWithAutoComplete.js | 178 + assets/js/app/tabs.js | 59 + assets/js/lib/all-site.min.js | 1 + assets/js/lib/backbone-1.1.2.min.js | 1 + assets/js/lib/backbone-1.min.js | 1 + assets/js/lib/fastclick.min.js | 1 + assets/js/lib/googleAnalytics.min.js | 1 + assets/js/lib/gridset-overlay.min.js | 1 + assets/js/lib/handlebars.min.js | 2 + assets/js/lib/html5shiv-printshiv.min.js | 1 + assets/js/lib/jquery-1.7.1.min.js | 4 + assets/js/lib/jquery-1.7.1.min.min.js | 3 + assets/js/lib/jquery-1.9.1.min.js | 3 + assets/js/lib/jquery-2.1.1.min.js | 3 + assets/js/lib/jwplayer.flash.swf | 1322 + assets/js/lib/jwplayer.html5.js | 242 + assets/js/lib/jwplayer.html5.min.js | 5 + assets/js/lib/jwplayer.js | 129 + assets/js/lib/jwplayer.min.js | 3 + assets/js/lib/modernizr-custom.js | 1 + assets/js/lib/modernizr-custom.min.js | 1 + assets/js/lib/modernizr.min.js | 1 + assets/js/lib/owl.carousel.min.js | 2 + assets/js/lib/require.min.js | 1 + assets/js/lib/respond.min.js | 1 + assets/js/lib/respond.proxy.min.js | 1 + assets/js/lib/typeahead.bundle.min.js | 1 + assets/js/lib/underscore-1.6.0.min.js | 1 + assets/js/lib/underscore-1.min.js | 1 + assets/js/lib/underscore-1.min.min.js | 1 + assets/js/main.js | 82 + assets/js/respond-proxy.html | 27 + assets/js/respond-x-domain/respond-proxy.html | 27 + assets/js/src/all-site.js | 384 + assets/js/src/all-site/googleAnalytics.js | 28 + assets/js/src/all-site/gridset-overlay.js | 198 + assets/js/src/all-site/lazyload.js | 80 + assets/js/src/all-site/new-relic.js | 2 + assets/js/src/all-site/ucl.js | 74 + assets/js/src/backbone-1.1.2.js | 1608 + assets/js/src/fastclick.js | 821 + assets/js/src/googleAnalytics.js | 43 + assets/js/src/gridset-overlay.js | 198 + assets/js/src/handlebars.js | 2 + assets/js/src/html5shiv-printshiv.js | 520 + assets/js/src/jquery-1.7.1.min.js | 4 + assets/js/src/jquery-1.9.1.js | 9597 ++ assets/js/src/jquery-2.1.1.js | 9190 ++ assets/js/src/jwplayer.flash.swf | 1322 + assets/js/src/jwplayer.html5.js | 242 + assets/js/src/jwplayer.js | 129 + assets/js/src/modernizr-custom.js | 1 + assets/js/src/modernizr.js | 1198 + assets/js/src/owl.carousel.js | 2 + assets/js/src/require.js | 2076 + assets/js/src/respond.js | 353 + assets/js/src/respond.proxy.js | 129 + assets/js/src/typeahead.bundle.js | 1733 + assets/js/src/underscore-1.6.0.js | 1343 + assets/js/src/underscore-1.min.js | 1 + assets/sass/base/_buttons.scss | 244 + assets/sass/base/_forms.scss | 237 + assets/sass/base/_grid.scss | 258 + assets/sass/base/_images.scss | 79 + assets/sass/base/_layout.scss | 161 + assets/sass/base/_reset.scss | 115 + assets/sass/base/_tables.scss | 139 + assets/sass/base/_typography.scss | 296 + assets/sass/mixins/_alignment.scss | 13 + assets/sass/mixins/_gradient.scss | 14 + assets/sass/mixins/_gridset.scss | 304 + assets/sass/mixins/_guttering.scss | 13 + assets/sass/mixins/_inline-block.scss | 13 + assets/sass/mixins/_list.scss | 21 + assets/sass/mixins/_media-query.scss | 67 + assets/sass/mixins/_mixins.scss | 12 + assets/sass/mixins/_modernizr.scss | 35 + assets/sass/mixins/_no-js.scss | 35 + assets/sass/mixins/_on-interaction.scss | 13 + assets/sass/mixins/_sass-ie.scss | 66 + assets/sass/mixins/_unit.scss | 13 + assets/sass/patterns/_accordion.scss | 36 + assets/sass/patterns/_advert.scss | 85 + assets/sass/patterns/_blocked-link.scss | 18 + assets/sass/patterns/_blurb.scss | 44 + assets/sass/patterns/_box.scss | 36 + assets/sass/patterns/_brand.scss | 61 + assets/sass/patterns/_breadcrumb.scss | 62 + assets/sass/patterns/_code.scss | 19 + assets/sass/patterns/_collapse.scss | 56 + assets/sass/patterns/_commentlist.scss | 68 + assets/sass/patterns/_cta.scss | 51 + assets/sass/patterns/_divider.scss | 23 + assets/sass/patterns/_flag.scss | 43 + assets/sass/patterns/_footer.scss | 46 + assets/sass/patterns/_header.scss | 110 + assets/sass/patterns/_hero.scss | 158 + assets/sass/patterns/_input-group.scss | 31 + assets/sass/patterns/_left-nav--img.scss | 17 + assets/sass/patterns/_lightbox.scss | 34 + assets/sass/patterns/_map.scss | 44 + assets/sass/patterns/_masthead-legacy.scss | 278 + assets/sass/patterns/_masthead.scss | 89 + assets/sass/patterns/_media.scss | 45 + assets/sass/patterns/_menu-block.scss | 64 + assets/sass/patterns/_message.scss | 39 + assets/sass/patterns/_nav.scss | 58 + assets/sass/patterns/_owl-carousel.scss | 244 + assets/sass/patterns/_pagination.scss | 56 + assets/sass/patterns/_photograph.scss | 51 + assets/sass/patterns/_pills.scss | 48 + assets/sass/patterns/_pull-quote.scss | 71 + assets/sass/patterns/_search-form.scss | 62 + assets/sass/patterns/_site-content.scss | 83 + assets/sass/patterns/_slides.scss | 41 + assets/sass/patterns/_social-buttons.scss | 57 + assets/sass/patterns/_tabs.scss | 178 + assets/sass/patterns/_tag.scss | 29 + assets/sass/patterns/_vcard.scss | 22 + assets/sass/patterns/_video-wrap.scss | 20 + assets/sass/print.scss | 0 assets/sass/screen.scss | 65 + assets/sass/utilities/_helpers.scss | 178 + assets/sass/utilities/_states.scss | 33 + assets/sass/variables/_breakpoints.scss | 10 + assets/sass/variables/_colors.scss | 111 + assets/sass/variables/_environment.scss | 5 + assets/sass/variables/_images.scss | 8 + assets/sass/variables/_spacing.scss | 6 + assets/sass/variables/_typography.scss | 20 + assets/sass/variables/_variables.scss | 10 + assets/sass/variables/_zindex.scss | 9 + attendee-info.md | 26 + bannermidgreen.pdf | Bin 0 -> 7146 bytes ch00git/01Intro.html | 769 + ch00git/01Intro.ipynb | 488 + ch00git/01Intro.ipynb.py | 204 + ch00git/02Solo.html | 1297 + ch00git/02Solo.ipynb | 952 + ch00git/02Solo.ipynb.py | 342 + ch00git/03Mistakes.html | 908 + ch00git/03Mistakes.ipynb | 473 + ch00git/03Mistakes.ipynb.py | 184 + ch00git/04Publishing.html | 934 + ch00git/04Publishing.ipynb | 558 + ch00git/04Publishing.ipynb.py | 218 + ch00git/05Collaboration.html | 2045 + ch00git/05Collaboration.ipynb | 1263 + ch00git/05Collaboration.ipynb.py | 463 + ch00git/06ForkAndPull.html | 447 + ch00git/06ForkAndPull.ipynb | 242 + ch00git/06ForkAndPull.ipynb.py | 174 + ch00git/10Branches.html | 1241 + ch00git/10Branches.ipynb | 681 + ch00git/10Branches.ipynb.py | 223 + ch00git/11Miscellany.html | 1100 + ch00git/11Miscellany.ipynb | 649 + ch00git/11Miscellany.ipynb.py | 257 + ch00git/12Remotes.html | 751 + ch00git/12Remotes.ipynb | 364 + ch00git/12Remotes.ipynb.py | 148 + ch00git/13Rebase.html | 553 + ch00git/13Rebase.ipynb | 370 + ch00git/13Rebase.ipynb.py | 244 + ch00git/14Bisect.html | 738 + ch00git/14Bisect.ipynb | 342 + ch00git/14Bisect.ipynb.py | 143 + ch00git/index.html | 303 + ch00git/learning_git/bare_repo/HEAD | 1 + ch00git/learning_git/bare_repo/config | 4 + ch00git/learning_git/bare_repo/description | 1 + .../bare_repo/hooks/applypatch-msg.sample | 15 + .../bare_repo/hooks/commit-msg.sample | 24 + .../bare_repo/hooks/fsmonitor-watchman.sample | 174 + .../bare_repo/hooks/post-update.sample | 8 + .../bare_repo/hooks/pre-applypatch.sample | 14 + .../bare_repo/hooks/pre-commit.sample | 49 + .../bare_repo/hooks/pre-merge-commit.sample | 13 + .../bare_repo/hooks/pre-push.sample | 53 + .../bare_repo/hooks/pre-rebase.sample | 169 + .../bare_repo/hooks/pre-receive.sample | 24 + .../bare_repo/hooks/prepare-commit-msg.sample | 42 + .../bare_repo/hooks/push-to-checkout.sample | 78 + .../bare_repo/hooks/sendemail-validate.sample | 77 + .../bare_repo/hooks/update.sample | 128 + ch00git/learning_git/bare_repo/info/exclude | 6 + .../02/2c38d1886bd36d1760ed255a4f6bbd1f43be8e | Bin 0 -> 117 bytes .../03/625bb37da8f24bd34e2acfaf716ab0ce23be5d | Bin 0 -> 160 bytes .../15/06ddb315a81961af669ac4ec0f7b8d39564ea6 | Bin 0 -> 117 bytes .../1b/45884214655a0199ed5b0c59f7c4c4c80b5e0c | Bin 0 -> 52 bytes .../21/c306bbfcd288f6f6e74c8495301159bb1f8ed2 | 1 + .../2c/bc3a1a3c145ef2087cef6303e6508feb2e95eb | Bin 0 -> 53 bytes .../34/6ff8f9f1e8b276e946a07066c5642cde43659e | Bin 0 -> 86 bytes .../3a/2f7b04eb843b63930b5a28aa3a60de1381c3cc | 1 + .../3f/fd43f7f4e8cb206c6f2cad9aa4e726686be06f | Bin 0 -> 86 bytes .../4b/52f45528b43ccb12f198a757d075527283faad | Bin 0 -> 172 bytes .../4f/db31b5b5aca2f87f78ef5da503cb823bbeab9d | Bin 0 -> 117 bytes .../59/2c6bd85210c34f2004788d3d31d15b35ec2325 | 4 + .../5a/a2999dd374d2da5874f5b379646b8f4c6d7a68 | 2 + .../5b/f472d82a68b889f58165491d712e8f15de0de7 | 2 + .../69/4c013206739cee29c71040e85a93b975d8e95b | Bin 0 -> 167 bytes .../6a/4906ac762e499014d38c7716fb80ed5ad668a2 | Bin 0 -> 66 bytes .../6c/90dbd9b71257440d4fb40e8ff8d8792194979f | 5 + .../77/cc962ec24725bd1140cd700d8dd254ec116c50 | Bin 0 -> 148 bytes .../7b/49cfb86012d2d4eb3ce12eeb879e1cbaaa8cd1 | Bin 0 -> 167 bytes .../7c/a7cdd047dece7431e031e62d82311c12db7d65 | Bin 0 -> 213 bytes .../7c/b0c94912e69f9774df89a2f8767a89a2adcdef | Bin 0 -> 99 bytes .../85/bec83768f27da53032a611e29595d5b5f40378 | 2 + .../86/0991ed05434dc1711ddc541fc00d43c803f056 | Bin 0 -> 137 bytes .../99/c927cbd456e04e1120a0882151740e30834507 | Bin 0 -> 96 bytes .../9f/5cbbb1154800845905d8429be8e8143c9505ea | Bin 0 -> 53 bytes .../a0/6db2485baa3b4b5b5c8b96bdb50af8c45300be | Bin 0 -> 53 bytes .../a1/363379944a5745ceb49c0e493d80eb9335c79a | Bin 0 -> 21 bytes .../a1/f85df90512bb61264c5c5e81ed206b00f9e745 | Bin 0 -> 130 bytes .../a2/0dedd3357a1f47838c6e7ecee14d818887bc87 | Bin 0 -> 60 bytes .../a6/361f3fc86493e1cffc5218e0c5a9312bd9b684 | 3 + .../a8/4a21b9c40e8bc87c42e88c84b2854dfa1ed652 | 2 + .../ad/82f9b3ac49e557eca127a56411d4f52d11d772 | 5 + .../ae/7410182b095a876b07012ff84c7c11cdffcd9d | 1 + .../b2/02e904a72055b1710d61f18e048a5ef425c7ae | 2 + .../b2/90508e7eecc632ee43f2a21b96641ca379a768 | 2 + .../b5/e301149bedee3b12a1299c4a9d1386bdd96375 | Bin 0 -> 117 bytes .../ba/320fb930f88e594958493645350b5ad9c9a4d5 | Bin 0 -> 172 bytes .../bf/be7ced096691b79738a3d5a0b16b6459a8a12c | Bin 0 -> 71 bytes .../c0/76f26f388a67ba6f95da979a1330edc957937d | Bin 0 -> 86 bytes .../c4/eb102a88795733013f6043ae91fb1cc3c1e425 | Bin 0 -> 73 bytes .../d8/c838427ba3492f00efab6100ad39e4880a802b | Bin 0 -> 100 bytes .../dd/5cf9cab9854226b00c0eaec356bedfcad3e184 | Bin 0 -> 135 bytes .../df/5fed820b7fa9834693c535803cefbd777411ca | 1 + .../e7/3f9893bc695e0a7129310f90597bbc6d8296ed | Bin 0 -> 214 bytes .../f2/bc29cdb9f78cd43ceccbcd25abf66080ee34ba | Bin 0 -> 213 bytes .../f3/e88b4409aba4cdf8a31d05ad725b3d69626a61 | Bin 0 -> 59 bytes .../learning_git/bare_repo/refs/heads/main | 1 + ch00git/learning_git/bisectdemo/LICENSE | 20 + ch00git/learning_git/bisectdemo/README.md | 71 + ch00git/learning_git/bisectdemo/break_output | 2005 + ch00git/learning_git/bisectdemo/breakme.sh | 29 + ch00git/learning_git/bisectdemo/gitbisect.out | 72 + ch00git/learning_git/bisectdemo/squares.py | 324 + ch00git/learning_git/git_example/Makefile | 8 + ch00git/learning_git/git_example/Pennines.md | 6 + ch00git/learning_git/git_example/Scotland.md | 6 + ch00git/learning_git/git_example/Wales.md | 9 + ch00git/learning_git/git_example/index.html | 297 + ch00git/learning_git/git_example/lakeland.md | 7 + ch00git/learning_git/git_example/wsd.py | 17 + ch00git/learning_git/partner_repo/Scotland.md | 5 + ch00git/learning_git/partner_repo/Wales.md | 8 + ch00git/learning_git/partner_repo/index.md | 5 + ch00git/learning_git/partner_repo/lakeland.md | 7 + ch00git/somefile.md | 1 + ch01python/00pythons.html | 804 + ch01python/00pythons.ipynb | 455 + ch01python/00pythons.ipynb.py | 235 + ch01python/010exemplar.html | 1487 + ch01python/010exemplar.ipynb | 961 + ch01python/010exemplar.ipynb.py | 495 + ch01python/015variables.html | 1279 + ch01python/015variables.ipynb | 711 + ch01python/015variables.ipynb.py | 259 + ch01python/016using_functions.html | 1398 + ch01python/016using_functions.ipynb | 683 + ch01python/016using_functions.ipynb.py | 241 + ch01python/023types.html | 1761 + ch01python/023types.ipynb | 958 + ch01python/023types.ipynb.py | 343 + ch01python/025containers.html | 1159 + ch01python/025containers.ipynb | 554 + ch01python/025containers.ipynb.py | 207 + ch01python/028dictionaries.html | 972 + ch01python/028dictionaries.ipynb | 480 + ch01python/028dictionaries.ipynb.py | 181 + ch01python/029structures.html | 572 + ch01python/029structures.ipynb | 254 + ch01python/029structures.ipynb.py | 108 + ch01python/030MazeSolution.html | 376 + ch01python/030MazeSolution.ipynb | 95 + ch01python/030MazeSolution.ipynb.py | 65 + ch01python/032conditionality.html | 1241 + ch01python/032conditionality.ipynb | 655 + ch01python/032conditionality.ipynb.py | 266 + ch01python/035looping.html | 688 + ch01python/035looping.ipynb | 309 + ch01python/035looping.ipynb.py | 152 + ch01python/036MazeSolution2.html | 395 + ch01python/036MazeSolution2.ipynb | 104 + ch01python/036MazeSolution2.ipynb.py | 67 + ch01python/037comprehensions.html | 956 + ch01python/037comprehensions.ipynb | 489 + ch01python/037comprehensions.ipynb.py | 186 + ch01python/038SolutionComprehension.html | 410 + ch01python/038SolutionComprehension.ipynb | 109 + ch01python/038SolutionComprehension.ipynb.py | 65 + ch01python/04functions.html | 1042 + ch01python/04functions.ipynb | 561 + ch01python/04functions.ipynb.py | 255 + ch01python/050import.html | 746 + ch01python/050import.ipynb | 294 + ch01python/050import.ipynb.py | 117 + ch01python/101Classes.html | 1302 + ch01python/101Classes.ipynb | 880 + ch01python/101Classes.ipynb.py | 455 + ch01python/draw_eight.py | 17 + ch01python/eight.py | 1 + ch01python/fourteen.py | 2 + ch01python/index.html | 302 + ch02data/060files.html | 1240 + ch02data/060files.ipynb | 719 + ch02data/060files.ipynb.py | 287 + ch02data/061internet.html | 722 + ch02data/061internet.ipynb | 400 + ch02data/061internet.ipynb.py | 170 + ch02data/062csv.html | 1296 + ch02data/062csv.ipynb | 718 + ch02data/062csv.ipynb.py | 313 + ch02data/064JsonYamlXML.html | 742 + ch02data/064JsonYamlXML.ipynb | 359 + ch02data/064JsonYamlXML.ipynb.py | 143 + ch02data/065MazeSaved.html | 565 + ch02data/065MazeSaved.ipynb | 195 + ch02data/065MazeSaved.ipynb.py | 93 + ch02data/066QuakeExercise.html | 388 + ch02data/066QuakeExercise.ipynb | 95 + ch02data/066QuakeExercise.ipynb.py | 53 + ch02data/068QuakesSolution.html | 684 + ch02data/068QuakesSolution.ipynb | 298 + ch02data/068QuakesSolution.ipynb.py | 129 + ch02data/070hdf5.html | 558 + ch02data/070hdf5.ipynb | 258 + ch02data/070hdf5.ipynb.py | 126 + ch02data/072plotting.html | 1099 + ch02data/072plotting.ipynb | 569 + ch02data/072plotting.ipynb.py | 213 + ch02data/082NumPy.html | 2902 + ch02data/082NumPy.ipynb | 1601 + ch02data/082NumPy.ipynb.py | 561 + ch02data/084Boids.html | 128017 +++++++++++++++ ch02data/084Boids.ipynb | 974 + ch02data/084Boids.ipynb.py | 435 + ch02data/110Capstone.html | 542 + ch02data/110Capstone.ipynb | 220 + ch02data/110Capstone.ipynb.py | 158 + ch02data/boids_1.mp4 | Bin 0 -> 69263 bytes ch02data/greengraph/graph.py | 48 + ch02data/greengraph/map.py | 57 + ch02data/hdf5_example.svg | 182 + ch02data/index.html | 300 + ch02data/maze.json | 1 + ch02data/maze.yaml | 25 + ch02data/my_file.hdf5 | Bin 0 -> 4816 bytes ch02data/my_graph.png | Bin 0 -> 32983 bytes ch02data/mydata.txt | 23 + ch02data/myfile.json | 3 + ch02data/myfile.yaml | 3 + ch02data/mywrittenfile | 1 + ch02data/planets_data.csv | 9 + ch03tests/01testingbasics.html | 505 + ch03tests/01testingbasics.ipynb | 208 + ch03tests/01testingbasics.ipynb.py | 123 + ch03tests/02SaskatchewanFields.html | 1155 + ch03tests/02SaskatchewanFields.ipynb | 650 + ch03tests/02SaskatchewanFields.ipynb.py | 284 + ch03tests/03pytest.html | 792 + ch03tests/03pytest.ipynb | 483 + ch03tests/03pytest.ipynb.py | 230 + ch03tests/04EnergyExample.html | 784 + ch03tests/04EnergyExample.ipynb | 369 + ch03tests/04EnergyExample.ipynb.py | 220 + ch03tests/05Mocks.html | 869 + ch03tests/05Mocks.ipynb | 466 + ch03tests/05Mocks.ipynb.py | 209 + ch03tests/06Debugger.html | 499 + ch03tests/06Debugger.ipynb | 187 + ch03tests/06Debugger.ipynb.py | 113 + ch03tests/07CI.html | 338 + ch03tests/07CI.ipynb | 63 + ch03tests/07CI.ipynb.py | 45 + ch03tests/08DiffusionExample.html | 70201 ++++++++ ch03tests/08DiffusionExample.ipynb | 412 + ch03tests/08DiffusionExample.ipynb.py | 334 + ch03tests/DiffusionExample/MonteCarlo.py | 120 + ch03tests/DiffusionExample/test_model.py | 102 + ch03tests/commands | 5 + ch03tests/diffusion/htmlcov/coverage_html.js | 624 + ch03tests/diffusion/htmlcov/favicon_32.png | Bin 0 -> 1732 bytes ch03tests/diffusion/htmlcov/index.html | 116 + ch03tests/diffusion/htmlcov/keybd_closed.png | Bin 0 -> 9004 bytes ch03tests/diffusion/htmlcov/keybd_open.png | Bin 0 -> 9003 bytes ch03tests/diffusion/htmlcov/model_py.html | 126 + ch03tests/diffusion/htmlcov/status.json | 1 + ch03tests/diffusion/htmlcov/style.css | 309 + .../diffusion/htmlcov/test_model_py.html | 155 + ch03tests/diffusion/model.py | 29 + ch03tests/diffusion/test_model.py | 58 + ch03tests/figures/callgrind.png | Bin 0 -> 274705 bytes ch03tests/figures/coverage.png | Bin 0 -> 53222 bytes ch03tests/figures/jenkins.png | Bin 0 -> 108319 bytes ch03tests/figures/tdd.dot | 18 + ch03tests/index.html | 301 + ch03tests/saskatchewan/overlap.py | 13 + ch03tests/saskatchewan/test_overlap.py | 10 + ch03tests/solutions/diffusionmodel/LICENSE.md | 21 + ch03tests/solutions/diffusionmodel/README.md | 4 + .../diffusionmodel/diffusion_model.py | 47 + .../diffusionmodel/energy_example.py | 2 + .../diffusionmodel/test_derivatives.py | 35 + .../diffusionmodel/test_diffusion_model.py | 62 + ch03tests/solutions/montecarlo/LICENSE.md | 21 + ch03tests/solutions/montecarlo/README.md | 4 + ch03tests/solutions/montecarlo/monte_carlo.py | 82 + .../solutions/montecarlo/test_monte_carlo.py | 130 + ch04packaging/010Installation.html | 693 + ch04packaging/010Installation.ipynb | 397 + ch04packaging/010Installation.ipynb.py | 182 + ch04packaging/01Libraries.html | 468 + ch04packaging/01Libraries.ipynb | 196 + ch04packaging/01Libraries.ipynb.py | 124 + ch04packaging/025TextFiles.html | 842 + ch04packaging/025TextFiles.ipynb | 452 + ch04packaging/025TextFiles.ipynb.py | 235 + ch04packaging/02Argparse.html | 661 + ch04packaging/02Argparse.ipynb | 286 + ch04packaging/02Argparse.ipynb.py | 143 + ch04packaging/03Packaging.html | 2247 + ch04packaging/03Packaging.ipynb | 1214 + ch04packaging/03Packaging.ipynb.py | 685 + ch04packaging/04documentation.html | 887 + ch04packaging/04documentation.ipynb | 542 + ch04packaging/04documentation.ipynb.py | 343 + ch04packaging/05Process.html | 637 + ch04packaging/05Process.ipynb | 414 + ch04packaging/05Process.ipynb.py | 257 + ch04packaging/06Issues.html | 425 + ch04packaging/06Issues.ipynb | 174 + ch04packaging/06Issues.ipynb.py | 111 + ch04packaging/07Licensing.html | 557 + ch04packaging/07Licensing.ipynb | 364 + ch04packaging/07Licensing.ipynb.py | 251 + ch04packaging/greeter.py | 26 + ch04packaging/greetings/CITATION.md | 5 + ch04packaging/greetings/LICENSE.md | 4 + ch04packaging/greetings/README.md | 10 + ch04packaging/greetings/conf.py | 43 + .../doc/_modules/greetings/greeter.html | 127 + .../greetings/doc/_modules/index.html | 94 + .../greetings/doc/_static/alabaster.css | 703 + ch04packaging/greetings/doc/_static/basic.css | 921 + .../greetings/doc/_static/custom.css | 1 + .../greetings/doc/_static/doctools.js | 156 + .../doc/_static/documentation_options.js | 14 + ch04packaging/greetings/doc/_static/file.png | Bin 0 -> 286 bytes .../greetings/doc/_static/language_data.js | 199 + ch04packaging/greetings/doc/_static/minus.png | Bin 0 -> 90 bytes ch04packaging/greetings/doc/_static/plus.png | Bin 0 -> 90 bytes .../greetings/doc/_static/pygments.css | 75 + .../greetings/doc/_static/searchtools.js | 566 + .../greetings/doc/_static/sphinx_highlight.js | 144 + ch04packaging/greetings/doc/genindex.html | 107 + ch04packaging/greetings/doc/index.html | 132 + ch04packaging/greetings/doc/objects.inv | Bin 0 -> 292 bytes ch04packaging/greetings/doc/search.html | 117 + ch04packaging/greetings/doc/searchindex.js | 1 + ch04packaging/greetings/greetings/command.py | 18 + ch04packaging/greetings/greetings/greeter.py | 32 + .../greetings/test/fixtures/samples.yaml | 11 + .../greetings/greetings/test/test_greeter.py | 12 + ch04packaging/greetings/index.rst | 6 + ch04packaging/greetings/setup.py | 11 + ch04packaging/greetings_repo/CITATION.md | 5 + ch04packaging/greetings_repo/LICENSE.md | 4 + ch04packaging/greetings_repo/README.md | 21 + .../greetings_repo/greetings/command.py | 24 + .../greetings_repo/greetings/greeter.py | 30 + .../greetings/test/fixtures/samples.yaml | 12 + .../greetings/test/test_greeter.py | 19 + ch04packaging/greetings_repo/pyproject.toml | 22 + ch04packaging/index.html | 302 + ch04packaging/mazetool/exit.py | 8 + ch04packaging/mazetool/maze.py | 42 + ch04packaging/mazetool/person.py | 20 + ch04packaging/mazetool/room.py | 25 + ch05construction/01introduction.html | 549 + ch05construction/01introduction.ipynb | 285 + ch05construction/01introduction.ipynb.py | 188 + ch05construction/02conventions.html | 769 + ch05construction/02conventions.ipynb | 504 + ch05construction/02conventions.ipynb.py | 261 + ch05construction/03comments.html | 611 + ch05construction/03comments.ipynb | 359 + ch05construction/03comments.ipynb.py | 188 + ch05construction/05refactoring.html | 1161 + ch05construction/05refactoring.ipynb | 1025 + ch05construction/05refactoring.ipynb.py | 590 + ch05construction/06objects.html | 798 + ch05construction/06objects.ipynb | 576 + ch05construction/06objects.ipynb.py | 342 + ch05construction/08objects.html | 1503 + ch05construction/08objects.ipynb | 1128 + ch05construction/08objects.ipynb.py | 656 + ch05construction/09patterns.html | 1494 + ch05construction/09patterns.ipynb | 1275 + ch05construction/09patterns.ipynb.py | 797 + ch05construction/10boids.html | 54909 +++++++ ch05construction/10boids.ipynb | 333 + ch05construction/10boids.ipynb.py | 207 + ch05construction/SIDC-SUNSPOTS_A.csv | 322 + ch05construction/anotherfile.py | 2 + ch05construction/config.yaml | 6 + ch05construction/context.py | 54 + ch05construction/conventions.py | 81 + ch05construction/index.html | 300 + ch05construction/species.py | 2 + ch07dry/01intro.html | 341 + ch07dry/01intro.ipynb | 70 + ch07dry/01intro.ipynb.py | 48 + ch07dry/020Functional.html | 1545 + ch07dry/020Functional.ipynb | 1035 + ch07dry/020Functional.ipynb.py | 590 + ch07dry/025Iterators.html | 1780 + ch07dry/025Iterators.ipynb | 1132 + ch07dry/025Iterators.ipynb.py | 565 + ch07dry/040Exceptions.html | 1281 + ch07dry/040Exceptions.ipynb | 742 + ch07dry/040Exceptions.ipynb.py | 430 + ch07dry/049Operators.html | 868 + ch07dry/049Operators.ipynb | 442 + ch07dry/049Operators.ipynb.py | 229 + ch07dry/050OperatorsExample.html | 859 + ch07dry/050OperatorsExample.ipynb | 581 + ch07dry/050OperatorsExample.ipynb.py | 369 + ch07dry/060Metaprogramming.html | 1095 + ch07dry/060Metaprogramming.ipynb | 689 + ch07dry/060Metaprogramming.ipynb.py | 351 + ch07dry/datasource2.yaml | 2 + ch07dry/datasource3.yaml | 2 + ch07dry/example.yaml | 1 + ch07dry/index.html | 300 + ch08performance/010intro.html | 600 + ch08performance/010intro.ipynb | 252 + ch08performance/010intro.ipynb.py | 117 + ch08performance/015mandels.html | 624 + ch08performance/015mandels.ipynb | 257 + ch08performance/015mandels.ipynb.py | 120 + ch08performance/020numpy.html | 2230 + ch08performance/020numpy.ipynb | 1291 + ch08performance/020numpy.ipynb.py | 537 + ch08performance/040cython.html | 943 + ch08performance/040cython.ipynb | 482 + ch08performance/040cython.ipynb.py | 233 + ch08performance/050scaling.html | 1103 + ch08performance/050scaling.ipynb | 657 + ch08performance/050scaling.ipynb.py | 292 + ch08performance/array_memory.svg | 938 + ch08performance/deque_memory.svg | 942 + ch08performance/index.html | 300 + ch08performance/list_memory.svg | 1364 + ch98rubrics/Assessment1.md | 136 + ch98rubrics/Assessment1.pdf | Bin 0 -> 155283 bytes ch98rubrics/Assessment2.md | 117 + ch98rubrics/Assessment2.pdf | Bin 0 -> 151939 bytes ch98rubrics/PackagingTreasure.html | 826 + ch98rubrics/PackagingTreasure.ipynb | 500 + ch98rubrics/PackagingTreasure.ipynb.py | 253 + ch98rubrics/RefactoringTrees.html | 471 + ch98rubrics/RefactoringTrees.ipynb | 175 + ch98rubrics/RefactoringTrees.ipynb.py | 86 + ch98rubrics/tree.png | Bin 0 -> 33359 bytes dates.md | 3 + index.html | 428 + intro.md | 121 + jekyll_template/conf.json | 6 + jekyll_template/index.html.j2 | 9 + latex_template/conf.json | 8 + latex_template/index.tex.j2 | 60 + nbmerge.py | 64 + pyproject.toml | 3 + requirements.txt | 27 + session99/index.html | 318 + session99/linux.html | 393 + session99/mac.html | 431 + session99/windows.html | 400 + site-styles/ipython.css | 69 + site-styles/local_styles.css | 7 + 619 files changed, 450041 insertions(+) create mode 100644 .nojekyll create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/css/jekyll-styles.css create mode 100644 assets/css/print.css create mode 100644 assets/css/screen.css create mode 100644 assets/css/screen.min.css create mode 100644 assets/css/ucl_reveal.css create mode 100644 assets/favicons/favicon-144.png create mode 100644 assets/favicons/favicon-152.png create mode 100644 assets/favicons/favicon.ico create mode 100644 assets/images/carousel/carousel-aslash.jpg create mode 100644 assets/images/carousel/carousel-aslms.jpg create mode 100644 assets/images/carousel/carousel-beams.jpg create mode 100644 assets/images/carousel/carousel-ioe.jpg create mode 100644 assets/images/cells.jpg create mode 100644 assets/images/close.png create mode 100644 assets/images/favicon.ico create mode 100644 assets/images/hero-bg-ie.png create mode 100644 assets/images/mob-nav.png create mode 100644 assets/images/respond.proxy.gif create mode 100644 assets/images/search-sprite.png create mode 100644 assets/images/social-buttons/social-icons.png create mode 100644 assets/images/social-buttons/social-icons@2x.png create mode 100644 assets/images/ucl-logo-cropped-white.png create mode 100644 assets/images/ucl-logo.png create mode 100644 assets/images/ucl-logo.svg create mode 100644 assets/images/ucl-menu.png create mode 100644 assets/images/ucl-menu.svg create mode 100644 assets/images/ucl-portico.jpg create mode 100644 assets/js/app/carousel.js create mode 100644 assets/js/app/demo-site.js create mode 100644 assets/js/app/general.js create mode 100644 assets/js/app/lightbox.js create mode 100644 assets/js/app/map.js create mode 100644 assets/js/app/searchWithAutoComplete.js create mode 100644 assets/js/app/tabs.js create mode 100644 assets/js/lib/all-site.min.js create mode 100644 assets/js/lib/backbone-1.1.2.min.js create mode 100644 assets/js/lib/backbone-1.min.js create mode 100644 assets/js/lib/fastclick.min.js create mode 100644 assets/js/lib/googleAnalytics.min.js create mode 100644 assets/js/lib/gridset-overlay.min.js create mode 100644 assets/js/lib/handlebars.min.js create mode 100644 assets/js/lib/html5shiv-printshiv.min.js create mode 100644 assets/js/lib/jquery-1.7.1.min.js create mode 100644 assets/js/lib/jquery-1.7.1.min.min.js create mode 100644 assets/js/lib/jquery-1.9.1.min.js create mode 100644 assets/js/lib/jquery-2.1.1.min.js create mode 100644 assets/js/lib/jwplayer.flash.swf create mode 100644 assets/js/lib/jwplayer.html5.js create mode 100644 assets/js/lib/jwplayer.html5.min.js create mode 100644 assets/js/lib/jwplayer.js create mode 100644 assets/js/lib/jwplayer.min.js create mode 100644 assets/js/lib/modernizr-custom.js create mode 100644 assets/js/lib/modernizr-custom.min.js create mode 100644 assets/js/lib/modernizr.min.js create mode 100644 assets/js/lib/owl.carousel.min.js create mode 100644 assets/js/lib/require.min.js create mode 100644 assets/js/lib/respond.min.js create mode 100644 assets/js/lib/respond.proxy.min.js create mode 100644 assets/js/lib/typeahead.bundle.min.js create mode 100644 assets/js/lib/underscore-1.6.0.min.js create mode 100644 assets/js/lib/underscore-1.min.js create mode 100644 assets/js/lib/underscore-1.min.min.js create mode 100644 assets/js/main.js create mode 100644 assets/js/respond-proxy.html create mode 100644 assets/js/respond-x-domain/respond-proxy.html create mode 100644 assets/js/src/all-site.js create mode 100644 assets/js/src/all-site/googleAnalytics.js create mode 100644 assets/js/src/all-site/gridset-overlay.js create mode 100644 assets/js/src/all-site/lazyload.js create mode 100644 assets/js/src/all-site/new-relic.js create mode 100644 assets/js/src/all-site/ucl.js create mode 100644 assets/js/src/backbone-1.1.2.js create mode 100644 assets/js/src/fastclick.js create mode 100644 assets/js/src/googleAnalytics.js create mode 100644 assets/js/src/gridset-overlay.js create mode 100644 assets/js/src/handlebars.js create mode 100644 assets/js/src/html5shiv-printshiv.js create mode 100644 assets/js/src/jquery-1.7.1.min.js create mode 100644 assets/js/src/jquery-1.9.1.js create mode 100644 assets/js/src/jquery-2.1.1.js create mode 100644 assets/js/src/jwplayer.flash.swf create mode 100644 assets/js/src/jwplayer.html5.js create mode 100644 assets/js/src/jwplayer.js create mode 100644 assets/js/src/modernizr-custom.js create mode 100644 assets/js/src/modernizr.js create mode 100644 assets/js/src/owl.carousel.js create mode 100644 assets/js/src/require.js create mode 100644 assets/js/src/respond.js create mode 100644 assets/js/src/respond.proxy.js create mode 100644 assets/js/src/typeahead.bundle.js create mode 100644 assets/js/src/underscore-1.6.0.js create mode 100644 assets/js/src/underscore-1.min.js create mode 100644 assets/sass/base/_buttons.scss create mode 100644 assets/sass/base/_forms.scss create mode 100644 assets/sass/base/_grid.scss create mode 100644 assets/sass/base/_images.scss create mode 100644 assets/sass/base/_layout.scss create mode 100644 assets/sass/base/_reset.scss create mode 100644 assets/sass/base/_tables.scss create mode 100644 assets/sass/base/_typography.scss create mode 100644 assets/sass/mixins/_alignment.scss create mode 100644 assets/sass/mixins/_gradient.scss create mode 100644 assets/sass/mixins/_gridset.scss create mode 100644 assets/sass/mixins/_guttering.scss create mode 100644 assets/sass/mixins/_inline-block.scss create mode 100644 assets/sass/mixins/_list.scss create mode 100644 assets/sass/mixins/_media-query.scss create mode 100644 assets/sass/mixins/_mixins.scss create mode 100644 assets/sass/mixins/_modernizr.scss create mode 100644 assets/sass/mixins/_no-js.scss create mode 100644 assets/sass/mixins/_on-interaction.scss create mode 100644 assets/sass/mixins/_sass-ie.scss create mode 100644 assets/sass/mixins/_unit.scss create mode 100644 assets/sass/patterns/_accordion.scss create mode 100644 assets/sass/patterns/_advert.scss create mode 100644 assets/sass/patterns/_blocked-link.scss create mode 100644 assets/sass/patterns/_blurb.scss create mode 100644 assets/sass/patterns/_box.scss create mode 100644 assets/sass/patterns/_brand.scss create mode 100644 assets/sass/patterns/_breadcrumb.scss create mode 100644 assets/sass/patterns/_code.scss create mode 100644 assets/sass/patterns/_collapse.scss create mode 100644 assets/sass/patterns/_commentlist.scss create mode 100644 assets/sass/patterns/_cta.scss create mode 100644 assets/sass/patterns/_divider.scss create mode 100644 assets/sass/patterns/_flag.scss create mode 100644 assets/sass/patterns/_footer.scss create mode 100644 assets/sass/patterns/_header.scss create mode 100644 assets/sass/patterns/_hero.scss create mode 100644 assets/sass/patterns/_input-group.scss create mode 100644 assets/sass/patterns/_left-nav--img.scss create mode 100644 assets/sass/patterns/_lightbox.scss create mode 100644 assets/sass/patterns/_map.scss create mode 100644 assets/sass/patterns/_masthead-legacy.scss create mode 100644 assets/sass/patterns/_masthead.scss create mode 100644 assets/sass/patterns/_media.scss create mode 100644 assets/sass/patterns/_menu-block.scss create mode 100644 assets/sass/patterns/_message.scss create mode 100644 assets/sass/patterns/_nav.scss create mode 100644 assets/sass/patterns/_owl-carousel.scss create mode 100644 assets/sass/patterns/_pagination.scss create mode 100644 assets/sass/patterns/_photograph.scss create mode 100644 assets/sass/patterns/_pills.scss create mode 100644 assets/sass/patterns/_pull-quote.scss create mode 100644 assets/sass/patterns/_search-form.scss create mode 100644 assets/sass/patterns/_site-content.scss create mode 100644 assets/sass/patterns/_slides.scss create mode 100644 assets/sass/patterns/_social-buttons.scss create mode 100644 assets/sass/patterns/_tabs.scss create mode 100644 assets/sass/patterns/_tag.scss create mode 100644 assets/sass/patterns/_vcard.scss create mode 100644 assets/sass/patterns/_video-wrap.scss create mode 100644 assets/sass/print.scss create mode 100644 assets/sass/screen.scss create mode 100644 assets/sass/utilities/_helpers.scss create mode 100644 assets/sass/utilities/_states.scss create mode 100644 assets/sass/variables/_breakpoints.scss create mode 100644 assets/sass/variables/_colors.scss create mode 100644 assets/sass/variables/_environment.scss create mode 100644 assets/sass/variables/_images.scss create mode 100644 assets/sass/variables/_spacing.scss create mode 100644 assets/sass/variables/_typography.scss create mode 100644 assets/sass/variables/_variables.scss create mode 100644 assets/sass/variables/_zindex.scss create mode 100644 attendee-info.md create mode 100644 bannermidgreen.pdf create mode 100644 ch00git/01Intro.html create mode 100644 ch00git/01Intro.ipynb create mode 100644 ch00git/01Intro.ipynb.py create mode 100644 ch00git/02Solo.html create mode 100644 ch00git/02Solo.ipynb create mode 100644 ch00git/02Solo.ipynb.py create mode 100644 ch00git/03Mistakes.html create mode 100644 ch00git/03Mistakes.ipynb create mode 100644 ch00git/03Mistakes.ipynb.py create mode 100644 ch00git/04Publishing.html create mode 100644 ch00git/04Publishing.ipynb create mode 100644 ch00git/04Publishing.ipynb.py create mode 100644 ch00git/05Collaboration.html create mode 100644 ch00git/05Collaboration.ipynb create mode 100644 ch00git/05Collaboration.ipynb.py create mode 100644 ch00git/06ForkAndPull.html create mode 100644 ch00git/06ForkAndPull.ipynb create mode 100644 ch00git/06ForkAndPull.ipynb.py create mode 100644 ch00git/10Branches.html create mode 100644 ch00git/10Branches.ipynb create mode 100644 ch00git/10Branches.ipynb.py create mode 100644 ch00git/11Miscellany.html create mode 100644 ch00git/11Miscellany.ipynb create mode 100644 ch00git/11Miscellany.ipynb.py create mode 100644 ch00git/12Remotes.html create mode 100644 ch00git/12Remotes.ipynb create mode 100644 ch00git/12Remotes.ipynb.py create mode 100644 ch00git/13Rebase.html create mode 100644 ch00git/13Rebase.ipynb create mode 100644 ch00git/13Rebase.ipynb.py create mode 100644 ch00git/14Bisect.html create mode 100644 ch00git/14Bisect.ipynb create mode 100644 ch00git/14Bisect.ipynb.py create mode 100644 ch00git/index.html create mode 100644 ch00git/learning_git/bare_repo/HEAD create mode 100644 ch00git/learning_git/bare_repo/config create mode 100755 ch00git/learning_git/bare_repo/description create mode 100755 ch00git/learning_git/bare_repo/hooks/applypatch-msg.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/commit-msg.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/fsmonitor-watchman.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/post-update.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-applypatch.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-commit.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-merge-commit.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-push.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-rebase.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/pre-receive.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/prepare-commit-msg.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/push-to-checkout.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/sendemail-validate.sample create mode 100755 ch00git/learning_git/bare_repo/hooks/update.sample create mode 100755 ch00git/learning_git/bare_repo/info/exclude create mode 100644 ch00git/learning_git/bare_repo/objects/02/2c38d1886bd36d1760ed255a4f6bbd1f43be8e create mode 100644 ch00git/learning_git/bare_repo/objects/03/625bb37da8f24bd34e2acfaf716ab0ce23be5d create mode 100644 ch00git/learning_git/bare_repo/objects/15/06ddb315a81961af669ac4ec0f7b8d39564ea6 create mode 100644 ch00git/learning_git/bare_repo/objects/1b/45884214655a0199ed5b0c59f7c4c4c80b5e0c create mode 100644 ch00git/learning_git/bare_repo/objects/21/c306bbfcd288f6f6e74c8495301159bb1f8ed2 create mode 100644 ch00git/learning_git/bare_repo/objects/2c/bc3a1a3c145ef2087cef6303e6508feb2e95eb create mode 100644 ch00git/learning_git/bare_repo/objects/34/6ff8f9f1e8b276e946a07066c5642cde43659e create mode 100644 ch00git/learning_git/bare_repo/objects/3a/2f7b04eb843b63930b5a28aa3a60de1381c3cc create mode 100644 ch00git/learning_git/bare_repo/objects/3f/fd43f7f4e8cb206c6f2cad9aa4e726686be06f create mode 100644 ch00git/learning_git/bare_repo/objects/4b/52f45528b43ccb12f198a757d075527283faad create mode 100644 ch00git/learning_git/bare_repo/objects/4f/db31b5b5aca2f87f78ef5da503cb823bbeab9d create mode 100644 ch00git/learning_git/bare_repo/objects/59/2c6bd85210c34f2004788d3d31d15b35ec2325 create mode 100644 ch00git/learning_git/bare_repo/objects/5a/a2999dd374d2da5874f5b379646b8f4c6d7a68 create mode 100644 ch00git/learning_git/bare_repo/objects/5b/f472d82a68b889f58165491d712e8f15de0de7 create mode 100644 ch00git/learning_git/bare_repo/objects/69/4c013206739cee29c71040e85a93b975d8e95b create mode 100644 ch00git/learning_git/bare_repo/objects/6a/4906ac762e499014d38c7716fb80ed5ad668a2 create mode 100644 ch00git/learning_git/bare_repo/objects/6c/90dbd9b71257440d4fb40e8ff8d8792194979f create mode 100644 ch00git/learning_git/bare_repo/objects/77/cc962ec24725bd1140cd700d8dd254ec116c50 create mode 100644 ch00git/learning_git/bare_repo/objects/7b/49cfb86012d2d4eb3ce12eeb879e1cbaaa8cd1 create mode 100644 ch00git/learning_git/bare_repo/objects/7c/a7cdd047dece7431e031e62d82311c12db7d65 create mode 100644 ch00git/learning_git/bare_repo/objects/7c/b0c94912e69f9774df89a2f8767a89a2adcdef create mode 100644 ch00git/learning_git/bare_repo/objects/85/bec83768f27da53032a611e29595d5b5f40378 create mode 100644 ch00git/learning_git/bare_repo/objects/86/0991ed05434dc1711ddc541fc00d43c803f056 create mode 100644 ch00git/learning_git/bare_repo/objects/99/c927cbd456e04e1120a0882151740e30834507 create mode 100644 ch00git/learning_git/bare_repo/objects/9f/5cbbb1154800845905d8429be8e8143c9505ea create mode 100644 ch00git/learning_git/bare_repo/objects/a0/6db2485baa3b4b5b5c8b96bdb50af8c45300be create mode 100644 ch00git/learning_git/bare_repo/objects/a1/363379944a5745ceb49c0e493d80eb9335c79a create mode 100644 ch00git/learning_git/bare_repo/objects/a1/f85df90512bb61264c5c5e81ed206b00f9e745 create mode 100644 ch00git/learning_git/bare_repo/objects/a2/0dedd3357a1f47838c6e7ecee14d818887bc87 create mode 100644 ch00git/learning_git/bare_repo/objects/a6/361f3fc86493e1cffc5218e0c5a9312bd9b684 create mode 100644 ch00git/learning_git/bare_repo/objects/a8/4a21b9c40e8bc87c42e88c84b2854dfa1ed652 create mode 100644 ch00git/learning_git/bare_repo/objects/ad/82f9b3ac49e557eca127a56411d4f52d11d772 create mode 100644 ch00git/learning_git/bare_repo/objects/ae/7410182b095a876b07012ff84c7c11cdffcd9d create mode 100644 ch00git/learning_git/bare_repo/objects/b2/02e904a72055b1710d61f18e048a5ef425c7ae create mode 100644 ch00git/learning_git/bare_repo/objects/b2/90508e7eecc632ee43f2a21b96641ca379a768 create mode 100644 ch00git/learning_git/bare_repo/objects/b5/e301149bedee3b12a1299c4a9d1386bdd96375 create mode 100644 ch00git/learning_git/bare_repo/objects/ba/320fb930f88e594958493645350b5ad9c9a4d5 create mode 100644 ch00git/learning_git/bare_repo/objects/bf/be7ced096691b79738a3d5a0b16b6459a8a12c create mode 100644 ch00git/learning_git/bare_repo/objects/c0/76f26f388a67ba6f95da979a1330edc957937d create mode 100644 ch00git/learning_git/bare_repo/objects/c4/eb102a88795733013f6043ae91fb1cc3c1e425 create mode 100644 ch00git/learning_git/bare_repo/objects/d8/c838427ba3492f00efab6100ad39e4880a802b create mode 100644 ch00git/learning_git/bare_repo/objects/dd/5cf9cab9854226b00c0eaec356bedfcad3e184 create mode 100644 ch00git/learning_git/bare_repo/objects/df/5fed820b7fa9834693c535803cefbd777411ca create mode 100644 ch00git/learning_git/bare_repo/objects/e7/3f9893bc695e0a7129310f90597bbc6d8296ed create mode 100644 ch00git/learning_git/bare_repo/objects/f2/bc29cdb9f78cd43ceccbcd25abf66080ee34ba create mode 100644 ch00git/learning_git/bare_repo/objects/f3/e88b4409aba4cdf8a31d05ad725b3d69626a61 create mode 100644 ch00git/learning_git/bare_repo/refs/heads/main create mode 100644 ch00git/learning_git/bisectdemo/LICENSE create mode 100644 ch00git/learning_git/bisectdemo/README.md create mode 100644 ch00git/learning_git/bisectdemo/break_output create mode 100755 ch00git/learning_git/bisectdemo/breakme.sh create mode 100644 ch00git/learning_git/bisectdemo/gitbisect.out create mode 100644 ch00git/learning_git/bisectdemo/squares.py create mode 100644 ch00git/learning_git/git_example/Makefile create mode 100644 ch00git/learning_git/git_example/Pennines.md create mode 100644 ch00git/learning_git/git_example/Scotland.md create mode 100644 ch00git/learning_git/git_example/Wales.md create mode 100644 ch00git/learning_git/git_example/index.html create mode 100644 ch00git/learning_git/git_example/lakeland.md create mode 100644 ch00git/learning_git/git_example/wsd.py create mode 100644 ch00git/learning_git/partner_repo/Scotland.md create mode 100644 ch00git/learning_git/partner_repo/Wales.md create mode 100644 ch00git/learning_git/partner_repo/index.md create mode 100644 ch00git/learning_git/partner_repo/lakeland.md create mode 100644 ch00git/somefile.md create mode 100644 ch01python/00pythons.html create mode 100644 ch01python/00pythons.ipynb create mode 100644 ch01python/00pythons.ipynb.py create mode 100644 ch01python/010exemplar.html create mode 100644 ch01python/010exemplar.ipynb create mode 100644 ch01python/010exemplar.ipynb.py create mode 100644 ch01python/015variables.html create mode 100644 ch01python/015variables.ipynb create mode 100644 ch01python/015variables.ipynb.py create mode 100644 ch01python/016using_functions.html create mode 100644 ch01python/016using_functions.ipynb create mode 100644 ch01python/016using_functions.ipynb.py create mode 100644 ch01python/023types.html create mode 100644 ch01python/023types.ipynb create mode 100644 ch01python/023types.ipynb.py create mode 100644 ch01python/025containers.html create mode 100644 ch01python/025containers.ipynb create mode 100644 ch01python/025containers.ipynb.py create mode 100644 ch01python/028dictionaries.html create mode 100644 ch01python/028dictionaries.ipynb create mode 100644 ch01python/028dictionaries.ipynb.py create mode 100644 ch01python/029structures.html create mode 100644 ch01python/029structures.ipynb create mode 100644 ch01python/029structures.ipynb.py create mode 100644 ch01python/030MazeSolution.html create mode 100644 ch01python/030MazeSolution.ipynb create mode 100644 ch01python/030MazeSolution.ipynb.py create mode 100644 ch01python/032conditionality.html create mode 100644 ch01python/032conditionality.ipynb create mode 100644 ch01python/032conditionality.ipynb.py create mode 100644 ch01python/035looping.html create mode 100644 ch01python/035looping.ipynb create mode 100644 ch01python/035looping.ipynb.py create mode 100644 ch01python/036MazeSolution2.html create mode 100644 ch01python/036MazeSolution2.ipynb create mode 100644 ch01python/036MazeSolution2.ipynb.py create mode 100644 ch01python/037comprehensions.html create mode 100644 ch01python/037comprehensions.ipynb create mode 100644 ch01python/037comprehensions.ipynb.py create mode 100644 ch01python/038SolutionComprehension.html create mode 100644 ch01python/038SolutionComprehension.ipynb create mode 100644 ch01python/038SolutionComprehension.ipynb.py create mode 100644 ch01python/04functions.html create mode 100644 ch01python/04functions.ipynb create mode 100644 ch01python/04functions.ipynb.py create mode 100644 ch01python/050import.html create mode 100644 ch01python/050import.ipynb create mode 100644 ch01python/050import.ipynb.py create mode 100644 ch01python/101Classes.html create mode 100644 ch01python/101Classes.ipynb create mode 100644 ch01python/101Classes.ipynb.py create mode 100644 ch01python/draw_eight.py create mode 100644 ch01python/eight.py create mode 100755 ch01python/fourteen.py create mode 100644 ch01python/index.html create mode 100644 ch02data/060files.html create mode 100644 ch02data/060files.ipynb create mode 100644 ch02data/060files.ipynb.py create mode 100644 ch02data/061internet.html create mode 100644 ch02data/061internet.ipynb create mode 100644 ch02data/061internet.ipynb.py create mode 100644 ch02data/062csv.html create mode 100644 ch02data/062csv.ipynb create mode 100644 ch02data/062csv.ipynb.py create mode 100644 ch02data/064JsonYamlXML.html create mode 100644 ch02data/064JsonYamlXML.ipynb create mode 100644 ch02data/064JsonYamlXML.ipynb.py create mode 100644 ch02data/065MazeSaved.html create mode 100644 ch02data/065MazeSaved.ipynb create mode 100644 ch02data/065MazeSaved.ipynb.py create mode 100644 ch02data/066QuakeExercise.html create mode 100644 ch02data/066QuakeExercise.ipynb create mode 100644 ch02data/066QuakeExercise.ipynb.py create mode 100644 ch02data/068QuakesSolution.html create mode 100644 ch02data/068QuakesSolution.ipynb create mode 100644 ch02data/068QuakesSolution.ipynb.py create mode 100644 ch02data/070hdf5.html create mode 100644 ch02data/070hdf5.ipynb create mode 100644 ch02data/070hdf5.ipynb.py create mode 100644 ch02data/072plotting.html create mode 100644 ch02data/072plotting.ipynb create mode 100644 ch02data/072plotting.ipynb.py create mode 100644 ch02data/082NumPy.html create mode 100644 ch02data/082NumPy.ipynb create mode 100644 ch02data/082NumPy.ipynb.py create mode 100644 ch02data/084Boids.html create mode 100644 ch02data/084Boids.ipynb create mode 100644 ch02data/084Boids.ipynb.py create mode 100644 ch02data/110Capstone.html create mode 100644 ch02data/110Capstone.ipynb create mode 100644 ch02data/110Capstone.ipynb.py create mode 100644 ch02data/boids_1.mp4 create mode 100644 ch02data/greengraph/graph.py create mode 100644 ch02data/greengraph/map.py create mode 100644 ch02data/hdf5_example.svg create mode 100644 ch02data/index.html create mode 100644 ch02data/maze.json create mode 100644 ch02data/maze.yaml create mode 100644 ch02data/my_file.hdf5 create mode 100644 ch02data/my_graph.png create mode 100644 ch02data/mydata.txt create mode 100644 ch02data/myfile.json create mode 100644 ch02data/myfile.yaml create mode 100644 ch02data/mywrittenfile create mode 100644 ch02data/planets_data.csv create mode 100644 ch03tests/01testingbasics.html create mode 100644 ch03tests/01testingbasics.ipynb create mode 100644 ch03tests/01testingbasics.ipynb.py create mode 100644 ch03tests/02SaskatchewanFields.html create mode 100644 ch03tests/02SaskatchewanFields.ipynb create mode 100644 ch03tests/02SaskatchewanFields.ipynb.py create mode 100644 ch03tests/03pytest.html create mode 100644 ch03tests/03pytest.ipynb create mode 100644 ch03tests/03pytest.ipynb.py create mode 100644 ch03tests/04EnergyExample.html create mode 100644 ch03tests/04EnergyExample.ipynb create mode 100644 ch03tests/04EnergyExample.ipynb.py create mode 100644 ch03tests/05Mocks.html create mode 100644 ch03tests/05Mocks.ipynb create mode 100644 ch03tests/05Mocks.ipynb.py create mode 100644 ch03tests/06Debugger.html create mode 100644 ch03tests/06Debugger.ipynb create mode 100644 ch03tests/06Debugger.ipynb.py create mode 100644 ch03tests/07CI.html create mode 100644 ch03tests/07CI.ipynb create mode 100644 ch03tests/07CI.ipynb.py create mode 100644 ch03tests/08DiffusionExample.html create mode 100644 ch03tests/08DiffusionExample.ipynb create mode 100644 ch03tests/08DiffusionExample.ipynb.py create mode 100644 ch03tests/DiffusionExample/MonteCarlo.py create mode 100644 ch03tests/DiffusionExample/test_model.py create mode 100644 ch03tests/commands create mode 100644 ch03tests/diffusion/htmlcov/coverage_html.js create mode 100644 ch03tests/diffusion/htmlcov/favicon_32.png create mode 100644 ch03tests/diffusion/htmlcov/index.html create mode 100644 ch03tests/diffusion/htmlcov/keybd_closed.png create mode 100644 ch03tests/diffusion/htmlcov/keybd_open.png create mode 100644 ch03tests/diffusion/htmlcov/model_py.html create mode 100644 ch03tests/diffusion/htmlcov/status.json create mode 100644 ch03tests/diffusion/htmlcov/style.css create mode 100644 ch03tests/diffusion/htmlcov/test_model_py.html create mode 100644 ch03tests/diffusion/model.py create mode 100644 ch03tests/diffusion/test_model.py create mode 100644 ch03tests/figures/callgrind.png create mode 100644 ch03tests/figures/coverage.png create mode 100644 ch03tests/figures/jenkins.png create mode 100644 ch03tests/figures/tdd.dot create mode 100644 ch03tests/index.html create mode 100644 ch03tests/saskatchewan/overlap.py create mode 100644 ch03tests/saskatchewan/test_overlap.py create mode 100644 ch03tests/solutions/diffusionmodel/LICENSE.md create mode 100644 ch03tests/solutions/diffusionmodel/README.md create mode 100644 ch03tests/solutions/diffusionmodel/diffusion_model.py create mode 100644 ch03tests/solutions/diffusionmodel/energy_example.py create mode 100644 ch03tests/solutions/diffusionmodel/test_derivatives.py create mode 100644 ch03tests/solutions/diffusionmodel/test_diffusion_model.py create mode 100644 ch03tests/solutions/montecarlo/LICENSE.md create mode 100644 ch03tests/solutions/montecarlo/README.md create mode 100644 ch03tests/solutions/montecarlo/monte_carlo.py create mode 100644 ch03tests/solutions/montecarlo/test_monte_carlo.py create mode 100644 ch04packaging/010Installation.html create mode 100644 ch04packaging/010Installation.ipynb create mode 100644 ch04packaging/010Installation.ipynb.py create mode 100644 ch04packaging/01Libraries.html create mode 100644 ch04packaging/01Libraries.ipynb create mode 100644 ch04packaging/01Libraries.ipynb.py create mode 100644 ch04packaging/025TextFiles.html create mode 100644 ch04packaging/025TextFiles.ipynb create mode 100644 ch04packaging/025TextFiles.ipynb.py create mode 100644 ch04packaging/02Argparse.html create mode 100644 ch04packaging/02Argparse.ipynb create mode 100644 ch04packaging/02Argparse.ipynb.py create mode 100644 ch04packaging/03Packaging.html create mode 100644 ch04packaging/03Packaging.ipynb create mode 100644 ch04packaging/03Packaging.ipynb.py create mode 100644 ch04packaging/04documentation.html create mode 100644 ch04packaging/04documentation.ipynb create mode 100644 ch04packaging/04documentation.ipynb.py create mode 100644 ch04packaging/05Process.html create mode 100644 ch04packaging/05Process.ipynb create mode 100644 ch04packaging/05Process.ipynb.py create mode 100644 ch04packaging/06Issues.html create mode 100644 ch04packaging/06Issues.ipynb create mode 100644 ch04packaging/06Issues.ipynb.py create mode 100644 ch04packaging/07Licensing.html create mode 100644 ch04packaging/07Licensing.ipynb create mode 100644 ch04packaging/07Licensing.ipynb.py create mode 100755 ch04packaging/greeter.py create mode 100644 ch04packaging/greetings/CITATION.md create mode 100644 ch04packaging/greetings/LICENSE.md create mode 100644 ch04packaging/greetings/README.md create mode 100644 ch04packaging/greetings/conf.py create mode 100644 ch04packaging/greetings/doc/_modules/greetings/greeter.html create mode 100644 ch04packaging/greetings/doc/_modules/index.html create mode 100644 ch04packaging/greetings/doc/_static/alabaster.css create mode 100644 ch04packaging/greetings/doc/_static/basic.css create mode 100644 ch04packaging/greetings/doc/_static/custom.css create mode 100644 ch04packaging/greetings/doc/_static/doctools.js create mode 100644 ch04packaging/greetings/doc/_static/documentation_options.js create mode 100644 ch04packaging/greetings/doc/_static/file.png create mode 100644 ch04packaging/greetings/doc/_static/language_data.js create mode 100644 ch04packaging/greetings/doc/_static/minus.png create mode 100644 ch04packaging/greetings/doc/_static/plus.png create mode 100644 ch04packaging/greetings/doc/_static/pygments.css create mode 100644 ch04packaging/greetings/doc/_static/searchtools.js create mode 100644 ch04packaging/greetings/doc/_static/sphinx_highlight.js create mode 100644 ch04packaging/greetings/doc/genindex.html create mode 100644 ch04packaging/greetings/doc/index.html create mode 100644 ch04packaging/greetings/doc/objects.inv create mode 100644 ch04packaging/greetings/doc/search.html create mode 100644 ch04packaging/greetings/doc/searchindex.js create mode 100755 ch04packaging/greetings/greetings/command.py create mode 100644 ch04packaging/greetings/greetings/greeter.py create mode 100644 ch04packaging/greetings/greetings/test/fixtures/samples.yaml create mode 100644 ch04packaging/greetings/greetings/test/test_greeter.py create mode 100644 ch04packaging/greetings/index.rst create mode 100644 ch04packaging/greetings/setup.py create mode 100644 ch04packaging/greetings_repo/CITATION.md create mode 100644 ch04packaging/greetings_repo/LICENSE.md create mode 100644 ch04packaging/greetings_repo/README.md create mode 100644 ch04packaging/greetings_repo/greetings/command.py create mode 100644 ch04packaging/greetings_repo/greetings/greeter.py create mode 100644 ch04packaging/greetings_repo/greetings/test/fixtures/samples.yaml create mode 100644 ch04packaging/greetings_repo/greetings/test/test_greeter.py create mode 100644 ch04packaging/greetings_repo/pyproject.toml create mode 100644 ch04packaging/index.html create mode 100644 ch04packaging/mazetool/exit.py create mode 100644 ch04packaging/mazetool/maze.py create mode 100644 ch04packaging/mazetool/person.py create mode 100644 ch04packaging/mazetool/room.py create mode 100644 ch05construction/01introduction.html create mode 100644 ch05construction/01introduction.ipynb create mode 100644 ch05construction/01introduction.ipynb.py create mode 100644 ch05construction/02conventions.html create mode 100644 ch05construction/02conventions.ipynb create mode 100644 ch05construction/02conventions.ipynb.py create mode 100644 ch05construction/03comments.html create mode 100644 ch05construction/03comments.ipynb create mode 100644 ch05construction/03comments.ipynb.py create mode 100644 ch05construction/05refactoring.html create mode 100644 ch05construction/05refactoring.ipynb create mode 100644 ch05construction/05refactoring.ipynb.py create mode 100644 ch05construction/06objects.html create mode 100644 ch05construction/06objects.ipynb create mode 100644 ch05construction/06objects.ipynb.py create mode 100644 ch05construction/08objects.html create mode 100644 ch05construction/08objects.ipynb create mode 100644 ch05construction/08objects.ipynb.py create mode 100644 ch05construction/09patterns.html create mode 100644 ch05construction/09patterns.ipynb create mode 100644 ch05construction/09patterns.ipynb.py create mode 100644 ch05construction/10boids.html create mode 100644 ch05construction/10boids.ipynb create mode 100644 ch05construction/10boids.ipynb.py create mode 100644 ch05construction/SIDC-SUNSPOTS_A.csv create mode 100644 ch05construction/anotherfile.py create mode 100644 ch05construction/config.yaml create mode 100644 ch05construction/context.py create mode 100644 ch05construction/conventions.py create mode 100644 ch05construction/index.html create mode 100644 ch05construction/species.py create mode 100644 ch07dry/01intro.html create mode 100644 ch07dry/01intro.ipynb create mode 100644 ch07dry/01intro.ipynb.py create mode 100644 ch07dry/020Functional.html create mode 100644 ch07dry/020Functional.ipynb create mode 100644 ch07dry/020Functional.ipynb.py create mode 100644 ch07dry/025Iterators.html create mode 100644 ch07dry/025Iterators.ipynb create mode 100644 ch07dry/025Iterators.ipynb.py create mode 100644 ch07dry/040Exceptions.html create mode 100644 ch07dry/040Exceptions.ipynb create mode 100644 ch07dry/040Exceptions.ipynb.py create mode 100644 ch07dry/049Operators.html create mode 100644 ch07dry/049Operators.ipynb create mode 100644 ch07dry/049Operators.ipynb.py create mode 100644 ch07dry/050OperatorsExample.html create mode 100644 ch07dry/050OperatorsExample.ipynb create mode 100644 ch07dry/050OperatorsExample.ipynb.py create mode 100644 ch07dry/060Metaprogramming.html create mode 100644 ch07dry/060Metaprogramming.ipynb create mode 100644 ch07dry/060Metaprogramming.ipynb.py create mode 100644 ch07dry/datasource2.yaml create mode 100644 ch07dry/datasource3.yaml create mode 100644 ch07dry/example.yaml create mode 100644 ch07dry/index.html create mode 100644 ch08performance/010intro.html create mode 100644 ch08performance/010intro.ipynb create mode 100644 ch08performance/010intro.ipynb.py create mode 100644 ch08performance/015mandels.html create mode 100644 ch08performance/015mandels.ipynb create mode 100644 ch08performance/015mandels.ipynb.py create mode 100644 ch08performance/020numpy.html create mode 100644 ch08performance/020numpy.ipynb create mode 100644 ch08performance/020numpy.ipynb.py create mode 100644 ch08performance/040cython.html create mode 100644 ch08performance/040cython.ipynb create mode 100644 ch08performance/040cython.ipynb.py create mode 100644 ch08performance/050scaling.html create mode 100644 ch08performance/050scaling.ipynb create mode 100644 ch08performance/050scaling.ipynb.py create mode 100644 ch08performance/array_memory.svg create mode 100644 ch08performance/deque_memory.svg create mode 100644 ch08performance/index.html create mode 100644 ch08performance/list_memory.svg create mode 100644 ch98rubrics/Assessment1.md create mode 100644 ch98rubrics/Assessment1.pdf create mode 100644 ch98rubrics/Assessment2.md create mode 100644 ch98rubrics/Assessment2.pdf create mode 100644 ch98rubrics/PackagingTreasure.html create mode 100644 ch98rubrics/PackagingTreasure.ipynb create mode 100644 ch98rubrics/PackagingTreasure.ipynb.py create mode 100644 ch98rubrics/RefactoringTrees.html create mode 100644 ch98rubrics/RefactoringTrees.ipynb create mode 100644 ch98rubrics/RefactoringTrees.ipynb.py create mode 100644 ch98rubrics/tree.png create mode 100644 dates.md create mode 100644 index.html create mode 100644 intro.md create mode 100644 jekyll_template/conf.json create mode 100644 jekyll_template/index.html.j2 create mode 100644 latex_template/conf.json create mode 100644 latex_template/index.tex.j2 create mode 100644 nbmerge.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 session99/index.html create mode 100644 session99/linux.html create mode 100644 session99/mac.html create mode 100644 session99/windows.html create mode 100644 site-styles/ipython.css create mode 100644 site-styles/local_styles.css diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..eb444c270 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,36 @@ +This work is the copyright of its various contributors. + +This collection of material is licensed under a Creative Commons - Attribution license. + +The list of contributors can be found at https://github.com/UCL/rsd-engineeringcourse/graphs/contributors. + +It is a derivative work of the work of the Software Carpentry Initiative, +a full list of contributors can be found at https://github.com/swcarpentry/boot-camps/graphs/contributors. + +You are free: + +- to **Share** - to copy, distribute and transmit the work +- to **Remix** - to adapt the work + +Under the following conditions: + +- **Attribution** - You must attribute the work in the manner specified by the + author or licensor (but not in any way that suggests that they endorse you or + your use of the work). + +With the understanding that: + +- **Waiver** - Any of the above conditions can be waived if you get permission + from the copyright holder. +- **Other Rights** - In no way are any of the following rights affected by the + license: + - Your fair dealing or fair use rights; + - The author’s moral rights; + - Rights other persons may have either in the work itself or in how the work + is used, such as publicity or privacy rights. +- **Notice** - For any reuse or distribution, you must make clear to others the + license terms of this work. The best way to do this is with a link to this + [web page](http://creativecommons.org/licenses/by/3.0/). + +For the full legal text of this license, please see +http://creativecommons.org/licenses/by/3.0/legalcode. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..5aac9f383 --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +PANDOC=pandoc + +ROOT="" + +PANDOCARGS=-t revealjs -s -V theme=night --css=http://lab.hakim.se/reveal-js/css/theme/night.css \ + --css=$(ROOT)/css/ucl_reveal.css --css=$(ROOT)/site-styles/reveal.css \ + --default-image-extension=png --highlight-style=zenburn --mathjax -V revealjs-url=http://lab.hakim.se/reveal-js +PRENOTEBOOKS=$(wildcard ch*/*.ipynb.py) +NOTEBOOKS_DONE=$(PRENOTEBOOKS:.ipynb.py=.ipynb) +NOTEBOOKS=$(filter-out %.v2.ipynb %.nbconvert.ipynb,$(sort $(wildcard ch*/*.ipynb))) +SVGS=$(wildcard ch*/*.svg) + +HTMLS=$(PRENOTEBOOKS:.ipynb.py=.html) + +EXECUTED=$(NOTEBOOKS:.ipynb=.nbconvert.ipynb) +PNGS=$(SVGS:.svg=.png) +NBV2=$(NOTEBOOKS:.ipynb=.v2.ipynb) + +default: _site + +%/slides.html: %/*.md Makefile + cat $^ | $(PANDOC) $(PANDOCARGS) -o $@ + +%.png: %.py Makefile + python $< $@ + +%.png: %.nto Makefile + neato $< -T png -o $@ + +%.png: %.dot Makefile + dot $< -T png -o $@ + +%.png: %.svg Makefile + dbus-run-session inkscape -z -e $@ -w 600 $< + +%.png: %.uml plantuml.jar Makefile + java -Djava.awt.headless=true -jar plantuml.jar -p < $< > $@ + +%.html: %.nbconvert.ipynb Makefile jekyll_template + jupyter nbconvert --to html --template jekyll_template --stdout $< > $@ + +%.v2.ipynb: %.nbconvert.ipynb + jupyter nbconvert --to notebook --nbformat 2 --stdout $< > $@ + +%.ipynb: %.ipynb.py + jupytext --to ipynb $< -o - > $@ + +%.nbconvert.ipynb: %.ipynb + jupyter nbconvert --to notebook --allow-errors --ExecutePreprocessor.timeout=120 --execute --stdout $< > $@ + +notes.pdf: combined.ipynb $(PNGS) Makefile + jupyter nbconvert --to pdf --template latex_template $< + mv combined.pdf notes.pdf + +combined.ipynb: $(EXECUTED) + python nbmerge.py $^ $@ + sed -i -e 's/\.svg/\.png/g' $@ + +notes.tex: combined.ipynb $(PNGS) Makefile + jupyter nbconvert --to latex --template latex_template $< + mv combined.tex notes.tex + +notebooks.zip: ${NBV2} + zip -r notebooks $^ + +ready: $(HTMLS) # notes.pdf notebooks.zip + +plantuml.jar: + wget http://sourceforge.net/projects/plantuml/files/plantuml.jar/download -O plantuml.jar + +.PHONY: ready + +_site: ready + jekyll build --verbose + +preview: ready + jekyll serve --verbose + +worked: $(NOTEBOOKS_DONE) + find ./ -iname '*ipynb' + +clean: + rm -f ch*/generated/*.png + rm -rf ch*/*.html + rm -f ch*/*.pyc + rm -f ch*/*.ipynb + rm -f index.html + rm -rf _site + rm -rf images js css _includes _layouts favicon* master.zip indigo-jekyll-master + rm -f indigo + rm -f ch01python/analyzer.py + rm -f ch01python/eight + rm -f ch01python/eight.py + rm -rf ch01python/module1/ + rm -f ch01python/pretty.py + rm -f ch*/*.nbconvert.ipynb + rm -rf ch*/*.v2.ipynb + rm -rf combined* + rm -f notes.pdf + rm -f notes.tex + rm -f ch04packaging/greeter.py + rm -f ch04packaging/map.png + rm -f ch05construction/anotherfile.py + rm -f ch05construction/config.yaml + rm -f ch05construction/context.py + rm -f ch06design/fixed.png + rm -f ch07dry/datasource*.yaml + rm -f ch07dry/example.yaml + rm -f notebooks.zip diff --git a/README.md b/README.md new file mode 100644 index 000000000..fd6194e45 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# rsd-engineeringcourse + +Course materials for Research Software Engineering course. + + +## Content: + +In this course, you will move beyond programming, to learn how to construct reliable, readable, efficient research software in a collaborative environment. The emphasis is on practical techniques, tips, and technologies to effectively build and maintain complex code. This is a semester module (30 hours over 10 half-days), intensive, practical course. The content of each of the 10 half-day units is as follows: + +1. Code management. Distributed version control. Git. Github +1. Collaborating around code. Issue tracking. Code review and pull requests. Branches and merging +1. Introduction to Python and Scientific programming +1. Analysing and plotting Research data +1. Testing scientific software. Unit testing, regression testing. Test-driven design. Expectations and assertions. Mocking. Build-and-test servers. Negative testing. Sensible error messages. Setting up Continuous Integration. +1. Documenting software projects. Managed logging. Debugging and debuggers. Coverage measurement. Finding errors in the past. +1. Writing libraries and creating packages. Software licenses. Citing software. Software sustainability. Comments. Coding conventions. +1. Software as engineering. Pragmatic use of diagram languages. Requirements engineering. Agile and Waterfall. Functional and architectural design. +1. Best practice in construction. Design and development. Object-oriented design. +1. Analysing performance. Profiling code. Developing faster code. + +## Prerequisites: + +- You must have reasonable experience in at least one compiled language, such as C++, C, or Fortran, and at least one dynamic language, such as Python, Ruby, Matlab or R. +- You must also have experience of the Unix shell. + +Examples and exercises for this course will be provided in Python. You will therefore find it easiest to follow along if you have experience in it. Appropriate Python experience could be obtained from the Software Carpentry workshops. Previous experience with version control (such as from Software Carpentry) would be helpful. + +You are required to bring your own laptop to the course as the classrooms we are using do not have desktop computers. + +# Contributing to this repository + +This repository contains the course notes as Jupyter notebooks converted into `py:percent` format. This allows to edit the files as plain text as well as jupyter notebooks. To edit them as jupyter notebooks you'll need to have installed jupytext and open the `ipynb.py` files as notebooks via right-click and select "open with" and "notebook" on the Jupyter file browser. + +⚠ Do not run `make` locally on your computer! ⚠ + +It will produce side effects on your global git configuration! +Instead, follow the instructions below. + +## Testing it locally + +The site is built using gh-actions. If you'd like to test the actions locally, +you can run the actions using [`act`](https://github.com/nektos/act) command +tool. By default this will run the action in a copy of the repository and you +won't be able to inspect the steps that happened. If you'd like to keep the +output in the current directory, use the `-b` (bind) flag. + +```bash +$ act -b +[Build website/Build-website] 🚀 Start image=catthehacker/ubuntu:act-latest +[Build website/Build-website] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[] +[Build website/Build-website] ⭐ Run actions/checkout@v2 +[Build website/Build-website] ✅ Success - actions/checkout@v2 +[Build website/Build-website] ⭐ Run actions/cache@v2 +INFO[0000] ☁ git clone 'https://github.com/actions/cache' # ref=v2 +[Build website/Build-website] ✅ Success - actions/cache@v2 +[Build website/Build-website] ⭐ Run Install TeXLive +INFO[0000] ☁ git clone 'https://github.com/DanySK/setup-texlive-action' # ref=0.1.1 +[Build website/Build-website] ✅ Success - Install TeXLive +[Build website/Build-website] ⭐ Run Setup Python +INFO[0001] ☁ git clone 'https://github.com/actions/setup-python' # ref=v2 +[Build website/Build-website] ✅ Success - Setup Python +[Build website/Build-website] ⭐ Run Install dependencies +INFO[0001] ☁ git clone 'https://github.com/py-actions/py-dependency-install' # ref=v2 +[Build website/Build-website] ✅ Success - Install dependencies +[Build website/Build-website] ⭐ Run Building notes +[Build website/Build-website] ✅ Success - Building notes +[Build website/Build-website] ⭐ Run Builds website +INFO[0001] ☁ git clone 'https://github.com/helaili/jekyll-action' # ref=v2 +[Build website/Build-website] 🐳 docker run image=act-helaili-jekyll-action-v2:latest platform= entrypoint=[] cmd=[] +[Build website/Build-website] ✅ Success - Builds website +``` + +Alternatively, if you want to only run the jekyll build step once you've run the whole action, you can use the official jekyll containers with: + +```bash +$ docker run --rm --volume="$PWD:/srv/jekyll" --volume="$PWD/vendor/bundle:/usr/local/bundle" -p 4000:4000 -it jekyll/jekyll:latest jekyll serve +``` + +and open http://localhost:4000/rsd-engineeringcourse (or the link provided). +Note that this is mounting the `bundle` directory where `act` will create them. + +# Migration from jupyter notebooks to py:percent + +Using `jupytext` we've converted all the jupyter notebooks into plain text python files (py:percent) with: + +```bash +# First cleaned all outputs and commited it +nbstripout --extra-keys metadata.kernelspec ch*/*ipynb +# convert them +find ./ -iname '*ipynb' -exec jupytext --opt notebook_metadata_filter="kernelspec,jupytext,jekyll" --to py:percent {} -o {}.py \; +# then deleted the ipynb +find ./ -iname '*ipynb' -delete +``` diff --git a/assets/css/jekyll-styles.css b/assets/css/jekyll-styles.css new file mode 100644 index 000000000..cba71c360 --- /dev/null +++ b/assets/css/jekyll-styles.css @@ -0,0 +1,73 @@ +div#slidelink { + float: right; +} + +.hll { background-color: #ffffcc } +.c { color: #999988; font-style: italic } /* Comment */ +.err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.k { color: #000000; font-weight: bold } /* Keyword */ +.o { color: #000000; font-weight: bold } /* Operator */ +.cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ +.c1 { color: #999988; font-style: italic } /* Comment.Single */ +.cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ +.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.ge { color: #000000; font-style: italic } /* Generic.Emph */ +.gr { color: #aa0000 } /* Generic.Error */ +.gh { color: #999999 } /* Generic.Heading */ +.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.go { color: #888888 } /* Generic.Output */ +.gp { color: #555555 } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #aaaaaa } /* Generic.Subheading */ +.gt { color: #aa0000 } /* Generic.Traceback */ +.kc { color: #000000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ +.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.m { color: #009999 } /* Literal.Number */ +.s { color: #d01040 } /* Literal.String */ +.na { color: #008080 } /* Name.Attribute */ +.nb { color: #0086B3 } /* Name.Builtin */ +.nc { color: #445588; font-weight: bold } /* Name.Class */ +.no { color: #008080 } /* Name.Constant */ +.nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ +.ni { color: #800080 } /* Name.Entity */ +.ne { color: #990000; font-weight: bold } /* Name.Exception */ +.nf { color: #990000; font-weight: bold } /* Name.Function */ +.nl { color: #990000; font-weight: bold } /* Name.Label */ +.nn { color: #555555 } /* Name.Namespace */ +.nt { color: #000080 } /* Name.Tag */ +.nv { color: #008080 } /* Name.Variable */ +.ow { color: #000000; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #009999 } /* Literal.Number.Float */ +.mh { color: #009999 } /* Literal.Number.Hex */ +.mi { color: #009999 } /* Literal.Number.Integer */ +.mo { color: #009999 } /* Literal.Number.Oct */ +.sb { color: #d01040 } /* Literal.String.Backtick */ +.sc { color: #d01040 } /* Literal.String.Char */ +.sd { color: #d01040 } /* Literal.String.Doc */ +.s2 { color: #d01040 } /* Literal.String.Double */ +.se { color: #d01040 } /* Literal.String.Escape */ +.sh { color: #d01040 } /* Literal.String.Heredoc */ +.si { color: #d01040 } /* Literal.String.Interpol */ +.sx { color: #d01040 } /* Literal.String.Other */ +.sr { color: #009926 } /* Literal.String.Regex */ +.s1 { color: #d01040 } /* Literal.String.Single */ +.ss { color: #990073 } /* Literal.String.Symbol */ +.bp { color: #999999 } /* Name.Builtin.Pseudo */ +.vc { color: #008080 } /* Name.Variable.Class */ +.vg { color: #008080 } /* Name.Variable.Global */ +.vi { color: #008080 } /* Name.Variable.Instance */ +.il { color: #009999 } /* Literal.Number.Integer.Long */ +.photograph { + background-color: #007e9e; +} +.header--mobile { + background-color: #007e9e; + border-bottom-color: #007e9e; +} + diff --git a/assets/css/print.css b/assets/css/print.css new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/assets/css/print.css @@ -0,0 +1 @@ + diff --git a/assets/css/screen.css b/assets/css/screen.css new file mode 100644 index 000000000..1ad3e58aa --- /dev/null +++ b/assets/css/screen.css @@ -0,0 +1,4269 @@ +/* Variables +-------------------------------------------------------------- */ +/* Variable imports +-------------------------------------------------------------- */ +/* UCL Colours +-------------------------------------------------------------- */ +/* Indigo colour palette +-------------------------------------------------------------- */ +/* Colour Variant - for gradients +-------------------------------------------------------------- */ +/* Generic grayscale +-------------------------------------------------------------- */ +/* Colour by element +-------------------------------------------------------------- */ +/* Project specific +-------------------------------------------------------------- */ +/*hover colour*/ +/* Image sizing +-------------------------------------------------------------- */ +/* Z index values +-------------------------------------------------------------- */ +/* Module spacing +-------------------------------------------------------------- */ +/* layout +-------------------------------------------------------------- */ +/* lists +-------------------------------------------------------------- */ +/* Font stacks +-------------------------------------------------------------- */ +/* Breakpoints +-------------------------------------------------------------- */ +/* Development/Production environments +-------------------------------------------------------------- */ +/* Mixins +-------------------------------------------------------------- */ +/* + + A simpler media query mixin + + This mixin allows you to quickly create a media query in your project. With the ability to define the breakpoint in PX (which get's converted into EMs). + There's also the ability to define min/max and width/height (with defaults to min-width). + + There's also an option to duplicate the content into a OldIE conditionally classed bit of CSS. + + You would use it like this + + body { + @include (280) { + background-color: blue; + } + @include mq(600, false) { + background-color: red; + } + @include mq(1200, true, max) { + font-size: 110%; + } + } + + Which would give you this compiled CSS + + @media (min-width: 17.5em) { + body { + background-color: blue; + } + } + @media (min-width: 37.5em) { + body { + background-color: red; + } + } + .lt-ie9 body { + font-size: 110%; + } + @media (max-width: 75em) { + body { + font-size: 110%; + } + } + +*/ +/* +* Mixin name: Gradient +* Use: Shorthand gradient declarations with vendor prefixer/ie fallbacks +* Note: N/A +-------------------------------------------------------------- */ +/* + By default this mixin is to be used for creating .no-js .element fallbacks. + + For example: + + .selector { + @include js { + width: 303px; + } + } + + Which gives: + + .no-js .selector { + width: 303px; + } + +*/ +/* + By default this mixin is to be used for creating html.no-CSS3Thing .element fallbacks. + + For example: + + .selector { + @include modernizr(cssanimations) { + left: 400px; + } + } + + Which gives: + + html.no-cssanimations .selector { + left: 400px; + } + +*/ +/* +* Mixin name: List +* Use: +* 1. Remove bullets and indentation from a list +* Note: N/A +-------------------------------------------------------------- */ +/* 1 +-------------------------------------------------------------- */ +/* +* Mixin name: On interaction +* Use: Shorthand for most common interaction states +* Note: N/A +-------------------------------------------------------------- */ +/* +* Mixin name: Guttering +* Use: Controlling the spacing at the bottom of a module +* Note: See http://css-tricks.com/spacing-the-bottom-of-modules/ +-------------------------------------------------------------- */ +/* +* Name: SASS-IE +* Use: Media query handling outside of Gridset +* Notes: See http://jakearchibald.github.io/sass-ie/ +-------------------------------------------------------------- */ +/* +* Mixin name: Alignment +* Use: Align content with horizontal padding +* Note: N/A +-------------------------------------------------------------- */ +/* +* Mixin name: Unit +* Use: +* 1. Set REM value with pixel value fallback +* Note: N/A +-------------------------------------------------------------- */ +/* 1 +-------------------------------------------------------------- */ +/* +* Mixin name: Inline block +* Use: Bullet proof inline block browser support +* Note: N/A +-------------------------------------------------------------- */ +/* Helper classes +-------------------------------------------------------------- */ +/* +* Utility name: Helpers +* Use: Commonly used dlasses (and silent classes) that can be used within CSS or within HTML +* Note: See http://csswizardry.com/2014/01/extending-silent-classes-in-sass/ +-------------------------------------------------------------- */ +/* Hide +-------------------------------------------------------------- */ +.hidden { + display: none !important; + visibility: hidden; } + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + .visually-hidden.focusable:active, .visually-hidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } + +/* Clear +-------------------------------------------------------------- */ +.clear { + clear: both; } + +/* Clearfix +-------------------------------------------------------------- */ +.dl-inline, .clearfix, .hero, .media { + zoom: 1; } + .dl-inline:before, .dl-inline:after, .clearfix:before, .hero:before, .media:before, .clearfix:after, .hero:after, .media:after { + content: ""; + display: table; } + .dl-inline:after, .clearfix:after, .hero:after, .media:after { + clear: both; } + +/* Floats +-------------------------------------------------------------- */ +.pull-left { + float: left; } + +.pull-right { + float: right; } + +/* Add margin +-------------------------------------------------------------- */ +.push-left { + margin-left: 1.5em; } + +.push-right { + margin-right: 1.5em; } + +.push-bottom { + margin-bottom: 1.5em; } + +.push-top { + margin-top: 1.5em; } + +/* Remove margin/padding +-------------------------------------------------------------- */ +.zero { + margin: 0; } + +.zero-top { + margin-top: 0; } + +.zero-bottom { + margin-bottom: 0; } + +.zero-left { + margin-left: 0; } + +.zero-right { + margin-right: 0; } + +.zero-pad { + padding: 0; } + +.zero-pad-top { + padding-top: 0; } + +.zero-pad-bottom { + padding-bottom: 0; } + +.zero-pad-left { + padding-left: 0; } + +.zero-pad-right { + padding-right: 0; } + +/* Alignment +-------------------------------------------------------------- */ +.text-left { + text-align: left; } + +.text-right { + text-align: right; } + +.text-center { + text-align: center; } + +.text-justify { + text-align: justify; } + +.text-nowrap { + white-space: nowrap; } + +/* Transforms +-------------------------------------------------------------- */ +.text-lowercase { + text-transform: lowercase; } + +.text-uppercase { + text-transform: uppercase; } + +time, .text-capitalize { + text-transform: capitalize; } + +.text-emphasise { + font-style: italic; } + +/* Responsiveness +-------------------------------------------------------------- */ +.no-respond { + width: auto; } + +/* +* Utility name: States +* Use: Classes to be used only in JS +* Note: It is a best practise to try to seperate classes used in JS and classes that style elements +-------------------------------------------------------------- */ +.is-active { + display: block; } + +.is-open { + max-height: 40em; + overflow: visible; + opacity: 1; + transition: all .5s; } + +.is-collapsed { + max-height: 0; + overflow: hidden; + opacity: 0; + transition: all .5s; } + +.is-hidden { + display: none; } + +.is-disabled { + opacity: .8; + text-decoration: line-through; + cursor: not-allowed; } + +/* Base dependencies +-------------------------------------------------------------- */ +/* +* Base name: Reset +* Note: N/A +-------------------------------------------------------------- */ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; } + +html { + font-size: 62.5%; + overflow-x: hidden; } + +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; } + +nav ul { + list-style: none; } + +blockquote, q { + quotes: none; } + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; } + +a { + margin: 0; + padding: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; } + +ins { + background-color: #ff9; + color: #000000; + text-decoration: none; } + +mark { + background-color: #ff9; + color: #000000; + font-style: italic; + font-weight: bold; } + +del { + text-decoration: line-through; } + +abbr[title], dfn[title] { + border-bottom: 1px dotted; + cursor: help; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +img { + width: auto; + max-width: 100%; + height: auto; } + +/* change border colour to suit your needs */ +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #cccccc; + margin: 1em 0; + padding: 0; + clear: both; } + +input, select { + vertical-align: middle; } + +/* removes rounded corners and things on inputs for iOS */ +input { + /* -webkit-appearance: none; ---- this is hiding checkboxes and radio buttons*/ + border-radius: 0; } + +audio { + width: 100%; } + +/* +* Base name: Layout +* Note: N/A +-------------------------------------------------------------- */ +.wrapper { + margin: 0 auto; + max-width: 1400px; + width: 90%; + position: relative; } + +.sidebar { + position: absolute; + margin-top: 3em; + left: 0; + width: 19.8653%; } + @media only screen and (max-width: 767px) { + .sidebar { + width: 100%; + margin-left: auto; + margin-right: auto; + position: relative; + margin-top: 0; } } + +/* Block columns +-------------------------------------------------------------- */ +.block { + margin-bottom: 1em; } + +.block--col-1 { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 31.30645%; } + @media only screen and (max-width: 989px) { + .block--col-1 { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 31.59722%; } } + @media only screen and (max-width: 767px) { + .block--col-1 { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.block--col-2 { + display: block; + float: left; + margin-left: 34.33675%; + margin-right: -100%; + width: 31.28827%; } + @media only screen and (max-width: 989px) { + .block--col-2 { + display: block; + float: left; + margin-left: 34.20139%; + margin-right: -100%; + width: 31.59722%; } } + @media only screen and (max-width: 767px) { + .block--col-2 { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.block--col-3 { + display: block; + float: left; + margin-left: 68.65532%; + margin-right: -100%; + width: 31.27734%; } + @media only screen and (max-width: 989px) { + .block--col-3 { + display: block; + float: left; + margin-left: 68.40278%; + margin-right: -100%; + width: 31.59722%; } } + @media only screen and (max-width: 767px) { + .block--col-3 { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +/* Layout +-------------------------------------------------------------- */ +.layout-horizontal .sidebar { + display: none; } + .layout-horizontal .sidebar .nav--left { + display: none; } + @media only screen and (max-width: 767px) { + .layout-horizontal .sidebar { + display: block; } } +.layout-horizontal .nav--top { + display: block; } + @media only screen and (max-width: 767px) { + .layout-horizontal .nav--top { + display: none; } } +.layout-horizontal .site-content__inner { + padding-top: 1em; } +.layout-horizontal .site-content__body { + width: 100%; + margin-left: 0; + margin-right: 0; } + +.layout-horizontal--nav-2col .site-content__main { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 71.37803%; } +.layout-horizontal--nav-2col .site-content__sidebar { + display: block; + float: left; + margin-left: 74.41038%; + margin-right: -100%; + width: 25.58962%; } +@media only screen and (max-width: 989px) { + .layout-horizontal--nav-2col .site-content__main, .layout-horizontal--nav-2col .site-content__sidebar { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.layout-horizontal--nav-1col .site-content__main { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 100%; } +.layout-horizontal--nav-1col .site-content__sidebar { + display: none; } + +@media only screen and (max-width: 767px) { + .layout-vertical .nav--left { + display: none; } } + +.layout-vertical--nav-1col .site-content__main { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 100%; } + +.layout-vertical--nav-2col .site-content__main { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 55.46605%; } +.layout-vertical--nav-2col .site-content__sidebar { + display: block; + float: left; + margin-left: 66.80507%; + margin-right: -100%; + width: 33.19493%; } +@media only screen and (max-width: 989px) { + .layout-vertical--nav-2col .site-content__main, .layout-vertical--nav-2col .site-content__sidebar { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.layout-vertical--show-top-nav .nav--top { + display: block; } + @media only screen and (max-width: 767px) { + .layout-vertical--show-top-nav .nav--top { + display: none; } } +.layout-vertical--show-top-nav .sidebar { + margin-top: 5em; } + @media only screen and (max-width: 767px) { + .layout-vertical--show-top-nav .sidebar { + margin-top: 0; } } + +/* +* Base name: Typography +* Note: N/A +-------------------------------------------------------------- */ +html { + font-size: 62.5%; } + +body { + color: #333333; + background-color: #d9d9d1; + overflow-x: hidden; + line-height: 1.7; + -webkit-hyphens: none; + -moz-hyphens: none; + hyphens: none; + -ms-hyphens: none; + word-wrap: break-word; + -webkit-text-size-adjust: none; + font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + font-size: 16px; + font-size: 1.6rem; } + +/* Vertical rhythym +-------------------------------------------------------------- */ +p, ul, ol, dl, blockquote, hgroup, address, table, fieldset, figure, pre, img, .page-head, .media, .island, .islet, .pill, .responsive-container, .advert, .menu-block, .box, .input-group, .accordion, .video-wrap { + margin-bottom: 1.5em; } + +/* Headings +-------------------------------------------------------------- */ +h1, h2, h3 { + font-weight: 300; } + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: .5em; + font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } + h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { + font-size: 80%; + color: #6f6f6f; + line-height: 0; } + +h1, .as-h1, .hero__title--large { + font-size: 36px; + font-size: 3.6rem; } + @media (max-width: 767px) { + h1, .as-h1, .hero__title--large { + font-size: 30px; + font-size: 3rem; } } + +h2, .as-h2 { + font-size: 30px; + font-size: 3rem; } + @media (max-width: 767px) { + h2, .as-h2 { + font-size: 24px; + font-size: 2.4rem; } } + +h3, .as-h3 { + font-size: 24px; + font-size: 2.4rem; } + @media (max-width: 767px) { + h3, .as-h3 { + font-size: 18px; + font-size: 1.8rem; } } + +h4, .as-h4 { + font-size: 18px; + font-size: 1.8rem; + text-transform: none; + letter-spacing: 0; } + @media (max-width: 767px) { + h4, .as-h4 { + font-size: 16px; + font-size: 1.6rem; } } + +h5, .as-h5 { + font-size: 14px; + font-size: 1.4rem; + font-style: normal; + text-transform: uppercase; + font-weight: bold; } + +h6, .as-h6 { + font-size: 12px; + font-size: 1.2rem; + color: #999; + margin: 1em 0 0; } + +a { + color: #034da1; + text-decoration: none; } + a:hover, a:active, a:focus { + color: #034da1; + text-decoration: underline; } + +/* Quotes +-------------------------------------------------------------- */ +blockquote { + font-style: italic; + overflow: hidden; } + +small, +.small { + font-weight: 400; + font-size: 12px; + font-size: 1.2rem; } + +strong, +.strong { + font-weight: 600; } + +cite, .cite, em, dfn { + font-style: italic; } + +/* Inline styles +-------------------------------------------------------------- */ +.text-muted { + color: #999999; } + +.text-success { + color: #468847; } + +.text-error { + color: #b94a48; } + +.mod { + color: #ccc; + clear: both; + font-size: 12px; + font-size: 1.2rem; } + +ins { + background-color: #F0F0F0; + color: #000000; + text-decoration: none; + padding: 0 0.125em; } + +mark { + padding: 0 0.125em; } + +del { + color: #666; } + +.standfirst { + font-size: 20px; } + @media only screen and (max-width: 767px) { + .standfirst { + font-size: 18px; } } + +/* Lists +-------------------------------------------------------------- */ +ol, ul { + margin-top: 0; + margin-left: 1.5em; } + ol ul, ul ul { + margin-top: 1em; + margin-left: 1em; } + +.list-unstyled, .list-inline, .pills { + margin-left: 0; + padding-left: 0; + list-style: none; } + .list-unstyled li, .list-inline li, .pills li { + margin-left: 0; + list-style-type: none; } + .list-unstyled li:before, .list-inline li:before, .pills li:before { + background-color: transparent !important; } + +.list-inline, .pills { + display: inline; + margin-left: 0; } + .list-inline > li, .pills > li { + margin-right: 10px; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + +.list-inline--divided li:after { + content: "|"; + margin-left: 10px; } +.list-inline--divided li:last-child:after { + content: ""; + display: none; } + +.list-divided li { + margin-bottom: 1em; + padding-bottom: 1em; + margin-bottom: 14px; + margin-bottom: 1.4rem; + padding-bottom: 14px; + padding-bottom: 1.4rem; + border-bottom: 1px solid #cccccc; } + .list-divided li:last-child { + border-bottom: 0; + padding-bottom: 0; + margin-bottom: 0; } + +.tabbed > div ul li, .box ul li { + margin-left: 1em; } + +/* Ordered lists +-------------------------------------------------------------- */ +ol li { + list-style-type: decimal; + margin-bottom: 1em; } + ol li ol { + margin-top: 1em; } + ol li li { + margin-left: 1.5em; } + +/* Definition lists +-------------------------------------------------------------- */ +dl { + margin-top: 1em; } + +dt { + font-weight: bold; } + +dd { + padding-left: 1.5em; } + +.dl-inline dt { + min-width: 100px; } +.dl-inline dt, .dl-inline dd { + float: left; + margin-top: 0; + margin-right: 8px; + margin-right: 0.8rem; + margin-bottom: 4px; + margin-bottom: 0.4rem; } +.dl-inline dd + dt, .dl-inline dd + dd { + clear: left; } +.dl-inline dd + dd { + float: none; } +.dl-inline dt { + font-weight: bold; } + +/* Code styling +-------------------------------------------------------------- */ +pre { + margin: 2em 1.5em; + white-space: pre; + overflow-x: auto; + padding-bottom: 1em; +} + +pre, code, tt { + font: 1em "consolas", "andale mono", "lucida console", monospace; } + +/* +* Base name: Forms +* Note: N/A +-------------------------------------------------------------- */ +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; } + +fieldset { + padding: 0; + margin: 0; + border: 0; + min-width: 0; } + +legend { + font-size: 1.25em; + margin-bottom: 0.5em; } + +label { + vertical-align: middle; + max-width: 100%; + margin-bottom: .5em; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; + font-size: 14px; + font-size: 1.4rem; } + .lt-ie8 label { + vertical-align: auto; } + +input { + box-sizing: border-box; + font-size: 14px; + font-size: 1.4rem; } + +input[type=submit] { + cursor: pointer; } + +input, select { + outline: 0; } + input:focus, select:focus { + outline: 0; } + +input:disabled { + cursor: not-allowed; } + +input[type="search"], textarea { + -webkit-appearance: none; } + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { + display: none; } + +input[type="checkbox"] + label, input[type="radio"] + label { + display: inline; + margin-left: 5px; } + +input[type="file"] { + display: block; + padding: 0; } + +input[type="range"] { + display: block; + width: 100%; } + +label { + display: block; + font-size: 0.9em; + color: #494949; + font-weight: 500; } + +fieldset abbr[title="Required"] { + border-bottom: 0 none; + color: #AF1616; + font-size: 1.25em; + font-weight: 400; + line-height: 0.1; } + +textarea { + padding-left: 2%; + padding-right: 2%; + resize: vertical; } + +select { + width: 100%; + margin-bottom: .5em; } + +form em { + font-size: 0.8em; + color: #848484; } + +/* Form fields +-------------------------------------------------------------- */ +/* +* Common form controls +* +* Shared size and type resets for form controls. Apply `.form__control` to any +* of the following form controls: +* +* select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"] +* input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"] +* input[type="url"], input[type="search"], input[type="tel"], input[type="color"] +-------------------------------------------------------------- */ +.form__control { + padding: 0.5em 2%; + vertical-align: middle; + margin: 0; + margin-bottom: .5em; + border: 1px solid #ccc; + color: #333; + width: 100%; + box-sizing: border-box; + transition: border, .25s; } + .form__control:focus { + transition: border, .25s; + border: 1px solid #666; } + +/* +* Form groups +* Designed to help with the organization and spacing of vertical forms. +-------------------------------------------------------------- */ +.form__group { + margin-bottom: 1.5em; + position: relative; } + .form__group > *:last-child, .form__group > *:last-child > *:last-child, .form__group > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +.form__group--inline input, .form__group--prepend input { + padding-left: 0; + padding-right: 0; + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 63.85859%; + box-sizing: border-box; + padding-left: 1.51515%; + padding-right: 1.51515%; } + +.form__group--inline label { + text-align: right; + line-height: 38px; + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 33%; + font-size: 14px; + font-size: 1.4rem; } +.form__group--inline em { + clear: left; + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 63.85859%; } + +.form__group--prepend em { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } +.form__group--prepend span { + margin-left: 1.51515%; + color: #848484; + text-align: right; + line-height: 26px; + margin-top: 5px; + display: block; + background-color: #eee; + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 33%; + left: -1.51515%; + position: relative; + padding-left: 1.51515%; + padding-right: 1.51515%; + font-size: 14px; + font-size: 1.4rem; } + +.form__group--options legend { + display: block; + font-size: 0.9em; + color: #494949; + font-weight: 500; } +.form__group--options ul { + margin-left: 0; + padding-left: 0; + list-style: none; } + .form__group--options ul li { + margin-left: 0; + list-style-type: none; } + .form__group--options ul li:before { + background-color: transparent !important; } + +input.error { + background-color: #ffffff; + border: 1px solid #DD6868; } + +.error { + color: #c60f13; } + +/* +* Base name: Buttons +* Note: N/A +-------------------------------------------------------------- */ +.btn { + background-color: #666; + color: #ffffff; + text-align: center; + cursor: pointer; + text-decoration: none; + color: #ffffff; + padding: .75em 1.5em; + text-align: left; + position: relative; + border: 1px solid rgba(0, 0, 0, 0.21); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15); + line-height: 1; + -webkit-appearance: none; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; + font-size: 14px; + font-size: 1.4rem; } + .btn:active { + background-color: #505050; } + .btn:hover, .btn:active, .btn:focus { + background-color: #615957; + color: #ffffff; + text-decoration: none; } + .lt-ie9 .btn { + border: 0; } + +.btn--small { + padding: 8px 20px 8px 20px; + font-size: 12px; + font-size: 1.2rem; } + +.btn--gradient.disabled { + border-bottom: 1px solid rgba(0, 0, 0, 0.21); + color: #999; + text-shadow: none; + background: #e0e0e0; + background: linear-gradient(#eee 0%, #e0e0e0 100%); } + .btn--gradient.disabled:hover, .btn--gradient.disabled :focus { + background: #eee; + background: linear-gradient(#ededed 0%, #eee 100%); } + .btn--gradient.disabled:active { + background: #cccccc; + background: linear-gradient(#e8e8e4 0%, #cccccc 100%); } + +button { + background: #40403e; + cursor: pointer; + padding: 0.7rem; + color: #ffffff; + border: 0; + border-radius: 0.4em; } + button:hover, button :focus { + background-color: #615957; + border: 0; } + button:active { + background-color: #505050; + border: 0; } + +/* Gradient Button's Colors +-------------------------------------------------------------- */ +.btn--gradient.darkgreen { + background: #555025; + background: linear-gradient(#4d4820 0%, #555025 100%); } + +.btn--gradient.midgreen { + background: #8f993e; + background: linear-gradient(#9ca35f 0%, #8f993e 100%); } + +.btn--gradient.brightgreen { + background: #b5bd00; + background: linear-gradient(#c9d10f 0%, #b5bd00 100%); } + +.btn--gradient.lightgreen { + background: #bbc592; + background: linear-gradient(#c9d3a1 0%, #bbc592 100%); } + +.btn--gradient.yellow { + background: #f6be00; + background: linear-gradient(#ffd030 0%, #f6be00 100%); } + +.btn--gradient.darkred { + background: #651d32; + background: linear-gradient(#792f44 0%, #651d32 100%); } + +.btn--gradient.midred { + background: #93272c; + background: linear-gradient(#a3383d 0%, #93272c 100%); } + +.btn--gradient.brightred { + background: #d50000; + background: linear-gradient(#e21111 0%, #d50000 100%); } + +.btn--gradient.lightred { + background: #e03c31; + background: linear-gradient(#ec6258 0%, #e03c31 100%); } + +.btn--gradient.orange { + background: #ea7600; + background: linear-gradient(#ff9120 0%, #ea7600 100%); } + +.btn--gradient.darkpurple { + background: #4b384c; + background: linear-gradient(#543e55 0%, #4b384c 100%); } + +.btn--gradient.midpurple { + background: #500778; + background: linear-gradient(#641391 0%, #500778 100%); } + +.btn--gradient.brightpink { + background: #ac145a; + background: linear-gradient(#c01d68 0%, #ac145a 100%); } + +.btn--gradient.lightpurple { + background: #c6b0bc; + background: linear-gradient(#dbc0cf 0%, #c6b0bc 100%); } + +.btn--gradient.warmgrey { + background: #8c8279; + background: linear-gradient(#9c9188 0%, #8c8279 100%); } + +.btn--gradient.darkblue { + background: #003d4c; + background: linear-gradient(#074453 0%, #003d4c 100%); } + +.btn--gradient.midblue { + background: #002855; + background: linear-gradient(#0f427a 0%, #002855 100%); } + +.btn--gradient.brightblue { + background: #0097a9; + background: linear-gradient(#0cadc0 0%, #0097a9 100%); } + +.btn--gradient.lightblue { + background: #8db9ca; + background: linear-gradient(#a5cddd 0%, #8db9ca 100%); } + +.btn--gradient.stone { + background: #d6d2c4; + background: linear-gradient(#e0ddd1 0%, #d6d2c4 100%); } + +.btn--gradient.darkbrown { + background: #4e3629; + background: linear-gradient(#5f4436 0%, #4e3629 100%); } + +/* Gradient button background color on click +-------------------------------------------------------------- */ +.btn--gradient.darkgreen:active { + background: #555025; } + +.btn--gradient.midgreen:active { + background: #8f993e; } + +.btn--gradient.brightgreen:active { + background: #b5bd00; } + +.btn--gradient.lightgreen:active { + background: #bbc592; } + +.btn--gradient.yellow:active { + background: #f6be00; } + +.btn--gradient.darkred:active { + background: #651d32; } + +.btn--gradient.midred:active { + background: #93272c; } + +.btn--gradient.brightred:active { + background: #d50000; } + +.btn--gradient.lightred:active { + background: #e03c31; } + +.btn--gradient.orange:active { + background: #ea7600; } + +.btn--gradient.darkpurple:active { + background: #4b384c; } + +.btn--gradient.midpurple:active { + background: #500778; } + +.btn--gradient.brightpink:active { + background: #ac145a; } + +.btn--gradient.lightpurple:active { + background: #c6b0bc; } + +.btn--gradient.warmgrey:active { + background: #8c8279; } + +.btn--gradient.darkblue:active { + background: #003d4c; } + +.btn--gradient.midblue:active { + background: #002855; } + +.btn--gradient.brightblue:active { + background: #0097a9; } + +.btn--gradient.lightblue:active { + background: #8db9ca; } + +.btn--gradient.stone:active { + background: #d6d2c4; } + +.btn--gradient.darkbrown:active { + background: #4e3629; } + +/* +* Base name: Tables +* Note: N/A +-------------------------------------------------------------- */ +table { + display: table; + table-layout: auto; + width: 100%; + border: 1px solid #e6e6e6; + border-collapse: collapse; + clear: left; + float: left; + border-spacing: 0; + font-size: 0.8em; } + +thead, tbody, tfoot { + display: table-row-group; } + +thead { + display: table-header-group; } + +tbody { + display: table-row-group; } + +tfoot { + display: table-footer-group; } + +tr { + display: table-row; } + +th, td { + display: table-cell; + text-align: left; + width: auto; + border-bottom: 1px solid #e6e6e6; + border-top: none; + vertical-align: middle; + padding: 0.25em 1em 0.25em 0; } + +th { + border-right: 1px solid #e6e6e6; + border-bottom-color: #e6e6e6; + color: #333333; + font-weight: 700; + background-color: #eee; } + +th, td, caption { + padding: 4px 10px 4px 5px; } + +tr.even td { + background: #e5ecf9; } + +tfoot { + font-style: italic; } + +caption { + caption-side: bottom; + color: #666666; + font-size: 0.875em; + line-height: 1.4286; + padding: 0.8571em 0 0.2857em; + text-align: left; } + +td, td img { + vertical-align: top; } + +/* Responsive Tables +-------------------------------------------------------------- */ +.table-responsive { + width: 100%; + border-collapse: collapse; + border-spacing: 0; } + +.table-responsive th, .table-responsive td { + margin: 0; + vertical-align: top; + border: 1px solid #e6e6e6; } + +.table-responsive th { + text-align: left; } + +@media (max-width: 989px) { + .table-responsive { + display: block; + position: relative; + width: 100%; + border-top: 0; } + .table-responsive thead { + display: block; + float: left; } + .table-responsive thead tr { + display: block; } + .table-responsive tbody { + display: block; + width: auto; + position: relative; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + white-space: nowrap; } + .table-responsive tbody tr { + vertical-align: top; + border-right: 1px solid #eee; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + .table-responsive th { + display: block; + border-bottom: 0; } + .table-responsive td { + display: block; + min-height: 1.25em; + border-left: 0; + border-right: 0; + border-bottom: 0; } } +/* +* Base name: Images +* Note: N/A +-------------------------------------------------------------- */ +figcaption { + color: #666666; + font-size: 0.9em; } + +figcaption h4 { + margin-top: 0.2em; } + +figure img { + width: 100%; + margin-bottom: .5em; } + +figure + figure, figure + p { + margin-top: 1em; } + +/* Image options +-------------------------------------------------------------- */ +.img-pull-left { + margin-bottom: 1em; + margin-top: 1em; + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 48.42929%; + clear: none; + float: left; + margin-left: 0; + margin-right: 3.0303%; } + +.img-pull-right { + margin-bottom: 1em; + margin-top: 1em; + display: block; + float: left; + margin-left: 51.4596%; + margin-right: -100%; + width: 48.42929%; + clear: none; + float: right; + margin-right: 0; + margin-left: 3.0303%; } + +.img-rounded { + border-radius: 50em; } + +.img-sm { + max-width: 200px; } + +.img-md { + max-width: 400px; } + +.img-lg { + max-width: 767px; } + +.img-xl { + max-width: 989px; } + +.img-xxl { + max-width: 1400px; } + +@media only screen and (max-width: 767px) { + .img-lg, .img-xl, .img-xxl { + width: 100%; } + + .decorative { + display: none !important; } + + .img-pull-left.large-image, .img-pull-left.xl-image, .img-pull-left.xxl-image, .img-pull-right.large-image, .img-pull-right.xl-image, .img-pull-right.xxl-image { + width: 100%; } } +/* +* Base name: Grid +* Note: Applying Indigo grid to custom breakpoints. See - http://www.ucl.ac.uk/indigo/grid.php +-------------------------------------------------------------- */ +@media (max-width: 619px) { + .col { + clear: both; } } + +/* Mobile content grid (2 columns) +-------------------------------------------------------------- */ +@media (max-width: 619px) { + .col--1-2-sm { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +@media (max-width: 619px) { + .col--1-sm { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 46.79688%; } } + +@media (max-width: 619px) { + .col--2-sm { + display: block; + float: left; + margin-left: 53.04688%; + margin-right: -100%; + width: 46.79688%; } } + +/* Tablet grid (3 columns) +-------------------------------------------------------------- */ +@media (min-width: 768px) { + .col--1-md { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 31.59722%; } } + +@media (min-width: 768px) { + .col--2-md { + display: block; + float: left; + margin-left: 34.20139%; + margin-right: -100%; + width: 31.59722%; } } + +@media (min-width: 768px) { + .col--3-md { + display: block; + float: left; + margin-left: 68.40278%; + margin-right: -100%; + width: 31.59722%; } } + +@media (min-width: 768px) { + .col--1-2-md { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 65.79861%; } } + +@media (min-width: 768px) { + .col--2-3-md { + display: block; + float: left; + margin-left: 34.20139%; + margin-right: -100%; + width: 65.79861%; } } + +@media (min-width: 768px) { + .col--1-3-md { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +/* Desktop layout grid (5 columns) +-------------------------------------------------------------- */ +@media (min-width: 990px) { + .col--1-lg-layout { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 19.86532%; } } + +@media (min-width: 990px) { + .col--1-2-lg-layout { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 31.30645%; } } + +@media (min-width: 990px) { + .col--1-3-lg-layout { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 65.62502%; } } + +@media (min-width: 990px) { + .col--1-4-lg-layout { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 71.32997%; } } + +@media (min-width: 990px) { + .col--1-5-lg-layout { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +@media (min-width: 990px) { + .col--2-lg-layout { + display: block; + float: left; + margin-left: 22.89562%; + margin-right: -100%; + width: 8.41083%; } } + +@media (min-width: 990px) { + .col--2-3-lg-layout { + display: block; + float: left; + margin-left: 22.89562%; + margin-right: -100%; + width: 42.7294%; } } + +@media (min-width: 990px) { + .col--2-4-lg-layout { + display: block; + float: left; + margin-left: 22.89562%; + margin-right: -100%; + width: 48.43434%; } } + +@media (min-width: 990px) { + .col--2-5-lg-layout { + display: block; + float: left; + margin-left: 22.89562%; + margin-right: -100%; + width: 77.03704%; } } + +@media (min-width: 990px) { + .col--3-lg-layout { + display: block; + float: left; + margin-left: 34.33675%; + margin-right: -100%; + width: 31.28827%; } } + +@media (min-width: 990px) { + .col--3-4-lg-layout { + display: block; + float: left; + margin-left: 34.33675%; + margin-right: -100%; + width: 36.99322%; } } + +@media (min-width: 990px) { + .col--3-5-lg-layout { + display: block; + float: left; + margin-left: 34.33675%; + margin-right: -100%; + width: 65.59591%; } } + +@media (min-width: 990px) { + .col--4-5-lg-layout { + display: block; + float: left; + margin-left: 68.65532%; + margin-right: -100%; + width: 31.27734%; } } + +@media (min-width: 990px) { + .col--5-lg-layout { + display: block; + float: left; + margin-left: 74.36027%; + margin-right: -100%; + width: 25.57239%; } } + +/* Desktop content grid (4 columns) +-------------------------------------------------------------- */ +@media (min-width: 990px) { + .col--1-lg-content { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 33%; } } + +@media (min-width: 990px) { + .col--2-lg-content { + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 12.39899%; } } + +@media (min-width: 990px) { + .col--3-lg-content { + display: block; + float: left; + margin-left: 51.4596%; + margin-right: -100%; + width: 12.39899%; } } + +@media (min-width: 990px) { + .col--4-lg-content { + display: block; + float: left; + margin-left: 66.88889%; + margin-right: -100%; + width: 33%; } } + +@media (min-width: 990px) { + .col--1-2-lg-content { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 48.42929%; } } + +@media (min-width: 990px) { + .col--2-3-lg-content { + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 27.82828%; } } + +@media (min-width: 990px) { + .col--2-4-lg-content { + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 63.85859%; } } + +@media (min-width: 990px) { + .col--3-4-lg-content { + display: block; + float: left; + margin-left: 51.4596%; + margin-right: -100%; + width: 48.42929%; } } + +@media (min-width: 990px) { + .col--1-4-lg-content { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +/* Column floats +-------------------------------------------------------------- */ +@media (max-width: 767px) { + .col--float { + clear: none; + float: left; + margin-left: 0; + margin-right: 6.25%; } } +@media (min-width: 768px) and (max-width: 989px) { + .col--float { + clear: none; + float: left; + margin-left: 0; + margin-right: 2.60417%; } } +@media (min-width: 990px) { + .col--float { + clear: none; + float: left; + margin-left: 0; + margin-right: 3.0303%; } } + +@media (min-width: 990px) { + .col--float-content { + clear: none; + float: left; + margin-left: 0; + margin-right: 3.0303%; } } + +/* Column last child +-------------------------------------------------------------- */ +@media (max-width: 619px) { + .col--sm-last { + margin-right: 0; } } + +@media (min-width: 768px) and (max-width: 989px) { + .col--md-last { + margin-right: 0; } } + +@media (min-width: 990px) { + .col--lg-last { + margin-right: 0; } } + +/* Patterns +-------------------------------------------------------------- */ +/* +* Pattern name: Accordion +* Use: Hide/reveal mechanism for concatenating similar/related content +* Note: N/A +-------------------------------------------------------------- */ +.accordion__title, .accordion__description { + padding: 10px; + border-top: 1px solid #ccc; + margin-top: 0; } + .accordion__title:last-of-type, .accordion__description:last-of-type { + border-bottom: 1px solid #ccc; } + .accordion__title a, .accordion__description a { + font-weight: bold; } + +.accordion__title a { + color: #000000; + display: block; } + +.accordion__description { + border-top: 0; + overflow: hidden; } + .accordion__description > *:last-child, .accordion__description > *:last-child > *:last-child, .accordion__description > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + .accordion__description:last-of-type { + position: relative; + top: -1px; } + +.announcement-bar { + background-color: #ff5a5f; } + .announcement-bar p { + max-width: 1400px; + width: 90%; + margin: 0 auto; + color: #fff; + position: relative; } + .announcement-bar a { + color: #fff; + text-decoration: underline; } + .announcement-bar .announcement-bar--close { + position: absolute; + padding: 0 6px; + top: 0.5em; + right: 5%; + border: 1px solid #fff; + border-radius: 100%; + font-size: 10px; + text-decoration: none; } + .announcement-bar .announcement-bar--close:hover { + text-decoration: none; } + @media only screen and (max-width: 767px) { + .announcement-bar p { + width: 70%; + margin: 0; + padding: 0.5em; } + .announcement-bar .announcement-bar--close { + position: absolute; + padding: 0 12px; + top: 0.5em; + right: 5%; + border: 1px solid #fff; + border-radius: 100%; + font-size: 20px; + text-decoration: none; + top: 1em; } + .announcement-bar .announcement-bar--close:hover { + text-decoration: none; } } + +/* +* Pattern name: Advert +* Use: Supporting material or advertisement signpost +* Note: N/A +-------------------------------------------------------------- */ +.advert { + position: relative; + box-sizing: border-box; } + +@media (max-width: 767px) { + .advert--restrict { + max-width: 25em; } } + +.advert-divide { + clear: both; + text-align: center; + margin-bottom: 1.5em; + font-weight: 400; } + +.advert__content { + padding: 1.5em; + position: relative; + background: white; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1); } + .advert__content > *:last-child, .advert__content > *:last-child > *:last-child, .advert__content > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +.advert__content--push { + padding-top: 5em; } + +.advert__list { + margin-left: 0; + padding-left: 0; + list-style: none; + color: #999; } + .advert__list li { + margin-left: 0; + list-style-type: none; } + .advert__list li:before { + background-color: transparent !important; } + +.advert__img { + min-height: 10em; + background-size: cover; + background-position: center center; + position: relative; } + @media (max-width: 767px) { + .advert__img { + min-height: 12em; } } + +.advert__overlay { + position: absolute; + display: block; + background-color: rgba(0, 0, 0, 0.25); + z-index: 9999; + top: 0; + right: 0; + width: 100%; + height: 100%; } + +.advert__header { + margin-bottom: 0; + padding: 1.5em; + color: white; + min-height: 5em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + background-color: #b5bd00; + background-color: rgba(181, 189, 0, 0.8); } + .advert__header a { + color: white; } + +.advert__header-overlay { + margin-bottom: 0; + font-size: 2.5em; + color: white; + left: .5em; + top: .25em; + position: absolute; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); + z-index: 1; } + +/* +* Pattern name: Box +* Use: Highlighted text area +* Note: N/A +-------------------------------------------------------------- */ +.box { + border-top: 5px solid #000; + background: #fafafa; + overflow: hidden; + padding-top: 1em; + padding-bottom: 1em; + box-sizing: border-box; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1); + position: relative; + box-sizing: border-box; + padding-left: 3.0303%; + padding-right: 3.0303%; } + .box > *:last-child, .box > *:last-child > *:last-child, .box > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +.box--tagged { + padding-top: 2em; } + +.box--small { + margin-bottom: 32/14em; + padding: 20/14em; + font-size: 14px; + font-size: 1.4rem; } + +.box--rounded { + border-radius: 4px; } + +.box--blank { + border-top: none; } + +/* +* Pattern name: Blocked link +* Use: Highlighting links with a background colour and padding +* Note: N/A +-------------------------------------------------------------- */ +.blocked-link { + padding: 5px; + color: #ffffff; + text-decoration: none; + background-color: #B5BD00; + font-size: 14px; + font-size: 1.4rem; } + .blocked-link:hover, .blocked-link:active, .blocked-link:focus { + color: #ffffff; + background: #8f993e; + text-decoration: none; } + +/* +* Pattern name: Blurb +* Use: Blocked media (image and text) +* Note: N/A +-------------------------------------------------------------- */ +.blurb { + position: relative; + padding-bottom: 1em; + overflow: hidden; } + +.blurb__title { + font-size: 16px; + margin-top: 0; + font-weight: 700; } + +.blurb__img { + margin-bottom: 0.5em; + width: 100%; } + .blurb--wide .blurb__img { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 33%; } + @media only screen and (max-width: 767px) { + .blurb--wide .blurb__img { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.blurb__video { + margin-bottom: 1em; } + +.blurb__body > *:last-child, .blurb__body > *:last-child > *:last-child, .blurb__body > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } +.blurb--wide .blurb__body { + top: -4px; + position: relative; + display: block; + float: left; + margin-left: 36.0303%; + margin-right: -100%; + width: 63.85859%; } + @media only screen and (max-width: 767px) { + .blurb--wide .blurb__body { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +/* +* Pattern name: Brand +* Use: The UCL banner area +* Note: The UCL logo is made clickable via using an absolutely positioned link. (see .brand__link) +-------------------------------------------------------------- */ +.brand { + position: absolute; + z-index: 1; + overflow: visible; + width: 100%; + max-width: 1470px; + left: 0; } + @media only screen and (max-width: 767px) { + .brand { + position: static; + padding-left: 0; } + .brand p { + left: 0; + top: 50%; } } + +.brand__link { + position: absolute; + height: 50%; + width: 18%; + right: 0; + display: block; + bottom: 0; } + .lt-ie9 .brand__link { + display: none; } + +.brand__heading { + position: absolute; + left: 35px; + top: 25%; + color: #ffffff; + text-transform: uppercase; + margin-bottom: 0; + font-weight: 400; + font-size: 14px; + font-size: 1.4rem; } + @media only screen and (max-width: 767px) { + .brand__heading { + position: relative; + left: 0; + width: 100%; + box-sizing: border-box; } } + +.brand__logo { + display: block; + margin-bottom: 0; + width: 100%; } + @media only screen and (max-width: 767px) { + .brand__logo { + display: none; } } + +/* +* Pattern name: Breadcrumbs +* Use: For highlighting where a user is within the site. Doubles up as a navigation +* Note: On mobile the user will only ever see the last 3 items in the breadcrumb trail +-------------------------------------------------------------- */ +.breadcrumb { + display: block; + overflow: hidden; + margin-bottom: 0; + list-style: none; + border: 0; + padding-bottom: 0; + background: white; + border-style: solid; + border-width: 0 0 1px 0; + border-color: #ccc; + margin-bottom: 1.5em; } + +.breadcrumb--nav { + margin-bottom: 0; + border-bottom: 0; + position: relative; + left: -35px; + padding-left: 35px; + padding-right: 35px; + width: 100%; } + .breadcrumb--nav .breadcrumb__list { + line-height: 4; } + +.breadcrumb__list { + margin-bottom: 0; + vertical-align: top; + margin-left: 0; + padding-left: 0; + list-style: none; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + .breadcrumb__list li { + margin-left: 0; + list-style-type: none; } + .breadcrumb__list li:before { + background-color: transparent !important; } + +.breadcrumb__item { + margin: 0; + float: left; + text-transform: none; + font-size: 14px; + font-size: 1.4rem; } + .breadcrumb__item:after { + content: "/"; + color: #aaaaaa; + margin: 0 0.75em; + position: relative; + top: 1px; } + .breadcrumb__item:last-child:after { + content: " "; + margin: 0; } + +.breadcrumb__item--current > a { + text-decoration: none; + cursor: default; + color: #333333; } + +/* +* Base name: Code +* Use: Displaying markup and code +* Note: N/A +-------------------------------------------------------------- */ +.code { + font: 400 1em/1.5 "source-code-pro", monospace; + margin: 1em 0 1.5em 2.5em; } + .code li { + background: none repeat scroll 0 0 #F6F6F6; + border-top: 1px solid #FFFFFF; + list-style: decimal-leading-zero outside none; + padding: 0 0.5em; } + .code li .tab1 { + padding-left: 4ex; } + +/* +* Pattern name: Collapse +* Use: Collapsing and revealing mechanism for long form content +* Note: Only applicable for smaller viewports - triggered via JavaScript +-------------------------------------------------------------- */ +.collapse__header { + clear: both; } + @media (max-width: 766px) { + .collapse__header { + cursor: pointer; + border-bottom: 1px solid #ccc; + padding-bottom: 8px; } } + +.collapse__content { + /* ensure accordion nav is hidden and content is visible on desktop version */ + /* @include respond-min($screen-md) { + display: block !important; + } */ } + @media (max-width: 766px) { + .collapse__content { + display: none; + border-top: 0; } } + +@media (max-width: 767px) { + .collapse__header--active { + background: url(//cdn.ucl.ac.uk/indigo/images/close.png); + background-repeat: no-repeat; + background-position: right 50%; + padding-right: 20px; + border-bottom: none; } + .collapse__header--active:hover, .collapse__header--active:active, .collapse__header--active:focus { + background: url(//cdn.ucl.ac.uk/indigo/images/close.png); + background-repeat: no-repeat; + background-position: right 50%; } } + +@media (max-width: 767px) { + .collapse__header--inactive { + background-repeat: no-repeat; + background-position: right 50%; + padding-right: 20px; } + .collapse__header--inactive:hover, .collapse__header--inactive:active, .collapse__header--inactive:focus { + background-repeat: no-repeat; + background-position: right 50%; } } + +/* +* Pattern name: Comment list +* Use: Styling comments within list +* Note: A lot of nasty nesting going on here - presumably classes are set by plugin (?) +-------------------------------------------------------------- */ +.commentlist { + list-style: none; + margin: 0; } + +.commentlist li { + list-style: none; + margin-left: 0; } + .commentlist li.odd { + background: #f6f6f6; } + .commentlist li.even { + background: #fff; } + .commentlist li.parent { + border-left: 5px solid #111; } + .commentlist li.comment { + padding: 10px; } + .commentlist li.comment .reply { + font-size: 11px; } + .commentlist li.comment .vcard { + margin-bottom: 0; } + .commentlist li.comment .vcard .fn { + font-style: normal; } + .commentlist li.comment .avatar { + width: auto; + float: left; + margin: 0 1em 1em 0; } + .commentlist li.comment .comment-meta { + font-size: 12px; + font-size: 1.2rem; } + .commentlist li.comment .comment-meta a { + color: #ccc; } + .commentlist li.comment .comment-meta p { + clear: left; } + .commentlist li.comment .children { + list-style: none; + margin: 10px 0 0; } + .commentlist li.comment .children .depth-2 { + border-left: 5px solid #555; + margin: 0 0 10px 10px; } + .commentlist li.comment .children .depth-3 { + border-left: 5px solid #999; + margin: 0 0 10px 10px; } + .commentlist li.comment .children .depth-4 { + border-left: 5px solid #bbb; + margin: 0 0 10px 10px; } + +/* +* Pattern name: Tag +* Use: Small block of overlayed text over an image +* Note: Modifiers adjust positioning of tag (default: left) +-------------------------------------------------------------- */ +.btn.cta { + background-color: #034da1; + text-align: center; + font-weight: bold; + font-size: 1.2em; + border: none; + border-radius: 0; + box-sizing: border-box; } + .btn.cta:hover { + background-color: #002854; } + +.btn.cta { + width: 100%; } + +.btn.cta-half { + width: 50%; } + @media only screen and (max-width: 767px) { + .btn.cta-half { + min-width: 330px; } } + +.btn.cta-threequarters { + width: 75%; } + @media only screen and (max-width: 767px) { + .btn.cta-threequarters { + min-width: 330px; } } + +.btn.cta-third { + width: 33%; } + @media only screen and (max-width: 767px) { + .btn.cta-third { + min-width: 330px; } } + +.btn.cta-quarter { + width: 25%; } + @media only screen and (max-width: 767px) { + .btn.cta-quarter { + min-width: 330px; } } + +.btn.cta-sixth { + width: 16%; } + @media only screen and (max-width: 767px) { + .btn.cta-sixth { + min-width: 330px; } } + +/* +* Pattern name: Divider +* Use: Displaying a bordered keyline either above or below an element +* Note: Top and bottom modifiers alter margin +-------------------------------------------------------------- */ +.divider { + border-color: #ddd; } + +.divider--top { + margin-top: 1.5em; + padding-top: 1.5em; + border-top-width: 1px; + border-top-style: solid; } + +.divider--bottom { + margin-bottom: 1.5em; + padding-bottom: 1.5em; + border-bottom-width: 1px; + border-bottom-style: solid; } + +/* +* Pattern name: Footer +* Use: Handling links, credits and footnotes +* Note: N/A +-------------------------------------------------------------- */ +.footer__inner { + background-color: #40403e; + padding: 40px 35px; + margin-left: -35px; + margin-right: -35px; + color: #fff; + clear: left; } + .footer__inner h2 { + font-size: 1.6rem; + text-transform: none; } + .footer__inner a { + color: #eee; } + .footer__inner a:hover, .footer__inner a:active, .footer__inner a:focus { + color: #ffffff; } + +.footer__list { + margin-left: 0; + padding-left: 0; + list-style: none; } + .footer__list li { + margin-left: 0; + list-style-type: none; } + .footer__list li:before { + background-color: transparent !important; } + +.footer__item { + padding-bottom: 4px; + padding-top: 4px; + display: block; + margin-bottom: 0; + font-size: 14px; + font-size: 1.4rem; } + +.footer__links { + margin-top: 0.5em; + font-size: 14px; + font-size: 1.4rem; } + @media (max-width: 989px) { + .footer__links { + font-size: 12px; + font-size: 1.2rem; } } + +/* +* Pattern name: Flag +* Use: Displaying content next to an image with the content vertically aligned to the middle of the image +* Note: http://csswizardry.com/2013/05/the-flag-object/ +-------------------------------------------------------------- */ +.flag { + display: table; + width: auto; } + +.flag__aside, .flag__body { + display: table-cell; + vertical-align: middle; } + .flag--top .flag__aside, .flag--top .flag__body { + vertical-align: top; } + .flag--bottom .flag__aside, .flag--bottom .flag__body { + vertical-align: bottom; } + +.flag__aside { + padding-right: 1.5em; } + .flag__aside > img { + display: block; + margin: 0; } + .lt-ie9 .flag__aside > img { + width: 100%; } + .flag--rev .flag__aside { + padding-right: 0; + padding-left: 1.5em; } + +.flag__body { + width: auto; } + .flag__body > *:last-child, .flag__body > *:last-child > *:last-child, .flag__body > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +/* +* Pattern name: Header +* Use: Wrapping masthead and branding areas +* Note: Desktop and mobile headers are seperated due to how the UCL menu is moved off convas on mobile +-------------------------------------------------------------- */ +.header { + position: relative; + z-index: 10; } + +@media only screen and (max-width: 767px) { + .header--desktop { + background-color: #5f5f5f; + position: absolute; + width: 100%; + height: 100%; + overflow: auto; + overflow-x: hidden; + transition: all, .5s; + -webkit-transform: translateX(-100%); + -ms-transform: translateX(-100%); + transform: translateX(-100%); } + .no-csstransforms .header--desktop { + left: -100%; } + .mobile-open .header--desktop { + transition: all, .5s; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); } + .no-csstransforms .mobile-open .header--desktop { + left: 0; } + .header--desktop a { + color: #ffffff; + padding: 0.5em 0; + display: block; + clear: both; + font-size: 16px; + font-size: 1.6rem; } } + +.header--mobile { + display: none; + width: 100%; + padding: 20% 35px 0; + background-size: auto 100%; + background-position: 106% bottom; + background-image: url("//cdn.ucl.ac.uk/indigo/images/ucl-logo.svg"); + background-repeat: repeat-x; + position: relative; + left: -35px; + background-color: #ff5a5f; + border-bottom: 0.5em solid #ff5a5f; } + @media only screen and (max-width: 767px) { + .header--mobile { + display: block; } } + .header--mobile.no-svg { + background-image: url("//cdn.ucl.ac.uk/indigo/images/ucl-logo-cropped-white.png"); + background-repeat: no-repeat; + background-position: right bottom; + background-color: #000000; + background-size: 90%; } + +@media only screen and (max-width: 767px) { + .header__open { + width: 25px; + top: 35%; + left: 35px; + position: absolute; + z-index: 100; } } +.header__open > img { + margin-bottom: 0; } + +.header__link { + position: absolute; + height: 100%; + right: 10%; + width: 30%; + top: 0; } + +.header__close { + display: none; } + @media only screen and (max-width: 767px) { + .header__close { + display: block; + position: absolute; + left: 5%; + top: 5px; + padding: 0; + z-index: 100; + width: 50%; } + .header__close img { + vertical-align: middle; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; + width: 30px; + position: relative; + margin: -5px -3px 0 -8px; } } + +/* +* Pattern name: Hero +* Use: Intro copy overlaying a feature image +* Note: N/A +-------------------------------------------------------------- */ +.hero { + padding-top: 2em; + padding-bottom: 2em; + background-color: white; + background-size: cover; + background-position: right center; + position: relative; + position: relative; + left: -35px; + padding-left: 35px; + padding-right: 35px; + width: 100%; } + @media (max-width: 989px) { + .hero { + padding-top: 1.5em; + padding-bottom: 1.5em; + background-position: -9999px; + background-repeat: no-repeat; } } + @media (max-width: 479px) { + .hero { + padding-top: 1em; + padding-bottom: 1em; } } + @media (max-width: 989px) { + .hero .hero__body--background { + padding: 0; } } + +.hero--home { + padding-left: 35px; + padding-right: 35px; + margin-left: -35px; + margin-right: -35px; + padding-bottom: 10em; + border: 0; + background-image: url("/assets/images/hero-image-taught-desktop.jpg"); + background-position: 90% center; + background-size: cover; + background-repeat: no-repeat; } + @media (max-width: 1179px) { + .hero--home { + background-position: 80% center; } } + @media (max-width: 767px) { + .hero--home { + background-image: none; } } + @media (min-width: 768px) and (max-width: 1179px) { + .hero--home { + padding-bottom: 14em; } } + @media (max-width: 767px) { + .hero--home { + padding-bottom: 1.5em; } } + @media (min-width: 768px) and (max-width: 989px) { + .hero--home .hero__body--background { + padding: 1.5em; } } + +.hero__body { + box-sizing: border-box; } + .hero__body > *:last-child { + margin-bottom: 0; } + +@media (min-width: 768px) { + .hero__body--background { + padding: 2em; + background-color: rgba(255, 255, 255, 0.6); } } +@media (max-width: 767px) { + .hero__body--background { + padding: 0; } } +.lt-ie9 .hero__body--background { + background-image: url(//cdn.ucl.ac.uk/indigo/images/hero-bg-ie.png); } + +.hero__title { + margin-bottom: 0; + padding-bottom: 0; } + @media (max-width: 767px) { + .hero__title { + font-size: 1.5em; } } + +@media (max-width: 1179px) { + .hero__title--large { + font-size: 2.25em; } } +@media (max-width: 989px) { + .hero__title--large { + font-size: 2em; } } +@media (max-width: 767px) { + .hero__title--large { + font-size: 1.5em; } } + +.hero__blurb { + margin-top: 1em; } + @media (max-width: 989px) { + .hero__blurb { + display: none; } } + +.hero__detail { + margin-top: .5em; } + @media (max-width: 767px) { + .hero__detail { + font-size: 1.25em; } } + +@media (max-width: 767px) { + .hero__sub { + display: none; } } + +.hero__tags { + margin-top: 1em; } + +.hero__dropdown { + margin-bottom: 2em; } + .hero__dropdown h2 { + margin-bottom: 16px; } + +.hero__dropdown__detail { + display: inline; + float: left; + padding: 6px 10px 6px 0; + font-size: 14px; + font-size: 1.4rem; } + +.hero__sidebar { + margin-bottom: 2em; + display: block; + float: left; + margin-left: 68.40278%; + margin-right: -100%; + width: 31.59722%; + display: block; + float: left; + margin-left: 74.36027%; + margin-right: -100%; + width: 25.57239%; } + +@media (min-width: 1050px) { + .hero__sidebar--small { + width: 20%; + float: right; + margin-left: 0; + margin-right: 0; } } + +/* +* Pattern name: Input group +* Use: Conjoin an input and a button form element +* Note: N/A +-------------------------------------------------------------- */ +.input-group { + display: table; + width: 100%; } + +.input-group__item { + display: table-cell; + width: 100%; + vertical-align: top; } + +.input-group__input { + height: 38px; + height: 3.8rem; + vertical-align: top; + padding-top: 0; + padding-bottom: 0; + margin-bottom: 0; } + +.input-group__input--btn { + white-space: nowrap; + box-sizing: border-box; + line-height: normal; + vertical-align: top; } + +/* Lightbox */ +.lightbox { + position: fixed; + top: 0; + left: 0; + z-index: 20; + width: 100%; + height: 100%; + text-align: center; + padding: 0 1em; + background: black; + background: rgba(0, 0, 0, 0.45); + -ms-box-sizing: border-box; + box-sizing: border-box; } + +.lightbox__item { + background-color: #000; + background: rgba(0, 0, 0, 0.65); + padding: 2em; + float: left; + left: 50%; + border-radius: 3px; + box-sizing: border-box; + position: relative; + top: 0; + -webkit-transform: translate(-50%); + -ms-transform: translate(-50%); + transform: translate(-50%); + z-index: 5150; } + .lightbox__item img { + max-height: 500px; } + +/* +Author: Stu Robson + +Ammends: Aaron Bery + - remove floats and associated clears as nothing is floated in our left nav + - set width to 100% for image to fill the left column, remove redundent max width +*/ +.org-unit-logo { + display: none; } + +@media only screen and (min-width: 768px) { + .org-unit-logo { + display: block; + margin-bottom: 1em; + width: 100%; } + + .org-unit-logo img { + width: 100%; } } +.map { + display: block; + width: 100%; + padding: 1em 0; + box-sizing: border-box; } + +.static-img { + height: auto; + max-width: 100%; + width: 100%; } + +.map-link { + display: block; + font: 0/0 a; } + +.static-img { + display: block; } + +.map-container { + width: 100%; + margin: 0 auto; + height: 0; + padding-top: 38%; + position: relative; + display: none; } + .map-container iframe { + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; } + +@media only screen and (min-width: 768px) { + .map-container { + display: block; } + + .static-img { + display: none; } } +/* +* Pattern name: Media +* Use: For a content block aligned to an image +* Note: http://www.stubbornella.org/content/2010/06/25/the-media-object-saves-hundreds-of-lines-of-code/ +-------------------------------------------------------------- */ +.media__aside { + float: left; + margin-right: 1em; + min-height: 1px; } + @media (min-width: 768px) { + .media__aside { + margin-right: 1.5em; } } + @media (max-width: 767px) { + .media__aside { + display: none; } } + +.media__aside--constrain { + max-width: 7em; } + +.media__aside img, +img.media__aside { + width: auto !important; + height: auto !important; } + +.media__body, .media__body--wide { + overflow: hidden; + margin: 0; } + @media (max-width: 767px) { + .media__body, .media__body--wide { + font-size: 14px; + font-size: 1.4rem; } } + +.media__body--wide { + max-width: 60em; } + +/* +* Pattern name: Menu Block +* Use: Stacked navigation menu with basic styling +* Note: N/A +-------------------------------------------------------------- */ +.menu-block { + padding: 0; + border: 1px solid #dfdfdf; + margin-left: 0; + padding-left: 0; + list-style: none; } + .menu-block li { + margin-left: 0; + list-style-type: none; } + .menu-block li:before { + background-color: transparent !important; } + +.menu-block--no-keyline { + border: 0; } + +.menu-block__list { + margin-bottom: 0; + margin-top: 0; + margin-left: 0; + padding-left: 0; + list-style: none; } + .menu-block__list li { + margin-left: 0; + list-style-type: none; } + .menu-block__list li:before { + background-color: transparent !important; } + +.menu-block__item { + position: relative; + padding: 0; + background-color: #EEE; + margin-bottom: 0; + line-height: 3; + border-bottom: 1px solid #dfdfdf; } + .menu-block__item:last-child { + border-bottom: 0; } + +.menu-block__item--heading:hover, .menu-block__item--heading:active, .menu-block__item--heading:focus { + text-decoration: none; } + +.menu-block__link { + padding-left: 1em; + display: block; + color: #333333; + transition: all .5s; + padding-right: 2.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } + .menu-block__link:hover, .menu-block__link:active, .menu-block__link:focus { + background-color: #dfdfdf; + transition: all .5s; + color: #333333; } + .menu-block__link.is-current { + margin-bottom: -1px; + background-color: black; + color: white; } + .is-active .menu-block__link { + border-left: .4em solid #333; } + +/* +* Pattern name: Menu Block +* Use: Success, notice and error boxes +* Note: Modifiers alter border and text colours to indicate the current state +-------------------------------------------------------------- */ +.message { + text-decoration: none; + border: 1px solid #DDDDDD; + clear: left; + color: #666666; + display: block; + font-size: 16px; + font-weight: 700; + width: 100%; + padding-bottom: 10px; + padding-top: 10px; + margin-bottom: 1em; + box-sizing: border-box; + padding-left: 3.0303%; + padding-right: 3.0303%; } + .message > *:last-child, .message > *:last-child > *:last-child, .message > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +.message--success { + background-color: #F7FFF2; + color: #7FB861; + border: 1px solid #7FB861; } + +.message--warning { + color: #DD6868; + border: 1px solid #DD6868; + font-weight: bold; } + +html.backgroundsize .message { + background: none repeat scroll 0 0 #FFFFFF; + background-position: 10px center; + background-repeat: no-repeat; + background-size: auto 80%; } +html.backgroundsize .message--success { + background-image: url("//cdn.ucl.ac.uk/indigo/images/success.png"); } +html.backgroundsize .message--warning { + background-image: url("//cdn.ucl.ac.uk/indigo/images/alert.png"); } + +/* +Notes: +1. Media queries: We're not using the standard gridset mq's as the masthead was built outside of the framework and as a consequence has different break points + +*/ +.masthead__search { + /*--- end .tt-dropdown-menu ---*/ + /*--- end .AC-result ---*/ + /*--- end .AC-result--directory ---*/ } + .masthead__search ul { + margin-left: 0; + margin-top: 0; + margin-top: 10px; } + .masthead__search ul.profile-details li { + display: block; } + .masthead__search li.tt-suggestion { + display: block; + overflow: hidden; + margin: 0; + padding: 0.5em 2%; } + .masthead__search li a { + color: #fff !important; } + .masthead__search form { + height: 30px; + width: 85%; + left: 0; + top: 0; } + .masthead__search form .fa { + float: left; + margin-right: 10px; + top: 3px; + position: relative; } + .masthead__search form ul.profile-details { + margin: 0; + width: 100%; } + .masthead__search form h2 { + margin-top: 0; + font-size: 18px !important; } + @media screen and (max-width: 1200px) { + .masthead__search form h2 { + height: 32px; } } + .masthead__search .sprite, .masthead__search .AC-result .email:before, .masthead__search .AC-result .tel:before { + background-image: url("/indigo/images/search-sprite.png"); + margin-left: 10px; } + .masthead__search .search { + left: 88%; + width: 12%; + border-radius: 4px; } + .masthead__search .tt-dropdown-menu { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; + box-shadow: 2px 2px 2px rgba(51, 51, 51, 0.3); + left: 5%; + position: absolute; + top: 38px; + width: 90%; + z-index: 5050; } + .masthead__search .tt-dropdown-menu ul { + margin-bottom: 10px; } + .masthead__search .tt-dropdown-menu li { + color: #333; + font-size: 12px; + list-style: none; + border-bottom: 1px solid #eee; + height: 64px; + max-height: 64px; + width: 96%; + text-align: left; } + .masthead__search .tt-dropdown-menu li h3 { + margin-top: 0; } + .masthead__search .tt-dropdown-menu li p { + font-size: 12px; + line-height: 1.4; + margin-bottom: 0; } + .masthead__search .tt-dropdown-menu li p a { + text-decoration: none; + font-size: 12px; } + .masthead__search .tt-dropdown-menu li a { + color: #4693ea !important; + display: block; + font-size: 12px; + font-weight: bold; + margin-bottom: 8px; } + .masthead__search .tt-dropdown-menu .no-results { + color: #4693ea !important; + display: block; + font-size: 12px; + font-weight: bold; + text-align: center; } + @media (max-width: 1023px) { + .masthead__search .tt-dropdown-menu { + display: none !important; } } + @media (max-width: 1220px) { + .masthead__search .tt-dropdown-menu .AC-result--directory img { + display: none !important; } } + .masthead__search .AC-result { + display: table-cell; + height: 100%; + margin-left: 5px; + padding: 0; + border-left: 1px solid #AAA; + width: 25%; } + .masthead__search .AC-result:first-child { + margin-left: 0; + border-left: 1px solid transparent; } + .masthead__search .AC-result h2 { + color: #000; + font-weight: bold; + font-size: 1em; + margin-bottom: 4px; + padding: 0.5em; } + .masthead__search .AC-result a { + color: #4693ea !important; + display: block; + font-size: 0.8em; + font-size: 12px; + font-weight: bold; } + .masthead__search .AC-result a.show-all { + text-align: center; + margin-bottom: 0.5em; } + .masthead__search .AC-result ul li:hover { + background-color: #e6f1fc; + font-size: auto; } + .masthead__search .AC-result ul li:hover li:hover { + background-color: transparent; } + .masthead__search .AC-result ul li li { + height: auto; } + .masthead__search .AC-result img { + max-height: 60px; + max-width: 60px; } + .masthead__search .AC-result .email { + clear: left; + float: left; + padding-left: 30px; + line-height: 24px; + position: relative; } + .masthead__search .AC-result .email:before { + content: ''; + position: absolute; + left: -35px; + width: 18px; + height: 12px; + top: 6px; } + .masthead__search .AC-result .email a { + color: #4c98e5; + font-size: 0.8em; + font-size: 12px; } + .masthead__search .AC-result .tel { + font-size: 12px; + line-height: 20px; + padding-left: 30px; + position: relative; } + .masthead__search .AC-result .tel:before { + content: ''; + position: absolute; + left: -35px; + width: 20px; + height: 18px; + top: 3px; + background-position: 0 -31px; } + .masthead__search .AC-result .tel span { + font-weight: bold; + display: inline; } + .masthead__search .AC-result .tel a { + display: inline; } + .masthead__search .AC-result .tel--external, .masthead__search .AC-result .email { + position: relative; + left: 2em; } + .masthead__search .AC-results { + box-sizing: border-box; + display: table; + height: 100%; + padding: 16px 0; + table-layout: fixed; + width: 100%; } + .masthead__search .AC-result--directory .AC-details__image { + display: table-cell; + padding-top: 5px; } + .masthead__search .AC-result--directory .AC-details__image img { + margin-right: 20px; + display: block; + padding-left: 16px; } + .masthead__search .AC-result--directory img { + float: left; + position: relative; + top: 0.5em; } + .masthead__search .AC-result--directory .AC-details { + display: block; + float: left; + width: 77%; } + .masthead__search .AC-result--directory .AC-details .profile-details li { + border-bottom: 0; + margin-bottom: 0; + padding-left: 0; + font-size: 12px; } + .masthead__search .AC-result--directory .AC-details .profile-details li a { + font-size: 12px; } + .masthead__search .twitter-typeahead { + width: 100%; } + .masthead__search #query { + outline: 0; + border: none; } + +/* +* Pattern name: Masthead +* Use: Container of global navigation items and site search +* Note: N/A +-------------------------------------------------------------- */ +.masthead { + background-color: #444; + color: #ffffff; + height: 45px; + font-size: 12px; + font-size: 1.2rem; } + @media only screen and (max-width: 767px) { + .masthead { + height: auto; + border-bottom: 1px solid #ccc; } + .masthead .wrapper { + background-color: #444; } } + .masthead img { + width: auto; } + +.masthead__search { + position: relative; + padding: 6px 0 3px 0; + box-sizing: border-box; + float: right; + width: 42%; } + @media only screen and (max-width: 767px) { + .masthead__search { + float: none; + width: 100%; + padding: 0; + margin-top: 3.5em; + height: 4em; + margin-left: auto; + margin-right: auto; } } + .masthead__search label { + margin-top: 9px; } + +.masthead__list { + float: left; + width: 55%; + font-size: 16px; + display: block; + margin-left: 0; } + @media only screen and (max-width: 767px) { + .masthead__list { + width: 100%; + margin-left: auto; + margin-right: auto; + float: none; + margin-bottom: 1em; } } + @media (max-width: 989px) { + .masthead__list { + font-size: 14px; + font-size: 1.4rem; } } + +.masthead__item { + padding-right: 1em; + height: 45px; + display: table-cell; + text-align: center; + vertical-align: middle; } + @media only screen and (max-width: 767px) { + .masthead__item { + display: block; + text-align: left; } } + @media (max-width: 989px) { + .masthead__item { + padding-right: .75em; } } + .lt-ie9 .masthead__item { + margin-top: 8px; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + +.masthead__link { + font-weight: 400; + color: #ffffff; + text-decoration: none; } + .masthead__link:hover, .masthead__link:active, .masthead__link:focus { + color: #ffffff; } + +/* +* Pattern name: Nav +* Use: Styling lists used for navigation areas +* Note: Modifiers define positioning of navigation +-------------------------------------------------------------- */ +.nav ul { + margin-left: 0; } + +.nav--left li { + margin-bottom: 0.5em; } + .nav--left li ul { + margin: 10px 5%; + font-size: 14px; + font-size: 1.4rem; } + +.nav--top { + display: none; + width: 100%; + background-color: #ffffff; + padding-bottom: 1em; + padding-top: 1em; + overflow: hidden; + position: relative; + left: -35px; + padding-left: 35px; + padding-right: 35px; + width: 100%; } + .nav--top ul li { + display: block; + float: left; + padding-right: 1em; + margin-right: 1em; + border-right: 1px solid #ccc; } + .nav--top ul li a { + text-decoration: none; + cursor: pointer; } + .nav--top ul li:last-child { + border-right: 0; } + .nav--top.nav--sticky-active { + position: fixed; + top: 0; + width: 100%; + background: transparent; + padding: 0; + left: 0; } + .nav--top.nav--sticky-active ul { + display: block; + background-color: #ff5a5f; + padding: 0.5em 35px; + max-width: 1400px; + margin: auto; + overflow: hidden; } + .nav--top.nav--sticky-active a { + color: #ffffff; } + +.nav--mobile { + display: none; } + @media only screen and (max-width: 767px) { + .nav--mobile { + display: block; } + .nav--mobile ul:nth-child(n+2) { + padding-left: 1em; } } + +/* +- Multi-layer sliding navigation +- Stu Robson +- http://codepen.io/sturobson/pen/d01f0fc84172afc650d1dedcaa2ab118?editors=010 +-------------------------------------------------------------------------------*/ +.nav--subnav { + box-sizing: border-box; + background-color: transparent; + color: white; + width: 100%; + position: relative; + overflow: hidden; } + .nav--subnav a { + text-decoration: none; + vertical-align: middle; } + +.subnav__list { + margin-top: 0; + position: absolute; + top: 0; + width: 100%; + background: transparent; + z-index: 1; + transition: all 0.35s cubic-bezier(0.645, 0.045, 0.355, 1); } + .subnav__list ul { + right: 200%; + left: 100%; + box-sizing: border-box; } + .subnav__list ul li { + box-sizing: border-box; } + .subnav__list .nav--active { + left: 0; + right: 0; } + .subnav__list .nav--active > li > a { + visibility: visible; } + .subnav__list.nav--hidden > li > a { + visibility: hidden; } + +.subnav__item.parent > a:after { + content: " >"; + position: absolute; + right: 0; } +.subnav__item.back a { + border-bottom: 1px transparent solid; } + .subnav__item.back a:before { + content: "< "; + position: relative; + top: -2px; } +.subnav__item.top-level a { + border-bottom: 1px transparent solid; } + .subnav__item.top-level a:before { + content: "< "; + position: relative; + top: -2px; } +.subnav__item a { + display: block; + position: relative; } + +.nav--left.nav--subnav { + position: relative; } + .nav--left.nav--subnav ul li { + margin: 0 0 0.5em 0; + line-height: 1.7; + font-size: 16px; + font-size: 1.6rem; } + .nav--left.nav--subnav li ul { + margin: 0; } + +.nav--mobile { + margin-top: 1em; } + .nav--mobile .subnav__list { + padding: 0; } + @media only screen and (max-width: 767px) { + .nav--mobile .subnav__list ul:nth-child(n+2) { + padding-left: 0; } } + .nav--mobile.nav--subnav ul li a { + color: white; } + .nav--mobile .subnav__item.back a { + color: #ccc; } + .nav--mobile .subnav__item.top-level a { + color: #ccc; } + .nav--mobile .subnav__item a:hover { + color: #ccc; } + +/*** CAROUSEL ***/ +.owl-theme .owl-controls { + margin-top: 10px; + text-align: center; + -webkit-tap-highlight-color: transparent; } + +.owl-theme .owl-controls .owl-nav [class*=owl-] { + color: #fff; + font-size: 14px; + margin: 5px; + padding: 4px 7px; + background: #0097a9; + display: inline-block; + cursor: pointer; + border-radius: 3px; } + +.owl-theme .owl-controls .owl-nav [class*=owl-]:hover { + background: rgba(0, 151, 169, 0.7); + color: #fff; + text-decoration: none; } + +.owl-theme .owl-controls .owl-nav .disabled { + opacity: .5; + cursor: default; } + +.owl-theme .owl-dots .owl-dot { + display: inline-block; + zoom: 1; + *display: inline; } + +.owl-theme .owl-dots .owl-dot span { + width: 10px; + height: 10px; + margin: 5px 7px; + background: #0097a9; + display: block; + -webkit-backface-visibility: visible; + transition: opacity 200ms ease; + border-radius: 30px; } + +.owl-theme .owl-dots .owl-dot.active span, +.owl-theme .owl-dots .owl-dot:hover span { + background: rgba(0, 151, 169, 0.5); } + +.owl-carousel .animated { + -webkit-animation-duration: 1000ms; + animation-duration: 1000ms; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; } + +.owl-carousel .owl-animated-in { + z-index: 0; } + +.owl-carousel .owl-animated-out { + z-index: 1; } + +.owl-carousel .fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; } + +@-webkit-keyframes fadeOut { + 0% { + opacity: 1; } + 100% { + opacity: 0; } } +@keyframes fadeOut { + 0% { + opacity: 1; } + 100% { + opacity: 0; } } +.owl-height { + transition: height 500ms ease-in-out; } + +.owl-carousel { + display: none; + width: 100%; + -webkit-tap-highlight-color: transparent; + position: relative; + z-index: 1; } + +.owl-carousel .owl-stage { + position: relative; + -ms-touch-action: pan-Y; } + +.owl-carousel .owl-stage:after { + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; } + +.owl-carousel .owl-stage-outer { + position: relative; + overflow: hidden; + -webkit-transform: translate3d(0px, 0, 0); } + +.owl-carousel .owl-controls .owl-dot, .owl-carousel .owl-controls .owl-nav .owl-next, .owl-carousel .owl-controls .owl-nav .owl-prev { + cursor: pointer; + cursor: hand; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +.owl-carousel.owl-loaded { + display: block; } + +.owl-carousel.owl-loading { + opacity: 0; + display: block; } + +.owl-carousel.owl-hidden { + opacity: 0; } + +.owl-carousel .owl-refresh .owl-item { + display: none; } + +.owl-carousel .owl-item { + position: relative; + min-height: 1px; + float: left; + -webkit-backface-visibility: hidden; + -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +.owl-carousel .owl-item img { + display: block; + width: 100%; + -webkit-transform-style: preserve-3d; + margin-bottom: 0; } + +.owl-carousel.owl-text-select-on .owl-item { + -webkit-user-select: auto; + -moz-user-select: auto; + -ms-user-select: auto; + user-select: auto; } + +.owl-carousel .owl-grab { + cursor: move; + cursor: -webkit-grab; + cursor: -o-grab; + cursor: -ms-grab; + cursor: grab; } + +.owl-carousel.owl-rtl { + direction: rtl; } + +.owl-carousel.owl-rtl .owl-item { + float: right; } + +.no-js .owl-carousel { + display: block; } + +.owl-carousel .owl-item .owl-lazy { + opacity: 0; + transition: opacity 400ms ease; } + +.owl-carousel .owl-item img { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; } + +.owl-carousel .owl-video-wrapper { + position: relative; + height: 100%; + background: #000; } + +.owl-carousel .owl-video-play-icon { + position: absolute; + height: 80px; + width: 80px; + left: 50%; + top: 50%; + margin-left: -40px; + margin-top: -40px; + background: url(owl.video.play.png) no-repeat; + cursor: pointer; + z-index: 1; + -webkit-backface-visibility: hidden; + transition: scale 100ms ease; } + +.owl-carousel .owl-video-play-icon:hover { + transition: scale(1.3, 1.3); } + +.owl-carousel .owl-video-playing .owl-video-play-icon, .owl-carousel .owl-video-playing .owl-video-tn { + display: none; } + +.owl-carousel .owl-video-tn { + opacity: 0; + height: 100%; + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + transition: opacity 400ms ease; } + +.owl-carousel .owl-video-frame { + position: relative; + z-index: 1; } + +.owl-carousel__item { + position: relative; + margin: 1px; + padding: 1px; } + .owl-carousel__item figcaption { + position: absolute; + bottom: 1px; + left: 1px; + background-color: #222; + opacity: 0.7; + padding: 10px; } + .owl-carousel__item figcaption h2 { + color: white; + font-weight: 500; + border-bottom: none !important; + margin: 0.3em 0 0 !important; } + .owl-carousel__item figcaption p { + color: #fff; } + .owl-carousel__item figcaption a { + font-weight: bold; + color: #fff; + text-decoration: underline; } + +.owl-nav { + display: none; } + +@media only screen and (max-width: 767px) { + .owl-carousel__item figcaption { + position: inherit; + bottom: 0; + left: 0; + opacity: 1; + background-color: #555; } + + #main .owl-carousel__item figcaption h2 { + font-size: 1.25em; } } +/*** END CAROUSEL ***/ +/* +* Pattern name: Pull quote +* Use: Stylised blockquote with increased spacing and speech marks +* Note: Modifiers allow for floating left/right within the grid +-------------------------------------------------------------- */ +.pull-quote { + margin-top: 0; + margin-bottom: 2em; } + +.pull-quote__wrap { + line-height: 1.5em; + font-size: 20px; + font-family: "Georgia", "Times New Roman", serif; + font-style: italic; + color: rgba(0, 0, 0, 0.7); } + +.pull-quote__meta { + border-top: 1px dotted; + padding-top: 1em; + border-color: rgba(0, 0, 0, 0.2); + float: left; + margin-top: 1em; + width: 100%; + font-size: 12px; + font-size: 1.2rem; } + +@media only screen and (max-width: 767px) { + .pull-quote--left, .pull-quote--right { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.pull-quote--left { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 48.42929%; + clear: none; + float: left; + margin-left: 0; + margin-right: 3.0303%; } + +.pull-quote--right { + display: block; + float: left; + margin-left: 51.4596%; + margin-right: -100%; + width: 48.42929%; + clear: none; + float: right; + margin-right: 0; + margin-left: 3.0303%; } + +.pull-quote__start, .pull-quote__end { + color: #c2c2ba; + font-size: 400%; + font-style: normal; + line-height: 0; + top: 37.5px; + position: relative; } + @media (max-width: 767px) { + .pull-quote__start, .pull-quote__end { + font-size: 350%; + top: 25px; } } + +.pull-quote__start { + padding-right: 10px; } + @media (max-width: 767px) { + .pull-quote__start { + padding-right: 5px; } } + +.pull-quote__end { + padding-left: 10px; } + @media (max-width: 767px) { + .pull-quote__end { + padding-left: 5px; } } + +/* +* Pattern name: Pill +* Use: For rounded inline label/tags +* Note: Has scope for inline icons within a pill +-------------------------------------------------------------- */ +.pills { + margin-bottom: 1em; + margin-left: 0; } + +.pills__item { + float: left; + padding: 0 1.5em; + background: #b5bd00; + color: white; + line-height: 2.5; + border-radius: 2em; + box-sizing: border-box; + font-size: 14px; + margin-bottom: .5em; + margin-right: .5em; } + .pills__item .icon { + margin-left: -24px; + font-size: 16px; + line-height: 1; } + .pills__item a { + color: white; + font-weight: bolder; } + @media (max-width: 767px) { + .pills__item { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 20em; + margin-right: 0; } } + +.pills__icon { + padding-left: 1em; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + +/* +* Pattern name: Photograph banner +* Use: Background photo/colour to be displayed behind UCL banner +* Note: Modifier gives .photograph more space as well as making the optional photography description visible. +-------------------------------------------------------------- */ +.photograph { + background-color: #ff5a5f; + background-repeat: no-repeat; + background-size: cover; + width: 100%; + position: relative; + left: -35px; + padding: 0 35px; + padding-bottom: 10.5%; } + @media only screen and (max-width: 767px) { + .photograph { + width: 100%; + margin-left: auto; + margin-right: auto; + padding: 2em 0 0em 0; + left: 0; + background-color: transparent; } } + +.photograph__description { + background-color: rgba(0, 0, 0, 0.7); + position: absolute; + bottom: 5%; + right: 35px; + padding: 10px; + font-style: italic; + color: #ffffff; + display: none; + font-size: 12px; + font-size: 1.2rem; } + +.photograph--show { + padding-bottom: 30%; } + .photograph--show .photograph__description { + display: block; } + +@media only screen and (max-width: 767px) { + .photograph--show { + display: none; } + .photograph--show .photograph__description { + display: none; } } +/* +* Pattern name: Pagination +* Use: For highlighting where a user is within a sequence of related pages +* Note: Modifiers indicate states (current = current page, active = clickable link, disabled = disabled link) +-------------------------------------------------------------- */ +.pagination { + margin-bottom: .5em; + margin-left: 0; + padding-left: 0; + list-style: none; } + .pagination li { + margin-left: 0; + list-style-type: none; } + .pagination li:before { + background-color: transparent !important; } + .pagination li { + text-align: center; + padding: 0.5em; + margin-right: .5em; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; } + .pagination li:last-child { + margin-right: 0; } + +.pagination__item { + min-width: 1.5em; + font-size: 14px; + background: #eae8e8; + transition: all .5s; } + .pagination__item a { + color: #555555; } + .pagination__item a:hover, .pagination__item a:active, .pagination__item a:focus { + text-decoration: none; } + +.pagination__item--current { + background: #4693ea; + border-color: #1a78e3; + color: white; } + +.pagination__item--active:hover, .pagination__item--active:active, .pagination__item--active:focus { + background: #4693ea; + border-color: #1a78e3; } + .pagination__item--active:hover a, .pagination__item--active:active a, .pagination__item--active:focus a { + color: white; } + +.pagination__item--disabled { + opacity: .25; } + @media (max-width: 767px) { + .pagination__item--disabled { + display: none !important; } } + +/* +* Pattern name: Slides +* Use: For a carousel image slideshow +* Note: N/A +-------------------------------------------------------------- */ +.rslides { + position: relative; + list-style: none; + overflow: hidden; + width: 100%; + padding: 0; } + +.rslides__item { + -webkit-backface-visibility: hidden; + position: absolute; + display: none; + width: 100%; + left: 0; + top: 0; + margin: 0; } + .rslides__item:first-child { + position: relative; + display: block; + float: left; } + .rslides__item:before { + display: none; } + +.rslides__img { + display: block; + height: auto; + float: left; + width: 100%; + border: 0; + margin-bottom: 0; } + +/* +* Pattern name: Site content +* Use: Structuring and alignment for main content +* Note: N/A +-------------------------------------------------------------- */ +.site-content { + position: relative; + z-index: 1; } + +.site-content--split { + overflow: hidden; } + @media only screen and (min-width: 768px) { + .site-content--split .site-content__section { + float: left; + margin-left: 2%; + margin-right: 2%; + width: 47%; } + .site-content--split .site-content__section:nth-child(odd) { + margin-right: 1%; } + .site-content--split .site-content__section:nth-child(even) { + margin-left: 1%; } } + +.site-content__inner { + background-color: #ffffff; + padding: 2em 35px; + left: -35px; + position: relative; + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } + .site-content__inner > *:last-child, .site-content__inner > *:last-child > *:last-child, .site-content__inner > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + @media only screen and (max-width: 989px) { + .site-content__inner { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + @media only screen and (max-width: 767px) { + .site-content__inner { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; + padding-top: 1.5em; } } + +.site-content__body { + display: block; + float: left; + margin-left: 22.89562%; + margin-right: -100%; + width: 77.03704%; } + @media only screen and (max-width: 989px) { + .site-content__body { + display: block; + float: left; + margin-left: 34.20139%; + margin-right: -100%; + width: 65.79861%; } } + @media only screen and (max-width: 767px) { + .site-content__body { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.site-content__main > *:last-child, .site-content__main > *:last-child > *:last-child, .site-content__main > *:last-child > *:last-child > *:last-child, .site-content__sidebar > *:last-child, .site-content__sidebar > *:last-child > *:last-child, .site-content__sidebar > *:last-child > *:last-child > *:last-child { + margin-bottom: 0; } + +.site-content__main { + display: block; + float: left; + margin-left: 0%; + margin-right: -100%; + width: 55.46605%; } + @media only screen and (max-width: 989px) { + .site-content__main { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + @media only screen and (max-width: 767px) { + .site-content__main { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.site-content__sidebar { + font-size: 14px; + font-size: 1.4rem; + display: block; + float: left; + margin-left: 66.80507%; + margin-right: -100%; + width: 33.19493%; } + @media only screen and (max-width: 989px) { + .site-content__sidebar { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + @media only screen and (max-width: 767px) { + .site-content__sidebar { + clear: both; + display: block; + float: left; + margin-left: 0; + width: 100%; } } + +.social-buttons { + margin: 0 auto; + text-align: center; } + .social-buttons--hoz .social-buttons__item { + float: left; + margin-left: 1em; } + .social-buttons--hoz .social-buttons__item:first-child { + margin-left: 0; } + .social-buttons--vert .social-buttons__item { + margin-top: 1em; } + .social-buttons--vert .social-buttons__item:first-child { + margin-top: 0; } + +.social-buttons__item { + list-style-type: none; } + +.social-buttons__link { + background-image: url("//cdn.ucl.ac.uk/indigo/images/social-buttons/social-icons.png"); + background-repeat: no-repeat; + display: block; + font: 0/0 a; + height: 44px; + transition: box-shadow ease-in-out .15s; + width: 44px; } + .social-buttons__link--twitter { + background-position: 0 0; } + .social-buttons__link--facebook { + background-position: -44px 0; } + .social-buttons__link--google { + background-position: -88px 0; } + +.social-buttons__link:hover { + box-shadow: 0 0 4px rgba(0, 0, 0, 0.4); + transition: box-shadow ease-in-out .15s; } + +@media (-webkit-min-device-pixel-ratio: 1.3), (-o-min-device-pixel-ratio: 2.6 / 2), (min--moz-device-pixel-ratio: 1.3), (min-device-pixel-ratio: 1.3), (min-resolution: 1.3dppx) { + .social-buttons__link { + background-image: url("//cdn.ucl.ac.uk/indigo/images/social-buttons/social-icons@2x.png"); + background-size: cover; } } +/* +* Pattern name: Search form +* Use: Inline search form used within the UCL masthead (used for global site search) +* Note: N/A +-------------------------------------------------------------- */ +.search-form { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAOCAMAAADHVLbdAAAANlBMVEUAAAD///+ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZkiXNL4AAAAEXRSTlMAABAgMEBQYHCAj5+vv8/f7/4ucL8AAABtSURBVAjXTc9JFsQgCARQHEhi2xDq/pcNQxZh5Qd9lNQaEXXmSVXuvuF1z9ddsZmXgcsLZxym3WXTennlhdawyoyrvMvHO//bSAtGmqE+8KU2a/9pUIkI0Yh8Y4nsQ7ORebM8F+TjaPy+9m/RA8R7BAiqV590AAAAAElFTkSuQmCC) no-repeat scroll 95% center; + padding: 0; + background-color: #ffffff; + margin-top: 7px; + margin-bottom: 7px; + position: absolute; + top: 0; + left: 15%; + width: 75%; + box-sizing: border-box; } + @media only screen and (max-width: 767px) { + .search-form { + left: 0; + width: 80%; } } + +.search-form__input { + margin-right: 0; + padding-left: 10px; + width: 85%; + box-sizing: border-box; + height: 30px; } + @media only screen and (max-width: 767px) { + .search-form__input { + width: 80%; } } + .lt-ie9 .search-form__input { + height: 27px; + line-height: 27px; } + +.search-form__input--search { + background: none repeat scroll 0 0 transparent; + border: none; + color: #666; } + +.search-form__input--submit { + text-align: center; + position: absolute; + top: 0; + left: 92%; + width: 3.5em; + margin: 7px 0 3px; + padding: 0; + box-sizing: border-box; + background-color: #4693ea; } + .search-form__input--submit:hover, .search-form__input--submit:active, .search-form__input--submit:focus { + background: #2f86e7; } + @media only screen and (max-width: 767px) { + .search-form__input--submit { + left: 80%; + width: 20%; } } + +/* +* Pattern name: Tabs +* Use: Related tabular content +* Note: N/A +-------------------------------------------------------------- */ +/* If boxsizing is needed then it must be scoped to tabs not affecting globally! +*, *:before, *:after { + box-sizing: border-box; +} */ +/*body { + padding: 1rem; + font-family: "Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; + color: #333; +}*/ +.no-js .tabs__navigation ul { + border-bottom: 4px solid #8c8279; + list-style: none; + padding: 0; + margin: 0; } +.no-js .tabs__navigation li { + display: inline-block; + margin-bottom: 8px; + padding: 3px 5px; + vertical-align: bottom; } +.no-js .tabs__navigation a { + color: #333; + display: block; + text-decoration: none; } + .no-js .tabs__navigation a.active { + position: relative; + text-decoration: underline; + z-index: 5150; } + .no-js .tabs__navigation a:hover { + background-color: #f0f0f0; } + +.no-js .tabs__group { + margin-top: 1em; } +.no-js .tabs__item { + margin-bottom: 8px; + padding: 1rem; + border: 1px solid #8c8279; + border-top: 2px solid #8c8279; } + +.js .tabs__navigation ul { + list-style: none; + padding: 0; + margin: 0; } +.js .tabs__navigation li { + display: inline-block; + padding: 3pz 5px; + vertical-align: bottom; } + +.js .tabs__navigation li { + background-color: #fff; + display: inline-block; + padding: 0; + vertical-align: bottom; } +.js .tabs__navigation a { + border: 2px solid #8c8279; + border-bottom: 0; + color: #333; + display: block; + padding: 4px 6px; + text-decoration: none; } + .js .tabs__navigation a.active { + font-weight: bold; + font-style: 15px; + position: relative; + z-index: 5150; } + .js .tabs__navigation a:hover { + background-color: #f0f0f0; } + +@media only screen and (max-width: 767px) { + .js .tabs__navigation ul { + border-bottom: 0; + overflow: hidden; + position: relative; } + .js .tabs__navigation ul:before { + background-color: #fff; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsAgMAAABj6rKSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAADFBMVEUAAAD///8AAAD////IZVEZAAAAAnRSTlMAAHaTzTgAAAABYktHRAH/Ai3eAAAACXBIWXMAAAsSAAALEgHS3X78AAAAG0lEQVQoz2NYhQAMtGAjg1AEoAkbGYz6iyw2AB4v6cHm3Lv8AAAAAElFTkSuQmCC"); + content: ''; + background-size: 100%; + width: 22px; + height: 22px; + position: absolute; + top: 0; + top: 8px; + border: 3px solid white; + box-sizing: border-box; + right: 16px; + z-index: 2; + pointer-events: none; } + .js .tabs__navigation ul.open a { + position: relative; + display: block; } + .js .tabs__navigation li { + display: block; } + .js .tabs__navigation a { + background: white; + position: absolute; + top: 0; + left: 0; } + .js .tabs__navigation a.active { + z-index: 1; } } + +@media only screen and (min-width: 768px) { + .js .tabs__navigation a.active { + background-color: #fff; + top: 1px; + padding-top: 5px; + z-index: 5150; } } + +.js .tabs__group > .tabs__item { + display: none; + padding: 0 1rem; + border: 2px solid #8c8279; + border-top: 1px solid #8c8279; } +.js .tabs__group > .active { + display: block; } + +.tab__content { + padding-top: 10px; } + +/* +* Pattern name: Tag +* Use: Small block of overlayed text over an image +* Note: Modifiers adjust positioning of tag (default: left) +-------------------------------------------------------------- */ +.tag { + position: absolute; + left: 0; + top: 0; + padding: 5px 15px; + color: #ffffff; + text-decoration: none; + background-color: #B5BD00; + z-index: 1; + font-size: 12px; + font-size: 1.2rem; } + +.tag--right { + left: auto; + right: 0; } + +.tag__heading { + color: white; } + .tag__heading a, .tag__heading a:hover { + color: white; } + +/* +* Pattern name: Vcard +* Use: Share contact information between devices +* Note: N/A +-------------------------------------------------------------- */ +.vcard { + margin-bottom: 1em; + width: 100%; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; + margin-left: 0; + padding-left: 0; + list-style: none; } + .vcard li { + margin-left: 0; + list-style-type: none; } + .vcard li:before { + background-color: transparent !important; } + +.vcard__item { + margin-bottom: 0.5em; + display: block; } + .vcard__item.fn { + font-weight: bold; + font-size: 0.9375em; } + +/* +* Pattern name: Video +* Use: Repsonsive video that maintains aspect ratio on browser resize +* Note: N/A +-------------------------------------------------------------- */ +.video-wrap { + position: relative; + padding-bottom: 56.25%; + padding-top: 30px; + height: 0; + overflow: hidden; } + .video-wrap iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } diff --git a/assets/css/screen.min.css b/assets/css/screen.min.css new file mode 100644 index 000000000..f968d572b --- /dev/null +++ b/assets/css/screen.min.css @@ -0,0 +1 @@ +.hidden{display:none!important;visibility:hidden}.visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.clear{clear:both}.clearfix,.dl-inline,.hero,.media{zoom:1}.clearfix:after,.clearfix:before,.dl-inline:after,.dl-inline:before,.hero:after,.hero:before,.media:after,.media:before{content:"";display:table}.clearfix:after,.dl-inline:after,.hero:after,.media:after{clear:both}.pull-left{float:left}.pull-right{float:right}.push-left{margin-left:1.5em}.push-right{margin-right:1.5em}.push-bottom{margin-bottom:1.5em}.push-top{margin-top:1.5em}.zero{margin:0}.zero-top{margin-top:0}.zero-bottom{margin-bottom:0}.zero-left{margin-left:0}.zero-right{margin-right:0}.zero-pad{padding:0}.zero-pad-top{padding-top:0}.zero-pad-bottom{padding-bottom:0}.zero-pad-left{padding-left:0}.zero-pad-right{padding-right:0}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize,time{text-transform:capitalize}.text-emphasise{font-style:italic}.no-respond{width:auto}.is-active{display:block}.is-open{max-height:40em;overflow:visible;opacity:1;transition:all .5s}.is-collapsed{max-height:0;overflow:hidden;opacity:0;transition:all .5s}.is-hidden{display:none}.is-disabled{opacity:.8;text-decoration:line-through;cursor:not-allowed}abbr,address,article,aside,audio,b,blockquote,body,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,p,pre,q,samp,section,small,span,strong,summary,table,tbody,td,tfoot,th,thead,time,tr,ul,var,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}html{overflow-x:hidden}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:0 0}mark{background-color:#ff9;color:#000;font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}img{width:auto;max-width:100%;height:auto}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;clear:both}input,select{vertical-align:middle}input{border-radius:0}audio{width:100%}.wrapper{margin:0 auto;max-width:1400px;width:90%;position:relative}.sidebar{position:absolute;margin-top:3em;left:0;width:19.8653%}@media only screen and (max-width:767px){.sidebar{width:100%;margin-left:auto;margin-right:auto;position:relative;margin-top:0}}.block{margin-bottom:1em}.block--col-1{display:block;float:left;margin-left:0;margin-right:-100%;width:31.30645%}@media only screen and (max-width:989px){.block--col-1{display:block;float:left;margin-left:0;margin-right:-100%;width:31.59722%}}@media only screen and (max-width:767px){.block--col-1{clear:both;display:block;float:left;margin-left:0;width:100%}}.block--col-2{display:block;float:left;margin-left:34.33675%;margin-right:-100%;width:31.28827%}@media only screen and (max-width:989px){.block--col-2{display:block;float:left;margin-left:34.20139%;margin-right:-100%;width:31.59722%}}@media only screen and (max-width:767px){.block--col-2{clear:both;display:block;float:left;margin-left:0;width:100%}}.block--col-3{display:block;float:left;margin-left:68.65532%;margin-right:-100%;width:31.27734%}@media only screen and (max-width:989px){.block--col-3{display:block;float:left;margin-left:68.40278%;margin-right:-100%;width:31.59722%}}@media only screen and (max-width:767px){.block--col-3{clear:both;display:block;float:left;margin-left:0;width:100%}}.layout-horizontal .sidebar,.layout-horizontal .sidebar .nav--left{display:none}@media only screen and (max-width:767px){.layout-horizontal .sidebar{display:block}}.layout-horizontal .nav--top{display:block}@media only screen and (max-width:767px){.layout-horizontal .nav--top{display:none}}.layout-horizontal .site-content__inner{padding-top:1em}.layout-horizontal .site-content__body{width:100%;margin-left:0;margin-right:0}.layout-horizontal--nav-2col .site-content__main{display:block;float:left;margin-left:0;margin-right:-100%;width:71.37803%}.layout-horizontal--nav-2col .site-content__sidebar{display:block;float:left;margin-left:74.41038%;margin-right:-100%;width:25.58962%}@media only screen and (max-width:989px){.layout-horizontal--nav-2col .site-content__main,.layout-horizontal--nav-2col .site-content__sidebar{clear:both;display:block;float:left;margin-left:0;width:100%}}.layout-horizontal--nav-1col .site-content__main{display:block;float:left;margin-left:0;margin-right:-100%;width:100%}.layout-horizontal--nav-1col .site-content__sidebar{display:none}@media only screen and (max-width:767px){.layout-vertical .nav--left{display:none}}.layout-vertical--nav-1col .site-content__main{display:block;float:left;margin-left:0;margin-right:-100%;width:100%}.layout-vertical--nav-2col .site-content__main{display:block;float:left;margin-left:0;margin-right:-100%;width:55.46605%}.layout-vertical--nav-2col .site-content__sidebar{display:block;float:left;margin-left:66.80507%;margin-right:-100%;width:33.19493%}@media only screen and (max-width:989px){.layout-vertical--nav-2col .site-content__main,.layout-vertical--nav-2col .site-content__sidebar{clear:both;display:block;float:left;margin-left:0;width:100%}}.layout-vertical--show-top-nav .nav--top{display:block}@media only screen and (max-width:767px){.layout-vertical--show-top-nav .nav--top{display:none}}.layout-vertical--show-top-nav .sidebar{margin-top:5em}@media only screen and (max-width:767px){.layout-vertical--show-top-nav .sidebar{margin-top:0}}html{font-size:62.5%}body{color:#333;background-color:#d9d9d1;overflow-x:hidden;line-height:1.7;-webkit-hyphens:none;-moz-hyphens:none;hyphens:none;-ms-hyphens:none;word-wrap:break-word;-webkit-text-size-adjust:none;font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:16px;font-size:1.6rem}.accordion,.advert,.box,.input-group,.island,.islet,.media,.menu-block,.page-head,.pill,.responsive-container,.video-wrap,address,blockquote,dl,fieldset,figure,hgroup,img,ol,p,pre,table,ul{margin-bottom:1.5em}h1,h2,h3{font-weight:300}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-size:80%;color:#6f6f6f;line-height:0}.as-h1,.hero__title--large,h1{font-size:36px;font-size:3.6rem}@media (max-width:767px){.as-h1,.hero__title--large,h1{font-size:30px;font-size:3rem}}.as-h2,h2{font-size:30px;font-size:3rem}@media (max-width:767px){.as-h2,h2{font-size:24px;font-size:2.4rem}}.as-h3,h3{font-size:24px;font-size:2.4rem}@media (max-width:767px){.as-h3,h3{font-size:18px;font-size:1.8rem}}.as-h4,h4{font-size:18px;font-size:1.8rem;text-transform:none;letter-spacing:0}@media (max-width:767px){.as-h4,h4{font-size:16px;font-size:1.6rem}}.as-h5,h5{font-size:14px;font-size:1.4rem;font-style:normal;text-transform:uppercase;font-weight:700}.as-h6,h6{font-size:12px;font-size:1.2rem;color:#999;margin:1em 0 0}a{color:#034da1;text-decoration:none}a:active,a:focus,a:hover{color:#034da1;text-decoration:underline}blockquote{font-style:italic;overflow:hidden}.small,small{font-weight:400;font-size:12px;font-size:1.2rem}.strong,strong{font-weight:600}.cite,cite,dfn,em{font-style:italic}.text-muted{color:#999}.text-success{color:#468847}.text-error{color:#b94a48}.mod{color:#ccc;clear:both;font-size:12px;font-size:1.2rem}ins{background-color:#F0F0F0;color:#000;text-decoration:none;padding:0 .125em}mark{padding:0 .125em}del{color:#666}.standfirst{font-size:20px}@media only screen and (max-width:767px){.standfirst{font-size:18px}}ol,ul{margin-top:0;margin-left:1.5em}ol ul,ul ul{margin-top:1em;margin-left:1em}.list-inline,.list-unstyled,.pills{margin-left:0;padding-left:0;list-style:none}.list-inline li,.list-unstyled li,.pills li{margin-left:0;list-style-type:none}.list-inline li:before,.list-unstyled li:before,.pills li:before{background-color:transparent!important}.list-inline,.pills{display:inline;margin-left:0}.list-inline>li,.pills>li{margin-right:10px;display:-moz-inline-stack;display:inline-block;zoom:1}.list-inline--divided li:after{content:"|";margin-left:10px}.list-inline--divided li:last-child:after{content:"";display:none}.list-divided li{margin-bottom:1.4rem;padding-bottom:1.4rem;border-bottom:1px solid #ccc}.list-divided li:last-child{border-bottom:0;padding-bottom:0;margin-bottom:0}.box ul li,.tabbed>div ul li{margin-left:1em}ol li{list-style-type:decimal;margin-bottom:1em}ol li ol{margin-top:1em}ol li li{margin-left:1.5em}dl{margin-top:1em}dt{font-weight:700}dd{padding-left:1.5em}.dl-inline dt{min-width:100px}.dl-inline dd,.dl-inline dt{float:left;margin-top:0;margin-right:.8rem;margin-bottom:.4rem}.dl-inline dd+dd,.dl-inline dd+dt{clear:left}.dl-inline dd+dd{float:none}.dl-inline dt{font-weight:700}pre{margin:1.5em 0;white-space:pre}code,pre,tt{font:1em consolas,"andale mono","lucida console",monospace}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}fieldset{padding:0;margin:0;border:0;min-width:0}legend{font-size:1.25em;margin-bottom:.5em}label{vertical-align:middle;max-width:100%;margin-bottom:.5em;zoom:1}.lt-ie8 label{vertical-align:auto}input{box-sizing:border-box;font-size:14px;font-size:1.4rem}input[type=submit]{cursor:pointer}input,input:focus,select,select:focus{outline:0}input:disabled{cursor:not-allowed}input[type=search],textarea{-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{display:none}input[type=checkbox]+label,input[type=radio]+label{display:inline;margin-left:5px}input[type=file]{display:block;padding:0}input[type=range]{display:block;width:100%}label{display:block;font-size:.9em;color:#494949;font-weight:500}fieldset abbr[title=Required]{border-bottom:0 none;color:#AF1616;font-size:1.25em;font-weight:400;line-height:.1}textarea{padding-left:2%;padding-right:2%;resize:vertical}select{width:100%;margin-bottom:.5em}form em{font-size:.8em;color:#848484}.form__control{padding:.5em 2%;vertical-align:middle;margin:0 0 .5em;border:1px solid #ccc;color:#333;width:100%;box-sizing:border-box;transition:border,.25s}.form__control:focus{transition:border,.25s;border:1px solid #666}.form__group{margin-bottom:1.5em;position:relative}.form__group>:last-child,.form__group>:last-child>:last-child,.form__group>:last-child>:last-child>:last-child{margin-bottom:0}.form__group--inline input,.form__group--prepend input{display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:63.85859%;box-sizing:border-box;padding-left:1.51515%;padding-right:1.51515%}.form__group--inline label{text-align:right;line-height:38px;display:block;float:left;margin-left:0;margin-right:-100%;width:33%;font-size:14px;font-size:1.4rem}.form__group--inline em{clear:left;display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:63.85859%}.form__group--prepend em{clear:both;display:block;float:left;margin-left:0;width:100%}.form__group--prepend span{color:#848484;text-align:right;line-height:26px;margin-top:5px;background-color:#eee;display:block;float:left;margin-left:0;margin-right:-100%;width:33%;left:-1.51515%;position:relative;padding-left:1.51515%;padding-right:1.51515%;font-size:14px;font-size:1.4rem}.form__group--options legend{display:block;font-size:.9em;color:#494949;font-weight:500}.form__group--options ul{margin-left:0;padding-left:0;list-style:none}.form__group--options ul li{margin-left:0;list-style-type:none}.form__group--options ul li:before{background-color:transparent!important}input.error{background-color:#fff;border:1px solid #DD6868}.error{color:#c60f13}.btn{background-color:#666;cursor:pointer;text-decoration:none;color:#fff;padding:.75em 1.5em;text-align:left;position:relative;border:1px solid rgba(0,0,0,.21);text-shadow:0 1px 0 rgba(0,0,0,.15);line-height:1;-webkit-appearance:none;display:-moz-inline-stack;display:inline-block;zoom:1;font-size:14px;font-size:1.4rem}.btn:active,.btn:focus,.btn:hover{background-color:#615957;color:#fff;text-decoration:none}.lt-ie9 .btn{border:0}.btn--small{padding:8px 20px;font-size:12px;font-size:1.2rem}.btn--gradient.disabled{border-bottom:1px solid rgba(0,0,0,.21);color:#999;text-shadow:none;background:#e0e0e0;background:linear-gradient(#eee 0,#e0e0e0 100%)}.btn--gradient.disabled :focus,.btn--gradient.disabled:hover{background:#eee;background:linear-gradient(#ededed 0,#eee 100%)}.btn--gradient.disabled:active{background:#ccc;background:linear-gradient(#e8e8e4 0,#ccc 100%)}button{background:#40403e;cursor:pointer;padding:.7rem;color:#fff;border:0;border-radius:.4em}button :focus,button:hover{background-color:#615957;border:0}button:active{background-color:#505050;border:0}.btn--gradient.darkgreen{background:#555025;background:linear-gradient(#4d4820 0,#555025 100%)}.btn--gradient.midgreen{background:#8f993e;background:linear-gradient(#9ca35f 0,#8f993e 100%)}.btn--gradient.brightgreen{background:#b5bd00;background:linear-gradient(#c9d10f 0,#b5bd00 100%)}.btn--gradient.lightgreen{background:#bbc592;background:linear-gradient(#c9d3a1 0,#bbc592 100%)}.btn--gradient.yellow{background:#f6be00;background:linear-gradient(#ffd030 0,#f6be00 100%)}.btn--gradient.darkred{background:#651d32;background:linear-gradient(#792f44 0,#651d32 100%)}.btn--gradient.midred{background:#93272c;background:linear-gradient(#a3383d 0,#93272c 100%)}.btn--gradient.brightred{background:#d50000;background:linear-gradient(#e21111 0,#d50000 100%)}.btn--gradient.lightred{background:#e03c31;background:linear-gradient(#ec6258 0,#e03c31 100%)}.btn--gradient.orange{background:#ea7600;background:linear-gradient(#ff9120 0,#ea7600 100%)}.btn--gradient.darkpurple{background:#4b384c;background:linear-gradient(#543e55 0,#4b384c 100%)}.btn--gradient.midpurple{background:#500778;background:linear-gradient(#641391 0,#500778 100%)}.btn--gradient.brightpink{background:#ac145a;background:linear-gradient(#c01d68 0,#ac145a 100%)}.btn--gradient.lightpurple{background:#c6b0bc;background:linear-gradient(#dbc0cf 0,#c6b0bc 100%)}.btn--gradient.warmgrey{background:#8c8279;background:linear-gradient(#9c9188 0,#8c8279 100%)}.btn--gradient.darkblue{background:#003d4c;background:linear-gradient(#074453 0,#003d4c 100%)}.btn--gradient.midblue{background:#002855;background:linear-gradient(#0f427a 0,#002855 100%)}.btn--gradient.brightblue{background:#0097a9;background:linear-gradient(#0cadc0 0,#0097a9 100%)}.btn--gradient.lightblue{background:#8db9ca;background:linear-gradient(#a5cddd 0,#8db9ca 100%)}.btn--gradient.stone{background:#d6d2c4;background:linear-gradient(#e0ddd1 0,#d6d2c4 100%)}.btn--gradient.darkbrown{background:#4e3629;background:linear-gradient(#5f4436 0,#4e3629 100%)}.btn--gradient.darkgreen:active{background:#555025}.btn--gradient.midgreen:active{background:#8f993e}.btn--gradient.brightgreen:active{background:#b5bd00}.btn--gradient.lightgreen:active{background:#bbc592}.btn--gradient.yellow:active{background:#f6be00}.btn--gradient.darkred:active{background:#651d32}.btn--gradient.midred:active{background:#93272c}.btn--gradient.brightred:active{background:#d50000}.btn--gradient.lightred:active{background:#e03c31}.btn--gradient.orange:active{background:#ea7600}.btn--gradient.darkpurple:active{background:#4b384c}.btn--gradient.midpurple:active{background:#500778}.btn--gradient.brightpink:active{background:#ac145a}.btn--gradient.lightpurple:active{background:#c6b0bc}.btn--gradient.warmgrey:active{background:#8c8279}.btn--gradient.darkblue:active{background:#003d4c}.btn--gradient.midblue:active{background:#002855}.btn--gradient.brightblue:active{background:#0097a9}.btn--gradient.lightblue:active{background:#8db9ca}.btn--gradient.stone:active{background:#d6d2c4}.btn--gradient.darkbrown:active{background:#4e3629}table{display:table;table-layout:auto;width:100%;border:1px solid #e6e6e6;border-collapse:collapse;clear:left;float:left;border-spacing:0;font-size:.8em}thead{display:table-header-group}tbody{display:table-row-group}tfoot{display:table-footer-group}tr{display:table-row}td,th{display:table-cell;text-align:left;width:auto;border-bottom:1px solid #e6e6e6;border-top:none;vertical-align:middle;padding:.25em 1em .25em 0}th{border-right:1px solid #e6e6e6;border-bottom-color:#e6e6e6;color:#333;font-weight:700;background-color:#eee}caption,td,th{padding:4px 10px 4px 5px}tr.even td{background:#e5ecf9}tfoot{font-style:italic}caption{caption-side:bottom;color:#666;font-size:.875em;line-height:1.4286;padding:.8571em 0 .2857em;text-align:left}td,td img{vertical-align:top}.table-responsive{width:100%;border-collapse:collapse;border-spacing:0}.table-responsive td,.table-responsive th{margin:0;vertical-align:top;border:1px solid #e6e6e6}.table-responsive th{text-align:left}@media (max-width:989px){.table-responsive{display:block;position:relative;width:100%;border-top:0}.table-responsive thead{display:block;float:left}.table-responsive thead tr{display:block}.table-responsive tbody{display:block;width:auto;position:relative;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;white-space:nowrap}.table-responsive tbody tr{vertical-align:top;border-right:1px solid #eee;display:-moz-inline-stack;display:inline-block;zoom:1}.table-responsive th{display:block;border-bottom:0}.table-responsive td{display:block;min-height:1.25em;border-left:0;border-right:0;border-bottom:0}}figcaption{color:#666;font-size:.9em}figcaption h4{margin-top:.2em}figure img{width:100%;margin-bottom:.5em}figure+figure,figure+p{margin-top:1em}.img-pull-left{margin:1em 3.0303% 1em 0;display:block;width:48.42929%;clear:none;float:left}.img-pull-right{margin:1em 0 1em 3.0303%;display:block;width:48.42929%;clear:none;float:right}.img-rounded{border-radius:50em}.img-sm{max-width:200px}.img-md{max-width:400px}.img-lg{max-width:767px}.img-xl{max-width:989px}.img-xxl{max-width:1400px}@media only screen and (max-width:767px){.img-lg,.img-xl,.img-xxl{width:100%}.decorative{display:none!important}.img-pull-left.large-image,.img-pull-left.xl-image,.img-pull-left.xxl-image,.img-pull-right.large-image,.img-pull-right.xl-image,.img-pull-right.xxl-image{width:100%}}@media (max-width:619px){.col{clear:both}}@media (max-width:619px){.col--1-2-sm{clear:both;display:block;float:left;margin-left:0;width:100%}}@media (max-width:619px){.col--1-sm{display:block;float:left;margin-left:0;margin-right:-100%;width:46.79688%}}@media (max-width:619px){.col--2-sm{display:block;float:left;margin-left:53.04688%;margin-right:-100%;width:46.79688%}}@media (min-width:768px){.col--1-md{display:block;float:left;margin-left:0;margin-right:-100%;width:31.59722%}}@media (min-width:768px){.col--2-md{display:block;float:left;margin-left:34.20139%;margin-right:-100%;width:31.59722%}}@media (min-width:768px){.col--3-md{display:block;float:left;margin-left:68.40278%;margin-right:-100%;width:31.59722%}}@media (min-width:768px){.col--1-2-md{display:block;float:left;margin-left:0;margin-right:-100%;width:65.79861%}}@media (min-width:768px){.col--2-3-md{display:block;float:left;margin-left:34.20139%;margin-right:-100%;width:65.79861%}}@media (min-width:768px){.col--1-3-md{clear:both;display:block;float:left;margin-left:0;width:100%}}@media (min-width:990px){.col--1-lg-layout{display:block;float:left;margin-left:0;margin-right:-100%;width:19.86532%}}@media (min-width:990px){.col--1-2-lg-layout{display:block;float:left;margin-left:0;margin-right:-100%;width:31.30645%}}@media (min-width:990px){.col--1-3-lg-layout{display:block;float:left;margin-left:0;margin-right:-100%;width:65.62502%}}@media (min-width:990px){.col--1-4-lg-layout{display:block;float:left;margin-left:0;margin-right:-100%;width:71.32997%}}@media (min-width:990px){.col--1-5-lg-layout{clear:both;display:block;float:left;margin-left:0;width:100%}}@media (min-width:990px){.col--2-lg-layout{display:block;float:left;margin-left:22.89562%;margin-right:-100%;width:8.41083%}}@media (min-width:990px){.col--2-3-lg-layout{display:block;float:left;margin-left:22.89562%;margin-right:-100%;width:42.7294%}}@media (min-width:990px){.col--2-4-lg-layout{display:block;float:left;margin-left:22.89562%;margin-right:-100%;width:48.43434%}}@media (min-width:990px){.col--2-5-lg-layout{display:block;float:left;margin-left:22.89562%;margin-right:-100%;width:77.03704%}}@media (min-width:990px){.col--3-lg-layout{display:block;float:left;margin-left:34.33675%;margin-right:-100%;width:31.28827%}}@media (min-width:990px){.col--3-4-lg-layout{display:block;float:left;margin-left:34.33675%;margin-right:-100%;width:36.99322%}}@media (min-width:990px){.col--3-5-lg-layout{display:block;float:left;margin-left:34.33675%;margin-right:-100%;width:65.59591%}}@media (min-width:990px){.col--4-5-lg-layout{display:block;float:left;margin-left:68.65532%;margin-right:-100%;width:31.27734%}}@media (min-width:990px){.col--5-lg-layout{display:block;float:left;margin-left:74.36027%;margin-right:-100%;width:25.57239%}}@media (min-width:990px){.col--1-lg-content{display:block;float:left;margin-left:0;margin-right:-100%;width:33%}}@media (min-width:990px){.col--2-lg-content{display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:12.39899%}}@media (min-width:990px){.col--3-lg-content{display:block;float:left;margin-left:51.4596%;margin-right:-100%;width:12.39899%}}@media (min-width:990px){.col--4-lg-content{display:block;float:left;margin-left:66.88889%;margin-right:-100%;width:33%}}@media (min-width:990px){.col--1-2-lg-content{display:block;float:left;margin-left:0;margin-right:-100%;width:48.42929%}}@media (min-width:990px){.col--2-3-lg-content{display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:27.82828%}}@media (min-width:990px){.col--2-4-lg-content{display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:63.85859%}}@media (min-width:990px){.col--3-4-lg-content{display:block;float:left;margin-left:51.4596%;margin-right:-100%;width:48.42929%}}@media (min-width:990px){.col--1-4-lg-content{clear:both;display:block;float:left;margin-left:0;width:100%}}@media (max-width:767px){.col--float{clear:none;float:left;margin-left:0;margin-right:6.25%}}@media (min-width:768px) and (max-width:989px){.col--float{clear:none;float:left;margin-left:0;margin-right:2.60417%}}@media (min-width:990px){.col--float{clear:none;float:left;margin-left:0;margin-right:3.0303%}}@media (min-width:990px){.col--float-content{clear:none;float:left;margin-left:0;margin-right:3.0303%}}@media (max-width:619px){.col--sm-last{margin-right:0}}@media (min-width:768px) and (max-width:989px){.col--md-last{margin-right:0}}@media (min-width:990px){.col--lg-last{margin-right:0}}.accordion__description,.accordion__title{padding:10px;border-top:1px solid #ccc;margin-top:0}.accordion__description:last-of-type,.accordion__title:last-of-type{border-bottom:1px solid #ccc}.accordion__description a,.accordion__title a{font-weight:700}.accordion__title a{color:#000;display:block}.accordion__description{border-top:0;overflow:hidden}.accordion__description>:last-child,.accordion__description>:last-child>:last-child,.accordion__description>:last-child>:last-child>:last-child{margin-bottom:0}.accordion__description:last-of-type{position:relative;top:-1px}.announcement-bar{background-color:#ff5a5f}.announcement-bar p{max-width:1400px;width:90%;margin:0 auto;color:#fff;position:relative}.announcement-bar a{color:#fff;text-decoration:underline}.announcement-bar .announcement-bar--close{position:absolute;padding:0 6px;top:.5em;right:5%;border:1px solid #fff;border-radius:100%;font-size:10px;text-decoration:none}.announcement-bar .announcement-bar--close:hover{text-decoration:none}@media only screen and (max-width:767px){.announcement-bar p{width:70%;margin:0;padding:.5em}.announcement-bar .announcement-bar--close{position:absolute;padding:0 12px;right:5%;border:1px solid #fff;border-radius:100%;font-size:20px;text-decoration:none;top:1em}.announcement-bar .announcement-bar--close:hover{text-decoration:none}}.advert{position:relative;box-sizing:border-box}@media (max-width:767px){.advert--restrict{max-width:25em}}.advert-divide{clear:both;text-align:center;margin-bottom:1.5em;font-weight:400}.advert__content{padding:1.5em;position:relative;background:#fff;box-shadow:0 1px 0 0 rgba(0,0,0,.1)}.advert__content>:last-child,.advert__content>:last-child>:last-child,.advert__content>:last-child>:last-child>:last-child{margin-bottom:0}.advert__content--push{padding-top:5em}.advert__list{margin-left:0;padding-left:0;list-style:none;color:#999}.advert__list li{margin-left:0;list-style-type:none}.advert__list li:before{background-color:transparent!important}.advert__img{min-height:10em;background-size:cover;background-position:center center;position:relative}@media (max-width:767px){.advert__img{min-height:12em}}.advert__overlay{position:absolute;display:block;background-color:rgba(0,0,0,.25);z-index:9999;top:0;right:0;width:100%;height:100%}.advert__header{margin-bottom:0;padding:1.5em;color:#fff;min-height:5em;text-shadow:0 1px 1px rgba(0,0,0,.2);background-color:#b5bd00;background-color:rgba(181,189,0,.8)}.advert__header a{color:#fff}.advert__header-overlay{margin-bottom:0;font-size:2.5em;color:#fff;left:.5em;top:.25em;position:absolute;text-shadow:0 1px 1px rgba(0,0,0,.6);z-index:1}.box{border-top:5px solid #000;background:#fafafa;overflow:hidden;padding:1em 3.0303%;box-shadow:0 1px 0 0 rgba(0,0,0,.1);position:relative;box-sizing:border-box}.box>:last-child,.box>:last-child>:last-child,.box>:last-child>:last-child>:last-child{margin-bottom:0}.box--tagged{padding-top:2em}.box--small{margin-bottom:32/14em;padding:20/14em;font-size:14px;font-size:1.4rem}.box--rounded{border-radius:4px}.box--blank{border-top:none}.blocked-link{padding:5px;color:#fff;text-decoration:none;background-color:#B5BD00;font-size:14px;font-size:1.4rem}.blocked-link:active,.blocked-link:focus,.blocked-link:hover{color:#fff;background:#8f993e;text-decoration:none}.blurb{position:relative;padding-bottom:1em;overflow:hidden}.blurb__title{font-size:16px;margin-top:0;font-weight:700}.blurb__img{margin-bottom:.5em;width:100%}.blurb--wide .blurb__img{display:block;float:left;margin-left:0;margin-right:-100%;width:33%}@media only screen and (max-width:767px){.blurb--wide .blurb__img{clear:both;display:block;float:left;margin-left:0;width:100%}}.blurb__video{margin-bottom:1em}.blurb__body>:last-child,.blurb__body>:last-child>:last-child,.blurb__body>:last-child>:last-child>:last-child{margin-bottom:0}.blurb--wide .blurb__body{top:-4px;position:relative;display:block;float:left;margin-left:36.0303%;margin-right:-100%;width:63.85859%}@media only screen and (max-width:767px){.blurb--wide .blurb__body{clear:both;display:block;float:left;margin-left:0;width:100%}}.brand{position:absolute;z-index:1;overflow:visible;width:100%;max-width:1470px;left:0}@media only screen and (max-width:767px){.brand{position:static;padding-left:0}.brand p{left:0;top:50%}}.brand__link{position:absolute;height:50%;width:18%;right:0;display:block;bottom:0}.lt-ie9 .brand__link{display:none}.brand__heading{position:absolute;left:35px;top:25%;color:#fff;text-transform:uppercase;margin-bottom:0;font-weight:400;font-size:14px;font-size:1.4rem}@media only screen and (max-width:767px){.brand__heading{position:relative;left:0;width:100%;box-sizing:border-box}}.brand__logo{display:block;margin-bottom:0;width:100%}@media only screen and (max-width:767px){.brand__logo{display:none}}.breadcrumb{display:block;overflow:hidden;list-style:none;border:0;padding-bottom:0;background:#fff;border-style:solid;border-width:0 0 1px;border-color:#ccc;margin-bottom:1.5em}.breadcrumb--nav{margin-bottom:0;border-bottom:0;position:relative;left:-35px;padding-left:35px;padding-right:35px;width:100%}.breadcrumb--nav .breadcrumb__list{line-height:4}.breadcrumb__list{margin-bottom:0;vertical-align:top;margin-left:0;padding-left:0;list-style:none;display:-moz-inline-stack;display:inline-block;zoom:1}.breadcrumb__list li{margin-left:0;list-style-type:none}.breadcrumb__list li:before{background-color:transparent!important}.breadcrumb__item{margin:0;float:left;text-transform:none;font-size:14px;font-size:1.4rem}.breadcrumb__item:after{content:"/";color:#aaa;margin:0 .75em;position:relative;top:1px}.breadcrumb__item:last-child:after{content:" ";margin:0}.breadcrumb__item--current>a{text-decoration:none;cursor:default;color:#333}.code{font:400 1em/1.5 source-code-pro,monospace;margin:1em 0 1.5em 2.5em}.code li{background:#F6F6F6;border-top:1px solid #FFF;list-style:decimal-leading-zero;padding:0 .5em}.code li .tab1{padding-left:4ex}.collapse__header{clear:both}@media (max-width:766px){.collapse__header{cursor:pointer;border-bottom:1px solid #ccc;padding-bottom:8px}}@media (max-width:766px){.collapse__content{display:none;border-top:0}}@media (max-width:767px){.collapse__header--active{background:url(//cdn.ucl.ac.uk/indigo/images/close.png) right 50% no-repeat;padding-right:20px;border-bottom:none}.collapse__header--active:active,.collapse__header--active:focus,.collapse__header--active:hover{background:url(//cdn.ucl.ac.uk/indigo/images/close.png) right 50% no-repeat}}@media (max-width:767px){.collapse__header--inactive{background-repeat:no-repeat;background-position:right 50%;padding-right:20px}.collapse__header--inactive:active,.collapse__header--inactive:focus,.collapse__header--inactive:hover{background-repeat:no-repeat;background-position:right 50%}}.commentlist{list-style:none;margin:0}.commentlist li{list-style:none;margin-left:0}.commentlist li.odd{background:#f6f6f6}.commentlist li.even{background:#fff}.commentlist li.parent{border-left:5px solid #111}.commentlist li.comment{padding:10px}.commentlist li.comment .reply{font-size:11px}.commentlist li.comment .vcard{margin-bottom:0}.commentlist li.comment .vcard .fn{font-style:normal}.commentlist li.comment .avatar{width:auto;float:left;margin:0 1em 1em 0}.commentlist li.comment .comment-meta{font-size:12px;font-size:1.2rem}.commentlist li.comment .comment-meta a{color:#ccc}.commentlist li.comment .comment-meta p{clear:left}.commentlist li.comment .children{list-style:none;margin:10px 0 0}.commentlist li.comment .children .depth-2{border-left:5px solid #555;margin:0 0 10px 10px}.commentlist li.comment .children .depth-3{border-left:5px solid #999;margin:0 0 10px 10px}.commentlist li.comment .children .depth-4{border-left:5px solid #bbb;margin:0 0 10px 10px}.btn.cta{background-color:#034da1;text-align:center;font-weight:700;font-size:1.2em;border:none;border-radius:0;box-sizing:border-box}.btn.cta:hover{background-color:#002854}.btn.cta{width:100%}.btn.cta-half{width:50%}@media only screen and (max-width:767px){.btn.cta-half{min-width:330px}}.btn.cta-threequarters{width:75%}@media only screen and (max-width:767px){.btn.cta-threequarters{min-width:330px}}.btn.cta-third{width:33%}@media only screen and (max-width:767px){.btn.cta-third{min-width:330px}}.btn.cta-quarter{width:25%}@media only screen and (max-width:767px){.btn.cta-quarter{min-width:330px}}.btn.cta-sixth{width:16%}@media only screen and (max-width:767px){.btn.cta-sixth{min-width:330px}}.divider{border-color:#ddd}.divider--top{margin-top:1.5em;padding-top:1.5em;border-top-width:1px;border-top-style:solid}.divider--bottom{margin-bottom:1.5em;padding-bottom:1.5em;border-bottom-width:1px;border-bottom-style:solid}.footer__inner{background-color:#40403e;padding:40px 35px;margin-left:-35px;margin-right:-35px;color:#fff;clear:left}.footer__inner h2{font-size:1.6rem;text-transform:none}.footer__inner a{color:#eee}.footer__inner a:active,.footer__inner a:focus,.footer__inner a:hover{color:#fff}.footer__list{margin-left:0;padding-left:0;list-style:none}.footer__list li{margin-left:0;list-style-type:none}.footer__list li:before{background-color:transparent!important}.footer__item{padding-bottom:4px;padding-top:4px;display:block;margin-bottom:0;font-size:14px;font-size:1.4rem}.footer__links{margin-top:.5em;font-size:14px;font-size:1.4rem}@media (max-width:989px){.footer__links{font-size:12px;font-size:1.2rem}}.flag{display:table;width:auto}.flag__aside,.flag__body{display:table-cell;vertical-align:middle}.flag--top .flag__aside,.flag--top .flag__body{vertical-align:top}.flag--bottom .flag__aside,.flag--bottom .flag__body{vertical-align:bottom}.flag__aside{padding-right:1.5em}.flag__aside>img{display:block;margin:0}.lt-ie9 .flag__aside>img{width:100%}.flag--rev .flag__aside{padding-right:0;padding-left:1.5em}.flag__body{width:auto}.flag__body>:last-child,.flag__body>:last-child>:last-child,.flag__body>:last-child>:last-child>:last-child{margin-bottom:0}.header{position:relative;z-index:10}@media only screen and (max-width:767px){.header--desktop{background-color:#5f5f5f;position:absolute;width:100%;height:100%;overflow:auto;overflow-x:hidden;transition:all,.5s;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%)}.no-csstransforms .header--desktop{left:-100%}.mobile-open .header--desktop{transition:all,.5s;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}.no-csstransforms .mobile-open .header--desktop{left:0}.header--desktop a{color:#fff;padding:.5em 0;display:block;clear:both;font-size:16px;font-size:1.6rem}}.header--mobile{display:none;width:100%;padding:20% 35px 0;background-size:auto 100%;background-position:106% bottom;background-image:url(//cdn.ucl.ac.uk/indigo/images/ucl-logo.svg);background-repeat:repeat-x;position:relative;left:-35px;background-color:#ff5a5f;border-bottom:.5em solid #ff5a5f}@media only screen and (max-width:767px){.header--mobile{display:block}}.header--mobile.no-svg{background-image:url(//cdn.ucl.ac.uk/indigo/images/ucl-logo-cropped-white.png);background-repeat:no-repeat;background-position:right bottom;background-color:#000;background-size:90%}@media only screen and (max-width:767px){.header__open{width:25px;top:35%;left:35px;position:absolute;z-index:100}}.header__open>img{margin-bottom:0}.header__link{position:absolute;height:100%;right:10%;width:30%;top:0}.header__close{display:none}@media only screen and (max-width:767px){.header__close{display:block;position:absolute;left:5%;top:5px;padding:0;z-index:100;width:50%}.header__close img{vertical-align:middle;display:-moz-inline-stack;display:inline-block;zoom:1;width:30px;position:relative;margin:-5px -3px 0 -8px}}.hero{padding:2em 35px;background-color:#fff;background-size:cover;background-position:right center;position:relative;left:-35px;width:100%}@media (max-width:989px){.hero{padding-top:1.5em;padding-bottom:1.5em;background-position:-9999px;background-repeat:no-repeat}}@media (max-width:479px){.hero{padding-top:1em;padding-bottom:1em}}@media (max-width:989px){.hero .hero__body--background{padding:0}}.hero--home{padding-left:35px;padding-right:35px;margin-left:-35px;margin-right:-35px;padding-bottom:10em;border:0;background-image:url(/assets/images/hero-image-taught-desktop.jpg);background-position:90% center;background-size:cover;background-repeat:no-repeat}@media (max-width:1179px){.hero--home{background-position:80% center}}@media (max-width:767px){.hero--home{background-image:none}}@media (min-width:768px) and (max-width:1179px){.hero--home{padding-bottom:14em}}@media (max-width:767px){.hero--home{padding-bottom:1.5em}}@media (min-width:768px) and (max-width:989px){.hero--home .hero__body--background{padding:1.5em}}.hero__body{box-sizing:border-box}.hero__body>:last-child{margin-bottom:0}@media (min-width:768px){.hero__body--background{padding:2em;background-color:rgba(255,255,255,.6)}}@media (max-width:767px){.hero__body--background{padding:0}}.lt-ie9 .hero__body--background{background-image:url(//cdn.ucl.ac.uk/indigo/images/hero-bg-ie.png)}.hero__title{margin-bottom:0;padding-bottom:0}@media (max-width:767px){.hero__title{font-size:1.5em}}@media (max-width:1179px){.hero__title--large{font-size:2.25em}}@media (max-width:989px){.hero__title--large{font-size:2em}}@media (max-width:767px){.hero__title--large{font-size:1.5em}}.hero__blurb{margin-top:1em}@media (max-width:989px){.hero__blurb{display:none}}.hero__detail{margin-top:.5em}@media (max-width:767px){.hero__detail{font-size:1.25em}}@media (max-width:767px){.hero__sub{display:none}}.hero__tags{margin-top:1em}.hero__dropdown{margin-bottom:2em}.hero__dropdown h2{margin-bottom:16px}.hero__dropdown__detail{display:inline;float:left;padding:6px 10px 6px 0;font-size:14px;font-size:1.4rem}.hero__sidebar{margin-bottom:2em;display:block;float:left;margin-left:74.36027%;margin-right:-100%;width:25.57239%}@media (min-width:1050px){.hero__sidebar--small{width:20%;float:right;margin-left:0;margin-right:0}}.input-group{display:table;width:100%}.input-group__item{display:table-cell;width:100%;vertical-align:top}.input-group__input{height:38px;height:3.8rem;vertical-align:top;padding-top:0;padding-bottom:0;margin-bottom:0}.input-group__input--btn{white-space:nowrap;box-sizing:border-box;line-height:normal;vertical-align:top}.lightbox{position:fixed;top:0;left:0;z-index:20;width:100%;height:100%;text-align:center;padding:0 1em;background:#000;background:rgba(0,0,0,.45);-ms-box-sizing:border-box;box-sizing:border-box}.lightbox__item{background-color:#000;background:rgba(0,0,0,.65);padding:2em;float:left;left:50%;border-radius:3px;box-sizing:border-box;position:relative;top:0;-webkit-transform:translate(-50%);-ms-transform:translate(-50%);transform:translate(-50%);z-index:5150}.lightbox__item img{max-height:500px}.org-unit-logo{display:none}@media only screen and (min-width:768px){.org-unit-logo{display:block;margin-bottom:1em;width:100%}.org-unit-logo img{width:100%}}.map{display:block;width:100%;padding:1em 0;box-sizing:border-box}.static-img{height:auto;max-width:100%;width:100%}.map-link{display:block;font:0/0 a}.static-img{display:block}.map-container{width:100%;margin:0 auto;height:0;padding-top:38%;position:relative;display:none}.map-container iframe{width:100%;height:100%;position:absolute;top:0;right:0;left:0;bottom:0}@media only screen and (min-width:768px){.map-container{display:block}.static-img{display:none}}.media__aside{float:left;margin-right:1em;min-height:1px}@media (min-width:768px){.media__aside{margin-right:1.5em}}@media (max-width:767px){.media__aside{display:none}}.media__aside--constrain{max-width:7em}.media__aside img,img.media__aside{width:auto!important;height:auto!important}.media__body,.media__body--wide{overflow:hidden;margin:0}@media (max-width:767px){.media__body,.media__body--wide{font-size:14px;font-size:1.4rem}}.media__body--wide{max-width:60em}.menu-block{padding:0;border:1px solid #dfdfdf;margin-left:0;list-style:none}.menu-block li{margin-left:0;list-style-type:none}.menu-block li:before{background-color:transparent!important}.menu-block--no-keyline{border:0}.menu-block__list{margin-bottom:0;margin-top:0;margin-left:0;padding-left:0;list-style:none}.menu-block__list li{margin-left:0;list-style-type:none}.menu-block__list li:before{background-color:transparent!important}.menu-block__item{position:relative;padding:0;background-color:#EEE;margin-bottom:0;line-height:3;border-bottom:1px solid #dfdfdf}.menu-block__item:last-child{border-bottom:0}.menu-block__item--heading:active,.menu-block__item--heading:focus,.menu-block__item--heading:hover{text-decoration:none}.menu-block__link{padding-left:1em;display:block;color:#333;transition:all .5s;padding-right:2.5em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.menu-block__link:active,.menu-block__link:focus,.menu-block__link:hover{background-color:#dfdfdf;transition:all .5s;color:#333}.menu-block__link.is-current{margin-bottom:-1px;background-color:#000;color:#fff}.is-active .menu-block__link{border-left:.4em solid #333}.message{text-decoration:none;border:1px solid #DDD;clear:left;color:#666;display:block;font-size:16px;font-weight:700;width:100%;padding:10px 3.0303%;margin-bottom:1em;box-sizing:border-box}.message>:last-child,.message>:last-child>:last-child,.message>:last-child>:last-child>:last-child{margin-bottom:0}.message--success{background-color:#F7FFF2;color:#7FB861;border:1px solid #7FB861}.message--warning{color:#DD6868;border:1px solid #DD6868;font-weight:700}html.backgroundsize .message{background:10px center/auto 80% no-repeat #FFF}html.backgroundsize .message--success{background-image:url(//cdn.ucl.ac.uk/indigo/images/success.png)}html.backgroundsize .message--warning{background-image:url(//cdn.ucl.ac.uk/indigo/images/alert.png)}.masthead__search ul{margin-left:0;margin-top:10px}.masthead__search ul.profile-details li{display:block}.masthead__search li.tt-suggestion{display:block;overflow:hidden;margin:0;padding:.5em 2%}.masthead__search li a{color:#fff!important}.masthead__search form{height:30px;width:85%;left:0;top:0}.masthead__search form .fa{float:left;margin-right:10px;top:3px;position:relative}.masthead__search form ul.profile-details{margin:0;width:100%}.masthead__search form h2{margin-top:0;font-size:18px!important}@media screen and (max-width:1200px){.masthead__search form h2{height:32px}}.masthead__search .AC-result .email:before,.masthead__search .AC-result .tel:before,.masthead__search .sprite{background-image:url(/indigo/images/search-sprite.png);margin-left:10px}.masthead__search .search{left:88%;width:12%;border-radius:4px}.masthead__search .tt-dropdown-menu{background-color:#fff;border:1px solid #aaa;border-radius:4px;box-shadow:2px 2px 2px rgba(51,51,51,.3);left:5%;position:absolute;top:38px;width:90%;z-index:5050}.masthead__search .tt-dropdown-menu ul{margin-bottom:10px}.masthead__search .tt-dropdown-menu li{color:#333;font-size:12px;list-style:none;border-bottom:1px solid #eee;height:64px;max-height:64px;width:96%;text-align:left}.masthead__search .tt-dropdown-menu li h3{margin-top:0}.masthead__search .tt-dropdown-menu li p{font-size:12px;line-height:1.4;margin-bottom:0}.masthead__search .tt-dropdown-menu li p a{text-decoration:none;font-size:12px}.masthead__search .tt-dropdown-menu li a{color:#4693ea!important;display:block;font-size:12px;font-weight:700;margin-bottom:8px}.masthead__search .tt-dropdown-menu .no-results{color:#4693ea!important;display:block;font-size:12px;font-weight:700;text-align:center}@media (max-width:1023px){.masthead__search .tt-dropdown-menu{display:none!important}}@media (max-width:1220px){.masthead__search .tt-dropdown-menu .AC-result--directory img{display:none!important}}.masthead__search .AC-result{display:table-cell;height:100%;margin-left:5px;padding:0;border-left:1px solid #AAA;width:25%}.masthead__search .AC-result:first-child{margin-left:0;border-left:1px solid transparent}.masthead__search .AC-result h2{color:#000;font-weight:700;font-size:1em;margin-bottom:4px;padding:.5em}.masthead__search .AC-result a{color:#4693ea!important;display:block;font-size:.8em;font-size:12px;font-weight:700}.masthead__search .AC-result a.show-all{text-align:center;margin-bottom:.5em}.masthead__search .AC-result ul li:hover{background-color:#e6f1fc;font-size:auto}.masthead__search .AC-result ul li:hover li:hover{background-color:transparent}.masthead__search .AC-result ul li li{height:auto}.masthead__search .AC-result img{max-height:60px;max-width:60px}.masthead__search .AC-result .email{clear:left;float:left;padding-left:30px;line-height:24px}.masthead__search .AC-result .email:before{content:'';position:absolute;left:-35px;width:18px;height:12px;top:6px}.masthead__search .AC-result .email a{color:#4c98e5;font-size:.8em;font-size:12px}.masthead__search .AC-result .tel{font-size:12px;line-height:20px;padding-left:30px;position:relative}.masthead__search .AC-result .tel:before{content:'';position:absolute;left:-35px;width:20px;height:18px;top:3px;background-position:0 -31px}.masthead__search .AC-result .tel span{font-weight:700;display:inline}.masthead__search .AC-result .tel a{display:inline}.masthead__search .AC-result .email,.masthead__search .AC-result .tel--external{position:relative;left:2em}.masthead__search .AC-results{box-sizing:border-box;display:table;height:100%;padding:16px 0;table-layout:fixed;width:100%}.masthead__search .AC-result--directory .AC-details__image{display:table-cell;padding-top:5px}.masthead__search .AC-result--directory .AC-details__image img{margin-right:20px;display:block;padding-left:16px}.masthead__search .AC-result--directory img{float:left;position:relative;top:.5em}.masthead__search .AC-result--directory .AC-details{display:block;float:left;width:77%}.masthead__search .AC-result--directory .AC-details .profile-details li{border-bottom:0;margin-bottom:0;padding-left:0;font-size:12px}.masthead__search .AC-result--directory .AC-details .profile-details li a{font-size:12px}.masthead__search .twitter-typeahead{width:100%}.masthead__search #query{outline:0;border:none}.masthead{background-color:#444;color:#fff;height:45px;font-size:12px;font-size:1.2rem}@media only screen and (max-width:767px){.masthead{height:auto;border-bottom:1px solid #ccc}.masthead .wrapper{background-color:#444}}.masthead img{width:auto}.masthead__search{position:relative;padding:6px 0 3px;box-sizing:border-box;float:right;width:42%}@media only screen and (max-width:767px){.masthead__search{float:none;width:100%;padding:0;margin-top:3.5em;height:4em;margin-left:auto;margin-right:auto}}.masthead__search label{margin-top:9px}.masthead__list{float:left;width:55%;font-size:16px;display:block;margin-left:0}@media only screen and (max-width:767px){.masthead__list{width:100%;margin-left:auto;margin-right:auto;float:none;margin-bottom:1em}}@media (max-width:989px){.masthead__list{font-size:14px;font-size:1.4rem}}.masthead__item{padding-right:1em;height:45px;display:table-cell;text-align:center;vertical-align:middle}@media only screen and (max-width:767px){.masthead__item{display:block;text-align:left}}@media (max-width:989px){.masthead__item{padding-right:.75em}}.lt-ie9 .masthead__item{margin-top:8px;display:-moz-inline-stack;display:inline-block;zoom:1}.masthead__link{font-weight:400;color:#fff;text-decoration:none}.masthead__link:active,.masthead__link:focus,.masthead__link:hover{color:#fff}.nav ul{margin-left:0}.nav--left li{margin-bottom:.5em}.nav--left li ul{margin:10px 5%;font-size:14px;font-size:1.4rem}.nav--top{display:none;background-color:#fff;padding:1em 35px;overflow:hidden;position:relative;left:-35px;width:100%}.nav--top ul li{display:block;float:left;padding-right:1em;margin-right:1em;border-right:1px solid #ccc}.nav--top ul li a{text-decoration:none;cursor:pointer}.nav--top ul li:last-child{border-right:0}.nav--top.nav--sticky-active{position:fixed;top:0;width:100%;background:0 0;padding:0;left:0}.nav--top.nav--sticky-active ul{display:block;background-color:#ff5a5f;padding:.5em 35px;max-width:1400px;margin:auto;overflow:hidden}.nav--top.nav--sticky-active a{color:#fff}.nav--mobile{display:none}@media only screen and (max-width:767px){.nav--mobile{display:block}.nav--mobile ul:nth-child(n+2){padding-left:1em}}.nav--subnav{box-sizing:border-box;background-color:transparent;color:#fff;width:100%;position:relative;overflow:hidden}.nav--subnav a{text-decoration:none;vertical-align:middle}.subnav__list{margin-top:0;position:absolute;top:0;width:100%;background:0 0;z-index:1;transition:all .35s cubic-bezier(0.645,.045,.355,1)}.subnav__list ul{right:200%;left:100%;box-sizing:border-box}.subnav__list ul li{box-sizing:border-box}.subnav__list .nav--active{left:0;right:0}.subnav__list .nav--active>li>a{visibility:visible}.subnav__list.nav--hidden>li>a{visibility:hidden}.subnav__item.parent>a:after{content:" >";position:absolute;right:0}.subnav__item.back a{border-bottom:1px transparent solid}.subnav__item.back a:before{content:"< ";position:relative;top:-2px}.subnav__item.top-level a{border-bottom:1px transparent solid}.subnav__item.top-level a:before{content:"< ";position:relative;top:-2px}.subnav__item a{display:block;position:relative}.nav--left.nav--subnav{position:relative}.nav--left.nav--subnav ul li{margin:0 0 .5em;line-height:1.7;font-size:16px;font-size:1.6rem}.nav--left.nav--subnav li ul{margin:0}.nav--mobile{margin-top:1em}.nav--mobile .subnav__list{padding:0}@media only screen and (max-width:767px){.nav--mobile .subnav__list ul:nth-child(n+2){padding-left:0}}.nav--mobile.nav--subnav ul li a{color:#fff}.nav--mobile .subnav__item a:hover,.nav--mobile .subnav__item.back a,.nav--mobile .subnav__item.top-level a{color:#ccc}.owl-theme .owl-controls{margin-top:10px;text-align:center;-webkit-tap-highlight-color:transparent}.owl-theme .owl-controls .owl-nav [class*=owl-]{color:#fff;font-size:14px;margin:5px;padding:4px 7px;background:#0097a9;display:inline-block;cursor:pointer;border-radius:3px}.owl-theme .owl-controls .owl-nav [class*=owl-]:hover{background:rgba(0,151,169,.7);color:#fff;text-decoration:none}.owl-theme .owl-controls .owl-nav .disabled{opacity:.5;cursor:default}.owl-theme .owl-dots .owl-dot{display:inline-block;zoom:1}.owl-theme .owl-dots .owl-dot span{width:10px;height:10px;margin:5px 7px;background:#0097a9;display:block;-webkit-backface-visibility:visible;transition:opacity 200ms ease;border-radius:30px}.owl-theme .owl-dots .owl-dot.active span,.owl-theme .owl-dots .owl-dot:hover span{background:rgba(0,151,169,.5)}.owl-carousel .animated{-webkit-animation-duration:1000ms;animation-duration:1000ms;-webkit-animation-fill-mode:both;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height 500ms ease-in-out}.owl-carousel{display:none;width:100%;-webkit-tap-highlight-color:transparent;position:relative;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0px,0,0)}.owl-carousel .owl-controls .owl-dot,.owl-carousel .owl-controls .owl-nav .owl-next,.owl-carousel .owl-controls .owl-nav .owl-prev{cursor:pointer;cursor:hand;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-loaded{display:block}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel .owl-refresh .owl-item{display:none}.owl-carousel .owl-item{position:relative;min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel .owl-item img{display:block;width:100%;margin-bottom:0}.owl-carousel.owl-text-select-on .owl-item{-webkit-user-select:auto;-moz-user-select:auto;-ms-user-select:auto;user-select:auto}.owl-carousel .owl-grab{cursor:move;cursor:-webkit-grab;cursor:-o-grab;cursor:-ms-grab;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.no-js .owl-carousel{display:block}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity 400ms ease}.owl-carousel .owl-item img{-webkit-transform-style:preserve-3d;transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:scale 100ms ease}.owl-carousel .owl-video-play-icon:hover{transition:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity 400ms ease}.owl-carousel .owl-video-frame{position:relative;z-index:1}.owl-carousel__item{position:relative;margin:1px;padding:1px}.owl-carousel__item figcaption{position:absolute;bottom:1px;left:1px;background-color:#222;opacity:.7;padding:10px}.owl-carousel__item figcaption h2{color:#fff;font-weight:500;border-bottom:none!important;margin:.3em 0 0!important}.owl-carousel__item figcaption p{color:#fff}.owl-carousel__item figcaption a{font-weight:700;color:#fff;text-decoration:underline}.owl-nav{display:none}@media only screen and (max-width:767px){.owl-carousel__item figcaption{position:inherit;bottom:0;left:0;opacity:1;background-color:#555}#main .owl-carousel__item figcaption h2{font-size:1.25em}}.pull-quote{margin-top:0;margin-bottom:2em}.pull-quote__wrap{line-height:1.5em;font-size:20px;font-family:Georgia,"Times New Roman",serif;font-style:italic;color:rgba(0,0,0,.7)}.pull-quote__meta{border-top:1px dotted;padding-top:1em;border-color:rgba(0,0,0,.2);float:left;margin-top:1em;width:100%;font-size:12px;font-size:1.2rem}@media only screen and (max-width:767px){.pull-quote--left,.pull-quote--right{clear:both;display:block;float:left;margin-left:0;width:100%}}.pull-quote--left{display:block;width:48.42929%;clear:none;float:left;margin-left:0;margin-right:3.0303%}.pull-quote--right{display:block;width:48.42929%;clear:none;float:right;margin-right:0;margin-left:3.0303%}.pull-quote__end,.pull-quote__start{color:#c2c2ba;font-size:400%;font-style:normal;line-height:0;top:37.5px;position:relative}@media (max-width:767px){.pull-quote__end,.pull-quote__start{font-size:350%;top:25px}}.pull-quote__start{padding-right:10px}@media (max-width:767px){.pull-quote__start{padding-right:5px}}.pull-quote__end{padding-left:10px}@media (max-width:767px){.pull-quote__end{padding-left:5px}}.pills{margin-bottom:1em;margin-left:0}.pills__item{float:left;padding:0 1.5em;background:#b5bd00;color:#fff;line-height:2.5;border-radius:2em;box-sizing:border-box;font-size:14px;margin-bottom:.5em;margin-right:.5em}.pills__item .icon{margin-left:-24px;font-size:16px;line-height:1}.pills__item a{color:#fff;font-weight:bolder}@media (max-width:767px){.pills__item{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:20em;margin-right:0}}.pills__icon{padding-left:1em;display:-moz-inline-stack;display:inline-block;zoom:1}.photograph{background-color:#ff5a5f;background-repeat:no-repeat;background-size:cover;width:100%;position:relative;left:-35px;padding:0 35px 10.5%}@media only screen and (max-width:767px){.photograph{width:100%;margin-left:auto;margin-right:auto;padding:2em 0 0;left:0;background-color:transparent}}.photograph__description{background-color:rgba(0,0,0,.7);position:absolute;bottom:5%;right:35px;padding:10px;font-style:italic;color:#fff;display:none;font-size:12px;font-size:1.2rem}.photograph--show{padding-bottom:30%}.photograph--show .photograph__description{display:block}@media only screen and (max-width:767px){.photograph--show,.photograph--show .photograph__description{display:none}}.pagination{margin-bottom:.5em;margin-left:0;padding-left:0;list-style:none}.pagination li{margin-left:0;list-style-type:none}.pagination li:before{background-color:transparent!important}.pagination li{text-align:center;padding:.5em;margin-right:.5em;display:-moz-inline-stack;display:inline-block;zoom:1}.pagination li:last-child{margin-right:0}.pagination__item{min-width:1.5em;font-size:14px;background:#eae8e8;transition:all .5s}.pagination__item a{color:#555}.pagination__item a:active,.pagination__item a:focus,.pagination__item a:hover{text-decoration:none}.pagination__item--current{background:#4693ea;border-color:#1a78e3;color:#fff}.pagination__item--active:active,.pagination__item--active:focus,.pagination__item--active:hover{background:#4693ea;border-color:#1a78e3}.pagination__item--active:active a,.pagination__item--active:focus a,.pagination__item--active:hover a{color:#fff}.pagination__item--disabled{opacity:.25}@media (max-width:767px){.pagination__item--disabled{display:none!important}}.rslides{position:relative;list-style:none;overflow:hidden;width:100%;padding:0}.rslides__item{-webkit-backface-visibility:hidden;position:absolute;display:none;width:100%;left:0;top:0;margin:0}.rslides__item:first-child{position:relative;display:block;float:left}.rslides__item:before{display:none}.rslides__img{display:block;height:auto;float:left;width:100%;border:0;margin-bottom:0}.site-content{position:relative;z-index:1}.site-content--split{overflow:hidden}@media only screen and (min-width:768px){.site-content--split .site-content__section{float:left;margin-left:2%;margin-right:2%;width:47%}.site-content--split .site-content__section:nth-child(odd){margin-right:1%}.site-content--split .site-content__section:nth-child(even){margin-left:1%}}.site-content__inner{background-color:#fff;padding:2em 35px;left:-35px;position:relative;clear:both;display:block;float:left;margin-left:0;width:100%}.site-content__inner>:last-child,.site-content__inner>:last-child>:last-child,.site-content__inner>:last-child>:last-child>:last-child{margin-bottom:0}@media only screen and (max-width:989px){.site-content__inner{clear:both;display:block;float:left;margin-left:0;width:100%}}@media only screen and (max-width:767px){.site-content__inner{clear:both;display:block;float:left;margin-left:0;width:100%;padding-top:1.5em}}.site-content__body{display:block;float:left;margin-left:22.89562%;margin-right:-100%;width:77.03704%}@media only screen and (max-width:989px){.site-content__body{display:block;float:left;margin-left:34.20139%;margin-right:-100%;width:65.79861%}}@media only screen and (max-width:767px){.site-content__body{clear:both;display:block;float:left;margin-left:0;width:100%}}.site-content__main>:last-child,.site-content__main>:last-child>:last-child,.site-content__main>:last-child>:last-child>:last-child,.site-content__sidebar>:last-child,.site-content__sidebar>:last-child>:last-child,.site-content__sidebar>:last-child>:last-child>:last-child{margin-bottom:0}.site-content__main{display:block;float:left;margin-left:0;margin-right:-100%;width:55.46605%}@media only screen and (max-width:989px){.site-content__main{clear:both;display:block;float:left;margin-left:0;width:100%}}@media only screen and (max-width:767px){.site-content__main{clear:both;display:block;float:left;margin-left:0;width:100%}}.site-content__sidebar{font-size:14px;font-size:1.4rem;display:block;float:left;margin-left:66.80507%;margin-right:-100%;width:33.19493%}@media only screen and (max-width:989px){.site-content__sidebar{clear:both;display:block;float:left;margin-left:0;width:100%}}@media only screen and (max-width:767px){.site-content__sidebar{clear:both;display:block;float:left;margin-left:0;width:100%}}.social-buttons{margin:0 auto;text-align:center}.social-buttons--hoz .social-buttons__item{float:left;margin-left:1em}.social-buttons--hoz .social-buttons__item:first-child{margin-left:0}.social-buttons--vert .social-buttons__item{margin-top:1em}.social-buttons--vert .social-buttons__item:first-child{margin-top:0}.social-buttons__item{list-style-type:none}.social-buttons__link{background-image:url(//cdn.ucl.ac.uk/indigo/images/social-buttons/social-icons.png);background-repeat:no-repeat;display:block;font:0/0 a;height:44px;transition:box-shadow ease-in-out .15s;width:44px}.social-buttons__link--twitter{background-position:0 0}.social-buttons__link--facebook{background-position:-44px 0}.social-buttons__link--google{background-position:-88px 0}.social-buttons__link:hover{box-shadow:0 0 4px rgba(0,0,0,.4);transition:box-shadow ease-in-out .15s}@media (-webkit-min-device-pixel-ratio:1.3),(-o-min-device-pixel-ratio:2.6 / 2),(min--moz-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.social-buttons__link{background-image:url(//cdn.ucl.ac.uk/indigo/images/social-buttons/social-icons@2x.png);background-size:cover}}.search-form{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAOCAMAAADHVLbdAAAANlBMVEUAAAD///+ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZkiXNL4AAAAEXRSTlMAABAgMEBQYHCAj5+vv8/f7/4ucL8AAABtSURBVAjXTc9JFsQgCARQHEhi2xDq/pcNQxZh5Qd9lNQaEXXmSVXuvuF1z9ddsZmXgcsLZxym3WXTennlhdawyoyrvMvHO//bSAtGmqE+8KU2a/9pUIkI0Yh8Y4nsQ7ORebM8F+TjaPy+9m/RA8R7BAiqV590AAAAAElFTkSuQmCC) 95% center no-repeat #fff;padding:0;margin-top:7px;margin-bottom:7px;position:absolute;top:0;left:15%;width:75%;box-sizing:border-box}@media only screen and (max-width:767px){.search-form{left:0;width:80%}}.search-form__input{margin-right:0;padding-left:10px;width:85%;box-sizing:border-box;height:30px}@media only screen and (max-width:767px){.search-form__input{width:80%}}.lt-ie9 .search-form__input{height:27px;line-height:27px}.search-form__input--search{background:none;border:none;color:#666}.search-form__input--submit{text-align:center;position:absolute;top:0;left:92%;width:3.5em;margin:7px 0 3px;padding:0;box-sizing:border-box;background-color:#4693ea}.search-form__input--submit:active,.search-form__input--submit:focus,.search-form__input--submit:hover{background:#2f86e7}@media only screen and (max-width:767px){.search-form__input--submit{left:80%;width:20%}}.no-js .tabs__navigation ul{border-bottom:4px solid #8c8279;list-style:none;padding:0;margin:0}.no-js .tabs__navigation li{display:inline-block;margin-bottom:8px;padding:3px 5px;vertical-align:bottom}.no-js .tabs__navigation a{color:#333;display:block;text-decoration:none}.no-js .tabs__navigation a.active{position:relative;text-decoration:underline;z-index:5150}.no-js .tabs__navigation a:hover{background-color:#f0f0f0}.no-js .tabs__group{margin-top:1em}.no-js .tabs__item{margin-bottom:8px;padding:1rem;border:1px solid #8c8279;border-top:2px solid #8c8279}.js .tabs__navigation ul{list-style:none;padding:0;margin:0}.js .tabs__navigation li{background-color:#fff;display:inline-block;padding:0;vertical-align:bottom}.js .tabs__navigation a{border:2px solid #8c8279;border-bottom:0;color:#333;display:block;padding:4px 6px;text-decoration:none}.js .tabs__navigation a.active{font-weight:700;font-style:15px;position:relative;z-index:5150}.js .tabs__navigation a:hover{background-color:#f0f0f0}@media only screen and (max-width:767px){.js .tabs__navigation ul{border-bottom:0;overflow:hidden;position:relative}.js .tabs__navigation ul:before{background-color:#fff;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsAgMAAABj6rKSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAADFBMVEUAAAD///8AAAD////IZVEZAAAAAnRSTlMAAHaTzTgAAAABYktHRAH/Ai3eAAAACXBIWXMAAAsSAAALEgHS3X78AAAAG0lEQVQoz2NYhQAMtGAjg1AEoAkbGYz6iyw2AB4v6cHm3Lv8AAAAAElFTkSuQmCC);content:'';background-size:100%;width:22px;height:22px;position:absolute;top:0;top:8px;border:3px solid #fff;box-sizing:border-box;right:16px;z-index:2;pointer-events:none}.js .tabs__navigation ul.open a{position:relative;display:block}.js .tabs__navigation li{display:block}.js .tabs__navigation a{background:#fff;position:absolute;top:0;left:0}.js .tabs__navigation a.active{z-index:1}}@media only screen and (min-width:768px){.js .tabs__navigation a.active{background-color:#fff;top:1px;padding-top:5px;z-index:5150}}.js .tabs__group>.tabs__item{display:none;padding:0 1rem;border:2px solid #8c8279;border-top:1px solid #8c8279}.js .tabs__group>.active{display:block}.tab__content{padding-top:10px}.tag{position:absolute;left:0;top:0;padding:5px 15px;color:#fff;text-decoration:none;background-color:#B5BD00;z-index:1;font-size:12px;font-size:1.2rem}.tag--right{left:auto;right:0}.tag__heading,.tag__heading a,.tag__heading a:hover{color:#fff}.vcard{margin-bottom:1em;width:100%;display:-moz-inline-stack;display:inline-block;zoom:1;margin-left:0;padding-left:0;list-style:none}.vcard li{margin-left:0;list-style-type:none}.vcard li:before{background-color:transparent!important}.vcard__item{margin-bottom:.5em;display:block}.vcard__item.fn{font-weight:700;font-size:.9375em}.video-wrap{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.video-wrap iframe{position:absolute;top:0;left:0;width:100%;height:100%} \ No newline at end of file diff --git a/assets/css/ucl_reveal.css b/assets/css/ucl_reveal.css new file mode 100644 index 000000000..e29a204bf --- /dev/null +++ b/assets/css/ucl_reveal.css @@ -0,0 +1,23 @@ +.level2 h1 { font-size: 2.0em; } +.reveal pre code { + padding: 0; + overflow-x: auto; +} + + +.reveal section img { + margin: 15px 0px; + background: rgba(255, 255, 255, 0.0); + border: 0px solid #eeeeee; +} + +.reveal table thead th { + font-weight: 700; +} +.reveal table { + display: inline-block; + text-align: center; +} +.reveal pre { + background: rgb(25,40,25); +} diff --git a/assets/favicons/favicon-144.png b/assets/favicons/favicon-144.png new file mode 100644 index 0000000000000000000000000000000000000000..674c93ce5f92f355e3f496603da36da04a09ca38 GIT binary patch literal 2576 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 A7ytkO literal 0 HcmV?d00001 diff --git a/assets/images/carousel/carousel-aslash.jpg b/assets/images/carousel/carousel-aslash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8139008ac2f88e6908beb8df379446aed7aaed8 GIT binary patch literal 57549 zcmYIP18`-*(!Q~6+xEt`Z5ub)*xcCW#x`$kb2m0NcCxXPf8VQm^?#?PW~!&AXU?gq z>F#g(e64={0ieoB$w~phzySb@e-Gg60)QsrZf@@l00Te*006RoqALKhsD+c6CBUNp zp9Jh{0}u*8frp1jfJZ?9SexVZSZxD-sJg#SRwM9V-)Ny$LV z#>T|N#wH}hFC_H;3cmUQXs}=$5c?2d20HC~<*=7BTp4NmW!|`xjdDtg5 z(U0NQTKijFS7!8c#*y4`%VWhfSIQjqtI65%6v6idPPh6NZuqz%Sscu<_N;&BwcJNkd;QKb0UiWu=&(9q{4SD#1=LSu; z#W{ZW(&WA9HGab*Wk^{$FH3;FCJ_h*G({;1p&Lyy$?#-f| zVbL)Bys0KMHQc_O2gDVg*aBdR{N;_k8ZhReVzwZzks@XfsW zq3`K|ubiv8c&ci$=2HXLox!I@$TdH7z-k7l>-XF&Xr2$93fwG_z{y41(neVPU&Mk7= zM9dCw++{aEe_;!#jWEz6zk%$D^Q)E|sBAGuCu5J-`tEJ4iRwMdajr7ZUWQs5a7_}O zaJJ1LiKf=)C3!q-=(_;LS^N!6JW4*?9K9gq>hgo$pNHayY5!>Yz6KRUG0o?bUNlCZ z%q@=)Ss-~DF!g4q1HNziJ5t9;+gkiPRHpuUX;S=+^99*K$b2` z2Ef21zCQwVg}mi3=_h_#K~0y0ZneKLk+TQzV_K(m%5=o@6G|$C)K;hV$}Nmn$JABg zL34ua+51R%SW&EP<37x_NI?UdBfQJvlE=yO0{l6r3CA&*Nd_IK@;!p37TdOOc=T#% zP@8&gbq`DzIWSzcotIc!r|_@XbiytgE^1zkA->;w3CCupyAf#cy3wPBy(9ZTs84aa zjwTSm7`kI#z=YURr7<!?pr_7-cliNP*E>~0_JRIki#ky%E0s@GMC?d2*znsBy}gR^K8lgjZgQH@NNwU9|Q%ojZ=JCHomjR-xjxH>OI z-D6V{Fpf&xD#4Eb_|aOYz|=%(LzeJIvaPK*3qMGlMRWP545L#eXDfRSS|Pp7Igm1{j2>8AU|_wAJ_a@&d^e+ zZA}`l8e=^ivqV9oJTJT^(Fn&{_s745glgz^fz9cMosJL8VNv^h2RO%Hg0!B_m2BY0 zb>Ds0Rv_r{wz1T+)FsV_kf_JgojVPp-p8}G7{u$erB);CWLtL$aI5o*Zv{DLF4~R) z7LdMyMk+z(oYiR?>PuW5)rnL1TI6M<@Vg*TW!j*iFGHB(w}y+-&U083-DE3Gyopn-QOdDY)mzVncPeUPQyC=fA$V_{4 zG^)<+W%4E0h%Bo-1u00%=+WbO?(D)b;9mxtQbXnT#np3lm_Ur`mja~8qw?Z#e$ zvfiWW1#f62C?PH&Nte;vFDqUC*@vepZip>j(w#3(x~i>gdn14E*WDJ|Y5&BzL7mh& zIgC+wZmCbx-R`93hPH%*FJ}Re-WR}GwxMS+02EvrWKw3@QaBf>uBurFGt^*GFK8fI zVj7%CHA6sXEQf?BmH)m)M*yb#q}7PE2_IkPiys@j%AnS)P-@6oRh(MgQo$?NCHLb$ z4Gv~8xe_-Mjn}H5nHI3Ohd*&wQ|Xr}*>j9jjo zQJ3}5zb*8opdc6wJfB|Uz9oGJI?V}!Fd5;Q2VvX7m*42c9DCe}^qSO%bYA`Ux&lgw z_&+%LDN;fp4r{lr&5AF}d8lHqu$CG>5Si-FS^QNM|82as<9@}#34>@DKA zX+bm>#Ult7S`V>FS4eDaYnJ=Y092`fTVI|f-k4L685bFAajWt+@RjSYghXPrJ7RP~ z2Rcq7TS(P0W6!JtRdf@RMfp552Z46>LCD4RK-KYMU=WcgPDPDn>SRO#yvDK8ST1CNGW#i6Og$s2IF?HHZlDlxCM)Gt891w7M0kZ0FxE`5CgJZ29{2?;dJ*z!A zmBLR}n)r-U^lA5qpza9`p;}&4qBCqm0j0Hr6$awM3g+G_ed{_aQmpdTSkKlt@fGA6 zWDHFT9s2Ck*GvZOZ?uZ3$!Y=5Qb0UEp@^PekR_0}@d4n#tSxlF=mKdanA=RCo zsztS^L{?%@!{$nw612YnVaTg^Q74$XHu26kkVI$!DH;f4L~T2^vl|N=7p2FJy_6LlH*a#qyRkROs+f%?Ay39Ui5jbS5n7%)1NjAihV{_d_Ijh^IqI zq1vHaE&053@d;)44z${|G1HU96fTa)1e{bQHQ2u$P-v7AuvW1Em9{X=)YF#x@_M|{ z=^F80Wjwdh>|xAbfSh`HkB5TW;vwpL>d}D+|F?7TUlW|)o)v1eY|)IrUjq#Cfp${@ z5}Bjo?5_LMDSz3AnrNlQ1&TbW=T0ew7462RMhXv$I@(j+iI78$&kEHk8$s54 zi{8n$Qlc2v#KLJh`xoF+H&ew~5^u-nR^ue!PH0%dQo=~X=4a*i;#!yjB@L%%QeA2$ zNaHk;V@2)Xpo|Cn;&oyj!nHLCmgeLpGg{O6Te!HZYpnmW9nDPtz)Fm0-RcLf*`Eij zuO5!Cu0F3qEB^CzJI)m2GT$T-N2eY66KyQUyhD}5<+|jt@F7g?41QA)CQA!Z+EYwo zg6|yy%ZJK=mIYFg6oxClr9s z2?v7;d@hFM*lpj+rEG4}#7<**5+F~cYeX)>7vQ7&Q=R19bjf!T?UL zsbT(CFAhmd#7W_LkS(`}+|`QscV-y*oz`{D_aFFw#(kjTJkdFy?kikTDXOF-?AqW= z7y%c{OPnk3ki!*iY0l1z!G#osRk6#ViT-BA?wyH)XO87dFSbX+n{oUyrELxQlIISQ z&TA0NIHNzm0JUR)>(e8eEhncHrG~~nr888&)dN}6PdBL(;cxC1LcPBgu^lpR3k@nF zTl&zScpOuqo+HZ)FgNqukX-T9J@pCn*8QPtx~awR@jM-#_kA}}9D83ALUx{Y>I?W= z9tcySbhmj(K^qaPkm@B}G$oB& zA9J-()58bOj`Pw@{P}y*Rc{6cZ)XvPKFdxsPQ>{6nAhX++B|8=@DmKI!p?!=eNttW z`)syXSj=D)p7I}71%me{O^7_JVFy6yR)98BZFi@A%}+Qq#*ld#kJH^}=_jqo{ueYkIJ)H<6izb;W-H(DRET zT{g>%m@Q<;wfd8Q{Psr>wU4hsJ!<=2C+yVuzroPGE}p>rw4r$c7s5bx6Hf~la1xyZ zwGz(|MjItQP8sLz!<*Gn+2ywYj7#k5Zs)|o)rEgvD7b1ub=!uD<;daD2<@{=MCw@c zw4%nFwz8%Bv&ZALAWMXBhQayc?IHb7v%CdWD+$t}JeOs((V0sKxrevo>faYNH7G|1 ziflM66liBXIAbZh+$ZiUO~EFWt2)^%aA;{a;QT-%0!P<}sU@0NM2GShkws^r14y?@ z9{o~Z%jtBF+dX+pGB809`F_E9Etg^Ke17|*M(b-$${bZ9a%yV5yOgY8%hvMz!%wc% zF`#x1@V>-gJ?p&Io^_^dzAi_bN#7#u8&`D@d}UHTB(KU|vfmoPF5sSY5eLC2ZOpMZ z8c1(K?zASiZj_!jX9tO?cOO?>@~DoNrqM+}AENUFCJFPBlVo5Z8XNK4{Lp5G-1|xr zyK`zrChw?P`xu|MAl9KU_Io;o^qq1I#w9)^`liZaFrukH!MUFM&9~Ka-i#)W1Q-m@ zC4}Z$U$VHGG=?pq2AX21go=B`->X}21ku+WHIVA7`;3KmXUwQUAIS7ck|@rA`NQbZ z%qSimHG818ex4LM*)O~Ac1@&$1nJb;iQKSMYD;@NDT2A5A2Ie7QA#{i4*25|u^$kY zY8VYBT$0Gai0YvCSLRCDe`Mkjt6#eQaqRse2}hKc-<>pWlIz!zu_ykJAc?ncp}R2Y zYKu``qU{vpJ@U@$m+dLckd(Bg%(+8bIeRKo3pzIxV2}INT87F`F9l?>*9L!DF6NvK zU4chaOWIPr-Kw)qL5|SWhd+8aS6sC(;!q#RPNM9grd)8uv{l%)4VyHnUR*(7v6Sr2 zJ~}g*_+4+wV+%Spy05b<_Y%m4@q5dh+`;eqvZF!{F(1MQ#Vaf>11JA0cETu@ZcRRP&gLjlSd=vyQQA2Rg z%1YS60T88JU_G5b3^%OW3+zV+d<-;fy1=d~4c87e73lZrloJq+#|%659V`$t>sDexCu4@w|lK5ZIJ+ml==I# zvEl{g8#+D|f9{L&Vd%X4ivpK4nx-yLYY^A{!d~oSy?$Qbn!;uqcjxMBUsJU04N@#K z-RePRG;c@j#xh^D`@JAimtD1-`MBzn1vbq*9k2XQR^)Iq*kbS%CIh!(;qnf4w9TiO zA&yeXhZ{tOzFdq`r*ijFBb6&@<8Zvxd82g895D@I>`ywNyS08y{!HOow&`e3x^kht z{E(P1T6l@k=;KFrra`@{fXVa`EB3Ca>_1kffnn8=-zI}-gc`H?TQg@o^dIirDgX|I zvO8la%+FzXrdAlCM~I6e3p@Ua9!u9HMQ*RutnXL3-JPV#SUoFs4bDPY8g$brdT3c_N-z@x3QlSfoQ}SS1Bmohy z8y00l!k6~oT<`z~xCfBk9Y*~)KJ!% z0y{s@W8?|lu1eN#f_+_||J0g?5m(=-L{+8GOFGV-5+Eo%BAi?yJ(lItSY**T-p}<0 zHoHJfm(MEd+?Vi8^?g&7-ZLaj$JdXm^nfIQnO9IZ^#hZQRA0TuW0_-6v6XE9LDc=V7K~3BQ5^s0 z3t%W&dfE!%!nzsDIJJ$$+Trl*7poqDX*x1;*`>6gZ2=k={IDm1HhA_r#hfGjX{p-e z6~HQdOLqBy&oy-W&D&&zSj2;ff=@=+vc^6)&NAEMhNtGkZB}kAbuIXe-eB5uY%(3^ zIW4~DY0-B?JRp2&DeRf|XA2l*%Y?n|>&8Rp5@YKAEtOdnnbjf^rtet9Ntq3D=a1P% zj+naLH7$+izluW^SWM_I#dF2JFiuU?n76ByhG9M-jCQ|h$dV$cPYiw< zZeAu~$N<7Gn8t6?4U6qmJ;4x%k_*qt{s=MX!Sljz^{UF z#g#qZ9_{^UM9~VT?R~ki=vy`YgX|X^(mPjNOl^w`{_wXGN02Z+*q$r@RLln!yN>ho z(U!nmpubgB^i<-NI2~*=)>x<;L)W#@2mIW(qiDtsHR`1D(Z-2wKX$U`Xq36i!Z5_~ z*2Yb}&sLwpu&Lc!yg311i9aoBVQ*6bvpyR1UjVvLc4E4vX&`!sERITPhU48?A+G^t z$8wehdbozpk@^j4ABoYcAgB+nQ9vQ6;y!qnVoQ@E{@)Hfju(kJY`RqSFK=mhqtj@2 z<-X_m0&o?nk_py?mi}(4vF?AtO&-jjAsG(+%=XH-RU0L^%eQJAEaVAr^;6WELU@%b zScE@1fUHApyP%_&sZ(J5jN?11yEYSE17MGS3F~@Q|^qNd0^1yW>JadN; zmjDnZ42!&{ZF^rj{pI%LPxx(vMCFPeGl@r@dJAYr^X!GbZIILTs=Du~fq8n0<+!&+>xl6Mz_H=uLM7;`Ubrzh70M$Nl8x&<&|4fFK3BF} z&@-L&VW@E}^|&srS6A#Ptu>%Fr0LA5_2^_0?Jrrxdtuv^iXCyh!h{BP5|{W?EuDC> zg~bWjatt>q)kN2fE<=H?U#s~ysG8?$EvD0~@OEwZZwlb zm;15*1?>iY+Z+jTq>M@W%1q45g36+b2ib593cdEQvz6Ue!#QzC)bqbA+b0bj9*j#B zBT3^VtWE&yPX0_~Wx0CNblSjwgb~h>-xq)a(~Rts^0luJ=8)6OHieibTiim_RcHG2 z-`gg?us^sK&=e5CGyO!I2-&iO-U8ORzZ2 zVA(%kh`=^7wIDj(GkM=$<-Win4cmxZyE+v*+F>LKt5f&;gaZ7TTn!DRO{+P4Fo9SC zIz(NX{xs_Miz8T;S!K>`Zs`Ds6XNW!;ey0|CeY$`RP6WKwhAm{*E(+gaf)p|wkw#{ zOe%zIofKv6k6k;gnIA53J&G|z4)jI)7D~sHanUJd*_z|u5Z9EWCw4Y;Z&!jhZOG8T zYrBbO-Ck+y5o1Wmz({p-Y#&2tjS0e@8P(yONjG&8r80=4`(j8HGL0Jzl^66+!}_vu zkOd+O(pnYScZf#nU`^mtwrY{JCI&O%M76~Kq+InFyzGSLv)j1t63pXH%EeS~xIcky z)&65~oxPc%`AZ+;nyQVS6nPLH*E^=Hk<;a0WTEALqbB+K#P1vKh@r}k%8)qcC#1YD ztpHyg+g2pg_AL=Z*>gp)&YvH1ll|u}joNOlzw|{;2C->st@y%zP6{uc&UT z4xLmST%2E=tM#-Mq&Jz0=*>~{Rt`wr1;fwo2&c!_G#Ld{MB>aRr{l?WO1ea9RBGGs z%B9gm{f8`Mi7fqIV6+u;f3D#zP!YV^l=E>I`#?0jB#J z{p6f~Z^ItKqtJ{#UKXkRbWBnYO_J6%;{3SWv6LN?I-)WniL*4~JU!TFeI^Tkp= ztTj?E7g8EUilbavDkJsO6~QW_6{oxCfp`(c`?i|XIK@lc%2m{v%l#%PsXY#yN1jZl zx-lwBMA}~pQ*7CrkZNy z%+pYPiCg!DIzu1~*V~uSd6$+TI%>v{ZO=%&c5!rip?g`2qq%810QfJRFg?tU{jxQn zge&du0vC3(*Mu}LMb3?a;p51-E3+Jy~h_xbZ+`*jgW zq#He%t~}CBUx0a=x+#^eJVy%KvN!?R`M7g|WEuEezSl-|gk|hvkgJr7wVi zuoTv7H4pOu@jRDcXZFH-r|^pWD_g}YcFuSE$>%QDGyaqd%wA)ucO&1tGEd@tyDQ_aRRiG-VYgsZRP^AF{Fa0-|- zjZW7!KbuA13??l4g;jPY~gdnYd* z2_fbEfmjjx`51bIgV|cYN8=^CjNZB!!Tjleq2Dqdz){_s^UTxt%{&KqlEkqm)?!p$ z@!9NcWj|mE#O#mXn+IQSNO;o@nbIfu3V1w(W8qu2chf1^L~Gw+b8ff6h&k2%G|++X z8reTSFu{GIkUM{ev7`rogn=;jw5%xk4l!lX141qOJmnJS$&QJi^wN~xGgmzAyo7n3J1C_S z5m{!hPVbjGyn1nxgxs`rm1PTo32#0AAeY$TRrXX>qd*c9%3Cq z`iSA`EwhFElkoskpROOxmUwA0v`LmTH)py0IJfZ<>FnFNx6;x!MW6h_;wC1=!fC}T0e;2h6s`K-5E-K32QECEbLB<1%X0(16j3n|&B#*1 z5rVmOdWY1`DaCB;75zs-fC`AwQPE%NSB;Wa(BZzUn`=^_n;xKbR@}Ko3 z47D2kJxX>9k(v4gV|Ri*s2#qY$FU6&FQbMpz?pk-H`jDWLE!qzY0aE{l+#BuN9X<- z7NfZ*9&(?L>OFq_aLNY<-v?B0RdcM<2+e0nzPd)OM018si9A6k7L+VfrDLdDL8e#o z&5JcXB>MUcJy)`L{V@vMW3cq0`T;MDy|=?aS0wc4kD&==?B$u5UkSFvZrVT;ujk&* zX+8d^8Rs<%F@o_4L6cMmf8Q>Yrh@Siq(pfB0SZTLT3|Qw=a7c*<-$1gdl@)l!>Vh;I%MnWSb2!(E|)SzdgSft7%IMjE$ZkW-H=owWihS- zvI!JqZXX*8U~=H8d|7W9$gtK_-Qb(~{wjiGDH#_YR_8;MGhbF-JDQf{^;p?{Tt;-e-gpO6+@ z@?zn8O)D98G6RG)Ht7CTY5)>qrxt9?(jWDQzt4{cRR?bbv4RJDf)(ZPm&V@*dm459 z8j=z2GQb_xZ;;zPY=c??9KH|@!?i?Vfjq7aLo+hl$h0bgu z?$MszAO|72Z_TbzARrJIt3U1l4vt1e{<4vq@5GUQS&dl=*8`9lpRpodsyQ{9XNzS< zX&^F%3yL9XHT7p^z$mLX_bTybz@zXpHxIiuWO1BYf8df#h&jlh^f~~J+;==A;*{#3 z<*h&3x4LI63v}7X)!dh)-n8|lW>X+1URoqnQ*c#6bbfD;|4L$>kaTlke$etsP3yW~GoGn6Tlbfjj%yGw-#Lu1A z0Y}vj*#x|sZoH>8$@Q{Bj zojnPBE>MK_mW!$px?$1B(e-ONE>+e@ADqgPBh)GulnF$0R%W-yYvTo#;jzqTyW?j! z;jtZb%oo=+w8;V8;@v8>ZB+?N4NFBQHPDeMpWo`ArlcD}paFOIt{YS_tgS9Zb&*PC&cpn}N+$zZSxLc%+IKmh7L>_P{7URdz@y9q zAl^We4|Hj4=(}+WBkq}2^puYq>&d>*?L@SF0a}#=lLO>Ph3w4+S4B402WGC%HYBVL ziD`Ae0I+0zxc`0(p2Rdlo>BvRL=Ngl#9gT=RqUJGzKOt+W1%l($KZ z53cIH@`S`eyl@g)R(q!Ff=77V+S@Mx`C$<>&bsV(L~Es9PX2&wgV_7zy(pNyKPg1N zwVXS*%N7464Vf}AfW>kD4Zw2Rv)_6$KH(|~Aa*6XX$qYqj$)6(d7HAs5=OzLDj%}h zsLzS@1Up^l`dGIZEEDTX_FEk=-N4&wxhG3pi|DL2?YyRVLy}LyjKrpG>VA$|=GD6D ze09efFW#VoQG;z$FSpiPu36#@yK7ASbpHbQv>IUb5uC;PQ#amvbvRgF0Y0o+g<0xf zJCt22YK`XIsA5HrIC8qCj9pNsv~gpyi^sg0Uts=ujnYq=k_L`_H#<7E*oNINItf4U zKo9BfX+JbI8Ow&zOdX5cRP`Mm$#|jPH%$b0&wn4dwy)-L_Eoi&J6!Y)F@HMY0yuq7 z)&2fi>4#LAc0{Op`!$h@UR{EY=|IUD>EFDL=BzvK%h(UL+AU|aeKAh;rS<{Fs?Xg& zHUY0t7K{o>!R!70T#YYoMh;~kq(YrJ-#cqfy*ef?fTk-OhJTd*mIusV1*9nMyC2@} z+y%B~KmteQX^Ny?;VQLY&FsU_^KxL8biQl+W}T~>mN6|2eoyNR7M>%8UXJzGjM|Dn z8Rh60nILd$#Ntv6k5oSW&VUdj?ev-T4l+ihKiH>=Lg+K!egf04nBTJVOJ{R#sKDtg zSSd?lIjnkC?h^ttEgF|0N*BL6*UPU8+@$^j5G#Ksqj<|vjoZcsJx-9Ui0i*}1b!xd z926O3+?w4_<^wHD*m65wn$U_yv!B-aXKOeI4tOID6sjpjwH=`R?9S)L?FkYz@yL%^ z&;O#88z07}{Y(fuxJG)f+)Dwj)g>2@fB2lL)fkQLfA6S~wVDj)AfG(JgC1?lWtK!X zLLj5ec;(iI4t^9+b3^1$kh>yb6UJg}pEn{HAC?xdHb)2Uy07Tbjk>6a3h7R0CH%_q z?`NzoXy;xzse~u~chk!=^2{rs zX2E9fzOLP%GV!b=EX+@X^LLLXhzvu@LFvX{yHwSj<8YcE?mLyARnH~fX8tCh%Jfeo zWx{oxaE-uxxag#tz5Jem^X{+b5()jTbgoTeKE_gNz6qA%Nw-%Da!SyO zD@}Os_7{PXTBVI_cwFuRM3P)xC+P-wsJ8jBd&g&*{~KV*7sRa1?@l zs84bB>K22wnU&ms<5++!8tmljjzSnxlp_;y@Hv@^F+dvIHZkDi5^vFk<95&b0$tEO z<=(IS9MH#-Y>DcI7W>?if?-CFE6(^j_b6iZO~%SP6cZ)t9*xXAG->{xLGW__TmVnf z$dGr*xZ4&hT+`kkh!YM&9SicPis!%_2ak!=f02yngzkhS&xiz8z)vx-Nq#>3^aZ|8bx zHl{;I$wrSrLXd3%DU?(o=tN4gg~g^V?i7cr}t%n$Kh$Jp{( z?Wq^n!~x)Q6CS1;Af@1A_kPwx;3wjtO$*XoQL28ums4wjBfWyHmY?}y=R4WEjr8}o zexHPn!thk)jEtIYeO&HRb5KfiupG+~U z)c@Gf{AKX-H?bhU9rRua-fz?R0x9n9z|hyu9~WHE z(#WklFkzzGg^UwjeWnXrc3|H&1)4)V9ZBsit=~gxM{eJ*93U|K%b#rv+ko3qrbEd^ zit~Yl!@r5AF1d-u&Z@Qp>~4e{6r@D5v}9onYwLQuRH$Bj$uMT(C9I&AEo?wvfJL#r z2j6JF@q-i+QnW^Kq+zxjxApU4x~kf&Y-$}d0u!BNYk0-!P$JQlJD~RSDOEZJXJh0+ zBD4t@@AU8I-R;=atLDsThpSwgFR>bk0nHGQ?G^j9I6=;L87G5^9MVCVB*7yy z8RX6=@_J_Zec~WONt_C<^Ck3rMbCmBS#5KD@KwD7C)!cWo6MQK1IoiNYBm6PnpqG) zegO*#ix2Paat&lr*yCg)_0r|yPYo`Wj;4G!fV}*aVHAW1K5DEwmI;o_Sf19N)p2v| zyYRk1Kc<$aD|HG-CitJ{m~C5~Vhhc~!8FSo1U$$zJ}qD~wL2fUv-M^AKDLE^UMk70 z6z{FSIiKv`+JERi@R)!AxLFiwNtQE7am!+8+#y^|=*)oPky^1F=J(+wf0Nk(;HWX0bM=8 z=482KmXqGp3kRpcmz;48Mjdoup-^@u<6Ta~1ZjW+J)qAcdy!akoC(hs)6tx)XRNi| zasweIDGajioX%zRPzC!{lH#XAR|e*lV;lukPY-xuyy9Fko~prGB0?gQlO%7^ow&`d z(Ey6PzXn`Mo)r`26aXJ6T9=nz_DM{!?@T#ftTw#w6cF*G!!y`INQ`;Bv1i5zJ?B?>& z+#Mjx_B@Gw1=HL`$CGJ3+N*~-#(38l#r!d`x6~M-(UmKly4TD4wx7BS@e59Y`RtF} zWu1yuWt=r{tG$Q4=_ih0?Ue{@-1y4}ztdR~;P8H1N>);WWN><;(fn0pN8v}8iN(3W22p{%*C$`+xa za>3%by`Lr(kCfp3`FgOuqPd+*Q^RZs|0rlo%}2%hzjyyle}Q!QZ(VI46WWpgS{A27^<-P)j6 znKDQW?GMoptMH=3j{PDXR=?6Yjc>%&IVuV#7&sFln8d1-9QxHrVso+VH#f2#U9cI$ z{u27Ui5D(d~1$6#Sg>~7#5%muh1q?RL@3cRSFKf%E&fj&% z-w#P7%~{_K_VY$y&Q$dSGbD$9@6asLd6k^9AbSd#e|jkNiGqwx+*-@_&#MIB^9N;) zucs({HWUgyZ zhxlEev0fCNeo1el=F(@-Z@dVUU1xm(hHWw?rUES5+^j;6xGy^Ld!>cD?S9!Ak<$*m zOHkBq`nxy2*y_ZF1L6GrFLepO>$8KsinR(eegWFD5*#AVh11KXDmT41vc_d!3KQD7 zJj1d7790uiZ&G+4Xj;(LhY#Qg;9$Bl$H zhs__N+xo>NS;@ov1p)D=IO7AZ_itZ-N#VgI%<;J637D2Iz<3Kn`b)5hj2;@Ji_}HU zk3n6joK*HS=Tef;KmyW-8^_uk$UA2D?F0%M{4RSb};9Q(G7&d1%tsc@JYy?2`77S|5PiY19G3=r#THt`Vfn zF5Rx|FIfkZ3BbYR4$PvANc=g7OTkNHt{Y6FX<>GdDQrp|qs_l6*P5OB7Px+XG(UWw<4C-lrcWoLlc5H!7 z#w^ynK`Jd$vULFpbK8aXC53k(b!4}RXzoh!Rxl+qOoCS`j3Q80jHe5KVJkw6=P_mHaRQvV|QOr_V(9^;#0&n^q+j* zY;X34@>6n<)k@~1)F+@s!E~L9;W0?S>$#=xJ1mDYh{f?Buf#-p#()(}Gb1jUVmDaR zaqcSvG;_Pcl1O=eT2=|v)T174;iApP%fuMk7@03W;@hX9T*=hU8Lgcc>g77(?obdW zjPMdET4Up3HbnD&?P^&ki$z0o)CRM!FI!UN~nAo-7&3B0SHJqC&g;mP#dBc72W%WX(~yxf_D2IZ5mlE~GSr?^fUtRhgdeN>1jqPAzpo3CarQo&|6uyHv zc8b}qGZ4WU5c!&&8$IH6xAE@mWP`WC3Gn4yjb`@66v=225Y4BqjS zIwgfDYWp3fvw!(4cC|A{0JbS7WebKBPs#>S{Frb_EIf&&2vrUb@!!LdqjDS5z%BBB zi8At0$yH8KY%yN|%m_EDkD%@Sor@yn9P*>I19Rcf6oOozou`NmAC@1iG!C&K^ztG` zf>RMD{54t~V?xi~xWGBlCk(yQyx(CkX%HKGA}AM)BtXMxi8LM>UvNRGYSA)k-YZ7o zPm_uc#>XKE)(c|uZ&u$&mnn+sjX-@dwmj({x-VR9@-(wjS(J?uEDTl1kEB{w0cPIP z(cZ|}ReEfJmNV)cTfwjh51ZQ@*S;%7?+X?CLM(bq#Kv&4Xl$ABMXbsT^VQ0~$J^it zonI|{^RG(Jd8j4kF+91idcq`h;(I$sUYP9sTo4cLn#W=9vbTy@F*X%vko|%l&3erP z+(J7(E`_4&olW#t40e1**0=SuCkC~~lUg>#K5b@w!wmOpy9tbr)!Yknp-wXJ#g#Zo z!GQD(l99ubh1is*PKxr0!eK-imFvp#PwLKE!c3v&!+hwzN+2h^Zd1Z7FhtSiVQs$o zzj25G%9|SvueEkW1Vlj?d-BBrP0xzx+*0>MHXdTBGl?kSn&>R`yG|9_#^L)8K8?4( zPk$Ge%rbgf9kClKP9(-(M<_SN>#Z+4#ibcpyOupM1(ZE;EgXMXxITA)N+mbJXpD9Y zVUEA|Ckv#-8P+#OaX;W^Wc(f<9FF@#CNN5t8MD6MyrAnB+i_%RYia9|ThMVKoZcKD z>zMvqHA+{52Bas8mu&D-6P zg;ftE^jv!K9!S8m1%S9FNP{ae#}PP99uwe%>QbLqhS$&C7g|RjR49aW6aIq0eEICR zZ8ts48#P#bNgy89rOQeV%*~qn#G+*=j26VavOMHOJ0LWPbCv}Tt7EwmtPtp9yh9F< zgYHeQF=w3W<>HLK2lD{HC~aL>WF&sybfKL$yfP@X{1g~_HRyt6LNj-=+SM@sok0-# zZs*kyrn~3I zA7+!~y`O+)SqzF~V4(fbx>}3^0Pd{M$}ATjCJ=5CTid&Etp?*>tu_9L^6$2H+Wk{* z0-=7j&@a@|WfrZ^34EiaU9MJ{DpaXZDpaXZDpaXZD&=;SK&eutLFSm`$o($E(c4#) ziRC!b>l>(D-=sTfi)Au0CAWMBw&$*t#+SOUMV*@ z(?wzd>)y0Fc%^8DOU6<+ZHo5LkS}51OAiV1vey}2G%a(wRtTC4>PY+-aytCe$n(- zI9^gLESBRYmvl~dW4XSU`=ro3Y1tX)RPyDFft!v6g8}|xuVEBRHaO(zdWPLqsX_Ud7XD#>~-@{DzfUGmJw^FPGGx7k2R1hA>I$r~U&#f7UikMjJya=SB$M5PwtNZV%ZsVR>w z5&j$rev!q0aI7B~rRJH><{#c7x7kpKhD1YWXvzcJNVR3);4(PXk>!zO53xZ|bn$lH zk#8*)SMa!r59Y=7EoOdIP;s)Kw?Gf6PvxyYDGajlSrI}vO3{@?9j4ZxpDy5J<8jk@ z>SB|51sH2$6keg*zcfnN{7i8J{MHEGkOuFvr-c@5JX0v~?Hx4Hlw4IGoR9i^L?=?s z)6gyU(P(02SXSQ(51~|4S=yH_z{<;gLIL}<1rVrh#>ckwXP0C6sRwqR>rj_Ff%WU} zu9Jp4Qoxop_8txr0TTTZi7yICV~+gvTc2q0E>>mDx}fjNpB|}B>e5jKEY3SlIzEw8Qeksz7Tuw^cvUvxnY9s z-t2y=7bhPRc;iUYWEWDR*Ys5QvdfLYe+b!&6=v-kTkNZxn8G-@JTZr#jwN3{ZNf2e zW2b+87Q<3F4S*6#sJCy#qO;t8ISwv3R#0P&!b}(qp;*`v;WyW)ts6c(JR8XV7yt&> z+o2<9xV5TPoGtQ9ztb{Xx;FZ&0{cis0BS(f+0@pb7EFFekIVc5I=e<=@{6^%H|V(_ z9S?11WWvkEnl+vljFuk|HtISal$=z&iu}a(cmlT447&GfqV>)ZguIK%w5u3NAoSIc zX;FO0U1x-7FXkO}(6AK$0G6Gk%bA#Tj1POIUXI5jKg5&tvZEg2)qgmW_@n;-A^H^1 z{C^W2I^_?bJrK+v#>F3|-{Gh`kNoU{dI=BNUVqNuOD82X(c0GlDD(>7V;h{EddlPJ zwPf=}@?}1nNt^hl*Bbs{U&`YlIWZHRs^p`o=s_A)m~df>FDfTTb%flIc91Q7HSVh% z1}szNWEU4Gz^Md+2qUGcUt^!L+z=b8C$L`m7>JtuzA@N8zLyB($^|ghpkTG z?rOKm@G`8Qh{Bz*?15P{Xsn{zsp+L=IGpo~9@L!2;=PnyUc>Yo>#fO2^LPTR!KR-a zg2ZS`DYk;H64q8eZah9gp~Y`C2vD-)xavR#=xnCOieU3AkC~E^l#*7Hbcwhmm#`NA z_)TGPak3_uV#f)2S@r}U5N?&}82lV3j>L~974zYZ0k{lR*Yv$~5b>E9Mk!)uSmb%+ zbXV|_7~E^pp#GVUo5T#v*hJE*yB7c}cnfqZZt92nb}7zegEf^)v6B_`xxTwYi}miP z;BIFTWXE{fVhZJ&H{66+{l*u+s-pd!UL!jn9dh{W)`B)waUAC0Zs<4Eqo3t)JRH6m zc=~zKjl!CF$S2}o5GpxDR8pqQwj$fns_gufGo~^_Cf8}vWkM`Uod_!3l~ALf?VxtU{c+k4_faL zg^`2EG%q_(AC`7*8h~}ADcmU2G=7KuRO}=VgDw8-%6_VE3x6{=^*=3lk76+A?fxPk zs;%uJd{}^Jr6#}u{uLFL`Q7rcxz&;h85UU8#g2gdC;T<3$CY-+8PScc?x27F01zWn zk&OF6B}m=q1nE*OQRBHzMhuPx#H!aABe;;$sDn%k_QD&;JZmz z9X+%&i-r>j3<2ejpFuUF(Ef3z%V`CwR;yiVbxT*9lZzBP-(G2yCRW$ zb*rkT!r%fl-C68zQJYYx52f|}&}f44&$jZ$n?UKmZ}^l}D^7A;wtQLY(FXG4Qd%Yo z18g>Jgll_hHS}7GPaML2?%pkp@8i|qi6nD% zj%JJtd?CC;*;V5OSI~QE{$yrIb2<!j4UPdKp<=5P~Tls z;U8}%otu@H=)c)T&xezcb~r}&g0;_XTi5*94h4tt6Qr91f!chh@mtt$M{%;_v&TCz zQo&MLECZGu7S)``*#={)N?};0kOyDUI#rMBFe`$<-5Tmv#X;{1dV4D~GUu6BX`dd0 z(mWC3jC@uRp}c7J+^F5gfpT-ZCze2U^$H ztKzxQ=lMT+=_0vvN|kMgDOYQh3Y98U3Y98U3Y98U3Y98U3Y98URshDwX$Qgww_2+` z&D*uSE9xJXh)AR%C#{9O+ErbboCENnwd=uNxS3-k9c`mNg!=TT)dNnXX=7%v`zq%HiYMj#Wn)N?7Twx3)2D4v z&On(-80r>l;GbFCzfLpv|9q)2I;t)Zqubsh|@)l8;_aw0`MhW*mQ;nxOSCqh4$-%$@1#YpLK!!% zCNTaISfWSaH$SD>e$(AUvp8;F9uLGrFkOdt;i48xdwwJ9qga_3`5~r(_XZ~Y>VGuV zkXWe%+p5Je&~IfXG5Q4bt|v{Ito0v1Uh+g!u||Wz=;bk_D=g zwaGtC2hYxn4cQ~!u}sX$v0Ga=PT^0-$<4`(7+1)Q^e0yJlUj_4&5;xpK4HD4d* z9C&4Bjaox(x+5^x)lm*T9j`GmH&eQe@Hnx?vk;?Fqe1JdQZ&;}+w`aa&GR1{xZioxsaLh?75STvTkaYYat{4{;=iIZR(4p?_(o{Ug6nV%x4K>bx5V(s~(EFnk#|MBz$?pk|!9Y z3~crW!1mBGv7kFn<^!ShR@cf$;-JTP!Htw})6r7aqzmD5IQ$o8e8}E5C6NL$k=f~@ zlYGI($h$njW7-3LO3vntd~CFYM5O_=mj^;@MUS?p<#9g>vB`@9xNTgKa65;#q@{~L z)i}8cUDhM5_kK!4FOJsI0e?yV0H%aaT?d;U7+Wy}x{+`I^!J*(J|eFinQ-HVk-$4F zO@Zp(s-nl^IPv~!+gs{-pV_7O&OfV44|SXSC{LJL7GZ~Z6-CXC-PbqzD)ts8E>{&c zVSxFucyT|%Hk%cTjomj}MI;S9 zHE)lD4?oTGAo5-@eF?X>T4%I#GZqw!Ay#a0i9CwScHJP_Z=$Hw)%>1s2PcYtJvBZ8 zyMY_FFl%Zpt*bXGMKrJEuEkdHYkQ9S(d?`YHRI8od3!>F$}YvP_L`QgjRy~j9zPps zkRwQZ#{;d~UbQPq0kd3ps}0J1^`1%jY-CnkrIt4M1czxpfqI;C*^ud#0Snq~xqbTm zbOdua?mLiGIS6i`g?QM4E^pBKYQG)D!^h6=6s+puR4c8BvjcmMy(zHsJZ$6-f%@sE z*QHxmSFwh5Z_sTj`8+#@4btMr@g)0^s(w#D8XUZM-MndbWEUg`um?(NES_vvO}3MB zk=0o zE0t`nION=`%VqNd-LG)H8?L6*BQicoC6ZW-b3{oY4}^i;db;!YXXKyL`GJKMLVW-=wo5$$ONvF=cAEW+naMQgcq+4A&zdqi8U?5*$R z&*PZ!^=5u(!po7M$&MFz5-qPPsVc3wn-V%xepP{vLd+1jM)3vKz>9sgx#M=67L5Jt z{Q+r-3@gZ^Dg|eNZrT934Qrcr)Yo6_%HH?;n?_+U6&pS{&9*m&z;=Q8YjJ}Qk%=4t zmJ!?nde|KYva-rQ)p8%L_6pZy{{S!Ae!9-Q-@5NSYxRt$8OB2g&9kk^5fdVo)ZLVI zKebv8a(Bk#GABndv|&hbYmX2ev@!sH`F2qM0LBmQtr6#3zTM@d9x`c zgLGkQ5Nh8bsUIrdL_n;lu8Ck2Z%}(pseYvTG(41;0(mhqBL~ezC6IvZJ}o{HY=1PG zqTG5(0bCr9GDTp*EP_c3Z-&OrK{Jv`@?(ie4n>4U)YHKGAC!O0w|vunfUyu=VuB6wy~WUCB-N)uynZGP%4i_dX6BuC~5g!3$ zRlXrx$z3!tkN(m8m7>Da83%B;(?B?4S_Ik&v}w5Z1LeP8c%#^d{W@RM^_mP{&he<{ zG2D-eG{3QpGnnI`{)LeuAQCGli?+g7A8kqV=H%sZ9CY&6L(}6mV=;^Hd1aTf_Ury? zpSFWL#&Pn>xbF&r&7lp@__pist*!?dkDJ9~G^=@{+ykj9R_%WYy~POmTrc6`W2w3N zB#yP%DyadDMSx;0Qo|t&1lak3C6%UAw`)C%TcwM3(AYTT8C;eiBnef(_uNtZ^#dw1 z=pS8BL?72CPtFbg8___<7?TSgGEJeCjlCrSzrAWcUmit@d7Za!oJr~|HvN=O9Y2L4 z{%ouCt1U;&pl&8d1Mfsnw2?{4(=%PYwCt{b#jA&=owd<|zO;a@6WQBHwwARAZE@Gu zX{3u-8s+|qX*SmM2%EC@+OOkDj*862`xkNAH+A&23uYc-k505x1{7l`VX1@Z1!^eP zHzKi&XyHw=Taa|iv#bWe0V!ljXhPXb3Vx@^BWBJ zRsi(Yt8$Q)l_dp2$i6F{(cSE+A(~)7B#=5>)cLAq#Jh_+5Wx0Zwxs1kgow%prv1aa z(rAQx=Ea^;Exr&vu@ERsnOKpqRd85#E!h6;=#CCf{B6I7mc&Ydf*o$E-3Gc^wONYu z#KRpL{crRUSsdUKI#NFDhPE;$>=Y#G9f!SdSq@?!FFHU8vsu0MYDSX-TyRv|U46B5 zX+yY{4~?dyL-bJ#n_8?w@JiMtmEp|T-{r|;>@~N^1mnWp!H2kmSsc7< ziE}1(jM%v#^c`z8rjHcvhuUSRe2d$6|&e|9`?0pGIJuHXgn;FDP(xqS(k9X z*;v`FUUSXkoIYbJ3wTySR3FW65c=wp$W(?~Y1wQ2b)fmz4qxkq<3?{f7~eony06(m zlZ?!6vF*R-5Gpm=ux1A8#2$qD4I1LUSIp!w#!r~AM#O~-4&poQ-CV%oUOAR*kdGR8 zjBKpPeGO|HR{6|jWAfB02-GUCKrNsZ)8p1{wlAZf$+eR(H;Un);?Rhlo!kxOt<-mE zrC+YnuS}9mN1ojIa+P+KQOZ=QP%2cZP%7nil?N$TX;5;Nc9lhPm3K;17zfC67~U6m zcD~AHGWm}0-PWvS#4arz{t?u4u45+1$=kPaCdRy7y=bLZy@Q>)=t-U6WY6WD5r|SiIuBiIzE=5H7F;;F9MuyU?qZ8ws-Ts-cau`mHCR4O&__Hj zB#uQ4t~wFlQ?}8kZ8{i|Bv|86h=R(hEC3yVQf{WAf_%dPmk#Q|8a50Wx_3~>F|y*v zmP}dJXGZ~H0^7m0^}Y1HALP|q0EApKD6#d5P~${lKv6+yOARfk?Wm@4c!L)iBeuO; z)@tJ^3VSMX3MkT)k+$0nX@tuyo0T}^oA^7#3*bF2t~Koy9!5?|@!m>eJZcw77R~~W z)9v z-D};6u=WZIhe276dy(YCg>g3daUU7I?dt>UtvMpVp4(~dtgktnHVupk8+okz!-J?E zs+1YvIVR*V!@iO!D!}S)Eq#5uQA!2!yk1-r5&$hF$Q=RNx1*QRg-jS&16%-CE2L0H zW7w|OC-z$tlNkh+e=olw6m%U|+tgQ}?6wqsg#y)rWr`vU z1n)MuJ-&)L9(Oy5j*l!NNQnR`+z!5$BvihQMXqIIk_FotEF>N)bO%nf-eMe;kN8Sm z1&gS;KiwT_H~EJkuGhl2wT;+QPs~%^{{Vy{*VA&Iy?bh4%A}2yaSWbZilhx)cIbM6 z?V@>5;he;kbM_Dd+Er|q@ekSx$P86))jq?|10>`Db^$zOb`G{@X_(vYW+xt|L z<`^Rc3|Rm=kPuj&y4ceOLh>=;T+tLG!h`|pxUDB8gB$Vk#KfgIt4$mMq9*cH9;< z^IE_zpk*J_~#HYk<}5y+6=p=Jed*I!AsSjmD(b8>NV6!`@~LD{s6 z?o;1!Os*Dqj$4Pt#g8A)R%IHo1wcO9i!3}v)qzuM*qikoLDNcZHU<<}So1pV)LnuH zZO7p|Tdg*EUp3jXORKASK4R*uKIsc)_SM$nvA(vwnRGuFLZ_u`Iia`Z;0Iuf>srRng;=RuURZ~q*_yVz z;NQXdX{$Gu6b{=JHETNfzmVzjvoTR6#%(rgmbfMEXetpY?OiGr0rF=MT|TAT7DWtSzv(2xPWYG3W^K8F5&UGLYza-e2%AavY5rL9&z zk^4{AS!Nc@3pO@6u& zfs>Cf%{fLn757F0l{dYWZ)z>dllUep@d7RDtzIiS^BAsk7C-Be1ppZV1pBr6YQ{ts z&cR76?1)ov7PuM;p_!39{Okg71cJiW+I2nGG-HuXoQ4?vdVIZ~4?6wXo=XZ$9z<*e zEUtAPyMjISe>NFsTnvIqc>#&a^(9H~)1_JYpbtL?_mlqsD64t+(Loj;ls1DXBo3D% zoA~3=beXumtN#Eh235?*aK(23h3$I*wUpb}RIvDrc-X6`V5~<@<3{*6l}zqBnI8WD zyHQNGo=(pZ`sz;#CdQNC6;TFv642ZeQjWCLn#*5*Ln@}91_bM0vqny#1%F=ZPup? z$-G>+kcY;NWQf1OkK$biMUiwmchQ_Pe0BAou7L4sW01zryzafpzjtWR^`qE-$R4xx z)kZV_0OAANki<(L<>*IC@99>jH{i10?(8=`m)#Nkv`^+g!iPbZj~+x$n(jLt2t85i zvPX-P#N=`C%40Gs5N5F1(D<2drmj-~-z)2gq-$n>tq<8-3{H}I9Q{JG91j904CN+W!C-+LTL^J~++057$GbeZ2fy zYTI@7iYlyDPsIbRq(fo)#8f<+tMIdc4g9#XQZ90udap~J#kCDy9Qw~%=h>U_3Nc=Jb*|`pf z*nO2>lV?l_oug?AITwuWWgb06s;)OBDe$C=l7sxYJTeba=xQz(_{(PS*$}%a$nmqs z+8Fq?2VSDHSkIc#g@^?~pcWUm?GZ-_!px9aj2Ub2hPpWYRu(k^@bg5$L9}3lyZ{@o ze?wIq$HwHI$IJkFN)3I0)u$YcX#Q^55GmjAumjVuhWaYAOUTCAwk2{g*eY1)BQ3O> zcIi_^E>xS7W#B-DFirmem;sTE^*HUO!&jehW;xTy?*gD5h_Z6G@Om|w8c zT*CNsj~KX61$l4Tv|Wy$t5#Pr9FYu8{*z@NO)-R9!U5Ny8VZ$S5jKaew(hEl=G^!y zS#}WQ6}|o?@1avPXt!$%k!{9`?^BBC$6e|``Y0zQgt;n(r1HWaeFX7Fvl6dsb@Ww; zv64H49d+qd@hq>j8;e*UWnC8?Dxqxe*0lNCUl$r;0U^_0))km1+gem2ZWcB@RS^FG zbk=ukUPHN@RGj0=!04q;x@d1h{H%1)?bF(5=5#405|e93CH3^_K_*!8FO7;r&g>Iv z9ajD1bgVS9#m6pYNEl9@KvU-zMmlt|o|HtZA=t*{JS3BIr>3oO(nX6LcoHGUk}+ij z-Cw+;cI#1P3uHf}-n)Dswfi}I+)uh1RI9b=aB`I@R0@?URRXS8X;ECIU8OtHAyN}~K`AN~0IzBT-`s4`O6ldnVd3Sm-ITe_VzZEeJL`)TSt zN-flYE5`LJNKX4+-33@fVp+N>vmao+J0>{8jj9KT{S^jF`FME^{{W1S?|5neD1V0R zdY`tvPk@if;qVmpK$?EA-S*d(ukr}nsUTke0A+f{D|tx zT(Pd+q6X#Vi8z!Oi1krWPl$phAEd18#ru12aIzKc_Y3RX251p`l27b(cU zQA*UeQe&a2fqaNlG4Jy2{pPpXT*{0XBtSkVRkx@LozYCJNYt3u>Agjg2~bpC@-frb zZ?d9+=fS%^;SRT)Y*{)SGizMyn$N{ZY|ees^P~&6Rbz_5e*@?5x)( zHU!!7@zXM#i9iahr1o8QpI*IdHsNH*{8q2NA6;x#EwLyEx2LkR_{a`SL-fbjTDCw{YFJ9E_;B56h5!v+ z3t@XwC=E<5RJ?^FQSYvRSNNQwfz%!bX1L`-O00!VmJ)`vqOvE1p_* zUakzP3|N2dC-+4)upnc9DnIUz$yW-2(w9nL%PYun5o2b+$^jBdxC>g^(V4jDCu1C` zxd6Bq=uJn=C^>k8(|@Lk$h%}#9ji^aguJuT{I7{}*zz)HPR=xWoHQ`y#-(Sg#Uvy054^yyj_ zkHfTpRP_g?Q=f>$pF(t?Z}W+`YSf?qHpV$XY$4Tv7gRh*n`m&j>eJ0Ogz zw|%!7r!UJ9MoZ$z&_8;Tzu>U?Dhz@1A03S=tipIzPT;^cp0(P4cBh3IhY~lz$5&87 zm0~?A4XSwYqLF-P2`#I3^zRzhe;R&&Bg%>Pq%slWvo6pFP*l;*FB6f2A3nzLV|Y!R0|WoU#7GyV~+dS zF5%njqz?EAfP!scLG1$7QIF2z=2@)pS$xDXx5IMZqo+gZ=~F)~G|LQGP=XB-Bu(%AQf-lmdn~lYll(b-4_bcJ*E!jbx`W+8t5})OqKQOE$qOx0WOL(_#1a{xiRb=7vpp~SLa0SB+mpfA0 zi>9}}y(yml0HUtm|e{%y%Ipk1lCZJ-5}Z}ri> zNPnh2mS3)eV9gtR&673uqr?i9)3Ha{MswH9#v43dB_hOjdg|T@%wqYa1LmA?2VgW$ zl-zT0i*^tzPU`rFY)dDKM9!aXj`K8k#upBFl_Uok+~`Pk=DCnbix z4(~-B-hb?)yvSea*riSUas8CvkNzf~wEqBi`H67X zyxrT|>Pm)x_cDI5srXVqi?825;Yx!j{{X6xevzrK6YOpKU%F@wfYPn0HA*HrYgZ#T zu_m;MAKzY zPllb7C_^TMKF3k;hP}fm?rT=32>$>BPM0-};dxQWXDqXzibe^yVv-n!`_}1O90U9U zJDcnjg$VhsDP%~zpxc768+*YYMN;Eq*^N5xy^xFOdmFn-FVDx3EJ+qS1SBymM&YHb z2%x+q+u7i8v@msWIx8q^*mp2DsoM1J22`8OF_(<3_qDdEH|PeS^{qA$1jt6W+J1vs zSTeD{8Rf)}mbh03!w>&x<9qa~@$z{qFP$eJk~AQlz5RP?SLb-?AeI?JYf&OoLq}`d?Mqwr9e3O z9m|2R9)O;M*tSpNX&c0-{DaU)BC>Z+V7Fp^0x*=@gVPW-P) z8wOh){cYC&0FtcY$tYwI0depWZP~3^bpR86YP<>#R7dk>TY8$nidle03CY83xiS$U zjlwXFJ}FYE@pw31lo93Qx0KQk;gW1Y z{NDS6*+a@4UOHK!MK2N+vkQ%f%iX7^TI8k$!iF;|t7SQp8AYr}0NkFS^Z@>KWlYKA zIZP$~Dt;~j`NHSe-ITENSU-^yvX=O<=DE|af%^7#(KY7s;%rvPP;ul-OmsVm(75du z>*?3Jt@1o}Hq^+C0?Mt`dzcyuQmP(9G#~RJE%Yl_F-&_99SKA3Qhy~WFa1*A_i^k! zv61#yIPe-cw`xIkC;Q))fC~#VFL5K?ObskDdGHO-(c?WGM`!B}%9|tw$d!AYhtpAY z7Osp6@>4Em40$nF>~G>*?(Q9O%TY8TG~3Fi5v4z*-pL9QH|aieAva?qo-tx-RthFp^?>OjNUfYPiXH` z=@gWAX5^6f3;i@NlEwT?Mlj_Hv9bKLRc4M9kjQKq3X7X+dQi!xgzM)5+Fsh9qV;l^ za}@+H9BXnE1-b2_9wX9+#XJGS1(DC706v8ucy^kV&vBHV`Rp0a-9mQzrgKdZH_dmye?L5Srg>r z7$ZOtUc_#*ewA}+qMjIMjK?yEL)d%hT<{nlX6AM3yN$>l*P`8h)klz9GBURlJr!5_b|3we{^EbrO080-fsmQhfTSDT3ZEGemX=0U zNYwmB*Cc6bA2vv_Z4%xqUvqtSs*+|QzR+ZFM%Hat+-+}lc3>N& zQ2t9QV~)`Y6h=th66@RAbiFa>$#HVTC~J=Jdn|g@Y`C2jS^Bv;o~GUO$%9}H&m3xh znCiQG4z;_+`vqq?FOv)vLG>!R4x`0IlzC`mj(2uoZBD=N05*=Sa>@q;wxeX?Sc1F>;^FO^3=|+LLlg z@AXjigceb4Sb^`NIk}k_Su%(*q?E|27DWK4us7<%zL;E!GZoYen;TG!-Yy)>)y{O2 z&r5Bw8+<)9s*TKZI@=aoTTcrA0M$)iTbv|+rJLA``DlIzu%mdfV~zA>05`6zF03m# z{bQLHABCF#0I0w9QRZS$9;uK#Sio-N8a4t z_G+ih%mRokcWB4D)n_Sq3bFE*)WuP2ROD%Dgqv2PRJe*i7YO|!T7_F${>sd8_iX5g zalg@8PZsS*W|cNs!b4ormt`u0x2{(Wa)HC8CN;rIsNw71NvpK9pbyG1JdPs&0LNM{ zH&yjeo<`r*affgpZ5Q&@+}`)F*V$8UA*wYtuG;3DBe=aiP>RZ<+B#Qk16$EjSaq%y z7pjI84brLdQCkXg)A()mRq(YAdTzs+e?Z=frjXh^Vdw+drCFAL!^T((zruj`)qYPY zu`zL7B57E-O^QG|k4>sBMp-<)2H8&7)lQ{ZwLLqkcD#mVzD7)pil-t&@!XJu*S6R9 z+NZkIHlO8e0!EmBIaxd|}OOe^9Z9|IlWI|Nyoq~5p zdhbv?ahvuPG#YG>;$p)z~|tZ2<*RFxlA{#0P!&b+G|_|SO!yn zNY=9xayhc&MGWDaBmxSdw)#Kb^dY)Wmu-EZGhm*udc4pEzGqZ?nuZWP+rUr$<_j?037nKIrx*{lZP zV{oGSA5~~tJE#D5eN-5u20!%}`5QV)tnVPVg~&H%?Y4)l=)Oi=8M$#vSfo*_cO81( z`u19j%g19(2y_PIox?+~q*XkRm+@KNNQpymKst+s+vqm6IppH_WWH%c9F8-i76c~K zrLBIK9krt5p)JSbN4Z0@rrVyjv8ZMFe=6%@vZx7gbvkIrTOCKHp+D5|ih28(Lt>wJ z3%1i>+fTv)seJzcg3lK}k)%y@}A+ zk-KI5U4q?c4se$%HhW5uqA)5+)NTQd?|agCm^Qh#`m~X9IBHJmN7GiKW=qZej@2a@lI^e10Lg{7Brud`jZPTgUMAts`3;@J;-=75xB%S?`|_ zPZbemxmiVzb-}93#^kAHgDNT8!c+zUv;;NBhp%Nt&SYjy^5Ge3M3LkwUAmhs&(lfD zUKRdXhBC2AMyY!bRFQsK+F<_xEr;oSg0a6W{{XfJ(*FSBh}O#sq@S%&0{PoNFOP=~ zd{)}VM^*=+8)$C>$sDPrlO+g|#^O+T2nhG~kPTn?`2+FLH|ERrsiFS>E<&aidw7{Z z2iNAW+e|J_Z0;|U%Epr#KwYg9M06!scls++$KQ2{*UMY?^shJJu>8rw4Hz_bJvy)* zp1Sm2C=jvS-1$p;`Yb7o8OglH98#ENadL!#`$PLGJatsCP!xJ9=OwHQ`oroqN-NM_ zQMqk?x>X$fIOWHhDO5UyHn{W}y$R3??5S|W^M#MPmJ+cX3Ot!9F4bt2LEF@V{vE!W zRk;ukA^ejj0wdz#2;4tJr<;=qW0JN=9@%B8fLiR}npGe(O zEt`f1{@my3QCX`NGD40wFf#8|M8 z2DAocUKu2v_W5B^nD?pK^wuRJK7-i2ujqB5Rr*s>ymSv;Y%ZBu3%jc@C#nBXD;V45pF?|`rew0LjbRdRgPPnt*Hxfgm;%SsJ=YWG4lal zz%8e){k3y8EAh<(p_;o|9K0mO0@`SK)9NduoTFTU({5*UM}-&Yp!oSfF8hXs^q! zw>8)Eqxy)ccp%=-8nzMF zoHF%|p;h+XjWZiGoNUP^1-}g}mhUPKfLAutVDgyRf5I1wC4M+0@VK{kc=c1c5#Uzt zKV5QeKMy(-XItsHBpNc`P*sMasx*9*Q3ZDyIJeQ;NmKQ^8nyAX!eJR+)Is!)0K)Rly`s-5zd)v42)F@1R zL=sN&Av=_t0s$KJ?(0>X<>-Xph}w?orA_V$ziGH@+K)ntC32#}L8UrG9$TB+LTQ9# zKcP+|n6zx_Z1JkS)G#{qBG(>@g}BU^qL<8>m7^obviVF!wZBa-TI_W#j>Om!*7q8G z>0n;h=qRjIkH`p;R*f83f|d_*rpCNTOzyp~w6dq%}g&!Aqt0(@Lq024%o zZLZ}S-h;`*;I2MYv98@4Xbp9@VA^z`5yY`|l2Qos8vY}5@35jfJ5O4N5&;}4J*KQb zg>Gv|ekyOsh<=#*=-kP;oAwPTKO5uAjCKQmaHFL`)Y6JC2>_bxY(H&(SuXfYC)^MdDxfT`JvD<%}(Cs=@@#Cl(vm5uCb(ro|HUtnf@1_j9jr=@V)OIMK z*qn0WayePs%eF{=0TwFEcW7y>In`~-VUoZA5t=h#JDYH>j!Tn={l*;5y}NeEJ?L-r zQziwt)G^^ghp0uruGgxZ)I!N8-ZxX~sG<3j2rx-Og!f1ae~1cPzGPCz6GnF-Jye1& zZQWLKl|l5X(kc$L99mSZYF(5F*9CM^wE%3*d8h|)A8i{$<8M%)xe5@r`Zi)%+sB+wX7GZC@Q& zWS5Wf&UQh_LoLzv9oO1^Er8LvSAUY8f=TyKkC|eZ@SbEXW`lFuz}~{vBBkX_jxH}D z+DI}&iBXlkCGX+|{d6+|39)&x46Wv|BgVZXT>v&8qScab#X~7**`v9+C`jsk6l0Id z%!4gYn&Fz(V4<6*>XTOltv@A;=FHgAL_CHYVHO74M^)}Op&UaY%xD0WD{xm@NUdnDgHVdtG4qq21m5(ivp#%czs!ro#FJXU0FbU_ylZA?4 z#c*>926tjhYy!bh_t^o}*^=CksyG@@3Uh;_Xn8}TVics8o z4`ola8%qK`Ju5PtwpZX%88=BX_)3Bw=>3?AE;nd_*#x$VP-7pzB$W zns}zjU&KZ)BzN7WrF#7o0Eacn=)zK@3kw0$-Ch3xVhBibv6EWCLgkV*1>Jg6CWN~8xnh`DV& zBf72QamxlKL&+Roak*I$ZJ_Dbr86@P`7f0sh6W8XS;FWG5X5>ZFO;*ucgqBvFnezx z_iO+JLHoWxWo&Tz%0Ei7A1>qMW8<_FMq`y4TcZmdMwP9>U}T&Qp@sdoH9&8k^x|V~ z=|@l1Y9I2^ybzn{LjM4c-)$86-~+;r@hbgBg#505(_^1d_e&q;s)3v(halRE0G{yc zTdq4ogY*dZ8p3f&x0T6W;7Rs9ZmwsZv8mwmtXR()1rBS+E%Y0NWz}0IFhdwu&b&BwK$K$IOwSwZew5 z_8J+Rv9YjchV2`fR1GG?N0AB$ z6?wU-Oqk?HwvPILv^)WFN*)(fEh0e0kuP5 zLGCu~s$=?>8z#xshy;7b(N`t|m~~fOIx?EEg*Dh>Ox>U9q;LM)AF7B;k%gG0oGUf& z+`t~r`l&Yzv#B_*zS`_#W*6=wLuw#*%!__GZY*P%U-p!bvZmo=4aUKJc7nd*Ynf1G zW?{+4WOy@|Ya>2_MN`+-1xdx?J|-ZA$YxSa*V9&VUY5utRqheWaxst248KKFzLhQm zmCu2%_tbmzrAD4#4rEQo?k=QyD$f|ciH?FPQp~x>btGVDa&%D*=WA$%7$^M%I)Iv@hXteR_UMl!NW9XFM~4 zIvZCh&OArcufCf=sZymtsZymtsZymum38pj(@~HGODVGeI+})4s2}mxypK{GWd|_c z!^}xm?b5*wXQd(XWGm3IwY9ATVONog8;=T*%EzwdV#5B~&q9(&=gYqBTTZ%bP#O7c zWLKVG50_!NbhsK_y|tm5R7`8m zO35rh2Dt?UV$!yj`=a$LT?j|aM3e5ac?E@uW9BkyJixncx3pZ`jYS$Z0grb|!b6QL z_%Y_>%yFAA$t)4vgxeSb7+d&Rd9$R0qP;7Wid}PJP#&qx0 z)sL8WwiEe(5piScp^?1DjhUS4@&GNRtSTwXw{3?}Z9R3#0U!ZRR0^Wri~FmMTBx(c zrLxDNNuvdg3gUI0&vJccikgGEnrRl$RZdTp$Kzl!NOE8h0XC*irus02uF&n>$L*eI}16M@vXZ3jcUpKn0>1rnDa1!K^Xb?#N_X}3nGvU3hPkfU?8 z_KVu!bz*xL(?Nf#=cYl*$C+|OKhMMqwRRu99rgCrK6{ri?iv@9K%JHy{{W)29|w`j zk0pHBJWOvKhoUj;{YL0VwzAXFakBBVF_S6={u7JerMuMk5m09H__$xpSCFY2#A~>y zJ+0V2x(92ThUy%M zvLadGVDU%+M^kV`MaU3v^@1k5h1^k;RJ}7$nrKwGBMEKYTC&sYKXLjBsRwm7=j?rVKEIf?<9Uc|KmnH(` zmQCTCZL|OrSN>mvZYyqG4%>fpRml?FJ1Ti;oy6sFKdxkE$lgS3s;sgCtgW`&P5o3~ zC!fKF6C$^p5%BMe$6`V12WFHH2O%;(W64K^k1zmH#q3o(L8H)d*qHJ(u)?vki$2j{ zN2-|vXKyw+r-^*TV2Ca+{4k)MJteiE8&i(u6(LfATIC2J6l)c=`i`Qke8J9Taaqoo z?7$WTMs2XJ(W>dDwL)Hfxf2ALF%VGvY5`&{4ZiPnKxa1+TwGUNM+-YCXs>fGB8c>Z48|Z!RV)cE5z%T{>$}OU2DEq^sQzjkcv-wPTBwdW~v9do=dy5T4 z2pK%Y-YB<#PMT^hrnap{QC8t6kX^tG;4X*5t%d6$A29=5rZI7He~9jWg&ZrK5vA04l6`YY%(xE z-D`?uPm>f`toba2M>93|DFWxH-KeP>BOqaZ8YNwdp%zU@0Y$a3Ap7dpC(RsC+m9<4 zoo{w^+Ae!Y8hYwQ#^dq=$pKPrtHjoCX->69YAj5sWIMnyk_ao%Rgj=jpXk%N7mjPS z&BKe1znr^GQ+y1?QpX&+i-OU3wFcchEml;*kv7l@q#ti0_tNSpy2XtC0__ann&a%y z{S+TF8_3S{>G=HlIg+6&&ZYf$4BG}#bl%^#U6i`^S(Wm0YYHa?#EIfi^EZ^OGw_Ye(c z-I8obG7Ie5%nF0mye)r79cx3!mhy${bW(r2NR%g#HpIzNG)7jxsjW=j#$0E$%Ry~A z8!HVxI#gMlkQo!h1TxCS*-e(h;k`7W%j5z^smHNcwOg`|hu1-BXAh5$DMG$fsBU`! zaob8Y0ON%Z$v#D0FWF$50N}8D?DK0A=Ec4&g$9Vea2y?5u ziOFu%P~-MkH38)~{{Y2uO)D|5W@yFx;#qC)6{6&VzZsMX(V=^j`m`KKC6^V!y2!Zg zf7Xn~eyR-dk24k=lMIat77|9NPOWb-Mg0qUCx`O}U*d6N%207~5mH|;T#~vL9+Ra_ zsAbpctv$(uyzKic6NSw3e0+Ij7_zK^!%A;uC!iPf)kX2hK`2$sL8i8G(2Xt+ z%0^?t8T2ba8VW>KfQKv?>TMonQs)G%i z%HXJnJ|mS9{5cVPeLnK`(JUTE4?a|v7C^)RvLW!gk=~}Ki56(1kn6crpLJ5o;jtaO z>Gh99SXU(s`X^*C<7fA(l7R8IHS$r>?cuZ?w{Q@aJ`xde+U%;l~(tnh?@!B8D!*TXaFMeOH&} zJQ(HhyF5T0s;C#$c5|+})-ukidz}WL9;Tdu=7=4-sM7VHchRqFO8)>e!m7i|XICQ3 z@>uEAe++lkS$<=gp$f6a6cVbzHxYAwu4qO+JntD-79&tSbuK0Z!L2dyooaik;xGRI zSIo)Eh3CAXSTG=kx3yOKQT%>=4~3)*01SN1ED5mjuZQ1i8^dwf`36jBBmzqohraq> z=&5qPUYoo@t2FU> zE_5TPK9%%T*W{MtwcAT3R;f~@TVaRfvLEp=;~*?Z+~IoLt-6lt<`b*TRek^if(32bI(Z4mm2!7~ovDqCwzD|WxrKr@yg zKnLj)^wkMe3@DeT_bh7v04-}O4Mnflx7$?b4dr8lri9q_?4%Le%kQYz)Q8~}8&-w> z91YU9sC{%!;A`Ej4&nu(k-OXK6hQnZ)M@~nn_@2cxd#Ip!FICTT%8TZy=srkrUc~W z7?KGw-FE^y608T*#8m$PG!49bm(z5BeK)9nQG^>mOGXZ#q~!gTBDF-+vHUd+O&`K4 zq2`;dRnE!_f4 z=%YGPDXLXDY7$RnO($A~>v07>UT@;Kw^B8uMCyHJgK!QKIb>sC8i_|X<7PUO_114I z(rwhMhTghBiUmrQ#k$fO^q}o3R;AlP+tW)9mD*`q0T{ryKPm3LS)J@ACtbb}KYaa_ zTLR~s$UWEk>C(lNxie1g4m6h87#i*t1h=}I8nUy5$mN%#Mu%@M6tUZ|FxOA6rSP-MiJ36sjHttV=sgnw zv76jCdgxPe>=XmyE}m+4k@v@}RQU_|XKY{0J}Z8C_^QMiW?wCgV15~pi(Ki^f$t5i z4pVUw9IRKxeOQgR?cRdu{<5Op%GO1e*?c5ZKl5&>FYBX)Z>qAGkQ^>rNSEOrHT5G0 zAG%FwptM-|c>Rfy8z}vIf73<03a+yt?oYJPubP856OnCi9j*ImMRx%zcZ45pQRNI$ zLea_ave@6J#3|q9FCT}QZk;6Bgs-UBRsiqdCTvHF;>h}m%}sv0LZd7ta)+AkiDK_hx%9+`e^?E%enC7{t%)&#dC4&Ml;q=p>BbdeVxjA4rDvh-PBngNJ z2(dc!xLyAMRZa6eVR^X-?I8UEiQq2azN7yD*)?wj7lQ^2z9l1)G-nFPtiy3#O{`A3 zRI$y@VYcO zb8%TGAmqS8%11{BRFHZYR$G&bOkDKMRwW!pLbpX$y6v}h6XFQQIY*WIEIinK z+O1hq#)=priCH2+8n&fZL334dxoj+bvC2qBV1`f$MLnC3V5#$F5aTmm-EMn;)YeNX zWXQ?cy7@av5MRONpYm9Du=;3Wt(GX-KM>ryb<|Z^cz!-+V9^gj9;;vJps;coUw~X> zZN)(lJCXs4*9FMe!|>Igm$?R6js({V;k=N>?Hj1OLw?cPzKR0yV!@4z_%LplU;*o6 z)K(sD9!z}MVBNMm-5b46O5RBVJkiR?8RNdjJy&qETi01nnMaqM6EHR@+2|zFkrW&x zM^QD;=_;SLn~|3n2wNZUkmXGy2YCwZ*p{-6+Y_%tRiNZ?`d!EN{?$TrVv{|w${7Yo zj0SGNFVjIumAGo4#NxTECCLg$nW0v*6+JED*XgJ-epQMlGGjAFzt(WP_e2gsYqXT0p~L2UA+SAf6esi_yGNG_2-6RyscI;=`>N#pK|y@Vj57l+uGZ2DHmYkd`!RvFk| z(NoR_IegFIjknnAuG{wzEm~eV%41zTdt*|d8{XHoh^&-}9K3R<14+gI0BL0WwWr{Q z*dtEQEZh6n`)MJRQWgADwYC=_%AU6(^>P@|wFj=X26RjK(*2|_)oPm`L1pagR6jQm zWa6|S*qd30T@RvzJ})tdR!B&lA|}Asc9XuQQJPgT@g#3_)(69*N+Q1GNm=m^lG_FK?s zr;&}B6mxA7BY?_C*2aQkaH)}q9bM8@l0>?m)pisXK#?=hNV{yISJDK(kkk>QAh;Su zFu$@#Pqwa}KM#93P^ z5-s2i1-q5k?5!3Y(Mu3yb$w4mLgkFFAj(CzBM)EkH2NBpfYb4*GsP(Zi@-tyuT7`^ zV^lIYvUsQ-X5Hk=;qCCSAdh-c=u`L@PI|8R$o+%=07X~Hj7h}I2l;b-9%ugmnxdc~ zN&vbFXtIJ1qNE5yF1?k_x>nh0c$W`>1Ad4e?JF{$G3Q}Tma>mE7H}7KimFM_X?j1v zxo$mF{nlah-hy&?aU7iGbqof{0QQh;HD?2ye7G*2r_bp=cH1G=hw!vUL& zdq?JZWJzw&7pW9am~oFF$vTCJiCfs6y4G-f)dc6rl~g3CQ0yg$Vh*>ptzC55O>DJ| zrT0r2&psWjM0ZitcUKNsk3>P@wXLmo1p_Wv?R~)hT56}^nj(m)bJiSQ+ zKs5yRQ{i@!c+r~uk}dmv6#$tclrU3pbsoNzWn-{OhMI$F0-r6)Kd4BL8WXgrxb*8r z7b7wfb~l=u0KXOWDB65Nv;0f>T+khiNFLGsv`a2T{9`~Bm85BqG97H{B?Hmotk$P( z1T0$|5)pnA;5QnJTeG^Ne3(!YS!_E=7P;)IbLGnq87#q6$gzfHBI4r8-$iDm=kr1l z84;tKl~q-?mhP^a?~P=0x@P66dAE@j$$30l17HZ#rk@V9!7}b=*jHt&u0kDj9}(;o zj+d9snFD#wz+U%aZDHB1SM%O<^A(98o!zAS>Fe508ZdZ{7(2H$9@ z@3YSbvR@pwe~!Zc0KB~~+tc#;DON}ET%W`ITlBu;*W=sjwv?*>0Oh|#{{Y86saLCM z+9-Zp4d)%h`HQ;4uY*SY(?pelUEP(~asL1tkc%5^k~_LN8-O5M#CC(w);e#te~`nL z3`plBnB{V+*SfWZ^!0aD5q+ASI@*nV(2WVk$1z0}x3ThMXI453m0&iS4v0*N9kq~w z?==Od$C@TobG^!fokoOTqkYlz*P?v4#tbYJAl%4`PhO;4_4U?UHx3wO0WQw$Rh@}v zHrh2~TfAUniBM_)1KcWQu9L4yY>RPgQ$PL#yH9P`&z7{s z&U`iC-jxC2dHFMWJe)ZYg#@$&DHcJ}_w4l1>ze4xKhMW2PXYnX>trsMuVb&J=dtcD=^ZcV^L&sSkY+CFWq{fFEFj7D9!2z?W>rL zM-~3o{qAb^mi5}RJZ5hvBS8Yj@kcGny9FanPo}hmf><#o#9aEQtnbc_7YP9S(_h>P zseV&Ju-yJ+L-$##51euGGNOSoV`;2en_#`Pu%a9aEO9vPQWXBmDEn$aT-3{=ii3Eo z0IjN^CpDi28wv6Q6jBvJjmGVBdsP^r67o=G<7*yNZsZjTbO8O7@105vctEwNs{ zHD`@Yxfp7G?lJmyipx`vu;gLF4Z_E^v)PTiHe=jPX&C8QnOGU-Kcs%T)Ml$W)}dl* zXVlaTYe+f3#IK6`fFQB!=&S1lXhSI1*Hw58Ft6T8tN#FY^_$9@eQ92~O7tDMqzR-c z0YHg8hL*P#))p1PKos>LNG+iN6ZO>|BVI=-y(jB6Yc+~t zIlo13sMOz+ULJedr&`i*OLWEc$ZIyH-D^+4i;Q2@kpA_lvg!fJHTevm{a5`o&hyAm?Ns{IC8aR=Ac6_>SM`0HkP?kY%P!@;1?p zBg3-a5k8+)kG`ll{{Y@peu4i0?a}BiUknJf zfZciRe*KXFS)Or9a%TO$QTKvtQN*u^TmJy^54c5I+xBN))t&jL&-3B)l|iw#(nvb5 zcob8Eo60G1d&w5R{+yLZ=Mnr|bKVcuwP%jeqy2dQ0Q+rC`943?C!LlVTXaMnHMgwM z{{WYv3{io#nm_PMKsm($RQ8*X`1jU>fw~0@czlRCtYX zhpB(SwR*Cw{7e4;`(8-D`9u6GN28E3f-5hDoF@6QQet@buFbA2%0dlKqe|0V6x#N+ zy;K-)pIT`1plHckqg)Q1MGWDxxd9-#CN>NTbm%X83M=#DZ=W7W*xJmbgQe~&hX@NO zB=~@r=#A=`@^{P}jyzn<@Z@8xs)cOFufV6M+J2h5!el|2AY655!iMlMzew0zTz7hC zH_Y+ho1NHvK^ylLjeNZlhAyB3YhTwCpOT`r*r>mT78>0Bbzhc}Tuwf0h^$^P3g4!z z2isEdvTnv-RYT94@flyLKg6^Zb1~(}IDv=_;@oYgXJuFr`5{%!>^jq=w(Mi+Bj^=l zApt9H(Nm@DkOBh(tqta7d{>V;MrVZCS``AojovMNbU;GeZno39seHwW_^u*Y`Z|K@ zdy4Bw0CJp{56PlS9J^;rC|l^tPO8J{qA@f}3>ig-@Rl<6Alqb}w!dQ<$8q?6aA@=e znLs|XYS#R%0fK#!*tC${KwAI}jk{{$1haW89zqhBvn-#Sqo}(tWdi2aadP06+h$;l zNE#BlHof+>73I-!u%^pJ2`oe%=%aSfMf)o+FU)ydhBudy2q9PcWx6ssZLzYKsnpVk+w{{YRrf1ioxrToluyVj>h6jzD7 zm@r&0^3)~R5q-WMhP%qc9Jo`$_tg!^^lMh6U?IiWMAEAWuV&k)Q*AB{t5`UU83sOH zI0+g=s602ZR@@uD`kJqk$KXZBtbiR#qcJD%>EG$8ep{YJ^KfSW04V8tL+U4i5J?@? zY@`d3()~#KXh+T%IT>?c&y4n89ByuGMvvL*HEZTBN+=?ajaMe8%Y=O6o53`zyWjF zL}McwIdW$^gkYN}_WR9Lhbk_21RHE9_jt~|FQKPLYF{kHCMI;S0DPuf`&9lzQ~d99 z#0wh_HEu_Bjb6ckB-_`p5OlYr?5dwQ0WSvZr*KzOaqG1p^SqdwcqI6UwY^kAC{Sa@ zny4C{`_pn`0y5-kIv-t5%Xo=8&UC8 z1LeqZ*YPqeawJz`HNT^+#aT>P7n70k83YgGQE(I&*|}}m+iGOOga-UsJ)%fk`~&`K z(r^sM7`Ny`k^8D>1bA~tgOiV9&lb_pkL8tX{{VF_MmSbwiAL>izjE9QThV6`sx^Y9$~c+qWTYawvk#%fqYp|kBT-L`c&35 zGd2zszY-&qb_8@bUl8?lqv^qU;Y)ZP&8Z$T=Jgm{;*i z5=kWT_S@k;zPnAu4RT+V$qGX=!cCO;mC#D@RQK%_0uviJgj-Y1(EzTR#O@ADPi#`sHzaVG)P|8wzla>+yW?6M?Qb^ zOl#N_SPJU#h5S-T%*5YQT8P+uxjMWuL|J#M?hAcvKFyvr9ld^y)5I8~Z6bq~Jym9CyxR>dEqm6mnF^U%h-Ff4h;;8$YQGWPiaW(mFDUYD(QE;?u6Lh^i17S<#CeIm28S-1Ep ztVupB(7CW=k$tw)>#VEd!MDV816tkw3&;{m2K{%c4LS`+(OCSvIgVq>HOFvnx79)Y z8cbm9G)02|ur~)^_`M-MEF#E;%q}`>U3iu@9&ogMoxpZj(^k>2k^E7UewQAL3JIc^ z?+UB8a+clKrnaLz22(aZQ=#(|01QmN5;ebXbq2?Tt0cd5&bK{pSAJV-Pm_#(@|`^* zmx6LL{YMw#{2;yj&Zpz}e>bHnAN@;RoTG>Iv~%Uy&E|gc0QL&EgprFJTspgbaFJ6bpOloD>p2GZU4thjC-r3aO@M zIrA6Uw~LF7xcmvjk2uW5$C^_+Z(k7>(6>YGt>S_PwUWYze>no5yR`oRbuU`}fK(~s z)Fb9R^No(MDi%2*)tG(MRV>G|S}Sd9G4p0-$1s5-J4RLzqr<*KwTblY78R(?XCjre zzk!nPBi`{f{uVl0Y3w>wCySh9d1RH>RwNY{?bBL*5=O5ROL)Lw;EOXI2ve#2;nIuC z8_5*S6E(|hAd8ErADi5JtDgS=CD&|D8`nu@Z!1``OTtSI#a!l)fj(N0Y#5IS?a+3b z*GB6iE2+A$xHabS86e4;11`v$P!i21Ckl276~s4PGWyQPJzL^9(!%nF;S3w=7?gi6C}8+B$sNv%e2 zLH_`Rf-&JhJv8WGyI-=c2zFK*#~W`wJtV(3?Zyu>`83~q}yFR zRS@8~lh+x28n@-Ae?a0$Hpd>q-V<*?6dd#d~ykYpP~*K2gK6byX7#cp5p7%?TCU{Z;7vX(3cmp0r@27l)kJMqxyOS#w30HU~$vyzP%bb&zju>6%5%sgn% z$%y%zJdkV%xNXM&0A*Ul$Mbmyp7vEp)DHf&7H}+B8~f`nekl)o8q+zlG42Ml0F9*j zO=>fFMEzp1aEnu`k7@>k zucDqQl<`2i(yktqyJ$MyDU|a~bKy4bpbf$hX3Jygs~n2~$4jqB-~0xuaL8ILv)@)Z zpsU47K-bG}@EV(PYr_Sj5Gz-}5B91x{u`g)tfj@hHLdx4SmSyjC*Jhgb!GC~c2w5t zWPkA*yTlDAMl|2X#W5u zV6h+ktMwWW7$4T;)9#V zlCmjQ00!&oCZy+4Oh8!b$$JvLhqO>BIQ)4F-7RC`uvRwxp?{{POd^|#;-&-1$Yfw{ zq3>&teH!91T=`;2_Q*q*5s+5wM@#pCK%b6?J0cqe9~Ie6{WfAgitpg^<0HzIx?bR` z0rVlOmBIYq8_s8KX=07cB!w~PNVw_`Zk2C~#*$ry0c$uG(0GzPq_YZP< z+%oCuE4WpN`F=+k)UqpEP*v5h*?$qGx#c92fd2rQ8yLTr3pV&ztcdHhf;>7N+R*Tu z6(c80`A1*Zx;u@1BBa3OICvNVs_)%rA&An}7woHUQfrS5KNNq{ zfo{eB00PzHr3&NuPA#G;lcHUig(lh)aYT|x@4g_{%o0|ZNZQtn;(cKqv)+EH@Kj|{N)Kz7VQahZ&B8$@M>L0VE!(@{+rQn zp0V;Tj=@2S=I_(QY(V-bZ_CSg{Y@x8{W&Vm4(h+m;Qs(qVomQF8~(_m{0jx^+i#{h zXh+QC4oT1uO2&ZOv%)CH2L|<5y{r>Y*p{AzX!v{n79QY%K$^*7H`lw-s;hOUk)5M4O*0vlqy!cOPh>vkU zlCgY4x0u&Aw$uBZu&>~9FQ_VR4PbcKVw)mp*j-H4x1hDY z+Vn0*Z{TtZ{2>^hP#3Q~;v`?iyD7SvRF7AfDLAO_F~`G?nTO?z31UJ=W$ZQf)vO*p z;l4tj1=N(V>$!*4mCu+QHdh`;WJ(X!_EBy+Fwi(Ujl-|&$@yyD2-J&LjJL(UgggJ0G4ux z=Ee`WgIVagcwoiHnNHbp)Tl0}VI+8ZRqW_HoXR;IywYzawAxSI7PwLBtDI&|L~_HG z9G+QnRoRB%#1nhofKMaMD^Au8AuPZh1jzva5q)$I$!ypoNprq zAUhgXX(mv57|zYUt=FkIsfITp9Fge;Vuk~-H)!|Pf98o}lb2%ytHi3d*g*k^8^)fV zv`37Se;Wj!dmd4*p|q!GO;0zS9v&K{%C=K^O3R@Di?e$L0)}@^I&Lo@?lG|0e$~Cj zTlta}%8nZlJhd*Pw&aQtJiC_`Vb#>e{{XYnlalZsnzlwAf}a@O3i?!iRPI4rW9{lk zZF3WU;n#5l+e;40-DWEv1>2{6O^eAv0r80&)NCoNq?3AzECDqBUhSav^*-vuZC3}G z27Y12^5g?!7Ys>TTH2Z=HwTaBERmh2*0JHHfNBL<>h@p$lGw>nm|>WVxjk)RQ%nS{_fwhqns-9@pgH* z&ONB${k2h>5CIeE@I1TGlRT)foSI5GNO$_-ClQAAI4T9_KtNb*AN63XmmU8=4 zl0bAe>*-p~!yara2bC`6NjIuCqj0x<6#04bM9&WlWQxe zQiQKt^s68ddzz~GSy0V9Vl*tWHO?Hq0=jF#=0%H}N9|Q|=YFh%AVbDex5* z9h&P>EeD^Fa}5+(^DX}N;5`kkdK*NhJcQf2_t5^3uRHm& z373=((X=K=*62t-4~F+s+gg=6K^|(N1=>jhk>u^r4{HyysH7W(&Yw*@W3T)Qxv7Y- z+?1B^mG%N@hdy$CBkTZERu#8xI3#1Ohpf{y#5J;|Zq`1w{FP@DJCloRLR`j&Mc&|F z(PK@GRgI$ok8Oo%eqALoC-Z{&2*8la3X6`~mn-28FU|Q8*=OFPp%MWq`^MI(W8`si z$qHkXwty)cwfz>_)Iqr3D>M?ZslRM2KB^5QxSS?*kSskwB3D571a=TXTFS4s9CBlz zDs8kY0=E|$9Su>*;o~t%5GdOd$e>&srL?sX#pf`mO@NE_vM?w6ue8;)d`Va)^#y?i zU%=KP#0}Q#L6U)q#$NUNDqbOsT-X3l$T*SF#bjbhmnL3sz|uR-V`F1- z2eg`(fOL~4Srru=`G;!hWg^R`qV*pY!ZuQtX0$BPypM1;pbfvTrqy?g!;c?6RK;;3 z%^G=f+e=)r{{Rhh#<{q@ZaU`U4bEWFpHdB68){N+HKiR^T+0z;FJ@4Q^DoXgK(;OlJap% z>x~BGVi-uSppjrLwXNOXSL3rV(v+6pPzOAZD4)#6l>Y$wVTbNBQciS4Z!BI}{Umhy z*3?$o^{z1_SelttFYCC|MCh!W%8)GU*4@-52?ND(-e|8W6eki=@uMB5QlOdrpYSveM!}uZ0jKjj9N@eOdzDx29q(u~2@9scqRs_uIs6lk=Na;%YchxAlFFQU=F&Q3J!IQG!1NP8eG^>}TY7XB^(`B%Yk8+=?o!M%O zB-#lNX2#dqKre#VA0@j9uJg_R0JTeg#BZwKZ8w6y;PO2txz8QkRLG!~C_n>V)v34a zv-M$t@pY|#0&F2Y&<~}nB4c$vAV;pYd>W0Ydj>N7wAk0SHbc5bziS_^ig07WKX@^Z z-k~JfiS4z!|?U(*jK!icLsdXc$ zKi`Z|ytM%13zK;d_)TZESLTvgqVl6%#4>u8oSLaQm8_54_dGfy<)q zO!(vnvS^3awH%Og;9gv8Oo85vP}|xlAmhB2%1GYcBqdbp2?nRn#fmNF5w~@&i)&o< zu=I|#T4>4J=JC_Q=x&b#E7<$%hcgh}a+-MU)69f`4}XT@f+>|O`T+J9vh5?FcMa6zy`QWd$vDA%(>N*W+=(HU(G!ZWN1? z`i!*0i7XdrXLZ|aZDW0X48Ls=lM^W(LSkUH7O*;jZkuW&6BcnOU@W8#>s!9mFsTMc zRyJJID+bCyfJjG(?gy^j-IPT>X6E5*OB`|mp}&Ow@Jgi|YIz)m^ha;*`YPCQ<~_+4 zaocX5x@5z-e=(fM#Ce!IkxlgRi}d@qs<^qFG;tw^-|Xki))JJ3#@> z?gjp;+7prMk$NXzo?N` z9YYWWVzILMjC(9u48p|iZx9yrh96a4i{>s~EhdR&09@FRZS7I@(J(ik8!wb}w*LUK zospzy$GFfL{%M2Di6@Rw>euM3TTjt7H~#=4#hc4nBa#$42^;{T{pR%nn3-6G@2cGD z59v8eu8}$Y(_D)y5rx)dBhYX4RLqcN<4FmXSzSmVH(?ZuUTwr1O8)?=54o#v$D7X! zDDbB6$M)o`((s&)4ns7N7m3EjXvOVw)QZ#b@JkaZVWXF~PQrB1pU$gN3g_GK&*>ie z^Q;s90J!9B(0PL1$t%$Fw|){$y+WUTtIfEO@y{TT5a0rD-Wr~#qTkF5vnK(;@icZm zp(6@E#<2W6faz=h0FV1<2h5<1oEQ71G6D37nvURqx0BKj_@h5IlJp+8Znxmoo%;_{-bGt`Ni%S$@XO{qO{1R^=m)NgaJjKZjV3bjo`%2aU zWyl(ax2T$brV3wc?z*^S@D)O z1e=XM6(6h9)JbHd? ze1=crriGz-&=DT;YzKrLEz}Fr%ga1?s-qe_36ly(5>5&@W1%~Z&AkGp7a~P-yTS+$ zXXw`Z_o;b=RLV&O{MJx9X=Oc!zN)kVWypgZ{{RuUlInU8Lt9()sH2=Lr)-k3xgZ7r zSOOS|c6k2)-MHrl#r)*C^sWBNrNT|(F* zRhp~GVmtFtnFsJ%FB6WNfi|G&5&Q~D*9G!Ct)z4}G;8KBFLVsdp_nj2dXfk_RWi~Q zk~VGDV8B0&1`TVC)K-U&Qn;csVpQ!u;Z_ksp-Wh6rBUIX-yPaU+{{B0YidQl%D$T1 zoosDY7+|4P5-ql3KgIbdj|NZUSqD(g2edUsJ|056)?1O6rM0pXqZ?notZ}xBk(rK# zX9#;bfv?k1RP$H=0Cvlb(5cQ$r@w>)rauk zPkQ}TmHFtEd_01>x;ohP79jn#xBwNg=~*A2;{O0cj^JpLu>SJ@0A&U~8IH$dFJqvq zF-7M_B)I_gUaH~%!d3kf^i|9ge1B1`M5@^vVnV@hZ%sP}VKjkC4F$(-OOk=2m%nYr zRO6g3LLTrz_D~iv^--qfBT6muYn|?gTQEBMZoh2}xelcCy;}Kad|ZRaZ;w^locZ0fu&U@R_yoO{dIFMK0Ze+@}h~8eTykQ53ZxzYqJfYYu{Q#enxi9_B(vP zGqd@7yua~Mt6P%+F&@t0%C`Ri@~_bJ^Dj!X=lO}+Js6I`pzNxAhc}--BV)6&MYm}{ zKm>PNwy*yHmQSG2T(z{pNC8P6FQ%;{kGaTS*Ng(mY>`Z5+fj*Af z#BHn&}rBj#MZj7~Jmk1ItWk}x&^S?+aJ-RPolIg$wEJzW)#v|fSo zvt&0U(8|+C3~{8ms{p%8AV0*ume6VF>qMcg&f|UzBfv9em@DNvSs9JbmI`%ZyL?8w zkIZp+!|{+}JXw-OCzZI}7$7XAwzqD&)L9(-b0ITf!hw;@b0yT9*qB2V1U=7+oZ0+& zG0T(1F2E#+$+!~97lzS+YwFgZ*~G-gg{`=#EC)-Gtw)+|izlR7n}L%aUL-j&g0aeh zl}}I)Wp^;z2K3Qr78=VO8FEXYBFZn>()AOvuF_41S|G`{22kED%m>p#b8bo~?x#WG zMPp!Yderz!e7J|{U)`-z#~w0$6hn^JF+Bl~(N+TtrK-I4g-ncWJ?Gde!#33?x`k#5 zGEUyHl2yTNx)I{mvq*M7Ba4#F9BrsJ>JFpQr2DG+p|_&3&x8Fhkci{8;hBPwe!_mR zb*+XgmW4}owzR_Zni1j#-32|AvAwjV5nad8Qd>$?L9lyjRg7&HrtVO{5&=CuG~!Jl zrOL(z6s>J7_Rs|ujZdPDbCU?L8t3953dzsSLmz=voQ*WCP7}4)J+&D(U^~d667zB& zf-pU1puP#iaNW?dm;5Y-JK8mBCCUZ!uEOMlhd^feN=(QWi%!V#C;l@GuykH610=Wma z=dW!oof9n{A{d}Z%QS@m8{bwwx}Nom{-w{Yz*(*Au76YKiTGyrAML4?!)s9Hn_^6V zLB6WU{{T|w!1yNj{{U@8mCW*sj?u`ot2&Y5BG&Egt2s*1a9`|ZBejp$UCG++snx7@ zi`%-hJYHun<+EbO`v$FMwf_Kg(RAnF0y#1unmL!^ynBG`(5UUK?dG+wh`L}?qs6>? z5BRN0IHQ6`kz-&THWdq?w|3jrR{4XAajcz^H`i)ma;E$j z7TUGb+DG`ROinH=EB+b726)+n5OhP?@2kQ~i>!y*m-NXqjUJY^MFC}j+~5Zj3w)$GlSk&EI7SMBr)c9(>Y ze>uHl>J;A<$hU1sw`n9JUlyL>RXJwzF`FTsqF6z8Wx3D|bOc_fYIas6S$M}EAxlQq zxCY$~#-gN(C59D-JMBOKzNW&n@qEjQ-v)Y*M2gGxt5w)JvnvIPYFYLXrD#;+&}=cpYixl=`qhDj8axCD!7>DJW+Jm{iaZ!lq|T#IkvSwUb= zc@-D*PAX^?QH|tCz!D>7LJ9p+D%5cUTk!rp4!_nUu;)j+6tf|v3$ zGGVtx-?)38^yk9mlT^)FKSo%T{VQrPu&a&8K@HILro0cJxTljWt3E<3+WY3Xe7lkYri$$Tzf*wXf??X{h)TF39rY0D?BLD0TEbXpdcW9n}Qb&^ZD~ zk-_g!Y5~akZ;r6EYb!2-NZDCfbPe%dse*{rq)s9={!T_jHs}^sBiz@jD8!x>3_MmnY!A~<+EV0P62I}NtK$QZ1nkY^1PQ^*crGd#*l<(~}UWymH8< z!$41OtdaCn#MtcHeE7h=o1br9@b;@QA46#FID=~(V4KP4J>cvvXy4BvYXv+P_&5B6Je!~w&c+l{{Z<` zU|pFwio)9MKV|4lvvAW%=1Gq>JCk;F?bh}>bgNB!me;gf+wG>5h;>nRxxI&VGGWZ9 zxGqKrCkTQT}GBFd(We7GGVriIF2x*Z~~BL4iu%U8mXHAUn3?&xzH2+pKV%$n&zPE zB2EWg!(Nx`b40+_J^Y-7{c-iyEDf?uqq8CZ04*BB=J`o{#)k}TW^0o6*SD(BqmMgF zl_JJ$CpQCI61vpehV;HeGcn3$NT>6U)2&Izp;@OL$cOi>Q}RkqdlEGue}q(6nq^Es z@PqDZU)3k=`E!zRc?V9?k)L{srr9!M7ZIBm05R?zYJOhp$;5jaDE^`Qbj7ms6A_>k;?6D@?aM1 zy*Mf!_4;X2+IyhRN!r`0ugC*pf%qlol~f%3%dbR zv}}MV)6+s!P_bed8xXR4JT+q%pU%V~cZovuC$F)ERfTOMucA+-4OO_cuVY!woI2k= zZ=+JbG{KgNnSqe0y1&I^YjvV{`5bms<v^BU!g=cd03R<#;p1eQB5oN=f_v_xUu~-IE?qHVaCJLpEv#{55wmxY$ZKZ8%ts7z=JNAR&!+whIUx9p)$3`>=f z6i>9so6GdEvc0NIu68a@2t$b^W^KTgklN}yuh7&oZ2}0NX$yI1@g~8 zLB8r`0P~h1$mS_+gn-OV)B$y2;?ko=k*A4^mO>GM(AlO3!%9)U$X7Z+s#G8)NQNK5*Z3xR?k@$cQMg)Bob`~+l z)`w?`*3{|WL$RA|@t;jxf<{TEQ*+&aU1}vgli`)QCxB$4cH2n2LDt9)ZcA1L$MGQEulT)tRs1Rlrnei zH9A(OkmQ#WDl>JONqkYNkKzD&ZX_Ff>b_H)xH0l}fwq05i*CDy2cbH2wN&MB$jeDm zREegHsA&M(b?_C}yjO7&Ok5eIAw-daHYh$>2*BFJ{UWl`iB^UzMnB}rQ{R8IS8fr& zU;f*t2KMW(Uu{jc!8OQK*wZ|v5!u>r}oj3P>8sA)gSN>dVcII)R%#CKpoxG_ML2=^I zPq5dcr^~z&n~Vqn9S+*^%mT@ujz)~K#(lemdk{Ay`b}!H(deO6DvKx<0CoXfx|3i< z$5BmVU`^~REhbMSxmkJqVmz4nY{EGrP{6V*XpVPwd{jp{o@0_}r^AW}(N+AUB~J7u zmu#AXr+;Or);AjqVnYUMESNv;!lN`GR8JVwhB!N?I=#)I+p%(A@Q@ zq4|fDiXev_OK*=I75uw&9PJ)5^6;3Cqr#& zuOlL=s5kAkfhK~kTM>qzr26}f4UrX&<{E)&A(%(uVFRHgk6@wZ>`I=Vil=CX(Qu;P zsv*RS9tTOs&t|pK=H`kzz=z#Do?tHH-1 zw4X75w~c$=gJo!nDzX{)I9K68Q)9rZEN!6i4xYM`W`xwRMhww_c7ShxR<&{AS`|J7 zw|z(kn$r<45-`^HwK%1ZLu$g;HfyI-PhlQBO*HHg6kL*Nl1UaUMdOH zypG>TTCWBl4V@pE9C6H-+Qb41u&Dy&l<_xOQE zQYbkPDgGWrC-1NPXlRSi^5+rN5u)^?Tk5F4phIip<>VrRZQGcw{ifAx&-_9x;De6C zbRe5=xYWJ61?We9l}2_M84CE*NSU#Z;7b1h%agt}PJn)m1fzuHQ~6os{<^9E0G5aH z3C=;23=br3uB5D+c8iXn*wOAkkdGtudfBah+g_gCPJXyK5SY`eLLn7UsJ!JDN7q!rylEONWLm z8R1!GR`TW&J%bAe!OEnEnVOdGO(@;f zJdD&B5GTyjqAt}{SqEDwE3If^h=NT9Mg!)l&s)TnAFycd?#c)xTR*m{WM@T>AdXnt z)(3DG6h&-Ky4FkQ$+9uO2-xy8M3=cjR`v_sLnBGaanGTBr_=qHso;1d7lp$G>;C`- zyyq8|<|UKM87xJKyMX-#{_r)t;R*a4Y;C#GS%?4vNYvPAMPG~_zMA21blXy^TEf~? zCdzI`fYcST;wK(Ej+3v|Q%;)^7!I}ErVWY+{{XtREaLd9>#Nb)lf@H{8Y|zt1NGEH zhvQ`hpBg>g>NcOon z6~?!}_j{*lSS!3pZ%r9E=!iAsdTz)Uc*N&PO3LW?9T_6pFi8Xu#{I!}%(=8Ig$( zG6rhj9$1R*Ad7b2S}@SM0$EsI!%^9K36VT0%0kZ@C_w7tX(fRg5ErdcH_R|$%-FeD z4kdN)OpDkDiC7PP2E*KPiz++V8-y2Y5pJH^Jv0!uJhGVd2+R-F>Y$uleq^rKBEV`r zCXm}c-@RI!D;!0$b1Qg3S5l`#RrzTmmnb|;9ZD{s-iWNMqRS~Nbh}uK4%;>8JcLN^ zAp6Wn00Hz0+;Zp2HSMldk;mp+cFPY*sC>C^ZKnRJ(Bb7T_~AXu7P_|Sf*Tg{KN?AM%Pno5l}=i%(MBhh7L)LtA8)4(05WMRKX#y zQ>>0HuC(!wjpo7{Qxt4JjK<7C{Sl?bb@9}(5x9{7J6lyJ*{xG5=-=v^{{VO&rAVrJ zSZh|cnDW(Iu9x=dRY>dTu6LJd#BhrX>DHWg4*MSZZ)di){@!98g4u0tUZr!wuckpTX$AsqMfXaz}WUtKbEp` z)jXHGLL_8s+rmwX`gOepN zFL?D+{#h#=1|FMt4{@lYP5^{~rqQ7FR$u4H_eF;_x))D)2DK-|q;`>6{{Wp(w+|bU zta7dWpo*~`dOR`%+-m$|{c~yIao5>U%BRXY3w5h8i5fL;Fn~bNR-zQJDQ@yX6eNot zoiw9+cDj;D*R;@#p9Jof_*ngPq!qy__*Evsx*uhBP#Q?&3V9E=o)(8)NS(U9<#V_p7*ge}v3|qnFHu+$4%F3X|SDK^17d zlNpaaffg?su^M$9+jOXj@cOFvA&)EM;=D>&v|C!;>*4zdX$zZaLKDplnF z0N4Kjj(*(V+x(sWPuro)9lz1e-kap>yU}}Y*Vpt^oK?RLzsR@#8||e^xPR<-`*P{= z@i^f_nA!OJUEhSiogUwh&iKdX>2F+7m3 z)c*j-Z7Nkm8?6xG`b+yiZ7NlIK?83`L)L`zzy9l={{Tqz^$Jw0HGNtC0F{59y*?>A z{-CYT-9Ke2RiOAf9=&V1H}W5^L+j~El|hR6cljCr0Q!F4y<1EV_K|*~e}qz{Q@V8n zK4Sh{U#x!04VU+6QmrDm_-aW10GK-TJv~&ZRp@Ckf7?UqHP|n{OZlCC{jK{prAo6F z@(J?}@5N(Z%=o-MWxSjIjlTEmP|5G-z4u$qd-<00ejROY&sS$kRH;b2!T$iUi>E`= z)@Zl+XW{66E}q&{sxeSZt6ZOcG9I!MZKZp?c`&0_&YlMRX#=cOYwA% zy8TV9==*xorCGezrxB0mKb;l#{p)V{diGoIesx^=cKf0DzBw)Vy*0P!es`wUrAnP7 z<#sE7PkpxjWB&jy`)|1Q-~RyNdJ}_x#pGYk_>*?~ZoTyB{$>6Pdvv8rgGB}Z0IpBp z%l?&!nJs zM`#5+{3oYMRI4h&a+myc_WW=28{xN?`xoq`$M5ro?f#E9ZtrH#wv{UBIA)BuN^16W zrAmk|75upN`Mp2NwZCh1O%Iv8-^~53pJgglmX<#6XIkfXPL!!s6>3`FHW#&GNT& z{{S<|(%salRf5kmZ@^^R$o^Y@D);^B$nm$c+js0+)7JEY+eN(2ottg+ z(>1XEXYAUQD(L0XNI#qRt=s2(KbE1Juiy4`rAnQmTt9)n8(;j-S7&W8F*~+e(!p*Ixc?H{H-%%(c4qTe7bo=1F^6;`=&# zf3lS-l!3Vx{8-y-czz4puV(6rujLN5-ra3(&YiTWR&Oqo^@``AuBEpN+Sl#9zKT?; z2hF$4@a(pq1@`{-wR<+uh&l`2)&ir(jc`<`q4Kdd{barjr%DN?Bv-ufZH-_FVI^3waQv*JB_Z%UP0!Y^~b z?I7v=`F+%t6e)357xQl(Z5BmV#+_G)~8_UY55 zDpjUZN%1dA6QBO_U)JpPQl&~ot}6aZ+Wkj$5|8m1ea#{vkg%+s`)H{L$9?@3+(R(#PHSS#P^-;kNC&Z@*72 z-Y+w{{heu2r$t%hZFKnh9+$VoY7SFx;svd5EiG-`-koVusW}65^!+rcQmT*t*}e|l A9RL6T literal 0 HcmV?d00001 diff --git a/assets/images/carousel/carousel-aslms.jpg b/assets/images/carousel/carousel-aslms.jpg new file mode 100644 index 0000000000000000000000000000000000000000..22f7ca97e50681116526c890f113162b92ceb023 GIT binary patch literal 26419 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
@|s+Gk$wsUg- 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¥ZUh2l!aRo~fUf1E(bJae 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%0D9oIB1wq19$>!=Y3wjI(nbK_>!owPAb-aX83 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%0A5vH?-0^ftG?v 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%0D9P3P%g_m{Kspo;1k*=w)T)zgCvQuVu}RCA8- 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 Ku&&C#`TqfpB}IDx literal 0 HcmV?d00001 diff --git a/assets/images/close.png b/assets/images/close.png new file mode 100644 index 0000000000000000000000000000000000000000..03cdf1ba4be5aacff9e388dc4d1334ce180d20a1 GIT binary patch literal 3300 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 literal 0 HcmV?d00001 diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a051e29aba99bcd38d897dc86ff4203d672941f8 GIT binary patch literal 894 zcmZQzU<5(|0R|u`!H~hsz#zuJz@P!dKp_SNAO?xUfU+I~0|>)_|FX}c2u}z=!b!2| zDnt#C0Te;w!nMN!l>sOMVM7?WxCGTB8v<7hWZ*Io%{sVBH1(M7M>c$vO+p9&022lE A7ytkO literal 0 HcmV?d00001 diff --git a/assets/images/hero-bg-ie.png b/assets/images/hero-bg-ie.png new file mode 100644 index 0000000000000000000000000000000000000000..aa27eedb5d10b231f275e5311e9424af8832c5bf GIT binary patch literal 17658 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 A{{R30 literal 0 HcmV?d00001 diff --git a/assets/images/mob-nav.png b/assets/images/mob-nav.png new file mode 100644 index 0000000000000000000000000000000000000000..bb07c29d2f059326ca1b064c26ee9cc0a63db31b GIT binary patch literal 959 zcmaJ=J#W)M7`C87P&Hyhv@9nVn2673>_oO=s>UBQ2uE!a(ZGabUlJ>|&)C<-$=Cr2 z6~T|_!oz6OF|IfqUAKA7_tu}3vE*-iF<~DuO!JrnpJ>0;qKYI2VS2*tC&7jq$ZRerp zkx+1RA0Z24hUU1+Mi#r?08`Mxy&%&0Kkq;AAnBqBZiQm;4rag_tZaUwmG}a&-4u`_dfgz=m{mfEx{k6u49caOLWE8MFGk8jsM{koMushU+c-&UU<{sT#e BDGmSt literal 0 HcmV?d00001 diff --git a/assets/images/respond.proxy.gif b/assets/images/respond.proxy.gif new file mode 100644 index 0000000000000000000000000000000000000000..ced1c0532c8669047dcaf7f93a48ae08ab6cf412 GIT binary patch literal 35 ncmZ?wbh9u|WMp7u_`tyM|Nnmm1_m7<2J#sh9GI9~7#XYqk@yCP literal 0 HcmV?d00001 diff --git a/assets/images/search-sprite.png b/assets/images/search-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c938dfd404c5261b3ea4abd9d2a3707cd03030 GIT binary patch literal 5210 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 L^--m&Hh2C5)jv}B literal 0 HcmV?d00001 diff --git a/assets/images/social-buttons/social-icons.png b/assets/images/social-buttons/social-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..9ce4ff1ca8befc12aa889dfe5021ed4ed9f195e0 GIT binary patch literal 3355 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 literal 0 HcmV?d00001 diff --git a/assets/images/ucl-logo-cropped-white.png b/assets/images/ucl-logo-cropped-white.png new file mode 100644 index 0000000000000000000000000000000000000000..d21bcb78958f2a9638f354820d8d7e154c03ab81 GIT binary patch literal 3161 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ucl-menu.png b/assets/images/ucl-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..724807af4dc37ad48b178476cb2b53d086d1f73f GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^(}38Bg&9ctEV9=GQd$8%A+A7r+qP}<=g;4?Y3Zs} zt5&aGy>{)|_3Kvx8SB@qS-W=Sx;3lTtz8X7Q_`o&16A{u1o;Isuycq=XzS?e1%!0; z97*t>1(fjgba4#vIG&tvfFY*5lTlcrgK=>hFaKtV-cCj)p2Z0wJxvq2EzSur3%Q-X zDwrbnJ1@!MK*lpk*2M`d%xdCE4TmyzT4jkPZf02Kadq*hf0`eFhBA1%`njxgN@xNA D4cJOr literal 0 HcmV?d00001 diff --git a/assets/images/ucl-menu.svg b/assets/images/ucl-menu.svg new file mode 100644 index 000000000..c3c3273a7 --- /dev/null +++ b/assets/images/ucl-menu.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/assets/images/ucl-portico.jpg b/assets/images/ucl-portico.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5373c590d5527ce20018cbde316af714b7f3c840 GIT binary patch literal 213279 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

R7|^Otc3_TYB`%TCDK!U+^SfM zS&51@O|swmnkW$B@*T@=A(*tIxdUVGe8@k*LWO@g z+hjDwy;@sV+@9N7J7Nt4`3LCt_3zK5ZOT3OMYNgU#!nn*&z{;<`5&ItwsHeX`}7CJ zpM=i;0gm|n6Yn^xhescrMd5Z=+icgHryDC=o%5RCq%Vy5%O3A4Og%+&Ts)gJoG+WW zo&Pdz9O<+2p~DBn-Vp_0nk_QWHe(;dl#bwtLTHuAcUo%Ae?}S(b;ro|zi1P%Y>%@T zGmy>hV)L1#XTU@y80kMo$0WOb^Y)VmlZ#Qq_I5&p#!N6*mosgz@@NZ^9yV)tpxKrVRTsHPX5^Nzv62ceVlITZu28y&#?d0-rNLRIsimvq193 zaTH=2fS^4timy0S9NvKx{7nRO3EYBsmh=uLx9+mqGy4KFPd(qP)`o`tT<|mr5&L6k z2rDI{)3(Bv)}o+oN-)aFr6y_50PSo+!6ik+`^1I{aQkk`7?`FE&8hDxlGq0=SN7Mi zdcD6c-%ez6bz+yXk{jtF^tVxvws#WzjONmraz@WHd-jg}}qUf|c5PbcI`$Jh$#||h8 z8e)BdU5hYaj713jpk$(?8k7^N1AkUj=BoSEfZxAMxG@p;eqWVB5b+Q=4bghZ*plcM z5uAD1lwPSfxSU2ca>4%rYZ7k6v_?_A+mRxxic*h!DPtxvdi%XT3vK> zD}{ZT9Yj@VYsyKAFs>y;HE@IgePWkx6B|j3#39tAPvwjHhuKtEH}w^tpvoa6hJTCq z5V@IT>b!`6z;ZHg8-~bNaBlTgXQ1@vaj(xm!16nhr^LY?-}${Khk%Uw2jJpUn}qdt zrHig7<NS1GSBikZhO|3wDHP?NgaA_Sc{~p@p6BS2*P>BGeThpg)^G z8ma(Vo`GYZ?iP{oc{y;L#2k1PsM>&#GNIGjvH={Gi`OoJdeqCp~D>QkO-$M5MM9|lam;-B2PN8-h zDh?y*hOOs+0Q#n#?_O!O)_syIF_lw00qDdczV#hW4FL>Ul2Q~eQ<}X2kKB5+CCS^q z&d*c<(E69eQ{&H?on6!|Be{VARh|!0>d&CB%hzL{@sN+R#-Gb_7a;y?vTn8TjiN*4 zr~HF9;fpmF=&#q5i~15@rnx_s?LZ_x={ES&&Wrg;sN_}ZbgA4#HX)Lv8UsaBOqRx5LM7O25*S=Te=Xem`<%Uq)cSyUzpVZ^2v z%yOKGEdvkLR4tEs4v^F`(i)#vMM#jY@U2gD+EUq{tl=aKI-ti|mSK)LOay)*P&%i^ z`jC1KfM%&Hy>p0&VImBvt~j!-QTUx*UI7W*6JwS{o3$DM9r}KIZE3WRpQGj{H$Njv zZ^6+<=K4oz<BHK=p{Cy{v1V6~ON#oD9_ zSm0Gp)*{vpIft{79*v<7PMXIyc183skNZt-;Mx-8bfJ@}FM+%C#AXedNJy;1R@nUg zVu>07PPuu92EFp^%CF}}T;Z8!X`ayG`{C$UG-fd@o57dI{6FT_U{`j zL$BclQV8kW!bQfzqMww=OG2vy!n8)+xso3NkTx|y`qJqC09Sl3JV`DR)}nteVr^G@ zbN({@1NbmJneZGA#Izjd_YZu3D)Qd&wFoDO;6lA~)}!tI>n%-_J){{k_7S0Ufa{gH zrsf$H`#5!L!&l z_vGxuZ=zznu=BC-?G63|LVZLhWNb7Sg*h!6Noh!$!y4LU$^xZlX~&pZXiDANzm;o! zV_T5NCH;roa;_+=L#NePzdpLLHj(`)se#Ejam6oUW$EJ{N!cvKq|AQNXB92qQzEi5 zn)}78(1+*sI#L)oF}R}WHQWYYrZ9rD*q!!7pEIi?pD5_)*Z;IyReGW$J_=f0z)hgA zvU8Y9X7)Y>6{i9bf2{lUΠQeb$tfrjk*_)q7!h$X>eT>d$#xUN6>inhl)4>4y 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 z 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⩔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

?0|h}uJ7+tK7uPD)aWzuf_~x2u&{_*FD+N4)cQ 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

bt zPg#225;<1a;saOUuEBV(L?Gp0fC>ura@Pjq>CDvTRGBwD2ml6&mh0t|*5IzG)$|rt zpaSkQS-4%Gnj`pzS2le#hSw^`!vLNwJ8vw`{G&IRA+hOTxU%C{{ZB@Os)ZIKJuvE zdP{-pxp4`jg1DlIq88u)+JNl=0^6c99HkuO5ruQXoSk8crl=rZ;aq&&ZfQ=OnLjeO-Yxm4 zTo=5|M7mpJppu=u-z>#?=_xgLiK3n&E$HvsaPJ1|=H8HTF8=^h$0L2uL9^Bjwhw)C z7LD#FA&ScW8A}rUX1YVAafY%%YSJMDF7XzNN_H63rMbSnqAzuES`(R5v#RZ!OUIZl z7Z|kId(XVQka3tr5m}oIdrWyehb4E2Hm{mGyhcW2UL5SMyhAa;RoG@>&bv!F%S`AW z@rtGe!0=~rh>ePlwW2C0001`BJ4&b>;wak+wKhe$qRiROaC}Cox-28Rztkcv02P&; z#q7M$V$SRlVuID680Oi9Ob55Ct6XSDq++6BH!fzTd15XM!lScK5}{JY4()cmZv`Vy}Ya_9fq5Pd`? literal 0 HcmV?d00001 diff --git a/assets/js/app/carousel.js b/assets/js/app/carousel.js new file mode 100644 index 000000000..dc90e2ea9 --- /dev/null +++ b/assets/js/app/carousel.js @@ -0,0 +1,47 @@ +define(["jquery","owl"],function($){ + $(document).ready(function(){ + if(typeof(globalSiteSpecificVars.carouselConfig)==="undefined"){ + carouselConfig = { + margin:10, responsiveClass:true, + loop:true, + autoplay:true, + lazyload:true, + autoHeight: false, + animateOut:'fadeOut', + autoplayHoverPause: true, + smartSpeed:450, + autoplayTimeout:5000, + dots:true, + items:1, + nav:false, + responsive:{ + 0:{ + + }, + 500:{ + + }, + 700:{ + nav:true, + + }, + 1000: + { + nav:true, + + }, + 1300:{ + nav:true, + + } + } + }; + }else{ + carouselConfig = (globalSiteSpecificVars.carouselConfig); + + } + $('.owl-carousel').owlCarousel(carouselConfig); + }); +}); + + diff --git a/assets/js/app/demo-site.js b/assets/js/app/demo-site.js new file mode 100644 index 000000000..5fc8ac0eb --- /dev/null +++ b/assets/js/app/demo-site.js @@ -0,0 +1,34 @@ +// Fit iframe JS + +define(["jquery"],function($){ + + //if(document.URL.search(/(\?|&)file=template(.*)?/i) < 0){ + function viewsource() { + //var body = $('body') + var body = $("iframe").contents().find("body"), + patternhtml = body.html().replace(/[<>]/g, + function(m) { + return { + '<':'<','>':'>' + }[m] + } + ).replace(/\t/g, ' ').replace(/((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi,'$1').replace(/<!--PATTERN-JS(.|\n)*/,''); + body.addClass('viewsource').prepend( '

Example

').append( '

HTML

' + patternhtml + '
' ); + }; + + //has to be duped from demo site to work with require + function resizeiframe(id, height){ + window.parent.$('#' + id).height(parseInt(height, 10) + 10 + 'px'); + + }; + + $(window).on('load',function(){ + viewsource(); + + var body = $("iframe").contents().find("body"); + resizeiframe('gitProxyIframe', body[0].scrollHeight); + }); + //} + +}); + diff --git a/assets/js/app/general.js b/assets/js/app/general.js new file mode 100644 index 000000000..7ff046b47 --- /dev/null +++ b/assets/js/app/general.js @@ -0,0 +1,204 @@ +define(["jquery","allsite"],function($,gen){ + + $(document).ready(function(){ + /* Browser feature detection and fixes + -----------------------------------------------------------------*/ + if(Modernizr.svg===false) {//target browsers that don't support SVG + + //update all instances of SVG in img tag + var $svgImage = $('img[src*="svg"]'); + $svgImage.each(function(){ + $(this).attr('src', function() { + var tempSrc = $(this).attr('src'); + var newSrc = tempSrc.replace('.svg', '.png'); + $(this).attr('src',newSrc); + }); + }); + + //lazy load fix for the above svg fix + $("img[data-src$='.svg']").each(function(){ + var tmpDataSrc = $(this).attr("data-src"); + $(this).attr("src",tmpDataSrc.replace(/([^\.]+)\.svg/i,'$1.png')); + }) + + //fix mobile header + var mobileHeaderObj = $('.header--mobile'); + mobileHeaderObj.removeClass("default-header"); + mobileHeaderObj.addClass("no-svg"); + } + if(!Modernizr.input.placeholder){//target browsers that doesn't support placeholder attribute + $('[placeholder]').focus(function() { + var input = $(this); + if (input.val() == input.attr('placeholder')) { + input.val(''); + input.removeClass('placeholder'); + } + }).blur(function() { + var input = $(this); + if (input.val() == '' || input.val() == input.attr('placeholder')) { + input.addClass('placeholder'); + input.val(input.attr('placeholder')); + } + }).blur(); + $('[placeholder]').parents('form').submit(function() { + $(this).find('[placeholder]').each(function() { + var input = $(this); + if (input.val() == input.attr('placeholder')) { + input.val(''); + } + }) + }); + } + Modernizr.load({ + test : Modernizr.touch//target browsers that support touch events + //if old browser load the shiv + ,yep : '//cdn.ucl.ac.uk/indigo/js/lib/fastclick.min.js' + ,complete: function(){ + $(function() { + if(typeof FastClick !=='undefined'){ + FastClick.attach(document.body); + } + }); + } + }); + /* sticky nav + -----------------------------------------------------------------*/ + var topNavObj = $("nav.nav--top"); + + if(topNavObj.hasClass("nav--sticky-init")){ + var topNavFullHeight = parseInt(topNavObj.height()) + parseInt(topNavObj.css("padding-top")) + parseInt(topNavObj.css("padding-bottom")); + var topNavHeightFromTop = parseInt(topNavObj.offset().top); + + function stickyNav(){ + var currentPos = parseInt($(window).scrollTop()); + + if(currentPos > (topNavFullHeight + topNavHeightFromTop)){ + topNavObj.addClass("nav--sticky-active"); + }else{ + topNavObj.removeClass("nav--sticky-active"); + } + } + + $(window).scroll(function(){ + stickyNav(); + }) + stickyNav(); + } + /* layout hacks + -----------------------------------------------------------------*/ + var bodyClass = $('body').attr("class"); + var leftNavList = $('.nav--left ul'); + var topNavList = $('.nav--top ul'); + var mobileNav = $('.nav--mobile'); + var mobileNavList = $('.nav--mobile ul'); + + function resetCols(){ + $('.site-content__inner,.sidebar').css({ + 'height':'auto' + ,'min-height':'0'} + ); + } + + function equalizeVerticalCol(){ + //start off by resetting the columns + resetCols(); + + if($(window).width() >= 768){ + var mainColHeight = $('.site-content__inner').height(); + var verticalNavColHeight = $('.sidebar').height(); + if(verticalNavColHeight > mainColHeight){ + $('.site-content__inner').css('min-height',verticalNavColHeight); + } + else{ + $('.site-content__inner').css({ + 'height':'auto' + ,'min-height':'0'} + ); + } + + //set sub nav to height of main content + $('.nav.nav--left.nav--subnav').height( + parseInt($('.site-content__inner').height()) + 'px' + ); + }else{ + $('.site-content__inner').css({ + 'height':'auto' + ,'min-height':'0'} + ); + //mobile sub nav + $('.nav.nav--mobile.nav--subnav').height( + parseInt($('nav.nav--mobile ul.subnav__list').height()) + 'px' + ); + } + } + + function buildmobileNav(){ + if(leftNavList.length > 0 && mobileNavList.length < 1){ + mobileNav.append("
    " + leftNavList.html() + "
"); + }else if(topNavList.length > 0 && mobileNavList.length < 1){ + mobileNav.append("
    " + topNavList.html() + "
"); + } + return; + } + buildmobileNav(); + + var verticalBodyClassPattern = /layout-vertical(.)*/i; + if(verticalBodyClassPattern.test(bodyClass)){ + equalizeVerticalCol(); + $(window).resize(function(){ + equalizeVerticalCol(); + }); + } + /* Detect IE compatability mode and show user alert + -----------------------------------------------------------------*/ + var agentStr = navigator.userAgent; + var isCompatabilityMode = false; + var debug = false;//toggle this when in dev + + if(agentStr.indexOf("Trident/5.0") > -1){ + if (agentStr.indexOf("MSIE 7.0") > -1) + isCompatabilityMode = true; + }else if (agentStr.indexOf("Trident/4.0") > -1){ + if (agentStr.indexOf("MSIE 7.0") > -1) + isCompatabilityMode = true; + } + if(isCompatabilityMode || debug){ + var messageStr = "

This website will not display correctly in compatibilty mode."; + messageStr += " For more information please see Indigo constraints

"; + + $('body').prepend("
" + messageStr + "x"); + + + $('.announcement-bar--close').click(function(){ + $('.announcement-bar').remove(); + }) + } + /* Multi-layer sliding navigation + -----------------------------------------------------------------*/ + $(".subnav__item a").on('click', function(){ + var parentLevel = $(this).parents('ul').length -1; + var currentMenu = $(this).closest('ul'); + var currentListItem = $(this).parent('li'); + var parentMenu = $('.subnav__list--level-' + parentLevel); + var subMenu = $(this).next('ul'); + + if(currentListItem.hasClass('back')) { + // back button hit + currentMenu.removeClass('nav--active'); + parentMenu.removeClass('nav--hidden'); + } + else if(currentListItem.hasClass('back-1')) { + $('.subnav__list').removeClass('nav--active'); + $('.subnav__list').removeClass('nav--hidden'); + } + else if (currentListItem.children('ul').length > 0) { + // menu item has children - expand the menu + subMenu.toggleClass('nav--active'); + currentMenu.addClass('nav--hidden'); + } + }); + /* anything else that needs to appear on all pages + -----------------------------------------------------------------*/ + + }); +}); \ No newline at end of file diff --git a/assets/js/app/lightbox.js b/assets/js/app/lightbox.js new file mode 100644 index 000000000..7428b032c --- /dev/null +++ b/assets/js/app/lightbox.js @@ -0,0 +1,59 @@ +define(["jquery"],function($){ + +/* IE support for this object */ +if (!window.getComputedStyle) { + + window.getComputedStyle = function(el, pseudo) { + this.el = el; + this.getPropertyValue = function(prop) { + var re = /(\-([a-z]){1})/g; + if (prop == 'float') prop = 'styleFloat'; + if (re.test(prop)) { + prop = prop.replace(re, function () { + return arguments[2].toUpperCase(); + }); + } + return el.currentStyle[prop] ? el.currentStyle[prop] : null; + } + return this; + } +} + +var size = $(window).width(); + + +$(document).ready(function(){ + // We only want to target images / links with a + $('.lightbox-img').addClass("let-there-be-light"); + lightboxInit(); +}); + + +function lightboxInit() { + + $('.let-there-be-light').click(function(e){ + var positiontop= $(document).scrollTop(); + if(Modernizr.mq && parseInt(size)>45) + { + e.preventDefault(); + var $thisHref = $(this).attr('href'); + buildLightBox($thisHref,positiontop); + } + }); +} + +function buildLightBox(src, positiontop) { + var height = $(document).height(); + $(' +
+ + + + + + + + + + + + + + + diff --git a/ch00git/01Intro.ipynb b/ch00git/01Intro.ipynb new file mode 100644 index 000000000..e23971538 --- /dev/null +++ b/ch00git/01Intro.ipynb @@ -0,0 +1,488 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "be4f7933", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "id": "46be33c8", + "metadata": {}, + "source": [ + "### What's version control?\n", + "\n", + "Version control is a tool for __managing changes__ to a set of files.\n", + "\n", + "There are many different __version control systems__: \n", + "\n", + "- Git \n", + "- Mercurial (`hg`)\n", + "- CVS\n", + "- Subversion (`svn`)\n", + "- ..." + ] + }, + { + "cell_type": "markdown", + "id": "7bcb937e", + "metadata": {}, + "source": [ + "### Why use version control?\n", + "\n", + "- Better kind of __backup__.\n", + "- Review __history__ (\"When did I introduce this bug?\").\n", + "- Restore older __code versions__.\n", + "- Ability to __undo mistakes__.\n", + "- Maintain __several versions__ of the code at a time." + ] + }, + { + "cell_type": "markdown", + "id": "53032035", + "metadata": {}, + "source": [ + "Git is also a __collaborative__ tool:\n", + "\n", + "- \"How can I share my code?\"\n", + "- \"How can I submit a change to someone else's code?\"\n", + "- \"How can I merge my work with Sue's?\"\n", + "\n", + "### Git != GitHub\n", + "\n", + "- __Git__: version control system tool to manage source code history.\n", + "\n", + "- __GitHub__: hosting service for Git repositories." + ] + }, + { + "cell_type": "markdown", + "id": "2502011a", + "metadata": {}, + "source": [ + "### How do we use version control?\n", + "\n", + "Do some programming, then commit our work:\n", + "\n", + "`my_vcs commit`\n", + "\n", + "Program some more.\n", + "\n", + "Spot a mistake:\n", + "\n", + "`my_vcs rollback`\n", + "\n", + "Mistake is undone." + ] + }, + { + "cell_type": "markdown", + "id": "76d50f48", + "metadata": {}, + "source": [ + "### What is version control? (Team version)\n", + "\n", + "Graham | Eric\n", + "------------------ |------ \n", + "`my_vcs commit` | ...\n", + "... | Join the team\n", + "... | `my_vcs checkout`\n", + "... | Do some programming\n", + "... | `my_vcs commit`\n", + "`my_vcs update`\t\t | ...\n", + "Do some programming|Do some programming\n", + "`my_vcs commit` | ...\n", + "`my_vcs update` | ...\n", + "`my_vcs merge` | ...\n", + "`my_vcs commit` | ..." + ] + }, + { + "cell_type": "markdown", + "id": "5aafe95e", + "metadata": {}, + "source": [ + "### Scope\n", + "\n", + "This course will use the `git` version control system, but much of what you learn will be valid with other version control \n", + "tools you may encounter, including subversion (`svn`) and mercurial (`hg`)." + ] + }, + { + "cell_type": "markdown", + "id": "e9e9d6a6", + "metadata": {}, + "source": [ + "## Practising with Git" + ] + }, + { + "cell_type": "markdown", + "id": "77c88dd8", + "metadata": {}, + "source": [ + "### Example Exercise\n", + "\n", + "In this course, we will use, as an example, the development of a few text files containing a description of a topic of your choice. \n", + "\n", + "This could be your research, a hobby, or something else. In the end, we will show you how to display the content of these files as a very simple website. " + ] + }, + { + "cell_type": "markdown", + "id": "2458960f", + "metadata": {}, + "source": [ + "### Programming and documents\n", + "\n", + "The purpose of this exercise is to learn how to use Git to manage program code you write, not simple text website content, but we'll just use these text files instead of code for now, so as not to confuse matters with trying to learn version control while thinking about programming too. \n", + "\n", + "In later parts of the course, you will use the version control tools you learn today with actual Python code." + ] + }, + { + "cell_type": "markdown", + "id": "4a267d08", + "metadata": {}, + "source": [ + "### Markdown\n", + "\n", + "The text files we create will use a simple \"wiki\" markup style called [markdown](http://daringfireball.net/projects/markdown/basics) to show formatting. This is the convention used in this file, too. \n", + "\n", + "You can view the content of this file in the way Markdown renders it by looking on the [web](https://github.com/UCL/ucl_software_carpentry/blob/master/git/git_instructions.md), and compare the [raw text](https://raw.github.com/UCL/ucl_software_carpentry/master/git/git_instructions.md)." + ] + }, + { + "cell_type": "markdown", + "id": "3962b9dd", + "metadata": {}, + "source": [ + "### Displaying Text in this Tutorial\n", + "\n", + "This tutorial is based on use of the Git command line. So you'll be typing commands in the shell." + ] + }, + { + "cell_type": "markdown", + "id": "4eb84008", + "metadata": {}, + "source": [ + "To make it easy for me to edit, I've built it using Jupyter notebook." + ] + }, + { + "cell_type": "markdown", + "id": "b59539d0", + "metadata": {}, + "source": [ + "Commands you can type will look like this, using the %%bash \"magic\" for the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9abcd82c", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "echo some output" + ] + }, + { + "cell_type": "markdown", + "id": "2c414d2c", + "metadata": {}, + "source": [ + "with the results you should see below. " + ] + }, + { + "cell_type": "markdown", + "id": "52a45883", + "metadata": {}, + "source": [ + "In this document, we will show the new content of an edited document like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b111ab3e", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile somefile.md\n", + "Some content here" + ] + }, + { + "cell_type": "markdown", + "id": "bce615c0", + "metadata": {}, + "source": [ + "But if you are following along, you should edit the file using a text editor.\n", + "On either Windows, Mac or Linux, we recommend [VS Code](https://code.visualstudio.com/)." + ] + }, + { + "cell_type": "markdown", + "id": "3b3cc175", + "metadata": {}, + "source": [ + "### Setting up somewhere to work" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f1efd9f", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "rm -rf learning_git/git_example # Just in case it's left over from a previous class; you won't need this\n", + "mkdir -p learning_git/git_example\n", + "cd learning_git/git_example" + ] + }, + { + "cell_type": "markdown", + "id": "ec8aae5d", + "metadata": {}, + "source": [ + "I just need to move this Jupyter notebook's current directory as well:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bee52460", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "top_dir" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14825f1d", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "git_dir" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96abf815", + "metadata": {}, + "outputs": [], + "source": [ + "working_dir=os.path.join(git_dir, 'git_example')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79e0a9a3", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "f6e6f5a2", + "metadata": {}, + "source": [ + "## Solo work\n", + "\n", + "### Configuring Git with your name and email\n", + "\n", + "First, we should configure Git to know our name and email address:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36ea2f2c", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git config --global user.name \"Lancelot the Brave\"\n", + "git config --global user.email \"l.brave@spamalot.uk\"" + ] + }, + { + "cell_type": "markdown", + "id": "f810661a", + "metadata": {}, + "source": [ + "Additionally, it's also a good idea to define what's the name of the default branch when we create a repository:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0052067", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "git config --global init.defaultBranch main" + ] + }, + { + "cell_type": "markdown", + "id": "16f78d2e", + "metadata": {}, + "source": [ + "Historically, the default branch was named `master`. Nowadays, the community and most of the hosting sites have changed the default ([read about this change in GitHub](https://github.com/github/renaming/) and [Gitlab](https://about.gitlab.com/blog/2021/03/10/new-git-default-branch-name/)." + ] + }, + { + "cell_type": "markdown", + "id": "d7e18c53", + "metadata": {}, + "source": [ + "### Initialising the repository\n", + "\n", + "Now, we will tell Git to track the content of this folder as a git \"repository\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24d07ec2", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "pwd # Note where we are standing-- MAKE SURE YOU INITIALISE THE RIGHT FOLDER\n", + "git init" + ] + }, + { + "cell_type": "markdown", + "id": "266f39e0", + "metadata": {}, + "source": [ + "As yet, this repository contains no files:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2ea5e42", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11290667", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Introduction to Version Control" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/01Intro.ipynb.py b/ch00git/01Intro.ipynb.py new file mode 100644 index 000000000..9b4db13fd --- /dev/null +++ b/ch00git/01Intro.ipynb.py @@ -0,0 +1,204 @@ +# --- +# jupyter: +# jekyll: +# display_name: Introduction to Version Control +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Introduction + +# %% [markdown] +# ### What's version control? +# +# Version control is a tool for __managing changes__ to a set of files. +# +# There are many different __version control systems__: +# +# - Git +# - Mercurial (`hg`) +# - CVS +# - Subversion (`svn`) +# - ... + +# %% [markdown] +# ### Why use version control? +# +# - Better kind of __backup__. +# - Review __history__ ("When did I introduce this bug?"). +# - Restore older __code versions__. +# - Ability to __undo mistakes__. +# - Maintain __several versions__ of the code at a time. + +# %% [markdown] +# Git is also a __collaborative__ tool: +# +# - "How can I share my code?" +# - "How can I submit a change to someone else's code?" +# - "How can I merge my work with Sue's?" +# +# ### Git != GitHub +# +# - __Git__: version control system tool to manage source code history. +# +# - __GitHub__: hosting service for Git repositories. + +# %% [markdown] +# ### How do we use version control? +# +# Do some programming, then commit our work: +# +# `my_vcs commit` +# +# Program some more. +# +# Spot a mistake: +# +# `my_vcs rollback` +# +# Mistake is undone. + +# %% [markdown] +# ### What is version control? (Team version) +# +# Graham | Eric +# ------------------ |------ +# `my_vcs commit` | ... +# ... | Join the team +# ... | `my_vcs checkout` +# ... | Do some programming +# ... | `my_vcs commit` +# `my_vcs update` | ... +# Do some programming|Do some programming +# `my_vcs commit` | ... +# `my_vcs update` | ... +# `my_vcs merge` | ... +# `my_vcs commit` | ... + +# %% [markdown] +# ### Scope +# +# This course will use the `git` version control system, but much of what you learn will be valid with other version control +# tools you may encounter, including subversion (`svn`) and mercurial (`hg`). + +# %% [markdown] +# ## Practising with Git + +# %% [markdown] +# ### Example Exercise +# +# In this course, we will use, as an example, the development of a few text files containing a description of a topic of your choice. +# +# This could be your research, a hobby, or something else. In the end, we will show you how to display the content of these files as a very simple website. + +# %% [markdown] +# ### Programming and documents +# +# The purpose of this exercise is to learn how to use Git to manage program code you write, not simple text website content, but we'll just use these text files instead of code for now, so as not to confuse matters with trying to learn version control while thinking about programming too. +# +# In later parts of the course, you will use the version control tools you learn today with actual Python code. + +# %% [markdown] +# ### Markdown +# +# The text files we create will use a simple "wiki" markup style called [markdown](http://daringfireball.net/projects/markdown/basics) to show formatting. This is the convention used in this file, too. +# +# You can view the content of this file in the way Markdown renders it by looking on the [web](https://github.com/UCL/ucl_software_carpentry/blob/master/git/git_instructions.md), and compare the [raw text](https://raw.github.com/UCL/ucl_software_carpentry/master/git/git_instructions.md). + +# %% [markdown] +# ### Displaying Text in this Tutorial +# +# This tutorial is based on use of the Git command line. So you'll be typing commands in the shell. + +# %% [markdown] +# To make it easy for me to edit, I've built it using Jupyter notebook. + +# %% [markdown] +# Commands you can type will look like this, using the %%bash "magic" for the notebook. + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# echo some output + +# %% [markdown] +# with the results you should see below. + +# %% [markdown] +# In this document, we will show the new content of an edited document like this: + +# %% jupyter={"outputs_hidden": false} +# %%writefile somefile.md +Some content here + +# %% [markdown] +# But if you are following along, you should edit the file using a text editor. +# On either Windows, Mac or Linux, we recommend [VS Code](https://code.visualstudio.com/). + +# %% [markdown] +# ### Setting up somewhere to work + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# rm -rf learning_git/git_example # Just in case it's left over from a previous class; you won't need this +# mkdir -p learning_git/git_example +# cd learning_git/git_example + +# %% [markdown] +# I just need to move this Jupyter notebook's current directory as well: + +# %% jupyter={"outputs_hidden": false} +import os +top_dir = os.getcwd() +top_dir + +# %% jupyter={"outputs_hidden": false} +git_dir = os.path.join(top_dir, 'learning_git') +git_dir + +# %% +working_dir=os.path.join(git_dir, 'git_example') + +# %% jupyter={"outputs_hidden": false} +os.chdir(working_dir) + +# %% [markdown] +# ## Solo work +# +# ### Configuring Git with your name and email +# +# First, we should configure Git to know our name and email address: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git config --global user.name "Lancelot the Brave" +# git config --global user.email "l.brave@spamalot.uk" + +# %% [markdown] +# Additionally, it's also a good idea to define what's the name of the default branch when we create a repository: + +# %% language="bash" +# git config --global init.defaultBranch main + +# %% [markdown] +# Historically, the default branch was named `master`. Nowadays, the community and most of the hosting sites have changed the default ([read about this change in GitHub](https://github.com/github/renaming/) and [Gitlab](https://about.gitlab.com/blog/2021/03/10/new-git-default-branch-name/). + +# %% [markdown] +# ### Initialising the repository +# +# Now, we will tell Git to track the content of this folder as a git "repository". + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# pwd # Note where we are standing-- MAKE SURE YOU INITIALISE THE RIGHT FOLDER +# git init + +# %% [markdown] +# As yet, this repository contains no files: + +# %% jupyter={"outputs_hidden": false} language="bash" +# ls + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git status diff --git a/ch00git/02Solo.html b/ch00git/02Solo.html new file mode 100644 index 000000000..db64eebc7 --- /dev/null +++ b/ch00git/02Solo.html @@ -0,0 +1,1297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Solo Git + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Solo work with Git

+
+
+
+
+
+
+

So, we're in our git working directory:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+working_dir
+
+
+
+
+
+
+
+
Out[1]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/git_example'
+
+
+
+
+
+
+
+
+

A first example file

So let's create an example file, and see how to start to manage a history of changes to it.

+
+
+
+
+
+
+
<my editor> index.md # Type some content into the file.
+
+
+
+
+
+
In [2]:
+
+
+
%%writefile index.md
+Mountains in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+
Writing index.md
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+

Telling Git about the File

So, let's tell Git that index.md is a file which is important, and we would like to keep track of its history:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+git add index.md
+
+
+
+
+
+
+
+
+

Don't forget: Any files in repositories which you want to "track" need to be added with git add after you create them.

+

Our first commit

Now, we need to tell Git to record the first version of this file in the history of changes:

+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+git commit -m "First commit of discourse on UK topography"
+
+
+
+
+
+
+
+
+
+
[main (root-commit) 6c90dbd] First commit of discourse on UK topography
+ 1 file changed, 4 insertions(+)
+ create mode 100644 index.md
+
+
+
+
+
+
+
+
+
+

And note the confirmation from Git.

+

There's a lot of output there you can ignore for now.

+
+
+
+
+
+
+

Configuring Git with your editor

If you don't type in the log message directly with -m "Some message", then an editor will pop up, to allow you +to edit your message on the fly.

+
+
+
+
+
+
+

For this to work, you have to tell git where to find your editor.

+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+git config --global core.editor nano
+
+
+
+
+
+
+
+
+

You can find out what you currently have with:

+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git config --get core.editor
+
+
+
+
+
+
+
+
+
+
nano
+
+
+
+
+
+
+
+
+
+

To configure VS Code on your operating system you'll need something like the below, ask a demonstrator to help for your machine.

+
+
+
+
+
+
+
$ git config --global core.editor "code --wait"
+
+
+
+
+
+
+
+

I'm going to be using nano as my editor, but you can use whatever editor you prefer. Find how to setup your favourite editor in the setup chapter of Software Carpentry's Git lesson.

+
+
+
+
+
+
+

Git log

Git now has one change in its history:

+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git log
+
+
+
+
+
+
+
+
+
+
commit 6c90dbd9b71257440d4fb40e8ff8d8792194979f
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:08 2023 +0000
+
+    First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

You can see the commit message, author, and date...

+
+
+
+
+
+
+

Hash Codes

The commit "hash code", e.g.

+

c438f1716b2515563e03e82231acbae7dd4f4656

+

is a unique identifier of that particular revision.

+

(This is a really long code, but whenever you need to use it, you can just use the first few characters, however many characters is long enough to make it unique, c438 for example. )

+
+
+
+
+
+
+

Nothing to see here

Note that git will now tell us that our "working directory" is up-to-date with the repository: there are no changes to the files that aren't recorded in the repository history:

+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+nothing to commit, working tree clean
+
+
+
+
+
+
+
+
+
+

Let's edit the file again:

+
nano index.md
+
+
+
+
+
+
In [10]:
+
+
+
%%writefile index.md
+Mountains in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world.
+
+
+
+
+
+
+
+
+
+
Overwriting index.md
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world.
+
+
+
+
+
+
+
+
+
+

Unstaged changes

+
+
+
+
+
+
In [12]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+	modified:   index.md
+
+no changes added to commit (use "git add" and/or "git commit -a")
+
+
+
+
+
+
+
+
+
+

We can now see that there is a change to "index.md" which is currently "not staged for commit". What does this mean?

+

If we do a git commit now nothing will happen.

+

Git will only commit changes to files that you choose to include in each commit.

+

This is a difference from other version control systems, where committing will affect all changed files.

+
+
+
+
+
+
+

We can see the differences in the file with:

+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+git diff
+
+
+
+
+
+
+
+
+
+
diff --git a/index.md b/index.md
+index a1f85df..3a2f7b0 100644
+--- a/index.md
++++ b/index.md
+@@ -2,3 +2,5 @@ Mountains in the UK
+ ===================   
+ England is not very mountainous.   
+ But has some tall hills, and maybe a mountain or two depending on your definition.
++
++Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world.
+
+
+
+
+
+
+
+
+
+

Deleted lines are prefixed with a minus, added lines prefixed with a plus.

+
+
+
+
+
+
+

Staging a file to be included in the next commit

To include the file in the next commit, we have a few choices. This is one of the things to be careful of with git: there are lots of ways to do similar things, and it can be hard to keep track of them all.

+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+git add --update
+
+
+
+
+
+
+
+
+

This says "include in the next commit, all files which have ever been included before".

+

Note that git add is the command we use to introduce git to a new file, but also the command we use to "stage" a file to be included in the next commit.

+
+
+
+
+
+
+

The staging area

The "staging area" or "index" is the git jargon for the place which contains the list of changes which will be included in the next commit.

+

You can include specific changes to specific files with git add, commit them, add some more files, and commit them. (You can even add specific changes within a file to be included in the index.)

+
+
+
+
+
+
+

Message Sequence Charts

+
+
+
+
+
+
+

In order to illustrate the behaviour of Git, it will be useful to be able to generate figures in Python +of a "message sequence chart" flavour.

+
+
+
+
+
+
+

There's a nice online tool to do this, called "Web Sequence diagrams".

+
+
+
+
+
+
+

Instead of just showing you these diagrams, I'm showing you in this notebook how I make them. +This is part of our "reproducible computing" approach; always generating all our figures from code.

+
+
+
+
+
+
+

Here's some quick code in the Notebook to download and display an MSC illustration, using the Web Sequence Diagrams API:

+
+
+
+
+
+
In [15]:
+
+
+
%%writefile wsd.py
+import requests
+import re
+import IPython
+
+def wsd(code):
+    response = requests.post("http://www.websequencediagrams.com/index.php", data={
+            'message': code,
+            'apiVersion': 1,
+        })
+    expr = re.compile("(\?(img|pdf|png|svg)=[a-zA-Z0-9]+)")
+    m = expr.search(response.text)
+    if m == None:
+        print("Invalid response from server.")
+        return False
+                            
+    image=requests.get("http://www.websequencediagrams.com/" + m.group(0))
+    return IPython.core.display.Image(image.content)
+
+
+
+
+
+
+
+
+
+
Writing wsd.py
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
from wsd import wsd
+%matplotlib inline
+wsd("Sender->Recipient: Hello\n Recipient->Sender: Message received OK")
+
+
+
+
+
+
+
+
Out[16]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The Levels of Git

+
+
+
+
+
+
+

Let's make ourselves a sequence chart to show the different aspects of Git we've seen so far:

+
+
+
+
+
+
In [17]:
+
+
+
message="""
+Working Directory -> Staging Area : git add
+Staging Area -> Local Repository : git commit
+Working Directory -> Local Repository : git commit -a
+"""
+wsd(message)
+
+
+
+
+
+
+
+
Out[17]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Review of status

+
+
+
+
+
+
In [18]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+	modified:   index.md
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	__pycache__/
+	wsd.py
+
+
+
+
+
+
+
+
+
+
In [19]:
+
+
+
%%bash
+git commit -m "Add a lie about a mountain"
+
+
+
+
+
+
+
+
+
+
[main 5aa2999] Add a lie about a mountain
+ 1 file changed, 2 insertions(+)
+
+
+
+
+
+
+
+
+
In [20]:
+
+
+
%%bash
+git log
+
+
+
+
+
+
+
+
+
+
commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:12 2023 +0000
+
+    Add a lie about a mountain
+
+commit 6c90dbd9b71257440d4fb40e8ff8d8792194979f
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:08 2023 +0000
+
+    First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Great, we now have a file which contains a mistake.

+
+
+
+
+
+
+

Carry on regardless

In a while, we'll use Git to roll back to the last correct version: this is one of the main reasons we wanted to use version control, after all! But for now, let's do just as we would if we were writing code, not notice our mistake and keep working...

+
+
+
+
+
+
+
nano index.md
+
+
+
+
+
+
+
In [21]:
+
+
+
%%writefile index.md
+Mountains and Hills in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world.
+
+
+
+
+
+
+
+
+
+
Overwriting index.md
+
+
+
+
+
+
+
+
+
In [22]:
+
+
+
cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains and Hills in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world.
+
+
+
+
+
+
+
+
+
+

Commit with a built-in-add

+
+
+
+
+
+
In [23]:
+
+
+
%%bash
+git commit -am "Change title"
+
+
+
+
+
+
+
+
+
+
[main 5bf472d] Change title
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+
+
+
+
+
+
+
+
+

This last command, git commit -a automatically adds changes to all tracked files to the staging area, as part of the commit command. So, if you never want to just add changes to some tracked files but not others, you can just use this and forget about the staging area!

+
+
+
+
+
+
+

Review of changes

+
+
+
+
+
+
In [24]:
+
+
+
%%bash
+git log | head
+
+
+
+
+
+
+
+
+
+
commit 5bf472d82a68b889f58165491d712e8f15de0de7
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:12 2023 +0000
+
+    Change title
+
+commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:12 2023 +0000
+
+
+
+
+
+
+
+
+
+
+

We now have three changes in the history:

+
+
+
+
+
+
In [25]:
+
+
+
%%bash
+git log --oneline
+
+
+
+
+
+
+
+
+
+
5bf472d Change title
+5aa2999 Add a lie about a mountain
+6c90dbd First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Git Solo Workflow

+
+
+
+
+
+
+

We can make a diagram that summarises the above story:

+
+
+
+
+
+
In [26]:
+
+
+
message="""
+participant "Cleese's repo" as R
+participant "Cleese's index" as I
+participant Cleese as C
+
+note right of C: nano index.md
+
+note right of C: git init
+C->R: create
+
+note right of C: git add index.md
+
+C->I: Add content of index.md
+
+note right of C: git commit
+I->R: Commit content of index.md
+
+note right of C:  nano index.md
+
+note right of C: git add --update
+C->I: Add content of index.md
+note right of C: git commit -m "Add a lie"
+I->R: Commit change to index.md
+
+note right of C:  nano index.md
+note right of C: git commit -am "Change title"
+C->R: Add and commit change to index.md (and all tracked files)
+"""
+wsd(message)
+
+
+
+
+
+
+
+
Out[26]:
+
+No description has been provided for this image +
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/02Solo.ipynb b/ch00git/02Solo.ipynb new file mode 100644 index 000000000..85bdd5719 --- /dev/null +++ b/ch00git/02Solo.ipynb @@ -0,0 +1,952 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2d51847f", + "metadata": {}, + "source": [ + "## Solo work with Git" + ] + }, + { + "cell_type": "markdown", + "id": "e4c0132b", + "metadata": {}, + "source": [ + "So, we're in our git working directory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02d72b9f", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)\n", + "working_dir" + ] + }, + { + "cell_type": "markdown", + "id": "50517242", + "metadata": {}, + "source": [ + "### A first example file\n", + "\n", + "So let's create an example file, and see how to start to manage a history of changes to it." + ] + }, + { + "cell_type": "markdown", + "id": "6b883f92", + "metadata": {}, + "source": [ + " index.md # Type some content into the file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2812c773", + "metadata": { + "jupyter": { + "outputs_hidden": false + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "Mountains in the UK \n", + "=================== \n", + "England is not very mountainous. \n", + "But has some tall hills, and maybe a mountain or two depending on your definition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "117766ec", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "cat index.md" + ] + }, + { + "cell_type": "markdown", + "id": "e10c8750", + "metadata": {}, + "source": [ + "### Telling Git about the File\n", + "\n", + "So, let's tell Git that `index.md` is a file which is important, and we would like to keep track of its history:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec23463a", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add index.md" + ] + }, + { + "cell_type": "markdown", + "id": "223b9fdf", + "metadata": {}, + "source": [ + "Don't forget: Any files in repositories which you want to \"track\" need to be added with `git add` after you create them.\n", + "\n", + "### Our first commit\n", + "\n", + "Now, we need to tell Git to record the first version of this file in the history of changes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5c0255d", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -m \"First commit of discourse on UK topography\"" + ] + }, + { + "cell_type": "markdown", + "id": "6270154e", + "metadata": {}, + "source": [ + "And note the confirmation from Git.\n", + "\n", + "There's a lot of output there you can ignore for now." + ] + }, + { + "cell_type": "markdown", + "id": "c395fad9", + "metadata": {}, + "source": [ + "### Configuring Git with your editor\n", + "\n", + "If you don't type in the log message directly with -m \"Some message\", then an editor will pop up, to allow you\n", + "to edit your message on the fly." + ] + }, + { + "cell_type": "markdown", + "id": "1cf02cea", + "metadata": {}, + "source": [ + "For this to work, you have to tell git where to find your editor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "506f4664", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git config --global core.editor nano" + ] + }, + { + "cell_type": "markdown", + "id": "8a625b7f", + "metadata": {}, + "source": [ + "You can find out what you currently have with:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e12d752b", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git config --get core.editor" + ] + }, + { + "cell_type": "markdown", + "id": "0ceb9576", + "metadata": {}, + "source": [ + "To configure VS Code on your operating system you'll need something like the below, ask a demonstrator to help for your machine." + ] + }, + { + "cell_type": "markdown", + "id": "f2bfe200", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + } + }, + "source": [ + "``` bash\n", + "$ git config --global core.editor \"code --wait\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5fecfbfa", + "metadata": {}, + "source": [ + "I'm going to be using `nano` as my editor, but you can use whatever editor you prefer. Find how to setup your favourite editor in [the setup chapter of Software Carpentry's Git lesson](https://swcarpentry.github.io/git-novice/02-setup.html)." + ] + }, + { + "cell_type": "markdown", + "id": "5168c402", + "metadata": {}, + "source": [ + "### Git log\n", + "\n", + "Git now has one change in its history:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b5719f9", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log" + ] + }, + { + "cell_type": "markdown", + "id": "a4c885a3", + "metadata": {}, + "source": [ + "You can see the commit message, author, and date..." + ] + }, + { + "cell_type": "markdown", + "id": "210bb75f", + "metadata": {}, + "source": [ + "### Hash Codes\n", + "\n", + "The commit \"hash code\", e.g.\n", + "\n", + "`c438f1716b2515563e03e82231acbae7dd4f4656`\n", + "\n", + "is a unique identifier of that particular revision. \n", + "\n", + "(This is a really long code, but whenever you need to use it, you can just use the first few characters, however many characters is long enough to make it unique, `c438` for example. )" + ] + }, + { + "cell_type": "markdown", + "id": "59ebb51a", + "metadata": {}, + "source": [ + "### Nothing to see here\n", + "\n", + "Note that git will now tell us that our \"working directory\" is up-to-date with the repository: there are no changes to the files that aren't recorded in the repository history:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c165346f", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "markdown", + "id": "8f9c0d08", + "metadata": {}, + "source": [ + "Let's edit the file again:\n", + "\n", + " nano index.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4edc7329", + "metadata": { + "jupyter": { + "outputs_hidden": false + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "Mountains in the UK \n", + "=================== \n", + "England is not very mountainous. \n", + "But has some tall hills, and maybe a mountain or two depending on your definition.\n", + "\n", + "Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "436b5109", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "cat index.md" + ] + }, + { + "cell_type": "markdown", + "id": "2ccd25a5", + "metadata": {}, + "source": [ + "### Unstaged changes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c767d71", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "markdown", + "id": "a179417a", + "metadata": {}, + "source": [ + "We can now see that there is a change to \"index.md\" which is currently \"not staged for commit\". What does this mean? \n", + "\n", + "If we do a `git commit` now *nothing will happen*. \n", + "\n", + "Git will only commit changes to files that you choose to include in each commit.\n", + "\n", + "This is a difference from other version control systems, where committing will affect all changed files. " + ] + }, + { + "cell_type": "markdown", + "id": "1550ee5a", + "metadata": {}, + "source": [ + "We can see the differences in the file with:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96a7efce", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git diff" + ] + }, + { + "cell_type": "markdown", + "id": "11519119", + "metadata": {}, + "source": [ + "Deleted lines are prefixed with a minus, added lines prefixed with a plus." + ] + }, + { + "cell_type": "markdown", + "id": "fe27e618", + "metadata": {}, + "source": [ + "### Staging a file to be included in the next commit\n", + "\n", + "To include the file in the next commit, we have a few choices. This is one of the things to be careful of with git: there are lots of ways to do similar things, and it can be hard to keep track of them all." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c2d1d95", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add --update" + ] + }, + { + "cell_type": "markdown", + "id": "5393818c", + "metadata": {}, + "source": [ + "This says \"include in the next commit, all files which have ever been included before\". \n", + "\n", + "Note that `git add` is the command we use to introduce git to a new file, but also the command we use to \"stage\" a file to be included in the next commit. " + ] + }, + { + "cell_type": "markdown", + "id": "cb3369cd", + "metadata": {}, + "source": [ + "### The staging area\n", + "\n", + "The \"staging area\" or \"index\" is the git jargon for the place which contains the list of changes which will be included in the next commit.\n", + "\n", + "You can include specific changes to specific files with `git add`, commit them, add some more files, and commit them. (You can even add specific changes within a file to be included in the index.)" + ] + }, + { + "cell_type": "markdown", + "id": "f85197ee", + "metadata": {}, + "source": [ + "### Message Sequence Charts" + ] + }, + { + "cell_type": "markdown", + "id": "25a096a9", + "metadata": {}, + "source": [ + "In order to illustrate the behaviour of Git, it will be useful to be able to generate figures in Python\n", + "of a \"message sequence chart\" flavour." + ] + }, + { + "cell_type": "markdown", + "id": "ac22939f", + "metadata": {}, + "source": [ + "There's a nice online tool to do this, called \"[Web Sequence diagrams](https://www.websequencediagrams.com)\"." + ] + }, + { + "cell_type": "markdown", + "id": "ec04bde5", + "metadata": {}, + "source": [ + "Instead of just showing you these diagrams, I'm showing you in this notebook how I make them.\n", + "This is part of our \"reproducible computing\" approach; always generating all our figures from code." + ] + }, + { + "cell_type": "markdown", + "id": "3cc8c727", + "metadata": {}, + "source": [ + "Here's some quick code in the Notebook to download and display an MSC illustration, using the Web Sequence Diagrams API:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28361788", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile wsd.py\n", + "import requests\n", + "import re\n", + "import IPython\n", + "\n", + "def wsd(code):\n", + " response = requests.post(\"http://www.websequencediagrams.com/index.php\", data={\n", + " 'message': code,\n", + " 'apiVersion': 1,\n", + " })\n", + " expr = re.compile(\"(\\?(img|pdf|png|svg)=[a-zA-Z0-9]+)\")\n", + " m = expr.search(response.text)\n", + " if m == None:\n", + " print(\"Invalid response from server.\")\n", + " return False\n", + " \n", + " image=requests.get(\"http://www.websequencediagrams.com/\" + m.group(0))\n", + " return IPython.core.display.Image(image.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf0c9c38", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from wsd import wsd\n", + "%matplotlib inline\n", + "wsd(\"Sender->Recipient: Hello\\n Recipient->Sender: Message received OK\")" + ] + }, + { + "cell_type": "markdown", + "id": "6ba7ecbb", + "metadata": {}, + "source": [ + "### The Levels of Git" + ] + }, + { + "cell_type": "markdown", + "id": "26e20198", + "metadata": {}, + "source": [ + "Let's make ourselves a sequence chart to show the different aspects of Git we've seen so far:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c815fd3a", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "Working Directory -> Staging Area : git add\n", + "Staging Area -> Local Repository : git commit\n", + "Working Directory -> Local Repository : git commit -a\n", + "\"\"\"\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "c7ddc5eb", + "metadata": {}, + "source": [ + "### Review of status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "974aa8d9", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8c64e79", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -m \"Add a lie about a mountain\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fd0eb0d", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log" + ] + }, + { + "cell_type": "markdown", + "id": "4fadfc5e", + "metadata": {}, + "source": [ + "Great, we now have a file which contains a mistake." + ] + }, + { + "cell_type": "markdown", + "id": "1762afd5", + "metadata": {}, + "source": [ + "### Carry on regardless\n", + "\n", + "In a while, we'll use Git to roll back to the last correct version: this is one of the main reasons we wanted to use version control, after all! But for now, let's do just as we would if we were writing code, not notice our mistake and keep working..." + ] + }, + { + "cell_type": "markdown", + "id": "80ef59cb", + "metadata": {}, + "source": [ + "```bash\n", + "nano index.md\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc8cda3c", + "metadata": { + "jupyter": { + "outputs_hidden": false + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "Mountains and Hills in the UK \n", + "=================== \n", + "England is not very mountainous. \n", + "But has some tall hills, and maybe a mountain or two depending on your definition.\n", + "\n", + "Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b63cef", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "cat index.md" + ] + }, + { + "cell_type": "markdown", + "id": "cf143322", + "metadata": {}, + "source": [ + "### Commit with a built-in-add" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b946d6d5", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Change title\"" + ] + }, + { + "cell_type": "markdown", + "id": "ccd4d999", + "metadata": {}, + "source": [ + "This last command, `git commit -a` automatically adds changes to all tracked files to the staging area, as part of the commit command. So, if you never want to just add changes to some tracked files but not others, you can just use this and forget about the staging area!" + ] + }, + { + "cell_type": "markdown", + "id": "3b0f5676", + "metadata": {}, + "source": [ + "### Review of changes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "457ea2ef", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log | head" + ] + }, + { + "cell_type": "markdown", + "id": "fab304e8", + "metadata": {}, + "source": [ + "We now have three changes in the history:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "876e66ba", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline" + ] + }, + { + "cell_type": "markdown", + "id": "b7b893ce", + "metadata": {}, + "source": [ + "### Git Solo Workflow" + ] + }, + { + "cell_type": "markdown", + "id": "2f8f76b0", + "metadata": {}, + "source": [ + "We can make a diagram that summarises the above story:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a885503", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "participant \"Cleese's repo\" as R\n", + "participant \"Cleese's index\" as I\n", + "participant Cleese as C\n", + "\n", + "note right of C: nano index.md\n", + "\n", + "note right of C: git init\n", + "C->R: create\n", + "\n", + "note right of C: git add index.md\n", + "\n", + "C->I: Add content of index.md\n", + "\n", + "note right of C: git commit\n", + "I->R: Commit content of index.md\n", + "\n", + "note right of C: nano index.md\n", + "\n", + "note right of C: git add --update\n", + "C->I: Add content of index.md\n", + "note right of C: git commit -m \"Add a lie\"\n", + "I->R: Commit change to index.md\n", + "\n", + "note right of C: nano index.md\n", + "note right of C: git commit -am \"Change title\"\n", + "C->R: Add and commit change to index.md (and all tracked files)\n", + "\"\"\"\n", + "wsd(message)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Solo Git" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/02Solo.ipynb.py b/ch00git/02Solo.ipynb.py new file mode 100644 index 000000000..ca5bfe303 --- /dev/null +++ b/ch00git/02Solo.ipynb.py @@ -0,0 +1,342 @@ +# --- +# jupyter: +# jekyll: +# display_name: Solo Git +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Solo work with Git + +# %% [markdown] +# So, we're in our git working directory: + +# %% jupyter={"outputs_hidden": false} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) +working_dir + +# %% [markdown] +# ### A first example file +# +# So let's create an example file, and see how to start to manage a history of changes to it. + +# %% [markdown] +# index.md # Type some content into the file. + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +Mountains in the UK +=================== +England is not very mountainous. +But has some tall hills, and maybe a mountain or two depending on your definition. + + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} +# cat index.md + +# %% [markdown] +# ### Telling Git about the File +# +# So, let's tell Git that `index.md` is a file which is important, and we would like to keep track of its history: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git add index.md + +# %% [markdown] +# Don't forget: Any files in repositories which you want to "track" need to be added with `git add` after you create them. +# +# ### Our first commit +# +# Now, we need to tell Git to record the first version of this file in the history of changes: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git commit -m "First commit of discourse on UK topography" + +# %% [markdown] +# And note the confirmation from Git. +# +# There's a lot of output there you can ignore for now. + +# %% [markdown] +# ### Configuring Git with your editor +# +# If you don't type in the log message directly with -m "Some message", then an editor will pop up, to allow you +# to edit your message on the fly. + +# %% [markdown] +# For this to work, you have to tell git where to find your editor. + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": true} language="bash" +# git config --global core.editor nano + +# %% [markdown] +# You can find out what you currently have with: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git config --get core.editor + +# %% [markdown] +# To configure VS Code on your operating system you'll need something like the below, ask a demonstrator to help for your machine. + +# %% [markdown] attributes={"classes": [" Bash"], "id": ""} +# ``` bash +# $ git config --global core.editor "code --wait" +# ``` + +# %% [markdown] +# I'm going to be using `nano` as my editor, but you can use whatever editor you prefer. Find how to setup your favourite editor in [the setup chapter of Software Carpentry's Git lesson](https://swcarpentry.github.io/git-novice/02-setup.html). + +# %% [markdown] +# ### Git log +# +# Git now has one change in its history: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log + +# %% [markdown] +# You can see the commit message, author, and date... + +# %% [markdown] +# ### Hash Codes +# +# The commit "hash code", e.g. +# +# `c438f1716b2515563e03e82231acbae7dd4f4656` +# +# is a unique identifier of that particular revision. +# +# (This is a really long code, but whenever you need to use it, you can just use the first few characters, however many characters is long enough to make it unique, `c438` for example. ) + +# %% [markdown] +# ### Nothing to see here +# +# Note that git will now tell us that our "working directory" is up-to-date with the repository: there are no changes to the files that aren't recorded in the repository history: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% [markdown] +# Let's edit the file again: +# +# nano index.md + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +Mountains in the UK +=================== +England is not very mountainous. +But has some tall hills, and maybe a mountain or two depending on your definition. + +Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world. + + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} +# cat index.md + +# %% [markdown] +# ### Unstaged changes + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% [markdown] +# We can now see that there is a change to "index.md" which is currently "not staged for commit". What does this mean? +# +# If we do a `git commit` now *nothing will happen*. +# +# Git will only commit changes to files that you choose to include in each commit. +# +# This is a difference from other version control systems, where committing will affect all changed files. + +# %% [markdown] +# We can see the differences in the file with: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git diff + +# %% [markdown] +# Deleted lines are prefixed with a minus, added lines prefixed with a plus. + +# %% [markdown] +# ### Staging a file to be included in the next commit +# +# To include the file in the next commit, we have a few choices. This is one of the things to be careful of with git: there are lots of ways to do similar things, and it can be hard to keep track of them all. + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": true} language="bash" +# git add --update + +# %% [markdown] +# This says "include in the next commit, all files which have ever been included before". +# +# Note that `git add` is the command we use to introduce git to a new file, but also the command we use to "stage" a file to be included in the next commit. + +# %% [markdown] +# ### The staging area +# +# The "staging area" or "index" is the git jargon for the place which contains the list of changes which will be included in the next commit. +# +# You can include specific changes to specific files with `git add`, commit them, add some more files, and commit them. (You can even add specific changes within a file to be included in the index.) + +# %% [markdown] +# ### Message Sequence Charts + +# %% [markdown] +# In order to illustrate the behaviour of Git, it will be useful to be able to generate figures in Python +# of a "message sequence chart" flavour. + +# %% [markdown] +# There's a nice online tool to do this, called "[Web Sequence diagrams](https://www.websequencediagrams.com)". + +# %% [markdown] +# Instead of just showing you these diagrams, I'm showing you in this notebook how I make them. +# This is part of our "reproducible computing" approach; always generating all our figures from code. + +# %% [markdown] +# Here's some quick code in the Notebook to download and display an MSC illustration, using the Web Sequence Diagrams API: + +# %% jupyter={"outputs_hidden": false} +# %%writefile wsd.py +import requests +import re +import IPython + +def wsd(code): + response = requests.post("http://www.websequencediagrams.com/index.php", data={ + 'message': code, + 'apiVersion': 1, + }) + expr = re.compile("(\?(img|pdf|png|svg)=[a-zA-Z0-9]+)") + m = expr.search(response.text) + if m == None: + print("Invalid response from server.") + return False + + image=requests.get("http://www.websequencediagrams.com/" + m.group(0)) + return IPython.core.display.Image(image.content) + + +# %% jupyter={"outputs_hidden": false} +from wsd import wsd +# %matplotlib inline +wsd("Sender->Recipient: Hello\n Recipient->Sender: Message received OK") + +# %% [markdown] +# ### The Levels of Git + +# %% [markdown] +# Let's make ourselves a sequence chart to show the different aspects of Git we've seen so far: + +# %% jupyter={"outputs_hidden": false} +message=""" +Working Directory -> Staging Area : git add +Staging Area -> Local Repository : git commit +Working Directory -> Local Repository : git commit -a +""" +wsd(message) + +# %% [markdown] +# ### Review of status + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -m "Add a lie about a mountain" + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log + +# %% [markdown] +# Great, we now have a file which contains a mistake. + +# %% [markdown] +# ### Carry on regardless +# +# In a while, we'll use Git to roll back to the last correct version: this is one of the main reasons we wanted to use version control, after all! But for now, let's do just as we would if we were writing code, not notice our mistake and keep working... + +# %% [markdown] +# ```bash +# nano index.md +# ``` + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +Mountains and Hills in the UK +=================== +England is not very mountainous. +But has some tall hills, and maybe a mountain or two depending on your definition. + +Mount Fictional, in Barsetshire, U.K. is the tallest mountain in the world. + + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} +# cat index.md + +# %% [markdown] +# ### Commit with a built-in-add + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Change title" + +# %% [markdown] +# This last command, `git commit -a` automatically adds changes to all tracked files to the staging area, as part of the commit command. So, if you never want to just add changes to some tracked files but not others, you can just use this and forget about the staging area! + +# %% [markdown] +# ### Review of changes + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log | head + +# %% [markdown] +# We now have three changes in the history: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log --oneline + +# %% [markdown] +# ### Git Solo Workflow + +# %% [markdown] +# We can make a diagram that summarises the above story: + +# %% jupyter={"outputs_hidden": false} +message=""" +participant "Cleese's repo" as R +participant "Cleese's index" as I +participant Cleese as C + +note right of C: nano index.md + +note right of C: git init +C->R: create + +note right of C: git add index.md + +C->I: Add content of index.md + +note right of C: git commit +I->R: Commit content of index.md + +note right of C: nano index.md + +note right of C: git add --update +C->I: Add content of index.md +note right of C: git commit -m "Add a lie" +I->R: Commit change to index.md + +note right of C: nano index.md +note right of C: git commit -am "Change title" +C->R: Add and commit change to index.md (and all tracked files) +""" +wsd(message) diff --git a/ch00git/03Mistakes.html b/ch00git/03Mistakes.html new file mode 100644 index 000000000..ce405393d --- /dev/null +++ b/ch00git/03Mistakes.html @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fixing Mistakes + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Fixing mistakes

+
+
+
+
+
+
+

We're still in our git working directory:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+working_dir
+
+
+
+
+
+
+
+
Out[1]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/git_example'
+
+
+
+
+
+
+
+
+

Referring to changes with HEAD and ^

The commit we want to revert to is the one before the latest.

+

HEAD refers to the latest commit. That is, we want to go back to the change before the current HEAD.

+

We could use the hash code (e.g. 73fbeaf) to reference this, but you can also refer to the commit before the HEAD as HEAD^, the one before that as HEAD^^, the one before that as HEAD~3.

+
+
+
+
+
+
+

Reverting

Ok, so now we'd like to undo the nasty commit with the lie about Mount Fictional.

+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+git revert HEAD^
+
+
+
+
+
+
+
+
+
+
Auto-merging index.md
+[main 21c306b] Revert "Add a lie about a mountain"
+ Date: Wed Nov 22 15:42:18 2023 +0000
+ 1 file changed, 2 deletions(-)
+
+
+
+
+
+
+
+
+
+

An editor may pop up, with some default text which you can accept and save.

+
+
+
+
+
+
+

Conflicted reverts

You may, depending on the changes you've tried to make, get an error message here.

+

If this happens, it is because git could not automagically decide how to combine the change you made after the change you want to revert, with the attempt to revert the change: this could happen, for example, if they both touch the same line.

+

If that happens, you need to manually edit the file to fix the problem. Skip ahead to the section on resolving conflicts, or ask a demonstrator to help.

+
+
+
+
+
+
+

Review of changes

The file should now contain the change to the title, but not the extra line with the lie. Note the log:

+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+git log --date=short
+
+
+
+
+
+
+
+
+
+
commit 21c306bbfcd288f6f6e74c8495301159bb1f8ed2
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Revert "Add a lie about a mountain"
+    
+    This reverts commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68.
+
+commit 5bf472d82a68b889f58165491d712e8f15de0de7
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Change title
+
+commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Add a lie about a mountain
+
+commit 6c90dbd9b71257440d4fb40e8ff8d8792194979f
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Antipatch

Notice how the mistake has stayed in the history.

+

There is a new commit which undoes the change: this is colloquially called an "antipatch". +This is nice: you have a record of the full story, including the mistake and its correction.

+
+
+
+
+
+
+

Rewriting history

It is possible, in git, to remove the most recent change altogether, "rewriting history". Let's make another bad change, and see how to do this.

+
+
+
+
+
+
+

A new lie

+
+
+
+
+
+
In [4]:
+
+
+
%%writefile index.md
+Mountains and Hills in the UK   
+===================   
+Engerland is not very mountainous.   
+But has some tall hills, and maybe a
+mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+
Overwriting index.md
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains and Hills in the UK   
+===================   
+Engerland is not very mountainous.   
+But has some tall hills, and maybe a
+mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+git diff
+
+
+
+
+
+
+
+
+
+
diff --git a/index.md b/index.md
+index dd5cf9c..4801c98 100644
+--- a/index.md
++++ b/index.md
+@@ -1,4 +1,5 @@
+ Mountains and Hills in the UK   
+ ===================   
+-England is not very mountainous.   
+-But has some tall hills, and maybe a mountain or two depending on your definition.
++Engerland is not very mountainous.   
++But has some tall hills, and maybe a
++mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git commit -am "Add a silly spelling"
+
+
+
+
+
+
+
+
+
+
[main 38c4c3a] Add a silly spelling
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git log --date=short
+
+
+
+
+
+
+
+
+
+
commit 38c4c3aa29dc0a4f3613851b644b86fb85384185
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Add a silly spelling
+
+commit 21c306bbfcd288f6f6e74c8495301159bb1f8ed2
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Revert "Add a lie about a mountain"
+    
+    This reverts commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68.
+
+commit 5bf472d82a68b889f58165491d712e8f15de0de7
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Change title
+
+commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Add a lie about a mountain
+
+commit 6c90dbd9b71257440d4fb40e8ff8d8792194979f
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Using reset to rewrite history

+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+git reset HEAD^
+
+
+
+
+
+
+
+
+
+
Unstaged changes after reset:
+M	index.md
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
%%bash
+git log --date=short
+
+
+
+
+
+
+
+
+
+
commit 21c306bbfcd288f6f6e74c8495301159bb1f8ed2
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Revert "Add a lie about a mountain"
+    
+    This reverts commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68.
+
+commit 5bf472d82a68b889f58165491d712e8f15de0de7
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Change title
+
+commit 5aa2999dd374d2da5874f5b379646b8f4c6d7a68
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    Add a lie about a mountain
+
+commit 6c90dbd9b71257440d4fb40e8ff8d8792194979f
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   2023-11-22
+
+    First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Covering your tracks

The silly spelling is no longer in the log. This approach to fixing mistakes, "rewriting history" with reset, instead of adding an antipatch with revert, is dangerous, and we don't recommend it. But you may want to do it for small silly mistakes, such as to correct a commit message.

+
+
+
+
+
+
+

Resetting the working area

When git reset removes commits, it leaves your working directory unchanged -- so you can keep the work in the bad change if you want.

+
+
+
+
+
+
In [11]:
+
+
+
%%bash
+cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains and Hills in the UK   
+===================   
+Engerland is not very mountainous.   
+But has some tall hills, and maybe a
+mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+

If you want to lose the change from the working directory as well, you can do git reset --hard.

+

I'm going to get rid of the silly spelling, and I didn't do --hard, so I'll reset the file from the working directory to be the same as in the index:

+
+
+
+
+
+
In [12]:
+
+
+
%%bash
+git checkout index.md
+
+
+
+
+
+
+
+
+
+
Updated 1 path from the index
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+cat index.md
+
+
+
+
+
+
+
+
+
+
Mountains and Hills in the UK   
+===================   
+England is not very mountainous.   
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+

We can add this to our diagram:

+
+
+
+
+
+
In [14]:
+
+
+
message="""
+Working Directory -> Staging Area : git add
+Staging Area -> Local Repository : git commit
+Working Directory -> Local Repository : git commit -a
+Staging Area -> Working Directory : git checkout
+Local Repository -> Staging Area : git reset
+Local Repository -> Working Directory: git reset --hard
+"""
+from wsd import wsd
+%matplotlib inline
+wsd(message)
+
+
+
+
+
+
+
+
Out[14]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We can add it to Cleese's story:

+
+
+
+
+
+
In [15]:
+
+
+
message="""
+participant "Cleese's repo" as R
+participant "Cleese's index" as I
+participant Cleese as C
+
+note right of C: git revert HEAD^
+
+C->R: Add new commit reversing change
+R->I: update staging area to reverted version
+I->C: update file to reverted version
+
+
+
+note right of C: vim index.md
+note right of C: git commit -am "Add another mistake"
+C->I: Add mistake
+I->R: Add mistake
+
+note right of C: git reset HEAD^
+
+C->R: Delete mistaken commit
+R->I: Update staging area to reset commit
+
+note right of C: git checkout index.md
+
+I->C: Update file to reverted version
+
+
+"""
+wsd(message)
+
+
+
+
+
+
+
+
Out[15]:
+
+No description has been provided for this image +
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/03Mistakes.ipynb b/ch00git/03Mistakes.ipynb new file mode 100644 index 000000000..a4a8b6a4c --- /dev/null +++ b/ch00git/03Mistakes.ipynb @@ -0,0 +1,473 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "891b86c5", + "metadata": {}, + "source": [ + "## Fixing mistakes" + ] + }, + { + "cell_type": "markdown", + "id": "15e70384", + "metadata": {}, + "source": [ + "We're still in our git working directory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6d6ef2b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)\n", + "working_dir" + ] + }, + { + "cell_type": "markdown", + "id": "caea4f3e", + "metadata": {}, + "source": [ + "### Referring to changes with HEAD and ^\n", + "\n", + "The commit we want to revert to is the one before the latest.\n", + "\n", + "`HEAD` refers to the latest commit. That is, we want to go back to the change before the current `HEAD`. \n", + "\n", + "We could use the hash code (e.g. 73fbeaf) to reference this, but you can also refer to the commit before the `HEAD` as `HEAD^`, the one before that as `HEAD^^`, the one before that as `HEAD~3`." + ] + }, + { + "cell_type": "markdown", + "id": "3f1731f1", + "metadata": {}, + "source": [ + "### Reverting\n", + " \n", + "Ok, so now we'd like to undo the nasty commit with the lie about Mount Fictional." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bd31474", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git revert HEAD^" + ] + }, + { + "cell_type": "markdown", + "id": "f3634950", + "metadata": {}, + "source": [ + "An editor may pop up, with some default text which you can accept and save. " + ] + }, + { + "cell_type": "markdown", + "id": "a2d10e4d", + "metadata": {}, + "source": [ + "### Conflicted reverts\n", + "\n", + "You may, depending on the changes you've tried to make, get an error message here. \n", + "\n", + "If this happens, it is because git could not automagically decide how to combine the change you made after the change you want to revert, with the attempt to revert the change: this could happen, for example, if they both touch the same line. \n", + "\n", + "If that happens, you need to manually edit the file to fix the problem. Skip ahead to the section on resolving conflicts, or ask a demonstrator to help." + ] + }, + { + "cell_type": "markdown", + "id": "df059e1b", + "metadata": {}, + "source": [ + "### Review of changes\n", + "\n", + "The file should now contain the change to the title, but not the extra line with the lie. Note the log:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37f4061a", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --date=short" + ] + }, + { + "cell_type": "markdown", + "id": "e6335cbb", + "metadata": {}, + "source": [ + "### Antipatch\n", + "\n", + "Notice how the mistake has stayed in the history.\n", + "\n", + "There is a new commit which undoes the change: this is colloquially called an \"antipatch\". \n", + "This is nice: you have a record of the full story, including the mistake and its correction." + ] + }, + { + "cell_type": "markdown", + "id": "a12dc593", + "metadata": {}, + "source": [ + "### Rewriting history\n", + "\n", + "It is possible, in git, to remove the most recent change altogether, \"rewriting history\". Let's make another bad change, and see how to do this." + ] + }, + { + "cell_type": "markdown", + "id": "b80b957c", + "metadata": {}, + "source": [ + "### A new lie" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "123de4b3", + "metadata": { + "jupyter": { + "outputs_hidden": false + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "Mountains and Hills in the UK \n", + "=================== \n", + "Engerland is not very mountainous. \n", + "But has some tall hills, and maybe a\n", + "mountain or two depending on your definition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8022ffe5", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat index.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c52bd64c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git diff" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b82a34ce", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add a silly spelling\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f3bbe30", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --date=short" + ] + }, + { + "cell_type": "markdown", + "id": "ad884425", + "metadata": {}, + "source": [ + "### Using reset to rewrite history" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4d08b31", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git reset HEAD^" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af83aa39", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --date=short" + ] + }, + { + "cell_type": "markdown", + "id": "674a9a74", + "metadata": {}, + "source": [ + "### Covering your tracks\n", + "\n", + "The silly spelling *is no longer in the log*. This approach to fixing mistakes, \"rewriting history\" with `reset`, instead of adding an antipatch with `revert`, is dangerous, and we don't recommend it. But you may want to do it for small silly mistakes, such as to correct a commit message." + ] + }, + { + "cell_type": "markdown", + "id": "fbd5c04f", + "metadata": {}, + "source": [ + "### Resetting the working area\n", + "\n", + "When `git reset` removes commits, it leaves your working directory unchanged -- so you can keep the work in the bad change if you want. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d724a601", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat index.md" + ] + }, + { + "cell_type": "markdown", + "id": "63f9b103", + "metadata": {}, + "source": [ + "If you want to lose the change from the working directory as well, you can do `git reset --hard`. \n", + "\n", + "I'm going to get rid of the silly spelling, and I didn't do `--hard`, so I'll reset the file from the working directory to be the same as in the index:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bad8b9c1", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git checkout index.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e083ea0", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat index.md" + ] + }, + { + "cell_type": "markdown", + "id": "feb60933", + "metadata": {}, + "source": [ + "We can add this to our diagram:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2961bef3", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "Working Directory -> Staging Area : git add\n", + "Staging Area -> Local Repository : git commit\n", + "Working Directory -> Local Repository : git commit -a\n", + "Staging Area -> Working Directory : git checkout\n", + "Local Repository -> Staging Area : git reset\n", + "Local Repository -> Working Directory: git reset --hard\n", + "\"\"\"\n", + "from wsd import wsd\n", + "%matplotlib inline\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "a9010ca5", + "metadata": {}, + "source": [ + "We can add it to Cleese's story:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a078499", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "participant \"Cleese's repo\" as R\n", + "participant \"Cleese's index\" as I\n", + "participant Cleese as C\n", + "\n", + "note right of C: git revert HEAD^\n", + "\n", + "C->R: Add new commit reversing change\n", + "R->I: update staging area to reverted version\n", + "I->C: update file to reverted version\n", + "\n", + "\n", + "\n", + "note right of C: vim index.md\n", + "note right of C: git commit -am \"Add another mistake\"\n", + "C->I: Add mistake\n", + "I->R: Add mistake\n", + "\n", + "note right of C: git reset HEAD^\n", + "\n", + "C->R: Delete mistaken commit\n", + "R->I: Update staging area to reset commit\n", + "\n", + "note right of C: git checkout index.md\n", + "\n", + "I->C: Update file to reverted version\n", + "\n", + "\n", + "\"\"\"\n", + "wsd(message)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Fixing Mistakes" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/03Mistakes.ipynb.py b/ch00git/03Mistakes.ipynb.py new file mode 100644 index 000000000..96db48425 --- /dev/null +++ b/ch00git/03Mistakes.ipynb.py @@ -0,0 +1,184 @@ +# --- +# jupyter: +# jekyll: +# display_name: Fixing Mistakes +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Fixing mistakes + +# %% [markdown] +# We're still in our git working directory: + +# %% jupyter={"outputs_hidden": false} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) +working_dir + +# %% [markdown] +# ### Referring to changes with HEAD and ^ +# +# The commit we want to revert to is the one before the latest. +# +# `HEAD` refers to the latest commit. That is, we want to go back to the change before the current `HEAD`. +# +# We could use the hash code (e.g. 73fbeaf) to reference this, but you can also refer to the commit before the `HEAD` as `HEAD^`, the one before that as `HEAD^^`, the one before that as `HEAD~3`. + +# %% [markdown] +# ### Reverting +# +# Ok, so now we'd like to undo the nasty commit with the lie about Mount Fictional. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git revert HEAD^ + +# %% [markdown] +# An editor may pop up, with some default text which you can accept and save. + +# %% [markdown] +# ### Conflicted reverts +# +# You may, depending on the changes you've tried to make, get an error message here. +# +# If this happens, it is because git could not automagically decide how to combine the change you made after the change you want to revert, with the attempt to revert the change: this could happen, for example, if they both touch the same line. +# +# If that happens, you need to manually edit the file to fix the problem. Skip ahead to the section on resolving conflicts, or ask a demonstrator to help. + +# %% [markdown] +# ### Review of changes +# +# The file should now contain the change to the title, but not the extra line with the lie. Note the log: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --date=short + +# %% [markdown] +# ### Antipatch +# +# Notice how the mistake has stayed in the history. +# +# There is a new commit which undoes the change: this is colloquially called an "antipatch". +# This is nice: you have a record of the full story, including the mistake and its correction. + +# %% [markdown] +# ### Rewriting history +# +# It is possible, in git, to remove the most recent change altogether, "rewriting history". Let's make another bad change, and see how to do this. + +# %% [markdown] +# ### A new lie + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +Mountains and Hills in the UK +=================== +Engerland is not very mountainous. +But has some tall hills, and maybe a +mountain or two depending on your definition. + + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# cat index.md + +# %% jupyter={"outputs_hidden": false} language="bash" +# git diff + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add a silly spelling" + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log --date=short + +# %% [markdown] +# ### Using reset to rewrite history + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git reset HEAD^ + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git log --date=short + +# %% [markdown] +# ### Covering your tracks +# +# The silly spelling *is no longer in the log*. This approach to fixing mistakes, "rewriting history" with `reset`, instead of adding an antipatch with `revert`, is dangerous, and we don't recommend it. But you may want to do it for small silly mistakes, such as to correct a commit message. + +# %% [markdown] +# ### Resetting the working area +# +# When `git reset` removes commits, it leaves your working directory unchanged -- so you can keep the work in the bad change if you want. + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat index.md + +# %% [markdown] +# If you want to lose the change from the working directory as well, you can do `git reset --hard`. +# +# I'm going to get rid of the silly spelling, and I didn't do `--hard`, so I'll reset the file from the working directory to be the same as in the index: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": true} language="bash" +# git checkout index.md + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat index.md + +# %% [markdown] +# We can add this to our diagram: + +# %% jupyter={"outputs_hidden": false} +message=""" +Working Directory -> Staging Area : git add +Staging Area -> Local Repository : git commit +Working Directory -> Local Repository : git commit -a +Staging Area -> Working Directory : git checkout +Local Repository -> Staging Area : git reset +Local Repository -> Working Directory: git reset --hard +""" +from wsd import wsd +# %matplotlib inline +wsd(message) + +# %% [markdown] +# We can add it to Cleese's story: + +# %% jupyter={"outputs_hidden": false} +message=""" +participant "Cleese's repo" as R +participant "Cleese's index" as I +participant Cleese as C + +note right of C: git revert HEAD^ + +C->R: Add new commit reversing change +R->I: update staging area to reverted version +I->C: update file to reverted version + + + +note right of C: vim index.md +note right of C: git commit -am "Add another mistake" +C->I: Add mistake +I->R: Add mistake + +note right of C: git reset HEAD^ + +C->R: Delete mistaken commit +R->I: Update staging area to reset commit + +note right of C: git checkout index.md + +I->C: Update file to reverted version + + +""" +wsd(message) diff --git a/ch00git/04Publishing.html b/ch00git/04Publishing.html new file mode 100644 index 000000000..faa5a5f8c --- /dev/null +++ b/ch00git/04Publishing.html @@ -0,0 +1,934 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Publishing + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Publishing

+
+
+
+
+
+
+

We're still in our working directory:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+working_dir
+
+
+
+
+
+
+
+
Out[1]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/git_example'
+
+
+
+
+
+
+
+
+

Sharing your work

+
+
+
+
+
+
+

So far, all our work has been on our own computer. But a big part of the point of version control is keeping your work safe, on remote servers. Another part is making it easy to share your work with the world In this example, we'll be using the "GitHub" cloud repository to store and publish our work.

+

If you have not done so already, you should create an account on GitHub: go to GitHub's website, fill in a username and password, and click on "sign up for GitHub".

+
+
+
+
+
+
+

Creating a repository

Ok, let's create a repository to store our work. Hit "new repository" on the right of the github home screen.

+

Fill in a short name, and a description. Choose a "public" repository. Don't choose to initialize the repository with a README. That will create a repository with content and we only want a placeholder where to upload what we've created locally.

+
+
+
+
+
+
+

Paying for GitHub

For this course, you should use public repositories in your personal account for your example work: it's good to share! GitHub is free for open source, but in general, charges a fee if you want to keep your work private.

+

In the future, you might want to keep your work on GitHub private.

+

Students can get free private repositories on GitHub, by going to GitHub Education and filling in a form (look for the Student Developer Pack).

+

UCL pays for private GitHub repositories for UCL research groups: you can find the service details on the Advanced Research Computing Centre's website.

+
+
+
+
+
+
+

Adding a new remote to your repository

Instructions will appear, once you've created the repository, as to how to add this new "remote" server to your repository, in the lower box on the screen. Mine say:

+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+git remote add origin git@github.com:UCL/github-example.git
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository.
+      #You should copy the instructions from YOUR repository.
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+ + ee4e4b4...21c306b main -> main (forced update)
+
+
+
+
+
+
+
branch 'main' set up to track 'origin/main'.
+
+
+
+
+
+
+
+
+
+

Remotes

The first command sets up the server as a new remote, called origin.

+

Git, unlike some earlier version control systems is a "distributed" version control system, which means you can work with multiple remote servers.

+

Usually, commands that work with remotes allow you to specify the remote to use, but assume the origin remote if you don't.

+

Here, git push will push your whole history onto the server, and now you'll be able to see it on the internet! Refresh your web browser where the instructions were, and you'll see your repository!

+
+
+
+
+
+
+

Let's add these commands to our diagram:

+
+
+
+
+
+
In [4]:
+
+
+
message="""
+Working Directory -> Staging Area : git add
+Staging Area -> Local Repository : git commit
+Working Directory -> Local Repository : git commit -a
+Staging Area -> Working Directory : git checkout
+Local Repository -> Staging Area : git reset
+Local Repository -> Working Directory: git reset --hard
+Local Repository -> Remote Repository : git push
+"""
+from wsd import wsd
+%matplotlib inline
+wsd(message)
+
+
+
+
+
+
+
+
Out[4]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Playing with GitHub

Take a few moments to click around and work your way through the GitHub interface. Try clicking on 'index.md' to see the content of the file: notice how the markdown renders prettily.

+

Click on "commits" near the top of the screen, to see all the changes you've made. Click on the commit number next to the right of a change, to see what changes it includes: removals are shown in red, and additions in green.

+
+
+
+
+
+
+

Working with multiple files

+
+
+
+
+
+
+

Some new content

So far, we've only worked with one file. Let's add another:

+
+
+
+
+
+
+
nano lakeland.md
+
+
+
+
+
+
+
In [5]:
+
+
+
%%writefile lakeland.md
+Lakeland  
+========   
+  
+Cumbria has some pretty hills, and lakes too.  
+
+
+
+
+
+
+
+
+
+
Writing lakeland.md
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
cat lakeland.md
+
+
+
+
+
+
+
+
+
+
Lakeland  
+========   
+  
+Cumbria has some pretty hills, and lakes too.  
+
+
+
+
+
+
+
+
+
+

Git will not by default commit your new file

+
+
+
+
+
+
In [7]:
+
+
+
%%bash --no-raise-error
+git commit -am "Try to add Lakeland"
+
+
+
+
+
+
+
+
+
+
On branch main
+Your branch is up to date with 'origin/main'.
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	__pycache__/
+	lakeland.md
+	wsd.py
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+
+
+

This didn't do anything, because we've not told git to track the new file yet.

+
+
+
+
+
+
+

Tell git about the new file

+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git add lakeland.md
+git commit -am "Add lakeland"
+
+
+
+
+
+
+
+
+
+
[main 7b49cfb] Add lakeland
+ 1 file changed, 4 insertions(+)
+ create mode 100644 lakeland.md
+
+
+
+
+
+
+
+
+
+

Ok, now we have added the change about Cumbria to the file. Let's publish it to the origin repository.

+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   21c306b..7b49cfb  main -> main
+
+
+
+
+
+
+
+
+
+

Visit GitHub, and notice this change is on your repository on the server. We could have said git push origin to specify the remote to use, but origin is the default.

+
+
+
+
+
+
+

Changing two files at once

+
+
+
+
+
+
+

What if we change both files?

+
+
+
+
+
+
In [10]:
+
+
+
%%writefile lakeland.md
+Lakeland  
+========   
+  
+Cumbria has some pretty hills, and lakes too
+
+Mountains:
+* Helvellyn
+
+
+
+
+
+
+
+
+
+
Overwriting lakeland.md
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
%%writefile index.md
+Mountains and Lakes in the UK   
+===================   
+Engerland is not very mountainous.
+But has some tall hills, and maybe a
+mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+
Overwriting index.md
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+Your branch is up to date with 'origin/main'.
+
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+	modified:   index.md
+	modified:   lakeland.md
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	__pycache__/
+	wsd.py
+
+no changes added to commit (use "git add" and/or "git commit -a")
+
+
+
+
+
+
+
+
+
+

These changes should really be separate commits. We can do this with careful use of git add, to stage first one commit, then the other.

+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+git add index.md
+git commit -m "Include lakes in the scope"
+
+
+
+
+
+
+
+
+
+
[main df5fed8] Include lakes in the scope
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+
+
+
+
+
+
+
+
+

Because we "staged" only index.md, the changes to lakeland.md were not included in that commit.

+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+git commit -am "Add Helvellyn"
+
+
+
+
+
+
+
+
+
+
[main 4b52f45] Add Helvellyn
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
%%bash
+git log --oneline
+
+
+
+
+
+
+
+
+
+
4b52f45 Add Helvellyn
+df5fed8 Include lakes in the scope
+7b49cfb Add lakeland
+21c306b Revert "Add a lie about a mountain"
+5bf472d Change title
+5aa2999 Add a lie about a mountain
+6c90dbd First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   7b49cfb..4b52f45  main -> main
+
+
+
+
+
+
+
+
+
In [17]:
+
+
+
message="""
+participant "Cleese's remote" as M
+participant "Cleese's repo" as R
+participant "Cleese's index" as I
+participant Cleese as C
+
+note right of C: nano index.md
+note right of C: nano lakeland.md
+
+note right of C: git add index.md
+C->I: Add *only* the changes to index.md to the staging area
+
+note right of C: git commit -m "Include lakes"
+I->R: Make a commit from currently staged changes: index.md only
+
+note right of C: git commit -am "Add Helvellyn"
+C->I: Stage *all remaining* changes, (lakeland.md)
+I->R: Make a commit from currently staged changes
+
+note right of C: git push
+R->M: Transfer commits to Github
+"""
+wsd(message)
+
+
+
+
+
+
+
+
Out[17]:
+
+No description has been provided for this image +
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/04Publishing.ipynb b/ch00git/04Publishing.ipynb new file mode 100644 index 000000000..d36277737 --- /dev/null +++ b/ch00git/04Publishing.ipynb @@ -0,0 +1,558 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8f310c06", + "metadata": {}, + "source": [ + "## Publishing" + ] + }, + { + "cell_type": "markdown", + "id": "b96b147b", + "metadata": {}, + "source": [ + "We're still in our working directory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e683dd2", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)\n", + "working_dir" + ] + }, + { + "cell_type": "markdown", + "id": "b3346179", + "metadata": {}, + "source": [ + "### Sharing your work" + ] + }, + { + "cell_type": "markdown", + "id": "089028b8", + "metadata": {}, + "source": [ + "So far, all our work has been on our own computer. But a big part of the point of version control is keeping your work safe, on remote servers. Another part is making it easy to share your work with the world In this example, we'll be using the \"GitHub\" cloud repository to store and publish our work. \n", + "\n", + "If you have not done so already, you should create an account on [GitHub](https://github.com/): go to [GitHub's website](https://github.com/), fill in a username and password, and click on \"sign up for GitHub\". " + ] + }, + { + "cell_type": "markdown", + "id": "947554b3", + "metadata": {}, + "source": [ + "### Creating a repository\n", + "\n", + "Ok, let's create a repository to store our work. Hit \"[new repository](https://github.com/new)\" on the right of the github home screen.\n", + "\n", + "Fill in a short name, and a description. Choose a \"public\" repository. Don't choose to initialize the repository with a README. That will create a repository with content and we only want a placeholder where to upload what we've created locally." + ] + }, + { + "cell_type": "markdown", + "id": "476f0045", + "metadata": {}, + "source": [ + "### Paying for GitHub\n", + "\n", + "For this course, you should use public repositories in your personal account for your example work: it's good to share! GitHub is free for open source, but in general, charges a fee if you want to keep your work private. \n", + "\n", + "In the future, you might want to keep your work on GitHub private. \n", + "\n", + "Students can get free private repositories on GitHub, by going to [GitHub Education](https://education.github.com/) and filling in a form (look for the Student Developer Pack). \n", + "\n", + "UCL pays for private GitHub repositories for UCL research groups: you can find the service details on the [Advanced Research Computing Centre's website](https://www.ucl.ac.uk/advanced-research-computing/expertise/research-software-development/research-software-development-tools/support-ucl-2)." + ] + }, + { + "cell_type": "markdown", + "id": "39f3d7c2", + "metadata": {}, + "source": [ + "### Adding a new remote to your repository\n", + "\n", + "Instructions will appear, once you've created the repository, as to how to add this new \"remote\" server to your repository, in the lower box on the screen. Mine say:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed823525", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git remote add origin git@github.com:UCL/github-example.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4c51fc6", + "metadata": { + "attributes": { + "classes": [ + "Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository.\n", + " #You should copy the instructions from YOUR repository." + ] + }, + { + "cell_type": "markdown", + "id": "e969930e", + "metadata": {}, + "source": [ + "### Remotes\n", + "\n", + "The first command sets up the server as a new `remote`, called `origin`. \n", + "\n", + "Git, unlike some earlier version control systems is a \"distributed\" version control system, which means you can work with multiple remote servers. \n", + "\n", + "Usually, commands that work with remotes allow you to specify the remote to use, but assume the `origin` remote if you don't. \n", + "\n", + "Here, `git push` will push your whole history onto the server, and now you'll be able to see it on the internet! Refresh your web browser where the instructions were, and you'll see your repository!" + ] + }, + { + "cell_type": "markdown", + "id": "413e7995", + "metadata": {}, + "source": [ + "Let's add these commands to our diagram:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1f2410b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "Working Directory -> Staging Area : git add\n", + "Staging Area -> Local Repository : git commit\n", + "Working Directory -> Local Repository : git commit -a\n", + "Staging Area -> Working Directory : git checkout\n", + "Local Repository -> Staging Area : git reset\n", + "Local Repository -> Working Directory: git reset --hard\n", + "Local Repository -> Remote Repository : git push\n", + "\"\"\"\n", + "from wsd import wsd\n", + "%matplotlib inline\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "ae8ecc92", + "metadata": {}, + "source": [ + "### Playing with GitHub\n", + "\n", + "Take a few moments to click around and work your way through the GitHub interface. Try clicking on 'index.md' to see the content of the file: notice how the markdown renders prettily.\n", + "\n", + "Click on \"commits\" near the top of the screen, to see all the changes you've made. Click on the commit number next to the right of a change, to see what changes it includes: removals are shown in red, and additions in green." + ] + }, + { + "cell_type": "markdown", + "id": "e0e62115", + "metadata": {}, + "source": [ + "## Working with multiple files" + ] + }, + { + "cell_type": "markdown", + "id": "557acf99", + "metadata": {}, + "source": [ + "### Some new content\n", + "\n", + "So far, we've only worked with one file. Let's add another:" + ] + }, + { + "cell_type": "markdown", + "id": "5181260e", + "metadata": {}, + "source": [ + "``` bash\n", + "nano lakeland.md\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a595712c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile lakeland.md\n", + "Lakeland \n", + "======== \n", + " \n", + "Cumbria has some pretty hills, and lakes too. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02ae1baa", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "cat lakeland.md" + ] + }, + { + "cell_type": "markdown", + "id": "e7419010", + "metadata": {}, + "source": [ + "### Git will not by default commit your new file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b13ccacb", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git commit -am \"Try to add Lakeland\"" + ] + }, + { + "cell_type": "markdown", + "id": "7fb1a059", + "metadata": {}, + "source": [ + "This didn't do anything, because we've not told git to track the new file yet." + ] + }, + { + "cell_type": "markdown", + "id": "6ebf930d", + "metadata": {}, + "source": [ + "### Tell git about the new file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02e37dc9", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add lakeland.md\n", + "git commit -am \"Add lakeland\"" + ] + }, + { + "cell_type": "markdown", + "id": "d3e203fd", + "metadata": {}, + "source": [ + "Ok, now we have added the change about Cumbria to the file. Let's publish it to the origin repository." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8ee2f35", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "1541b117", + "metadata": {}, + "source": [ + "Visit GitHub, and notice this change is on your repository on the server. We could have said `git push origin` to specify the remote to use, but origin is the default." + ] + }, + { + "cell_type": "markdown", + "id": "2079f02b", + "metadata": {}, + "source": [ + "## Changing two files at once" + ] + }, + { + "cell_type": "markdown", + "id": "978f3c57", + "metadata": {}, + "source": [ + "What if we change both files?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93841f98", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile lakeland.md\n", + "Lakeland \n", + "======== \n", + " \n", + "Cumbria has some pretty hills, and lakes too\n", + "\n", + "Mountains:\n", + "* Helvellyn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac7e259b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "Mountains and Lakes in the UK \n", + "=================== \n", + "Engerland is not very mountainous.\n", + "But has some tall hills, and maybe a\n", + "mountain or two depending on your definition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ace33076", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "markdown", + "id": "aa41c5e1", + "metadata": {}, + "source": [ + "These changes should really be separate commits. We can do this with careful use of git add, to **stage** first one commit, then the other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da29a085", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add index.md\n", + "git commit -m \"Include lakes in the scope\"" + ] + }, + { + "cell_type": "markdown", + "id": "8e4d2944", + "metadata": {}, + "source": [ + "Because we \"staged\" only index.md, the changes to lakeland.md were not included in that commit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74b01523", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add Helvellyn\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b51d5e7d", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cbdbc0e", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4b818b5", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "participant \"Cleese's remote\" as M\n", + "participant \"Cleese's repo\" as R\n", + "participant \"Cleese's index\" as I\n", + "participant Cleese as C\n", + "\n", + "note right of C: nano index.md\n", + "note right of C: nano lakeland.md\n", + "\n", + "note right of C: git add index.md\n", + "C->I: Add *only* the changes to index.md to the staging area\n", + "\n", + "note right of C: git commit -m \"Include lakes\"\n", + "I->R: Make a commit from currently staged changes: index.md only\n", + "\n", + "note right of C: git commit -am \"Add Helvellyn\"\n", + "C->I: Stage *all remaining* changes, (lakeland.md)\n", + "I->R: Make a commit from currently staged changes\n", + "\n", + "note right of C: git push\n", + "R->M: Transfer commits to Github\n", + "\"\"\"\n", + "wsd(message)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Publishing" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/04Publishing.ipynb.py b/ch00git/04Publishing.ipynb.py new file mode 100644 index 000000000..769f14368 --- /dev/null +++ b/ch00git/04Publishing.ipynb.py @@ -0,0 +1,218 @@ +# --- +# jupyter: +# jekyll: +# display_name: Publishing +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Publishing + +# %% [markdown] +# We're still in our working directory: + +# %% jupyter={"outputs_hidden": false} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) +working_dir + +# %% [markdown] +# ### Sharing your work + +# %% [markdown] +# So far, all our work has been on our own computer. But a big part of the point of version control is keeping your work safe, on remote servers. Another part is making it easy to share your work with the world In this example, we'll be using the "GitHub" cloud repository to store and publish our work. +# +# If you have not done so already, you should create an account on [GitHub](https://github.com/): go to [GitHub's website](https://github.com/), fill in a username and password, and click on "sign up for GitHub". + +# %% [markdown] +# ### Creating a repository +# +# Ok, let's create a repository to store our work. Hit "[new repository](https://github.com/new)" on the right of the github home screen. +# +# Fill in a short name, and a description. Choose a "public" repository. Don't choose to initialize the repository with a README. That will create a repository with content and we only want a placeholder where to upload what we've created locally. + +# %% [markdown] +# ### Paying for GitHub +# +# For this course, you should use public repositories in your personal account for your example work: it's good to share! GitHub is free for open source, but in general, charges a fee if you want to keep your work private. +# +# In the future, you might want to keep your work on GitHub private. +# +# Students can get free private repositories on GitHub, by going to [GitHub Education](https://education.github.com/) and filling in a form (look for the Student Developer Pack). +# +# UCL pays for private GitHub repositories for UCL research groups: you can find the service details on the [Advanced Research Computing Centre's website](https://www.ucl.ac.uk/advanced-research-computing/expertise/research-software-development/research-software-development-tools/support-ucl-2). + +# %% [markdown] +# ### Adding a new remote to your repository +# +# Instructions will appear, once you've created the repository, as to how to add this new "remote" server to your repository, in the lower box on the screen. Mine say: + +# %% jupyter={"outputs_hidden": true} language="bash" +# git remote add origin git@github.com:UCL/github-example.git + +# %% attributes={"classes": ["Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository. +# #You should copy the instructions from YOUR repository. + +# %% [markdown] +# ### Remotes +# +# The first command sets up the server as a new `remote`, called `origin`. +# +# Git, unlike some earlier version control systems is a "distributed" version control system, which means you can work with multiple remote servers. +# +# Usually, commands that work with remotes allow you to specify the remote to use, but assume the `origin` remote if you don't. +# +# Here, `git push` will push your whole history onto the server, and now you'll be able to see it on the internet! Refresh your web browser where the instructions were, and you'll see your repository! + +# %% [markdown] +# Let's add these commands to our diagram: + +# %% jupyter={"outputs_hidden": false} +message=""" +Working Directory -> Staging Area : git add +Staging Area -> Local Repository : git commit +Working Directory -> Local Repository : git commit -a +Staging Area -> Working Directory : git checkout +Local Repository -> Staging Area : git reset +Local Repository -> Working Directory: git reset --hard +Local Repository -> Remote Repository : git push +""" +from wsd import wsd +# %matplotlib inline +wsd(message) + +# %% [markdown] +# ### Playing with GitHub +# +# Take a few moments to click around and work your way through the GitHub interface. Try clicking on 'index.md' to see the content of the file: notice how the markdown renders prettily. +# +# Click on "commits" near the top of the screen, to see all the changes you've made. Click on the commit number next to the right of a change, to see what changes it includes: removals are shown in red, and additions in green. + +# %% [markdown] +# ## Working with multiple files + +# %% [markdown] +# ### Some new content +# +# So far, we've only worked with one file. Let's add another: + +# %% [markdown] +# ``` bash +# nano lakeland.md +# ``` + +# %% jupyter={"outputs_hidden": false} +# %%writefile lakeland.md +Lakeland +======== + +Cumbria has some pretty hills, and lakes too. + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} +# cat lakeland.md + +# %% [markdown] +# ### Git will not by default commit your new file + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git commit -am "Try to add Lakeland" + +# %% [markdown] +# This didn't do anything, because we've not told git to track the new file yet. + +# %% [markdown] +# ### Tell git about the new file + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git add lakeland.md +# git commit -am "Add lakeland" + +# %% [markdown] +# Ok, now we have added the change about Cumbria to the file. Let's publish it to the origin repository. + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% [markdown] +# Visit GitHub, and notice this change is on your repository on the server. We could have said `git push origin` to specify the remote to use, but origin is the default. + +# %% [markdown] +# ## Changing two files at once + +# %% [markdown] +# What if we change both files? + +# %% jupyter={"outputs_hidden": false} +# %%writefile lakeland.md +Lakeland +======== + +Cumbria has some pretty hills, and lakes too + +Mountains: +* Helvellyn + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +Mountains and Lakes in the UK +=================== +Engerland is not very mountainous. +But has some tall hills, and maybe a +mountain or two depending on your definition. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% [markdown] +# These changes should really be separate commits. We can do this with careful use of git add, to **stage** first one commit, then the other. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git add index.md +# git commit -m "Include lakes in the scope" + +# %% [markdown] +# Because we "staged" only index.md, the changes to lakeland.md were not included in that commit. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add Helvellyn" + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --oneline + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% jupyter={"outputs_hidden": false} +message=""" +participant "Cleese's remote" as M +participant "Cleese's repo" as R +participant "Cleese's index" as I +participant Cleese as C + +note right of C: nano index.md +note right of C: nano lakeland.md + +note right of C: git add index.md +C->I: Add *only* the changes to index.md to the staging area + +note right of C: git commit -m "Include lakes" +I->R: Make a commit from currently staged changes: index.md only + +note right of C: git commit -am "Add Helvellyn" +C->I: Stage *all remaining* changes, (lakeland.md) +I->R: Make a commit from currently staged changes + +note right of C: git push +R->M: Transfer commits to Github +""" +wsd(message) diff --git a/ch00git/05Collaboration.html b/ch00git/05Collaboration.html new file mode 100644 index 000000000..3e4c833a5 --- /dev/null +++ b/ch00git/05Collaboration.html @@ -0,0 +1,2045 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Collaboration + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Collaboration

Form a team

+
+
+
+
+
+
+

Now we're going to get to the most important question of all with Git and GitHub: working with others.

+

Organise into pairs. You're going to be working on the website of one of the two of you, together, so decide who is going to be the leader, and who the collaborator.

+
+
+
+
+
+
+

Giving permission

The leader needs to let the collaborator have the right to make changes to his code.

+

In GitHub, go to Settings on the right, then Collaborators & teams on the left.

+

Add the user name of your collaborator to the box. They now have the right to push to your repository.

+
+
+
+
+
+
+

Obtaining a colleague's code

Next, the collaborator needs to get a copy of the leader's code. For this example notebook, +I'm going to be collaborating with myself, swapping between my two repositories. +Make yourself a space to put it your work. (I will have two)

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(git_dir)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+pwd
+rm -rf github-example # cleanup after previous example
+rm -rf partner_repo # cleanup after previous example
+
+
+
+
+
+
+
+
+
+
/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git
+
+
+
+
+
+
+
+
+
+

Next, the collaborator needs to find out the URL of the repository: they should go to the leader's repository's GitHub page, and note the URL on the top of the screen. Make sure the "ssh" button is pushed, the URL should begin with git@github.com.

+

Copy the URL into your clipboard by clicking on the icon to the right of the URL, and then:

+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+pwd
+git clone git@github.com:UCL/github-example.git partner_repo
+
+
+
+
+
+
+
+
+
+
/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git
+
+
+
+
+
+
+
Cloning into 'partner_repo'...
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
partner_dir = os.path.join(git_dir, 'partner_repo')
+os.chdir(partner_dir)
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+pwd
+ls
+
+
+
+
+
+
+
+
+
+
/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/partner_repo
+Scotland.md
+index.md
+lakeland.md
+
+
+
+
+
+
+
+
+
+

Note that your partner's files are now present on your disk:

+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+cat lakeland.md
+
+
+
+
+
+
+
+
+
+
Lakeland  
+========   
+  
+Cumbria has some pretty hills, and lakes too
+
+Mountains:
+* Helvellyn
+
+
+
+
+
+
+
+
+
+

Nonconflicting changes

Now, both of you should make some changes. To start with, make changes to different files. This will mean your work doesn't "conflict". Later, we'll see how to deal with changes to a shared file.

+
+
+
+
+
+
+

Both of you should commit, but not push, your changes to your respective files:

+
+
+
+
+
+
+

E.g., the leader:

+
+
+
+
+
+
In [7]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Tryfan
+* Yr Wyddfa
+
+
+
+
+
+
+
+
+
+
Writing Wales.md
+
+
+
+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+ls
+
+
+
+
+
+
+
+
+
+
Wales.md
+__pycache__
+index.md
+lakeland.md
+wsd.py
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
%%bash
+git add Wales.md
+git commit -m "Add wales"
+
+
+
+
+
+
+
+
+
+
[main a6361f3] Add wales
+ 1 file changed, 5 insertions(+)
+ create mode 100644 Wales.md
+
+
+
+
+
+
+
+
+
+

And the partner:

+
+
+
+
+
+
In [11]:
+
+
+
os.chdir(partner_dir)
+
+
+
+
+
+
+
+
In [12]:
+
+
+
%%writefile Scotland.md
+Mountains In Scotland
+==================
+
+* Ben Eighe
+* Cairngorm
+
+
+
+
+
+
+
+
+
+
Overwriting Scotland.md
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+ls
+
+
+
+
+
+
+
+
+
+
Scotland.md
+index.md
+lakeland.md
+
+
+
+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+git add Scotland.md
+git commit -m "Add Scotland"
+
+
+
+
+
+
+
+
+
+
On branch master
+Your branch is up to date with 'origin/master'.
+
+nothing to commit, working tree clean
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[14], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'git add Scotland.md\ngit commit -m "Add Scotland"\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'git add Scotland.md\ngit commit -m "Add Scotland"\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

One of you should now push with git push:

+
+
+
+
+
+
In [15]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
+

Rejected push

+
+
+
+
+
+
+

The other should then push, but should receive an error message:

+
+
+
+
+
+
In [16]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [17]:
+
+
+
%%bash --no-raise-error
+git push
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   4b52f45..a6361f3  main -> main
+
+
+
+
+
+
+
+
+
+

Do as it suggests. However, we need first to tell git how we want it to act when there are diverging branches (as in this case). We will set the default to be to create a merge commit, then we proceed to pull.

+
+
+
+
+
+
In [18]:
+
+
+
%%bash
+git config --global pull.rebase false
+git pull
+
+
+
+
+
+
+
+
+
+
From github.com:UCL/github-example
+ * [new branch]      gh-pages   -> origin/gh-pages
+ * [new branch]      master     -> origin/master
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
+

Merge commits

A window may pop up with a suggested default commit message. This commit is special: it is a merge commit. It is a commit which combines your collaborator's work with your own.

+
+
+
+
+
+
+

Now, push again with git push. This time it works. If you look on GitHub, you'll now see that it contains both sets of changes.

+
+
+
+
+
+
In [19]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
+

The partner now needs to pull down that commit:

+
+
+
+
+
+
In [20]:
+
+
+
os.chdir(partner_dir)
+
+
+
+
+
+
+
+
In [21]:
+
+
+
%%bash
+git pull
+
+
+
+
+
+
+
+
+
+
From github.com:UCL/github-example
+   4b52f45..a6361f3  main       -> origin/main
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
In [22]:
+
+
+
%%bash
+ls
+
+
+
+
+
+
+
+
+
+
Scotland.md
+index.md
+lakeland.md
+
+
+
+
+
+
+
+
+
+

Nonconflicted commits to the same file

Go through the whole process again, but this time, both of you should make changes to a single file, but make sure that you don't touch the same line. Again, the merge should work as before:

+
+
+
+
+
+
In [23]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Tryfan
+* Snowdon
+
+
+
+
+
+
+
+
+
+
Writing Wales.md
+
+
+
+
+
+
+
+
+
In [24]:
+
+
+
%%bash
+git diff
+
+
+
+
+
+
+
+
In [25]:
+
+
+
%%bash
+git commit -am "Translating from the Welsh"
+
+
+
+
+
+
+
+
+
+
On branch master
+Your branch is up to date with 'origin/master'.
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	Wales.md
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[25], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'git commit -am "Translating from the Welsh"\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'git commit -am "Translating from the Welsh"\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
In [26]:
+
+
+
%%bash
+git log --oneline
+
+
+
+
+
+
+
+
+
+
581eb0b Add Scotland
+a873a29 Add Helvellyn
+bc37e5f Include lakes in the scope
+001011c Add lakeland
+c693e31 Revert "Add a lie about a mountain"
+124ae40 Change title
+b6dc168 Add a lie about a mountain
+902f791 First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
In [27]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [28]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
In [29]:
+
+
+
%%bash
+git commit -am "Add a beacon"
+
+
+
+
+
+
+
+
+
+
[main 592c6bd] Add a beacon
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+
+
+
+
+
+
+
+
In [30]:
+
+
+
%%bash
+git log --oneline
+
+
+
+
+
+
+
+
+
+
592c6bd Add a beacon
+a6361f3 Add wales
+4b52f45 Add Helvellyn
+df5fed8 Include lakes in the scope
+7b49cfb Add lakeland
+21c306b Revert "Add a lie about a mountain"
+5bf472d Change title
+5aa2999 Add a lie about a mountain
+6c90dbd First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
In [31]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   a6361f3..592c6bd  main -> main
+
+
+
+
+
+
+
+
+
+

Switching back to the other partner...

+
+
+
+
+
+
In [32]:
+
+
+
os.chdir(partner_dir)
+
+
+
+
+
+
+
+
In [33]:
+
+
+
%%bash --no-raise-error
+git push
+
+
+
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
In [34]:
+
+
+
%%bash
+git pull
+
+
+
+
+
+
+
+
+
+
From github.com:UCL/github-example
+   a6361f3..592c6bd  main       -> origin/main
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
In [35]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
In [36]:
+
+
+
%%bash
+git log --oneline --graph
+
+
+
+
+
+
+
+
+
+
* 581eb0b Add Scotland
+* a873a29 Add Helvellyn
+* bc37e5f Include lakes in the scope
+* 001011c Add lakeland
+* c693e31 Revert "Add a lie about a mountain"
+* 124ae40 Change title
+* b6dc168 Add a lie about a mountain
+* 902f791 First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
In [37]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [38]:
+
+
+
%%bash
+git pull
+
+
+
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
In [39]:
+
+
+
%%bash
+git log --graph --oneline
+
+
+
+
+
+
+
+
+
+
* 592c6bd Add a beacon
+* a6361f3 Add wales
+* 4b52f45 Add Helvellyn
+* df5fed8 Include lakes in the scope
+* 7b49cfb Add lakeland
+* 21c306b Revert "Add a lie about a mountain"
+* 5bf472d Change title
+* 5aa2999 Add a lie about a mountain
+* 6c90dbd First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
In [40]:
+
+
+
message="""
+participant Palin as P
+participant "Palin's repo" as PR
+participant "Shared remote" as M
+participant "Cleese's repo" as CR
+participant Cleese as C
+
+note left of P: git clone
+M->PR: fetch commits
+PR->P: working directory as at latest commit
+
+note left of P: edit Scotland.md
+note right of C: edit Wales.md
+
+note left of P: git commit -am "Add scotland"
+P->PR: create commit with Scotland file
+
+note right of C: git commit -am "Add wales"
+C->CR: create commit with Wales file
+
+note left of P: git push
+PR->M: update remote with changes
+
+note right of C: git push
+CR-->M: !Rejected change
+
+note right of C: git pull
+M->CR: Pull in Palin's last commit, merge histories
+CR->C: Add Scotland.md to working directory
+
+note right of C: git push
+CR->M: Transfer merged history to remote
+
+"""
+from wsd import wsd
+%matplotlib inline
+wsd(message)
+
+
+
+
+
+
+
+
Out[40]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Conflicting commits

Finally, go through the process again, but this time, make changes which touch the same line.

+
+
+
+
+
+
In [41]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Fan y Big
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
In [42]:
+
+
+
%%bash
+git commit -am "Add another Beacon"
+git push
+
+
+
+
+
+
+
+
+
+
[main ae74101] Add another Beacon
+ 1 file changed, 1 insertion(+)
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   592c6bd..ae74101  main -> main
+
+
+
+
+
+
+
+
+
In [43]:
+
+
+
os.chdir(partner_dir)
+
+
+
+
+
+
+
+
In [44]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
In [45]:
+
+
+
%%bash --no-raise-error
+git commit -am "Add Glyder"
+git push
+
+
+
+
+
+
+
+
+
+
On branch master
+Your branch is up to date with 'origin/master'.
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	Wales.md
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
+

When you pull, instead of offering an automatic merge commit message, it says:

+
+
+
+
+
+
In [46]:
+
+
+
%%bash --no-raise-error
+git pull
+
+
+
+
+
+
+
+
+
+
From github.com:UCL/github-example
+   592c6bd..ae74101  main       -> origin/main
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
+

Resolving conflicts

Git couldn't work out how to merge the two different sets of changes.

+

You now need to manually resolve the conflict.

+

It has marked the conflicted area:

+
+
+
+
+
+
In [47]:
+
+
+
%%bash
+cat Wales.md
+
+
+
+
+
+
+
+
+
+
Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+
+
+
+
+
+
+
+
+
+

Manually edit the file, to combine the changes as seems sensible and get rid of the symbols:

+
+
+
+
+
+
In [48]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+* Fan y Big
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
+

Commit the resolved file

Now commit the merged result:

+
+
+
+
+
+
In [49]:
+
+
+
%%bash
+git commit -a --no-edit # I added a No-edit for this non-interactive session. You can edit the commit if you like.
+
+
+
+
+
+
+
+
+
+
On branch master
+Your branch is up to date with 'origin/master'.
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	Wales.md
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[49], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'git commit -a --no-edit # I added a No-edit for this non-interactive session. You can edit the commit if you like.\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'git commit -a --no-edit # I added a No-edit for this non-interactive session. You can edit the commit if you like.\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
In [50]:
+
+
+
%%bash
+git push
+
+
+
+
+
+
+
+
+
+
Everything up-to-date
+
+
+
+
+
+
+
+
+
In [51]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [52]:
+
+
+
%%bash
+git pull
+
+
+
+
+
+
+
+
+
+
Already up to date.
+
+
+
+
+
+
+
+
+
In [53]:
+
+
+
%%bash
+cat Wales.md
+
+
+
+
+
+
+
+
+
+
Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Fan y Big
+
+
+
+
+
+
+
+
+
In [54]:
+
+
+
%%bash
+git log --oneline --graph
+
+
+
+
+
+
+
+
+
+
* ae74101 Add another Beacon
+* 592c6bd Add a beacon
+* a6361f3 Add wales
+* 4b52f45 Add Helvellyn
+* df5fed8 Include lakes in the scope
+* 7b49cfb Add lakeland
+* 21c306b Revert "Add a lie about a mountain"
+* 5bf472d Change title
+* 5aa2999 Add a lie about a mountain
+* 6c90dbd First commit of discourse on UK topography
+
+
+
+
+
+
+
+
+
+

Distributed VCS in teams with conflicts

+
+
+
+
+
+
In [55]:
+
+
+
message="""
+participant Palin as P
+participant "Palin's repo" as PR
+participant "Shared remote" as M
+participant "Cleese's repo" as CR
+participant Cleese as C
+
+note left of P: edit the same line in wales.md
+note right of C: edit the same line in wales.md
+    
+note left of P: git commit -am "update wales.md"
+P->PR: add commit to local repo
+    
+note right of C: git commit -am "update wales.md"
+C->CR: add commit to local repo
+    
+note left of P: git push
+PR->M: transfer commit to remote
+    
+note right of C: git push
+CR->M: !Rejected
+
+note right of C: git pull
+M->C: Make conflicted file with conflict markers
+    
+note right of C: edit file to resolve conflicts
+note right of C: git add wales.md
+note right of C: git commit
+C->CR: Mark conflict as resolved
+
+note right of C: git push
+CR->M: Transfer merged history to remote
+
+note left of P: git pull
+M->SR: Download Cleese's resolution of conflict.
+    
+"""
+
+wsd(message)
+
+
+
+
+
+
+
+
Out[55]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The Levels of Git

+
+
+
+
+
+
In [56]:
+
+
+
message="""
+Working Directory -> Staging Area : git add
+Staging Area -> Local Repository : git commit
+Working Directory -> Local Repository : git commit -a
+Staging Area -> Working Directory : git checkout
+Local Repository -> Staging Area : git reset
+Local Repository -> Working Directory: git reset --hard
+Local Repository -> Remote Repository : git push
+Remote Repository -> Local Repository : git fetch
+Local Repository -> Working Directory : git merge
+Remote Repository -> Working Directory: git pull
+"""
+
+wsd(message)
+
+
+
+
+
+
+
+
Out[56]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Editing directly on GitHub

Editing directly on GitHub

Note that you can also make changes in the GitHub website itself. Visit one of your files, and hit "edit".

+

Make a change in the edit window, and add an appropriate commit message.

+

That change now appears on the website, but not in your local copy. (Verify this).

+
+
+
+
+
+
+

Now pull, and check the change is now present on your local version.

+
+
+
+
+
+
+

Social Coding

GitHub as a social network

In addition to being a repository for code, and a way to publish code, GitHub is a social network.

+

You can follow the public work of other coders: go to the profile of your collaborator in your browser, and hit the "follow" button.

+

Check out the profiles of Linus Torvalds - creator of git (first git commit ever) and Linux - , Guido van Rossum - creator of Python -, or +James Hetherington - the creator of these course notes.

+

Using GitHub to build up a good public profile of software projects you've worked on is great for your CV!

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/05Collaboration.ipynb b/ch00git/05Collaboration.ipynb new file mode 100644 index 000000000..dc6bee04a --- /dev/null +++ b/ch00git/05Collaboration.ipynb @@ -0,0 +1,1263 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7c25db6", + "metadata": {}, + "source": [ + "## Collaboration\n", + "\n", + "### Form a team" + ] + }, + { + "cell_type": "markdown", + "id": "b8664bf8", + "metadata": {}, + "source": [ + "Now we're going to get to the most important question of all with Git and GitHub: working with others.\n", + "\n", + "Organise into pairs. You're going to be working on the website of one of the two of you, together, so decide who is going to be the leader, and who the collaborator." + ] + }, + { + "cell_type": "markdown", + "id": "58f88afb", + "metadata": {}, + "source": [ + "### Giving permission\n", + "\n", + "The leader needs to let the collaborator have the right to make changes to his code.\n", + "\n", + "In GitHub, go to `Settings` on the right, then `Collaborators & teams` on the left.\n", + "\n", + "Add the user name of your collaborator to the box. They now have the right to push to your repository." + ] + }, + { + "cell_type": "markdown", + "id": "48deabab", + "metadata": {}, + "source": [ + "### Obtaining a colleague's code\n", + "\n", + "Next, the collaborator needs to get a copy of the leader's code. For this example notebook,\n", + "I'm going to be collaborating with myself, swapping between my two repositories.\n", + "Make yourself a space to put it your work. (I will have two)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1794c6fc", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(git_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c89217d5", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "pwd\n", + "rm -rf github-example # cleanup after previous example\n", + "rm -rf partner_repo # cleanup after previous example\n" + ] + }, + { + "cell_type": "markdown", + "id": "10dc34e8", + "metadata": {}, + "source": [ + "Next, the collaborator needs to find out the URL of the repository: they should go to the leader's repository's GitHub page, and note the URL on the top of the screen. Make sure the \"ssh\" button is pushed, the URL should begin with `git@github.com`. \n", + "\n", + "Copy the URL into your clipboard by clicking on the icon to the right of the URL, and then:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1a867a6", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "pwd\n", + "git clone git@github.com:UCL/github-example.git partner_repo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "768aacdc", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "partner_dir = os.path.join(git_dir, 'partner_repo')\n", + "os.chdir(partner_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e6aa8cb", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "pwd\n", + "ls" + ] + }, + { + "cell_type": "markdown", + "id": "5d0938f9", + "metadata": {}, + "source": [ + "Note that your partner's files are now present on your disk:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be2413dd", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat lakeland.md" + ] + }, + { + "cell_type": "markdown", + "id": "00242924", + "metadata": {}, + "source": [ + "### Nonconflicting changes\n", + "\n", + "Now, both of you should make some changes. To start with, make changes to *different* files. This will mean your work doesn't \"conflict\". Later, we'll see how to deal with changes to a shared file." + ] + }, + { + "cell_type": "markdown", + "id": "35f74fbc", + "metadata": {}, + "source": [ + "Both of you should commit, but not push, your changes to your respective files:" + ] + }, + { + "cell_type": "markdown", + "id": "12978348", + "metadata": {}, + "source": [ + "E.g., the leader:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ef6fda0", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "903f5dc0", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Tryfan\n", + "* Yr Wyddfa" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c58a908", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58168f51", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add Wales.md\n", + "git commit -m \"Add wales\"" + ] + }, + { + "cell_type": "markdown", + "id": "642a4200", + "metadata": {}, + "source": [ + "And the partner:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "162fce68", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(partner_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e2c0ee2", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Scotland.md\n", + "Mountains In Scotland\n", + "==================\n", + "\n", + "* Ben Eighe\n", + "* Cairngorm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69f41a2d", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91f36875", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add Scotland.md\n", + "git commit -m \"Add Scotland\"" + ] + }, + { + "cell_type": "markdown", + "id": "997e2463", + "metadata": {}, + "source": [ + "One of you should now push with `git push`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9c0c387", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "0b90d31e", + "metadata": {}, + "source": [ + "### Rejected push" + ] + }, + { + "cell_type": "markdown", + "id": "2a7a0f9b", + "metadata": {}, + "source": [ + "The other should then push, but should receive an error message:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3cca43a", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdc68085", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "f4d94333", + "metadata": {}, + "source": [ + "Do as it suggests. However, we need first to tell git how we want it to act when there are diverging branches (as in this case). We will set the default to be to create a merge commit, then we proceed to `pull`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc3326cc", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git config --global pull.rebase false\n", + "git pull" + ] + }, + { + "cell_type": "markdown", + "id": "6623366e", + "metadata": {}, + "source": [ + "### Merge commits\n", + "\n", + "A window may pop up with a suggested default commit message. This commit is special: it is a *merge* commit. It is a commit which combines your collaborator's work with your own." + ] + }, + { + "cell_type": "markdown", + "id": "0e4c1327", + "metadata": {}, + "source": [ + "Now, push again with `git push`. This time it works. If you look on GitHub, you'll now see that it contains both sets of changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a1b79df", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "c5d95163", + "metadata": {}, + "source": [ + "The partner now needs to pull down that commit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3ee40d0", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(partner_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa0bd9fa", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git pull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32e81dc7", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "ls" + ] + }, + { + "cell_type": "markdown", + "id": "4204a5e3", + "metadata": {}, + "source": [ + "### Nonconflicted commits to the same file\n", + "\n", + "Go through the whole process again, but this time, both of you should make changes to a single file, but make sure that you don't touch the same *line*. Again, the merge should work as before:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "014fbae8", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Tryfan\n", + "* Snowdon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbdd05d0", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git diff" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4256951", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Translating from the Welsh\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea8bd452", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce449b2d", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bc178f8", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "831953a8", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add a beacon\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8538457", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d01d6478", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "92582bcb", + "metadata": {}, + "source": [ + "Switching back to the other partner..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73bda272", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(partner_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2bc055b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git push" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66224143", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git pull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c84f906d", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f100ee1", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline --graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fd6ed81", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab634065", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git pull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd7d901a", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --graph --oneline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7c2bf0e", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "participant Palin as P\n", + "participant \"Palin's repo\" as PR\n", + "participant \"Shared remote\" as M\n", + "participant \"Cleese's repo\" as CR\n", + "participant Cleese as C\n", + "\n", + "note left of P: git clone\n", + "M->PR: fetch commits\n", + "PR->P: working directory as at latest commit\n", + "\n", + "note left of P: edit Scotland.md\n", + "note right of C: edit Wales.md\n", + "\n", + "note left of P: git commit -am \"Add scotland\"\n", + "P->PR: create commit with Scotland file\n", + "\n", + "note right of C: git commit -am \"Add wales\"\n", + "C->CR: create commit with Wales file\n", + "\n", + "note left of P: git push\n", + "PR->M: update remote with changes\n", + "\n", + "note right of C: git push\n", + "CR-->M: !Rejected change\n", + "\n", + "note right of C: git pull\n", + "M->CR: Pull in Palin's last commit, merge histories\n", + "CR->C: Add Scotland.md to working directory\n", + "\n", + "note right of C: git push\n", + "CR->M: Transfer merged history to remote\n", + "\n", + "\"\"\"\n", + "from wsd import wsd\n", + "%matplotlib inline\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "34ca94ce", + "metadata": {}, + "source": [ + "### Conflicting commits\n", + "\n", + "Finally, go through the process again, but this time, make changes which touch the same line." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb9b1e6f", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon\n", + "* Fan y Big" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c64353de", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add another Beacon\"\n", + "git push" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d1a1f4b", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(partner_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2857a48c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon\n", + "* Glyder Fawr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdbcad3c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git commit -am \"Add Glyder\"\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "3ab9554f", + "metadata": {}, + "source": [ + "When you pull, instead of offering an automatic merge commit message, it says:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "683b6037", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git pull" + ] + }, + { + "cell_type": "markdown", + "id": "08840da0", + "metadata": {}, + "source": [ + "### Resolving conflicts\n", + "\n", + "Git couldn't work out how to merge the two different sets of changes.\n", + "\n", + "You now need to manually resolve the conflict.\n", + "\n", + "It has marked the conflicted area:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ce1ecbe", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat Wales.md" + ] + }, + { + "cell_type": "markdown", + "id": "dd61efd5", + "metadata": {}, + "source": [ + "Manually edit the file, to combine the changes as seems sensible and get rid of the symbols:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c11fabfd", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon\n", + "* Glyder Fawr\n", + "* Fan y Big" + ] + }, + { + "cell_type": "markdown", + "id": "7d16a52b", + "metadata": {}, + "source": [ + "### Commit the resolved file\n", + "\n", + "Now commit the merged result:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98c0399e", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -a --no-edit # I added a No-edit for this non-interactive session. You can edit the commit if you like." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69ec9371", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7583ed79", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02bc8b35", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git pull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6403c594", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat Wales.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "253f1ea7", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --oneline --graph" + ] + }, + { + "cell_type": "markdown", + "id": "d84c7f11", + "metadata": {}, + "source": [ + "### Distributed VCS in teams with conflicts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88ca28dd", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "participant Palin as P\n", + "participant \"Palin's repo\" as PR\n", + "participant \"Shared remote\" as M\n", + "participant \"Cleese's repo\" as CR\n", + "participant Cleese as C\n", + "\n", + "note left of P: edit the same line in wales.md\n", + "note right of C: edit the same line in wales.md\n", + " \n", + "note left of P: git commit -am \"update wales.md\"\n", + "P->PR: add commit to local repo\n", + " \n", + "note right of C: git commit -am \"update wales.md\"\n", + "C->CR: add commit to local repo\n", + " \n", + "note left of P: git push\n", + "PR->M: transfer commit to remote\n", + " \n", + "note right of C: git push\n", + "CR->M: !Rejected\n", + "\n", + "note right of C: git pull\n", + "M->C: Make conflicted file with conflict markers\n", + " \n", + "note right of C: edit file to resolve conflicts\n", + "note right of C: git add wales.md\n", + "note right of C: git commit\n", + "C->CR: Mark conflict as resolved\n", + "\n", + "note right of C: git push\n", + "CR->M: Transfer merged history to remote\n", + "\n", + "note left of P: git pull\n", + "M->SR: Download Cleese's resolution of conflict.\n", + " \n", + "\"\"\"\n", + "\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "0338a2ad", + "metadata": {}, + "source": [ + "### The Levels of Git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b68b4df9", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "message=\"\"\"\n", + "Working Directory -> Staging Area : git add\n", + "Staging Area -> Local Repository : git commit\n", + "Working Directory -> Local Repository : git commit -a\n", + "Staging Area -> Working Directory : git checkout\n", + "Local Repository -> Staging Area : git reset\n", + "Local Repository -> Working Directory: git reset --hard\n", + "Local Repository -> Remote Repository : git push\n", + "Remote Repository -> Local Repository : git fetch\n", + "Local Repository -> Working Directory : git merge\n", + "Remote Repository -> Working Directory: git pull\n", + "\"\"\"\n", + "\n", + "wsd(message)" + ] + }, + { + "cell_type": "markdown", + "id": "7f89a2f4", + "metadata": {}, + "source": [ + "## Editing directly on GitHub\n", + "\n", + "### Editing directly on GitHub\n", + "\n", + "Note that you can also make changes in the GitHub website itself. Visit one of your files, and hit \"edit\".\n", + "\n", + "Make a change in the edit window, and add an appropriate commit message.\n", + "\n", + "That change now appears on the website, but not in your local copy. (Verify this). " + ] + }, + { + "cell_type": "markdown", + "id": "dac422a9", + "metadata": {}, + "source": [ + "Now pull, and check the change is now present on your local version. " + ] + }, + { + "cell_type": "markdown", + "id": "1fac2674", + "metadata": {}, + "source": [ + "## Social Coding\n", + "\n", + "### GitHub as a social network\n", + "\n", + "In addition to being a repository for code, and a way to publish code, GitHub is a social network. \n", + "\n", + "You can follow the public work of other coders: go to the profile of your collaborator in your browser, and hit the \"follow\" button. \n", + "\n", + "Check out the profiles of [Linus Torvalds](https://github.com/torvalds) - creator of [git](https://git-scm.com/) ([first git commit ever](https://github.com/git/git/commit/e83c5163316f89bfbde7d9ab23ca2e25604af290)) and [Linux](https://en.wikipedia.org/wiki/Linux) - , [Guido van Rossum](https://github.com/gvanrossum) - creator of Python -, or \n", + "[James Hetherington](https://github.com/jamespjh) - the creator of these course notes.\n", + "\n", + "Using GitHub to build up a good public profile of software projects you've worked on is great for your CV!" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Collaboration" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/05Collaboration.ipynb.py b/ch00git/05Collaboration.ipynb.py new file mode 100644 index 000000000..ad46fe53e --- /dev/null +++ b/ch00git/05Collaboration.ipynb.py @@ -0,0 +1,463 @@ +# --- +# jupyter: +# jekyll: +# display_name: Collaboration +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Collaboration +# +# ### Form a team + +# %% [markdown] +# Now we're going to get to the most important question of all with Git and GitHub: working with others. +# +# Organise into pairs. You're going to be working on the website of one of the two of you, together, so decide who is going to be the leader, and who the collaborator. + +# %% [markdown] +# ### Giving permission +# +# The leader needs to let the collaborator have the right to make changes to his code. +# +# In GitHub, go to `Settings` on the right, then `Collaborators & teams` on the left. +# +# Add the user name of your collaborator to the box. They now have the right to push to your repository. + +# %% [markdown] +# ### Obtaining a colleague's code +# +# Next, the collaborator needs to get a copy of the leader's code. For this example notebook, +# I'm going to be collaborating with myself, swapping between my two repositories. +# Make yourself a space to put it your work. (I will have two) + +# %% jupyter={"outputs_hidden": true} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(git_dir) + +# %% language="bash" +# pwd +# rm -rf github-example # cleanup after previous example +# rm -rf partner_repo # cleanup after previous example +# + +# %% [markdown] +# Next, the collaborator needs to find out the URL of the repository: they should go to the leader's repository's GitHub page, and note the URL on the top of the screen. Make sure the "ssh" button is pushed, the URL should begin with `git@github.com`. +# +# Copy the URL into your clipboard by clicking on the icon to the right of the URL, and then: + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# pwd +# git clone git@github.com:UCL/github-example.git partner_repo + +# %% jupyter={"outputs_hidden": true} +partner_dir = os.path.join(git_dir, 'partner_repo') +os.chdir(partner_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# pwd +# ls + +# %% [markdown] +# Note that your partner's files are now present on your disk: + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat lakeland.md + +# %% [markdown] +# ### Nonconflicting changes +# +# Now, both of you should make some changes. To start with, make changes to *different* files. This will mean your work doesn't "conflict". Later, we'll see how to deal with changes to a shared file. + +# %% [markdown] +# Both of you should commit, but not push, your changes to your respective files: + +# %% [markdown] +# E.g., the leader: + +# %% jupyter={"outputs_hidden": true} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Tryfan +* Yr Wyddfa + +# %% jupyter={"outputs_hidden": false} language="bash" +# ls + +# %% jupyter={"outputs_hidden": false} language="bash" +# git add Wales.md +# git commit -m "Add wales" + +# %% [markdown] +# And the partner: + +# %% jupyter={"outputs_hidden": true} +os.chdir(partner_dir) + +# %% jupyter={"outputs_hidden": false} +# %%writefile Scotland.md +Mountains In Scotland +================== + +* Ben Eighe +* Cairngorm + +# %% jupyter={"outputs_hidden": false} language="bash" +# ls + +# %% jupyter={"outputs_hidden": false} language="bash" +# git add Scotland.md +# git commit -m "Add Scotland" + +# %% [markdown] +# One of you should now push with `git push`: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% [markdown] +# ### Rejected push + +# %% [markdown] +# The other should then push, but should receive an error message: + +# %% jupyter={"outputs_hidden": true} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git push + +# %% [markdown] +# Do as it suggests. However, we need first to tell git how we want it to act when there are diverging branches (as in this case). We will set the default to be to create a merge commit, then we proceed to `pull`. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git config --global pull.rebase false +# git pull + +# %% [markdown] +# ### Merge commits +# +# A window may pop up with a suggested default commit message. This commit is special: it is a *merge* commit. It is a commit which combines your collaborator's work with your own. + +# %% [markdown] +# Now, push again with `git push`. This time it works. If you look on GitHub, you'll now see that it contains both sets of changes. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% [markdown] +# The partner now needs to pull down that commit: + +# %% jupyter={"outputs_hidden": true} +os.chdir(partner_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# git pull + +# %% jupyter={"outputs_hidden": false} language="bash" +# ls + +# %% [markdown] +# ### Nonconflicted commits to the same file +# +# Go through the whole process again, but this time, both of you should make changes to a single file, but make sure that you don't touch the same *line*. Again, the merge should work as before: + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Tryfan +* Snowdon + +# %% jupyter={"outputs_hidden": false} language="bash" +# git diff + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Translating from the Welsh" + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --oneline + +# %% jupyter={"outputs_hidden": true} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add a beacon" + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --oneline + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% [markdown] +# Switching back to the other partner... + +# %% jupyter={"outputs_hidden": true} +os.chdir(partner_dir) + +# %% jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git push + +# %% jupyter={"outputs_hidden": false} language="bash" +# git pull + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --oneline --graph + +# %% jupyter={"outputs_hidden": true} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# git pull + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --graph --oneline + +# %% jupyter={"outputs_hidden": false} +message=""" +participant Palin as P +participant "Palin's repo" as PR +participant "Shared remote" as M +participant "Cleese's repo" as CR +participant Cleese as C + +note left of P: git clone +M->PR: fetch commits +PR->P: working directory as at latest commit + +note left of P: edit Scotland.md +note right of C: edit Wales.md + +note left of P: git commit -am "Add scotland" +P->PR: create commit with Scotland file + +note right of C: git commit -am "Add wales" +C->CR: create commit with Wales file + +note left of P: git push +PR->M: update remote with changes + +note right of C: git push +CR-->M: !Rejected change + +note right of C: git pull +M->CR: Pull in Palin's last commit, merge histories +CR->C: Add Scotland.md to working directory + +note right of C: git push +CR->M: Transfer merged history to remote + +""" +from wsd import wsd +# %matplotlib inline +wsd(message) + +# %% [markdown] +# ### Conflicting commits +# +# Finally, go through the process again, but this time, make changes which touch the same line. + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Fan y Big + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add another Beacon" +# git push + +# %% jupyter={"outputs_hidden": true} +os.chdir(partner_dir) + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr + +# %% jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git commit -am "Add Glyder" +# git push + +# %% [markdown] +# When you pull, instead of offering an automatic merge commit message, it says: + +# %% jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git pull + +# %% [markdown] +# ### Resolving conflicts +# +# Git couldn't work out how to merge the two different sets of changes. +# +# You now need to manually resolve the conflict. +# +# It has marked the conflicted area: + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat Wales.md + +# %% [markdown] +# Manually edit the file, to combine the changes as seems sensible and get rid of the symbols: + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr +* Fan y Big + +# %% [markdown] +# ### Commit the resolved file +# +# Now commit the merged result: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -a --no-edit # I added a No-edit for this non-interactive session. You can edit the commit if you like. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push + +# %% jupyter={"outputs_hidden": true} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# git pull + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat Wales.md + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --oneline --graph + +# %% [markdown] +# ### Distributed VCS in teams with conflicts + +# %% jupyter={"outputs_hidden": false} +message=""" +participant Palin as P +participant "Palin's repo" as PR +participant "Shared remote" as M +participant "Cleese's repo" as CR +participant Cleese as C + +note left of P: edit the same line in wales.md +note right of C: edit the same line in wales.md + +note left of P: git commit -am "update wales.md" +P->PR: add commit to local repo + +note right of C: git commit -am "update wales.md" +C->CR: add commit to local repo + +note left of P: git push +PR->M: transfer commit to remote + +note right of C: git push +CR->M: !Rejected + +note right of C: git pull +M->C: Make conflicted file with conflict markers + +note right of C: edit file to resolve conflicts +note right of C: git add wales.md +note right of C: git commit +C->CR: Mark conflict as resolved + +note right of C: git push +CR->M: Transfer merged history to remote + +note left of P: git pull +M->SR: Download Cleese's resolution of conflict. + +""" + +wsd(message) + +# %% [markdown] +# ### The Levels of Git + +# %% jupyter={"outputs_hidden": false} +message=""" +Working Directory -> Staging Area : git add +Staging Area -> Local Repository : git commit +Working Directory -> Local Repository : git commit -a +Staging Area -> Working Directory : git checkout +Local Repository -> Staging Area : git reset +Local Repository -> Working Directory: git reset --hard +Local Repository -> Remote Repository : git push +Remote Repository -> Local Repository : git fetch +Local Repository -> Working Directory : git merge +Remote Repository -> Working Directory: git pull +""" + +wsd(message) + +# %% [markdown] +# ## Editing directly on GitHub +# +# ### Editing directly on GitHub +# +# Note that you can also make changes in the GitHub website itself. Visit one of your files, and hit "edit". +# +# Make a change in the edit window, and add an appropriate commit message. +# +# That change now appears on the website, but not in your local copy. (Verify this). + +# %% [markdown] +# Now pull, and check the change is now present on your local version. + +# %% [markdown] +# ## Social Coding +# +# ### GitHub as a social network +# +# In addition to being a repository for code, and a way to publish code, GitHub is a social network. +# +# You can follow the public work of other coders: go to the profile of your collaborator in your browser, and hit the "follow" button. +# +# Check out the profiles of [Linus Torvalds](https://github.com/torvalds) - creator of [git](https://git-scm.com/) ([first git commit ever](https://github.com/git/git/commit/e83c5163316f89bfbde7d9ab23ca2e25604af290)) and [Linux](https://en.wikipedia.org/wiki/Linux) - , [Guido van Rossum](https://github.com/gvanrossum) - creator of Python -, or +# [James Hetherington](https://github.com/jamespjh) - the creator of these course notes. +# +# Using GitHub to build up a good public profile of software projects you've worked on is great for your CV! diff --git a/ch00git/06ForkAndPull.html b/ch00git/06ForkAndPull.html new file mode 100644 index 000000000..5b76945c4 --- /dev/null +++ b/ch00git/06ForkAndPull.html @@ -0,0 +1,447 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fork and Pull + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Fork and Pull

Different ways of collaborating

We have just seen how we can work with others on GitHub: we add them as collaborators on our repositories and give them permissions to push changes.

+

Let's talk now about some other type of collaboration.

+

Imagine you are a user of an Open Source project like Numpy and find a bug in one of their methods.

+

You can inspect and clone Numpy's code in GitHub, play around a bit and find how to fix the bug.

+

Numpy has done so much for you asking nothing in return, that you really want to contribute back by fixing the bug for them.

+

You make all of the changes but you can't push it back to Numpy's repository because you don't have permissions.

+

The right way to do this is forking Numpy's repository.

+
+
+
+
+
+
+

Forking a repository on GitHub

By forking a repository, all you do is make a copy of it in your GitHub account, where you will have write permissions as well.

+

If you fork Numpy's repository, you will find a new repository in your GitHub account that is an exact copy of Numpy. You can then clone it to your computer, work locally on fixing the bug and push the changes to your fork of Numpy.

+

Once you are happy with with the changes, GitHub also offers you a way to notify Numpy's developers of this changes so that they can include them in the official Numpy repository via starting a Pull Request.

+
+
+
+
+
+
+

Pull Request

You can create a Pull Request and select those changes that you think can be useful for fixing Numpy's bug.

+

Numpy's developers will review your code and make comments and suggestions on your fix. Then, you can commit more improvements in the pull request for them to review and so on.

+

Once Numpy's developers are happy with your changes, they'll accept your Pull Request and merge the changes into their original repository, for everyone to use.

+
+
+
+
+
+
+

Practical example - Team up!

We will be working in the same repository with one of you being the leader and the other being the collaborator.

+

Collaborators need to go to the leader's GitHub profile and find the repository we created for that lesson. Mine is in https://github.com/jamespjh/github-example

+
+
+
+
+
+
+

1. Fork repository

You will see on the top right of the page a Fork button with an accompanying number indicating how many GitHub users have forked that repository.

+

Collaborators need to navigate to the leader's repository and click the Fork button.

+

Collaborators: note how GitHub has redirected you to your own GitHub page and you are now looking at an exact copy of the team leader's repository.

+
+
+
+
+
+
+

2. Clone your forked repo

Collaborators: go to your terminal and clone the newly created fork.

+
git clone git@github.com:jamespjh/github-example.git
+
+
+
+
+
+
+
+

3. Create a feature branch

It's a good practice to create a new branch that'll contain the changes we want. We'll learn more about branches later on. For now, just think of this as a separate area where our changes will be kept not to interfere with other people's work.

+
git switch -c southwest
+
+
+
+
+
+
+
+

4. Make, commit and push changes to new branch

For example, let's create a new file called SouthWest.md and edit it to add this text:

+
* Exmoor
+* Dartmoor
+* Bodmin Moor
+
+

Save it, and push this changes to your fork's new branch:

+
git add SouthWest.md
+git commit -m "The South West is also hilly."
+git push origin southwest
+
+
+
+
+
+
+
+

5. Create Pull Request

Go back to the collaborator's GitHub site and reload the fork. GitHub has noticed there is a new branch and is presenting us with a green button to Compare & pull request. Fantastic! Click that button.

+

Fill in the form with additional information about your change, as you consider necesary to make the team leader understand what this is all about.

+

Take some time to inspect the commits and the changes you are submitting for review. When you are ready, click on the Create Pull Request button.

+

Now, the leader needs to go to their GitHub site. They have been notified there is a pull request in their repo awaiting revision.

+
+
+
+
+
+
+

6. Feedback from team leader

Leaders can see the list of pull requests in the vertical menu of the repo, on the right hand side of the screen. Select the pull request the collaborator has done, and inspect the changes.

+

There are three tabs: in one you can start a conversation with the collaborator about their changes, and in the others you can have a look at the commits and changes made.

+

Go to the tab labeled as "Files Changed". When you hover over the changes, a small + button appears. Select one line you want to make a comment on. For example, the line that contains "Exmoor".

+

GitHub allows you to add a comment about that specific part of the change. Your collaborator has forgotten to add a title at the beginning of the file right before "Exmoor", so tell them so in the form presented after clicking the + button.

+
+
+
+
+
+
+

7. Fixes by collaborator

Collaborators will be notified of this comment by email and also in their profiles page. Click the link accompanying this notification to read the comment from the team leader.

+

Go back to your local repository, make the changes suggested and push them to the new branch.

+

Add this at the beginning of your file:

+
Hills in the South West:
+=======================
+
+
+

Then push the change to your fork:

+
git add .
+git commit -m "Titles added as requested."
+git push origin southwest
+
+

This change will automatically be added to the pull request you started.

+
+
+
+
+
+
+

8. Leader accepts pull request

The team leader will be notified of the new changes that can be reviewed in the same fashion as earlier.

+

Let's assume the team leader is now happy with the changes.

+

Leaders can see in the "Conversation" tab of the pull request a green button labelled Merge pull request. Click it and confirm the decision.

+

The collaborator's pull request has been accepted and appears now in the original repository owned by the team leader.

+

Fork and Pull Request done!

+
+
+
+
+
+
+

Some Considerations

    +
  • Fork and Pull Request are things happening only on the repository's server side (GitHub in our case). Consequently, you can't do things like git fork or git pull-request from the local copy of a repository.

    +
  • +
  • You don't always need to fork repositories with the intention of contributing. You can fork a library you use, install it manually on your computer, and add more functionality or customise the existing one, so that it is more useful for you and your team.

    +
  • +
  • Numpy's example is only illustrative. Normally, Open Source projects have in their documentation (sometimes in the form of a wiki) a set of instructions you need to follow if you want to contribute to their software.

    +
  • +
  • Pull Requests can also be done for merging branches in a non-forked repository. It's typically used in teams to merge code from a branch into the main branch and ask team colleagues for code reviews before merging.

    +
  • +
  • It's a good practice before starting a fork and a pull request to have a look at existing forks and pull requests. On GitHub, you can find the list of pull requests on the horizontal menu on the top of the page. Try to also find the network graph displaying all existing forks of a repo, e.g., NumpyDoc repo's network graph.

    +
  • +
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/06ForkAndPull.ipynb b/ch00git/06ForkAndPull.ipynb new file mode 100644 index 000000000..7f7886ba1 --- /dev/null +++ b/ch00git/06ForkAndPull.ipynb @@ -0,0 +1,242 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "011078fc", + "metadata": {}, + "source": [ + "## Fork and Pull\n", + "\n", + "### Different ways of collaborating \n", + "\n", + "We have just seen how we can work with others on GitHub: we add them as collaborators on our repositories and give them permissions to push changes. \n", + "\n", + "Let's talk now about some other type of collaboration. \n", + "\n", + "Imagine you are a user of an Open Source project like Numpy and find a bug in one of their methods. \n", + "\n", + "You can inspect and clone [Numpy's code in GitHub](https://github.com/numpy/numpy), play around a bit and find how to fix the bug. \n", + "\n", + "Numpy has done so much for you asking nothing in return, that you really want to contribute back by fixing the bug for them. \n", + "\n", + "You make all of the changes but you can't push it back to Numpy's repository because you don't have permissions.\n", + "\n", + "The right way to do this is __forking Numpy's repository__. " + ] + }, + { + "cell_type": "markdown", + "id": "3703207c", + "metadata": {}, + "source": [ + "### Forking a repository on GitHub\n", + "\n", + "By forking a repository, all you do is make a copy of it in your GitHub account, where you will have write permissions as well.\n", + "\n", + "If you fork Numpy's repository, you will find a new repository in your GitHub account that is an exact copy of Numpy. You can then clone it to your computer, work locally on fixing the bug and push the changes to your _fork_ of Numpy. \n", + "\n", + "Once you are happy with with the changes, GitHub also offers you a way to notify Numpy's developers of this changes so that they can include them in the official Numpy repository via starting a __Pull Request__." + ] + }, + { + "cell_type": "markdown", + "id": "d13713dd", + "metadata": {}, + "source": [ + "### Pull Request\n", + "\n", + "You can create a Pull Request and select those changes that you think can be useful for fixing Numpy's bug. \n", + "\n", + "Numpy's developers will review your code and make comments and suggestions on your fix. Then, you can commit more improvements in the pull request for them to review and so on. \n", + "\n", + "Once Numpy's developers are happy with your changes, they'll accept your Pull Request and merge the changes into their original repository, for everyone to use." + ] + }, + { + "cell_type": "markdown", + "id": "839bf57e", + "metadata": {}, + "source": [ + "### Practical example - Team up!\n", + "\n", + "We will be working in the same repository with one of you being the leader and the other being the collaborator. \n", + "\n", + "Collaborators need to go to the leader's GitHub profile and find the repository we created for that lesson. Mine is in https://github.com/jamespjh/github-example" + ] + }, + { + "cell_type": "markdown", + "id": "7b392718", + "metadata": {}, + "source": [ + "#### 1. Fork repository\n", + "\n", + "You will see on the top right of the page a `Fork` button with an accompanying number indicating how many GitHub users have forked that repository. \n", + "\n", + "Collaborators need to navigate to the leader's repository and click the `Fork` button. \n", + "\n", + "Collaborators: note how GitHub has redirected you to your own GitHub page and you are now looking at an exact copy of the team leader's repository." + ] + }, + { + "cell_type": "markdown", + "id": "5907c4b5", + "metadata": {}, + "source": [ + "#### 2. Clone your forked repo\n", + "\n", + "Collaborators: go to your terminal and clone the newly created fork.\n", + "\n", + "```\n", + "git clone git@github.com:jamespjh/github-example.git\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "717ba2b3", + "metadata": {}, + "source": [ + "#### 3. Create a feature branch\n", + "\n", + "It's a good practice to create a new branch that'll contain the changes we want. We'll learn more about branches later on. For now, just think of this as a separate area where our changes will be kept not to interfere with other people's work.\n", + "\n", + "```\n", + "git switch -c southwest\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5c8d77a6", + "metadata": {}, + "source": [ + "#### 4. Make, commit and push changes to new branch\n", + "\n", + "For example, let's create a new file called `SouthWest.md` and edit it to add this text:\n", + "\n", + "```\n", + "* Exmoor\n", + "* Dartmoor\n", + "* Bodmin Moor\n", + "```\n", + "\n", + "Save it, and push this changes to your fork's new branch:\n", + "\n", + "```\n", + "git add SouthWest.md\n", + "git commit -m \"The South West is also hilly.\"\n", + "git push origin southwest\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "049c4047", + "metadata": {}, + "source": [ + "#### 5. Create Pull Request\n", + "\n", + "Go back to the collaborator's GitHub site and reload the fork. GitHub has noticed there is a new branch and is presenting us with a green button to `Compare & pull request`. Fantastic! Click that button.\n", + "\n", + "Fill in the form with additional information about your change, as you consider necesary to make the team leader understand what this is all about.\n", + "\n", + "Take some time to inspect the commits and the changes you are submitting for review. When you are ready, click on the `Create Pull Request` button. \n", + "\n", + "Now, the leader needs to go to their GitHub site. They have been notified there is a pull request in their repo awaiting revision. " + ] + }, + { + "cell_type": "markdown", + "id": "ff40ed4c", + "metadata": {}, + "source": [ + "#### 6. Feedback from team leader\n", + "\n", + "Leaders can see the list of pull requests in the vertical menu of the repo, on the right hand side of the screen. Select the pull request the collaborator has done, and inspect the changes. \n", + "\n", + "There are three tabs: in one you can start a conversation with the collaborator about their changes, and in the others you can have a look at the commits and changes made. \n", + "\n", + "Go to the tab labeled as \"Files Changed\". When you hover over the changes, a small `+` button appears. Select one line you want to make a comment on. For example, the line that contains \"Exmoor\". \n", + "\n", + "GitHub allows you to add a comment about that specific part of the change. Your collaborator has forgotten to add a title at the beginning of the file right before \"Exmoor\", so tell them so in the form presented after clicking the `+` button." + ] + }, + { + "cell_type": "markdown", + "id": "429d40b9", + "metadata": {}, + "source": [ + "#### 7. Fixes by collaborator\n", + "\n", + "Collaborators will be notified of this comment by email and also in their profiles page. Click the link accompanying this notification to read the comment from the team leader. \n", + "\n", + "Go back to your local repository, make the changes suggested and push them to the new branch.\n", + "\n", + "Add this at the beginning of your file:\n", + "\n", + "```\n", + "Hills in the South West:\n", + "=======================\n", + "\n", + "```\n", + "\n", + "Then push the change to your fork:\n", + "\n", + "```\n", + "git add .\n", + "git commit -m \"Titles added as requested.\"\n", + "git push origin southwest\n", + "```\n", + "\n", + "This change will automatically be added to the pull request you started." + ] + }, + { + "cell_type": "markdown", + "id": "9aa21820", + "metadata": {}, + "source": [ + "#### 8. Leader accepts pull request \n", + "The team leader will be notified of the new changes that can be reviewed in the same fashion as earlier. \n", + "\n", + "Let's assume the team leader is now happy with the changes.\n", + "\n", + "Leaders can see in the \"Conversation\" tab of the pull request a green button labelled ```Merge pull request```. Click it and confirm the decision. \n", + "\n", + "The collaborator's pull request has been accepted and appears now in the original repository owned by the team leader. \n", + "\n", + "Fork and Pull Request done!" + ] + }, + { + "cell_type": "markdown", + "id": "663e94d8", + "metadata": {}, + "source": [ + "### Some Considerations\n", + "\n", + "* Fork and Pull Request are things happening only on the repository's server side (GitHub in our case). Consequently, you can't do things like `git fork` or `git pull-request` from the local copy of a repository.\n", + "\n", + "* You don't always need to fork repositories with the intention of contributing. You can fork a library you use, install it manually on your computer, and add more functionality or customise the existing one, so that it is more useful for you and your team. \n", + "\n", + "* Numpy's example is only illustrative. Normally, Open Source projects have in their documentation (sometimes in the form of a wiki) a set of instructions you need to follow if you want to contribute to their software.\n", + "\n", + "* Pull Requests can also be done for merging branches in a non-forked repository. It's typically used in teams to merge code from a branch into the `main` branch and ask team colleagues for code reviews before merging.\n", + "\n", + "* It's a good practice before starting a fork and a pull request to have a look at existing forks and pull requests. On GitHub, you can find the list of pull requests on the horizontal menu on the top of the page. Try to also find the network graph displaying all existing forks of a repo, e.g., [NumpyDoc repo's network graph](https://github.com/numpy/numpydoc/network)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Fork and Pull" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/06ForkAndPull.ipynb.py b/ch00git/06ForkAndPull.ipynb.py new file mode 100644 index 000000000..4af89b754 --- /dev/null +++ b/ch00git/06ForkAndPull.ipynb.py @@ -0,0 +1,174 @@ +# --- +# jupyter: +# jekyll: +# display_name: Fork and Pull +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Fork and Pull +# +# ### Different ways of collaborating +# +# We have just seen how we can work with others on GitHub: we add them as collaborators on our repositories and give them permissions to push changes. +# +# Let's talk now about some other type of collaboration. +# +# Imagine you are a user of an Open Source project like Numpy and find a bug in one of their methods. +# +# You can inspect and clone [Numpy's code in GitHub](https://github.com/numpy/numpy), play around a bit and find how to fix the bug. +# +# Numpy has done so much for you asking nothing in return, that you really want to contribute back by fixing the bug for them. +# +# You make all of the changes but you can't push it back to Numpy's repository because you don't have permissions. +# +# The right way to do this is __forking Numpy's repository__. + +# %% [markdown] +# ### Forking a repository on GitHub +# +# By forking a repository, all you do is make a copy of it in your GitHub account, where you will have write permissions as well. +# +# If you fork Numpy's repository, you will find a new repository in your GitHub account that is an exact copy of Numpy. You can then clone it to your computer, work locally on fixing the bug and push the changes to your _fork_ of Numpy. +# +# Once you are happy with with the changes, GitHub also offers you a way to notify Numpy's developers of this changes so that they can include them in the official Numpy repository via starting a __Pull Request__. + +# %% [markdown] +# ### Pull Request +# +# You can create a Pull Request and select those changes that you think can be useful for fixing Numpy's bug. +# +# Numpy's developers will review your code and make comments and suggestions on your fix. Then, you can commit more improvements in the pull request for them to review and so on. +# +# Once Numpy's developers are happy with your changes, they'll accept your Pull Request and merge the changes into their original repository, for everyone to use. + +# %% [markdown] +# ### Practical example - Team up! +# +# We will be working in the same repository with one of you being the leader and the other being the collaborator. +# +# Collaborators need to go to the leader's GitHub profile and find the repository we created for that lesson. Mine is in https://github.com/jamespjh/github-example + +# %% [markdown] +# #### 1. Fork repository +# +# You will see on the top right of the page a `Fork` button with an accompanying number indicating how many GitHub users have forked that repository. +# +# Collaborators need to navigate to the leader's repository and click the `Fork` button. +# +# Collaborators: note how GitHub has redirected you to your own GitHub page and you are now looking at an exact copy of the team leader's repository. + +# %% [markdown] +# #### 2. Clone your forked repo +# +# Collaborators: go to your terminal and clone the newly created fork. +# +# ``` +# git clone git@github.com:jamespjh/github-example.git +# ``` + +# %% [markdown] +# #### 3. Create a feature branch +# +# It's a good practice to create a new branch that'll contain the changes we want. We'll learn more about branches later on. For now, just think of this as a separate area where our changes will be kept not to interfere with other people's work. +# +# ``` +# git switch -c southwest +# ``` + +# %% [markdown] +# #### 4. Make, commit and push changes to new branch +# +# For example, let's create a new file called `SouthWest.md` and edit it to add this text: +# +# ``` +# * Exmoor +# * Dartmoor +# * Bodmin Moor +# ``` +# +# Save it, and push this changes to your fork's new branch: +# +# ``` +# git add SouthWest.md +# git commit -m "The South West is also hilly." +# git push origin southwest +# ``` + +# %% [markdown] +# #### 5. Create Pull Request +# +# Go back to the collaborator's GitHub site and reload the fork. GitHub has noticed there is a new branch and is presenting us with a green button to `Compare & pull request`. Fantastic! Click that button. +# +# Fill in the form with additional information about your change, as you consider necesary to make the team leader understand what this is all about. +# +# Take some time to inspect the commits and the changes you are submitting for review. When you are ready, click on the `Create Pull Request` button. +# +# Now, the leader needs to go to their GitHub site. They have been notified there is a pull request in their repo awaiting revision. + +# %% [markdown] +# #### 6. Feedback from team leader +# +# Leaders can see the list of pull requests in the vertical menu of the repo, on the right hand side of the screen. Select the pull request the collaborator has done, and inspect the changes. +# +# There are three tabs: in one you can start a conversation with the collaborator about their changes, and in the others you can have a look at the commits and changes made. +# +# Go to the tab labeled as "Files Changed". When you hover over the changes, a small `+` button appears. Select one line you want to make a comment on. For example, the line that contains "Exmoor". +# +# GitHub allows you to add a comment about that specific part of the change. Your collaborator has forgotten to add a title at the beginning of the file right before "Exmoor", so tell them so in the form presented after clicking the `+` button. + +# %% [markdown] +# #### 7. Fixes by collaborator +# +# Collaborators will be notified of this comment by email and also in their profiles page. Click the link accompanying this notification to read the comment from the team leader. +# +# Go back to your local repository, make the changes suggested and push them to the new branch. +# +# Add this at the beginning of your file: +# +# ``` +# Hills in the South West: +# ======================= +# +# ``` +# +# Then push the change to your fork: +# +# ``` +# git add . +# git commit -m "Titles added as requested." +# git push origin southwest +# ``` +# +# This change will automatically be added to the pull request you started. + +# %% [markdown] +# #### 8. Leader accepts pull request +# The team leader will be notified of the new changes that can be reviewed in the same fashion as earlier. +# +# Let's assume the team leader is now happy with the changes. +# +# Leaders can see in the "Conversation" tab of the pull request a green button labelled ```Merge pull request```. Click it and confirm the decision. +# +# The collaborator's pull request has been accepted and appears now in the original repository owned by the team leader. +# +# Fork and Pull Request done! + +# %% [markdown] +# ### Some Considerations +# +# * Fork and Pull Request are things happening only on the repository's server side (GitHub in our case). Consequently, you can't do things like `git fork` or `git pull-request` from the local copy of a repository. +# +# * You don't always need to fork repositories with the intention of contributing. You can fork a library you use, install it manually on your computer, and add more functionality or customise the existing one, so that it is more useful for you and your team. +# +# * Numpy's example is only illustrative. Normally, Open Source projects have in their documentation (sometimes in the form of a wiki) a set of instructions you need to follow if you want to contribute to their software. +# +# * Pull Requests can also be done for merging branches in a non-forked repository. It's typically used in teams to merge code from a branch into the `main` branch and ask team colleagues for code reviews before merging. +# +# * It's a good practice before starting a fork and a pull request to have a look at existing forks and pull requests. On GitHub, you can find the list of pull requests on the horizontal menu on the top of the page. Try to also find the network graph displaying all existing forks of a repo, e.g., [NumpyDoc repo's network graph](https://github.com/numpy/numpydoc/network). diff --git a/ch00git/10Branches.html b/ch00git/10Branches.html new file mode 100644 index 000000000..75def17b9 --- /dev/null +++ b/ch00git/10Branches.html @@ -0,0 +1,1241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Branches + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Branches

Branches are incredibly important to why git is cool and powerful.

+

They are an easy and cheap way of making a second version of your software, which you work on in parallel, +and pull in your changes when you are ready.

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+git branch # Tell me what branches exist
+
+
+
+
+
+
+
+
+
+
* main
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+git switch -c experiment # Make a new branch (use instead `checkout -b` if you have a version of git older than 2.23)
+
+
+
+
+
+
+
+
+
+
Switched to a new branch 'experiment'
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+git branch
+
+
+
+
+
+
+
+
+
+
* experiment
+  main
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+* Fan y Big
+* Cadair Idris
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+git commit -am "Add Cadair Idris"
+
+
+
+
+
+
+
+
+
+
[experiment b290508] Add Cadair Idris
+ 1 file changed, 2 insertions(+)
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git switch main # Switch to an existing branch (use `checkout` if you are using git older than 2.23)
+
+
+
+
+
+
+
+
+
+
Switched to branch 'main'
+
+
+
+
+
+
+
Your branch is up to date with 'origin/main'.
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+cat Wales.md
+
+
+
+
+
+
+
+
+
+
Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Fan y Big
+
+
+
+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+git switch experiment
+
+
+
+
+
+
+
+
+
+
Switched to branch 'experiment'
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
cat Wales.md
+
+
+
+
+
+
+
+
+
+
Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+* Fan y Big
+* Cadair Idris
+
+
+
+
+
+
+
+
+
+

Publishing branches

To let the server know there's a new branch use:

+
+
+
+
+
+
In [11]:
+
+
+
%%bash
+git push -u origin experiment
+
+
+
+
+
+
+
+
+
+
remote: 
+remote: Create a pull request for 'experiment' on GitHub by visiting:        
+remote:      https://github.com/UCL/github-example/pull/new/experiment        
+remote: 
+To github.com:UCL/github-example.git
+ * [new branch]      experiment -> experiment
+
+
+
+
+
+
+
branch 'experiment' set up to track 'origin/experiment'.
+
+
+
+
+
+
+
+
+
+

We use --set-upstream origin (Abbreviation -u) to tell git that this branch should be pushed to and pulled from origin per default.

+

If you are following along, you should be able to see your branch in the list of branches in GitHub.

+
+
+
+
+
+
+

Once you've used git push -u once, you can push new changes to the branch with just a git push.

+
+
+
+
+
+
+

If others checkout your repository, they will be able to do git switch experiment to see your branch content, +and collaborate with you in the branch.

+
+
+
+
+
+
In [12]:
+
+
+
%%bash
+git branch -r
+
+
+
+
+
+
+
+
+
+
  origin/experiment
+  origin/gh-pages
+  origin/main
+  origin/master
+
+
+
+
+
+
+
+
+
+

Local branches can be, but do not have to be, connected to remote branches +They are said to "track" remote branches. push -u sets up the tracking relationship. +You can see the remote branch for each of your local branches if you ask for "verbose" output from git branch:

+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+git branch -vv
+
+
+
+
+
+
+
+
+
+
* experiment b290508 [origin/experiment] Add Cadair Idris
+  main       ae74101 [origin/main] Add another Beacon
+
+
+
+
+
+
+
+
+
+

Find out what is on a branch

In addition to using git diff to compare to the state of a branch, +you can use git log to look at lists of commits which are in a branch +and haven't been merged yet.

+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+git log main..experiment
+
+
+
+
+
+
+
+
+
+
commit b290508e7eecc632ee43f2a21b96641ca379a768
+Author: Lancelot the Brave <l.brave@spamalot.uk>
+Date:   Wed Nov 22 15:42:50 2023 +0000
+
+    Add Cadair Idris
+
+
+
+
+
+
+
+
+
+

Git uses various symbols to refer to sets of commits. +The double dot A..B means "ancestor of B and not ancestor of A"

+

So in a purely linear sequence, it does what you'd expect.

+
+
+
+
+
+
In [15]:
+
+
+
%%bash
+git log --graph --oneline HEAD~9..HEAD~5
+
+
+
+
+
+
+
+
+
+
* df5fed8 Include lakes in the scope
+* 7b49cfb Add lakeland
+* 21c306b Revert "Add a lie about a mountain"
+* 5bf472d Change title
+
+
+
+
+
+
+
+
+
+

But in cases where a history has branches, +the definition in terms of ancestors is important.

+
+
+
+
+
+
In [16]:
+
+
+
%%bash
+git log --graph --oneline HEAD~5..HEAD
+
+
+
+
+
+
+
+
+
+
* b290508 Add Cadair Idris
+* ae74101 Add another Beacon
+* 592c6bd Add a beacon
+* a6361f3 Add wales
+* 4b52f45 Add Helvellyn
+
+
+
+
+
+
+
+
+
+

If there are changes on both sides, like this:

+
+
+
+
+
+
In [17]:
+
+
+
%%bash
+git switch main
+
+
+
+
+
+
+
+
+
+
Switched to branch 'main'
+
+
+
+
+
+
+
Your branch is up to date with 'origin/main'.
+
+
+
+
+
+
+
+
+
In [18]:
+
+
+
%%writefile Scotland.md
+Mountains In Scotland
+==================
+
+* Ben Eighe
+* Cairngorm
+* Aonach Eagach
+
+
+
+
+
+
+
+
+
+
Writing Scotland.md
+
+
+
+
+
+
+
+
+
In [19]:
+
+
+
%%bash
+git diff Scotland.md
+
+
+
+
+
+
+
+
In [20]:
+
+
+
%%bash
+git commit -am "Commit Aonach onto main branch"
+
+
+
+
+
+
+
+
+
+
On branch main
+Your branch is up to date with 'origin/main'.
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	Scotland.md
+	__pycache__/
+	wsd.py
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[20], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'git commit -am "Commit Aonach onto main branch"\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'git commit -am "Commit Aonach onto main branch"\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

Then this notation is useful to show the content of what's on what branch:

+
+
+
+
+
+
In [21]:
+
+
+
%%bash
+git log --left-right --oneline main...experiment
+
+
+
+
+
+
+
+
+
+
> b290508 Add Cadair Idris
+
+
+
+
+
+
+
+
+
+

Three dots means "everything which is not a common ancestor" of the two commits, i.e. the differences between them.

+
+
+
+
+
+
+

Merging branches

+
+
+
+
+
+
+

We can merge branches, and just as we would pull in remote changes, there may or may not be conflicts.

+
+
+
+
+
+
In [22]:
+
+
+
%%bash
+git branch
+git merge experiment
+
+
+
+
+
+
+
+
+
+
  experiment
+* main
+Updating ae74101..b290508
+Fast-forward
+ Wales.md | 2 ++
+ 1 file changed, 2 insertions(+)
+
+
+
+
+
+
+
+
+
In [23]:
+
+
+
%%bash
+git log --graph --oneline HEAD~3..HEAD
+
+
+
+
+
+
+
+
+
+
* b290508 Add Cadair Idris
+* ae74101 Add another Beacon
+* 592c6bd Add a beacon
+
+
+
+
+
+
+
+
+
+

Cleaning up after a branch

+
+
+
+
+
+
In [24]:
+
+
+
%%bash
+git branch
+
+
+
+
+
+
+
+
+
+
  experiment
+* main
+
+
+
+
+
+
+
+
+
In [25]:
+
+
+
%%bash
+git branch -d experiment
+
+
+
+
+
+
+
+
+
+
Deleted branch experiment (was b290508).
+
+
+
+
+
+
+
+
+
In [26]:
+
+
+
%%bash
+git branch
+
+
+
+
+
+
+
+
+
+
* main
+
+
+
+
+
+
+
+
+
In [27]:
+
+
+
%%bash
+git branch --remote
+
+
+
+
+
+
+
+
+
+
  origin/experiment
+  origin/gh-pages
+  origin/main
+  origin/master
+
+
+
+
+
+
+
+
+
In [28]:
+
+
+
%%bash
+git push --delete origin experiment 
+# Remove remote branch 
+# - also can use github interface
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+ - [deleted]         experiment
+
+
+
+
+
+
+
+
+
In [29]:
+
+
+
%%bash
+git branch --remote
+
+
+
+
+
+
+
+
+
+
  origin/gh-pages
+  origin/main
+  origin/master
+
+
+
+
+
+
+
+
+
+

A good branch strategy

    +
  • A develop or main branch: for general new code - (the cutting edge version of your software)
  • +
  • feature branches: for specific new ideas. Normally branched out from main.
  • +
  • release branches: when you share code with users. A particular moment of the develop process that it's considered stable.
      +
    • Useful for including security and bug patches once it's been released.
    • +
    +
  • +
  • A production branch: code used for active work. Normally it's the same than the latest release.
  • +
+
+
+
+
+
+
+

Grab changes from a branch

Make some changes on one branch, switch back to another, and use:

+
+
+
+
+
+
+
git checkout <branch> <path>
+
+
+
+
+
+
+
+

to quickly grab a file from one branch into another. This will create a copy of the file as it exists in <branch> into your current branch, overwriting it if it already existed. +For example, if you have been experimenting in a new branch but want to undo all your changes to a particular file (that is, restore the file to its version in the main branch), you can do that with:

+
git checkout main test_file
+
+

Using git checkout with a path takes the content of files. +To grab the content of a specific commit from another branch, +and apply it as a patch to your branch, use:

+
+
+
+
+
+
+
git cherry-pick <commit>
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/10Branches.ipynb b/ch00git/10Branches.ipynb new file mode 100644 index 000000000..72d5b368c --- /dev/null +++ b/ch00git/10Branches.ipynb @@ -0,0 +1,681 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "23d0a77d", + "metadata": {}, + "source": [ + "## Branches\n", + "\n", + "Branches are incredibly important to why `git` is cool and powerful.\n", + "\n", + "They are an easy and cheap way of making a second version of your software, which you work on in parallel,\n", + "and pull in your changes when you are ready." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b941292", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a24384c5", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch # Tell me what branches exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "650e7698", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git switch -c experiment # Make a new branch (use instead `checkout -b` if you have a version of git older than 2.23)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbd3d49c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e5ac4f8", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon\n", + "* Glyder Fawr\n", + "* Fan y Big\n", + "* Cadair Idris" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e48a8360", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add Cadair Idris\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af052f71", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git switch main # Switch to an existing branch (use `checkout` if you are using git older than 2.23)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65090294", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cat Wales.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcfc7a05", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git switch experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d43eeeb1", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "cat Wales.md" + ] + }, + { + "cell_type": "markdown", + "id": "57c8dd0d", + "metadata": {}, + "source": [ + "### Publishing branches\n", + "\n", + "To let the server know there's a new branch use:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ea9d784", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push -u origin experiment" + ] + }, + { + "cell_type": "markdown", + "id": "30ec420e", + "metadata": {}, + "source": [ + "We use `--set-upstream origin` (Abbreviation `-u`) to tell git that this branch should be pushed to and pulled from origin per default.\n", + "\n", + "If you are following along, you should be able to see your branch in the list of branches in GitHub." + ] + }, + { + "cell_type": "markdown", + "id": "cf9e2af6", + "metadata": {}, + "source": [ + "Once you've used `git push -u` once, you can push new changes to the branch with just a git push." + ] + }, + { + "cell_type": "markdown", + "id": "67781655", + "metadata": {}, + "source": [ + "If others checkout your repository, they will be able to do `git switch experiment` to see your branch content,\n", + "and collaborate with you **in the branch**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "951a82fa", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch -r" + ] + }, + { + "cell_type": "markdown", + "id": "3548c5c3", + "metadata": {}, + "source": [ + "Local branches can be, but do not have to be, connected to remote branches\n", + "They are said to \"track\" remote branches. `push -u` sets up the tracking relationship.\n", + "You can see the remote branch for each of your local branches if you ask for \"verbose\" output from `git branch`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05a325d0", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch -vv" + ] + }, + { + "cell_type": "markdown", + "id": "4974e461", + "metadata": {}, + "source": [ + "### Find out what is on a branch\n", + "\n", + "In addition to using `git diff` to compare to the state of a branch,\n", + "you can use `git log` to look at lists of commits which are in a branch\n", + "and haven't been merged yet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc8ad50f", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log main..experiment" + ] + }, + { + "cell_type": "markdown", + "id": "77e75f3f", + "metadata": {}, + "source": [ + "Git uses various symbols to refer to sets of commits.\n", + "The double dot `A..B` means \"ancestor of B and not ancestor of A\"\n", + "\n", + "So in a purely linear sequence, it does what you'd expect." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ad893cd", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --graph --oneline HEAD~9..HEAD~5" + ] + }, + { + "cell_type": "markdown", + "id": "b2ba978d", + "metadata": {}, + "source": [ + "But in cases where a history has branches,\n", + "the definition in terms of ancestors is important." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed66c09f", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --graph --oneline HEAD~5..HEAD" + ] + }, + { + "cell_type": "markdown", + "id": "91211115", + "metadata": {}, + "source": [ + "If there are changes on both sides, like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a046dad", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git switch main" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30811c08", + "metadata": { + "jupyter": { + "outputs_hidden": false + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "%%writefile Scotland.md\n", + "Mountains In Scotland\n", + "==================\n", + "\n", + "* Ben Eighe\n", + "* Cairngorm\n", + "* Aonach Eagach" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3341b93c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git diff Scotland.md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a245441e", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Commit Aonach onto main branch\"" + ] + }, + { + "cell_type": "markdown", + "id": "b6b138eb", + "metadata": {}, + "source": [ + "Then this notation is useful to show the content of what's on what branch:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d932f28c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --left-right --oneline main...experiment" + ] + }, + { + "cell_type": "markdown", + "id": "e95b2d52", + "metadata": {}, + "source": [ + "Three dots means \"everything which is not a common ancestor\" of the two commits, i.e. the differences between them." + ] + }, + { + "cell_type": "markdown", + "id": "c077b615", + "metadata": {}, + "source": [ + "### Merging branches" + ] + }, + { + "cell_type": "markdown", + "id": "b7b7a29c", + "metadata": {}, + "source": [ + "We can merge branches, and just as we would pull in remote changes, there may or may not be conflicts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "669ee5c6", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch\n", + "git merge experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "096ac615", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log --graph --oneline HEAD~3..HEAD" + ] + }, + { + "cell_type": "markdown", + "id": "c94e7e58", + "metadata": {}, + "source": [ + "### Cleaning up after a branch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb960a40", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d298a3a4", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch -d experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95901831", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1c4f3df", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch --remote" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb2c56ad", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push --delete origin experiment \n", + "# Remove remote branch \n", + "# - also can use github interface" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06f44145", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch --remote" + ] + }, + { + "cell_type": "markdown", + "id": "ab8da3bc", + "metadata": {}, + "source": [ + "### A good branch strategy\n", + "\n", + "* A `develop` or `main` branch: for general new code - (the cutting edge version of your software)\n", + "* `feature` branches: for specific new ideas. Normally branched out from `main`.\n", + "* `release` branches: when you share code with users. A particular moment of the `develop` process that it's considered stable.\n", + " * Useful for including security and bug patches once it's been released.\n", + "* A `production` branch: code used for active work. Normally it's the same than the latest release." + ] + }, + { + "cell_type": "markdown", + "id": "a1b6b3d6", + "metadata": {}, + "source": [ + "### Grab changes from a branch\n", + "\n", + "Make some changes on one branch, switch back to another, and use:" + ] + }, + { + "cell_type": "markdown", + "id": "d0f39d64", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "source": [ + "```bash\n", + "git checkout \n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "bca59c44", + "metadata": {}, + "source": [ + "to quickly grab a file from one branch into another. This will create a copy of the file as it exists in `` into your current branch, overwriting it if it already existed.\n", + "For example, if you have been experimenting in a new branch but want to undo all your changes to a particular file (that is, restore the file to its version in the `main` branch), you can do that with:\n", + "\n", + "```\n", + "git checkout main test_file\n", + "```\n", + "\n", + "Using `git checkout` with a path takes the content of files.\n", + "To grab the content of a specific *commit* from another branch,\n", + "and apply it as a patch to your branch, use:" + ] + }, + { + "cell_type": "markdown", + "id": "40937443", + "metadata": {}, + "source": [ + "``` bash\n", + "git cherry-pick \n", + "```" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Branches" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/10Branches.ipynb.py b/ch00git/10Branches.ipynb.py new file mode 100644 index 000000000..d47af667e --- /dev/null +++ b/ch00git/10Branches.ipynb.py @@ -0,0 +1,223 @@ +# --- +# jupyter: +# jekyll: +# display_name: Branches +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Branches +# +# Branches are incredibly important to why `git` is cool and powerful. +# +# They are an easy and cheap way of making a second version of your software, which you work on in parallel, +# and pull in your changes when you are ready. + +# %% jupyter={"outputs_hidden": true} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git branch # Tell me what branches exist + +# %% jupyter={"outputs_hidden": false} language="bash" +# git switch -c experiment # Make a new branch (use instead `checkout -b` if you have a version of git older than 2.23) + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch + +# %% +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr +* Fan y Big +* Cadair Idris + +# %% language="bash" +# git commit -am "Add Cadair Idris" + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git switch main # Switch to an existing branch (use `checkout` if you are using git older than 2.23) + +# %% jupyter={"outputs_hidden": false} language="bash" +# cat Wales.md + +# %% jupyter={"outputs_hidden": false} language="bash" +# git switch experiment + +# %% jupyter={"outputs_hidden": false} +# cat Wales.md + +# %% [markdown] +# ### Publishing branches +# +# To let the server know there's a new branch use: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push -u origin experiment + +# %% [markdown] +# We use `--set-upstream origin` (Abbreviation `-u`) to tell git that this branch should be pushed to and pulled from origin per default. +# +# If you are following along, you should be able to see your branch in the list of branches in GitHub. + +# %% [markdown] +# Once you've used `git push -u` once, you can push new changes to the branch with just a git push. + +# %% [markdown] +# If others checkout your repository, they will be able to do `git switch experiment` to see your branch content, +# and collaborate with you **in the branch**. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch -r + +# %% [markdown] +# Local branches can be, but do not have to be, connected to remote branches +# They are said to "track" remote branches. `push -u` sets up the tracking relationship. +# You can see the remote branch for each of your local branches if you ask for "verbose" output from `git branch`: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch -vv + +# %% [markdown] +# ### Find out what is on a branch +# +# In addition to using `git diff` to compare to the state of a branch, +# you can use `git log` to look at lists of commits which are in a branch +# and haven't been merged yet. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log main..experiment + +# %% [markdown] +# Git uses various symbols to refer to sets of commits. +# The double dot `A..B` means "ancestor of B and not ancestor of A" +# +# So in a purely linear sequence, it does what you'd expect. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --graph --oneline HEAD~9..HEAD~5 + +# %% [markdown] +# But in cases where a history has branches, +# the definition in terms of ancestors is important. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --graph --oneline HEAD~5..HEAD + +# %% [markdown] +# If there are changes on both sides, like this: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git switch main + +# %% jupyter={"outputs_hidden": false} +# %%writefile Scotland.md +Mountains In Scotland +================== + +* Ben Eighe +* Cairngorm +* Aonach Eagach + + +# %% jupyter={"outputs_hidden": false} language="bash" +# git diff Scotland.md + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Commit Aonach onto main branch" + +# %% [markdown] +# Then this notation is useful to show the content of what's on what branch: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --left-right --oneline main...experiment + +# %% [markdown] +# Three dots means "everything which is not a common ancestor" of the two commits, i.e. the differences between them. + +# %% [markdown] +# ### Merging branches + +# %% [markdown] +# We can merge branches, and just as we would pull in remote changes, there may or may not be conflicts. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch +# git merge experiment + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log --graph --oneline HEAD~3..HEAD + +# %% [markdown] +# ### Cleaning up after a branch + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch -d experiment + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch --remote + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push --delete origin experiment +# # Remove remote branch +# # - also can use github interface + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch --remote + +# %% [markdown] +# ### A good branch strategy +# +# * A `develop` or `main` branch: for general new code - (the cutting edge version of your software) +# * `feature` branches: for specific new ideas. Normally branched out from `main`. +# * `release` branches: when you share code with users. A particular moment of the `develop` process that it's considered stable. +# * Useful for including security and bug patches once it's been released. +# * A `production` branch: code used for active work. Normally it's the same than the latest release. + +# %% [markdown] +# ### Grab changes from a branch +# +# Make some changes on one branch, switch back to another, and use: + +# %% [markdown] attributes={"classes": [" bash"], "id": ""} +# ```bash +# git checkout +# ``` + +# %% [markdown] +# to quickly grab a file from one branch into another. This will create a copy of the file as it exists in `` into your current branch, overwriting it if it already existed. +# For example, if you have been experimenting in a new branch but want to undo all your changes to a particular file (that is, restore the file to its version in the `main` branch), you can do that with: +# +# ``` +# git checkout main test_file +# ``` +# +# Using `git checkout` with a path takes the content of files. +# To grab the content of a specific *commit* from another branch, +# and apply it as a patch to your branch, use: + +# %% [markdown] +# ``` bash +# git cherry-pick +# ``` diff --git a/ch00git/11Miscellany.html b/ch00git/11Miscellany.html new file mode 100644 index 000000000..83615065d --- /dev/null +++ b/ch00git/11Miscellany.html @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Git miscellany + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Git Stash

+
+
+
+
+
+
+

Before you can git pull, you need to have committed any changes you have made. If you find you want to pull, but you're not ready to commit, you have to temporarily "put aside" your uncommitted changes. +For this, you can use the git stash command, like in the following example:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%writefile Wales.md
+Mountains In Wales
+==================
+
+* Pen y Fan
+* Tryfan
+* Snowdon
+* Glyder Fawr
+* Fan y Big
+* Cadair Idris
+
+
+
+
+
+
+
+
+
+
Overwriting Wales.md
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+git stash
+git pull
+
+
+
+
+
+
+
+
+
+
No local changes to save
+Already up to date.
+
+
+
+
+
+
+
+
+
+

By stashing your work first, your repository becomes clean, allowing you to pull. To restore your changes, use git stash apply.

+
+
+
+
+
+
In [4]:
+
+
+
%%bash --no-raise-error
+git stash apply
+
+
+
+
+
+
+
+
+
+
No stash entries found.
+
+
+
+
+
+
+
+
+
+

The "Stash" is a way of temporarily saving your working area, and can help out in a pinch.

+
+
+
+
+
+
+

Tagging

Tags are easy to read labels for revisions, and can be used anywhere we would name a commit.

+

Produce real results only with tagged revisions

+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+git tag -a v1.0 -m "Release 1.0"
+git push --tags
+
+
+
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+ ! [rejected]        v1.0 -> v1.0 (already exists)
+error: failed to push some refs to 'github.com:UCL/github-example.git'
+hint: Updates were rejected because the tag already exists in the remote.
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[5], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'git tag -a v1.0 -m "Release 1.0"\ngit push --tags\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'git tag -a v1.0 -m "Release 1.0"\ngit push --tags\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
In [6]:
+
+
+
%%writefile Pennines.md
+
+Mountains In the Pennines
+========================
+
+* Cross Fell
+
+
+
+
+
+
+
+
+
+
Writing Pennines.md
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git add Pennines.md
+git commit -am "Add Pennines"
+
+
+
+
+
+
+
+
+
+
[main 694c013] Add Pennines
+ 1 file changed, 5 insertions(+)
+ create mode 100644 Pennines.md
+
+
+
+
+
+
+
+
+
+

You can also use tag names in the place of commmit hashes, such as to list the history between particular commits:

+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git log v1.0.. --graph --oneline
+
+
+
+
+
+
+
+
+
+
* 694c013 Add Pennines
+
+
+
+
+
+
+
+
+
+

If .. is used without a following commit name, HEAD is assumed.

+
+
+
+
+
+
+

Working with generated files: gitignore

+
+
+
+
+
+
+

We often end up with files that are generated by our program. It is bad practice to keep these in Git; just keep the sources.

+
+
+
+
+
+
+

Examples include .o and .x files for compiled languages, .pyc files in Python.

+
+
+
+
+
+
+

In our example, we might want to make our .md files into a PDF with pandoc:

+
+
+
+
+
+
In [9]:
+
+
+
%%writefile Makefile
+
+MDS=$(wildcard *.md)
+PDFS=$(MDS:.md=.pdf)
+
+default: $(PDFS)
+
+%.pdf: %.md
+	pandoc $< -o $@
+
+
+
+
+
+
+
+
+
+
Writing Makefile
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
%%bash
+make
+
+
+
+
+
+
+
+
+
+
make[1]: Entering directory '/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/git_example'
+pandoc Pennines.md -o Pennines.pdf
+pandoc Scotland.md -o Scotland.pdf
+pandoc Wales.md -o Wales.pdf
+pandoc index.md -o index.pdf
+pandoc lakeland.md -o lakeland.pdf
+make[1]: Leaving directory '/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/git_example'
+
+
+
+
+
+
+
+
+
+

We now have a bunch of output .pdf files corresponding to each Markdown file.

+
+
+
+
+
+
+

But we don't want those to show up in git:

+
+
+
+
+
+
In [11]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+Your branch is ahead of 'origin/main' by 2 commits.
+  (use "git push" to publish your local commits)
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	Makefile
+	Pennines.pdf
+	Scotland.md
+	Scotland.pdf
+	Wales.pdf
+	__pycache__/
+	index.pdf
+	lakeland.pdf
+	wsd.py
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+
+
+

Use .gitignore files to tell Git not to pay attention to files with certain paths:

+
+
+
+
+
+
In [12]:
+
+
+
%%writefile .gitignore
+*.pdf
+
+
+
+
+
+
+
+
+
+
Writing .gitignore
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+git status
+
+
+
+
+
+
+
+
+
+
On branch main
+Your branch is ahead of 'origin/main' by 2 commits.
+  (use "git push" to publish your local commits)
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+	.gitignore
+	Makefile
+	Scotland.md
+	__pycache__/
+	wsd.py
+
+nothing added to commit but untracked files present (use "git add" to track)
+
+
+
+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+git add Makefile
+git add .gitignore
+git commit -am "Add a makefile and ignore generated files"
+git push
+
+
+
+
+
+
+
+
+
+
[main 85bec83] Add a makefile and ignore generated files
+ 2 files changed, 9 insertions(+)
+ create mode 100644 .gitignore
+ create mode 100644 Makefile
+
+
+
+
+
+
+
To github.com:UCL/github-example.git
+   ae74101..85bec83  main -> main
+
+
+
+
+
+
+
+
+
+

Git clean

+
+
+
+
+
+
+

Sometimes you end up creating various files that you do not want to include in version control. An easy way of deleting them (if that is what you want) is the git clean command, which will remove the files that git is not tracking.

+
+
+
+
+
+
In [15]:
+
+
+
%%bash
+git clean -fX
+
+
+
+
+
+
+
+
+
+
Removing Pennines.pdf
+Removing Scotland.pdf
+Removing Wales.pdf
+Removing index.pdf
+Removing lakeland.pdf
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
%%bash
+ls
+
+
+
+
+
+
+
+
+
+
Makefile
+Pennines.md
+Scotland.md
+Wales.md
+__pycache__
+index.md
+lakeland.md
+wsd.py
+
+
+
+
+
+
+
+
+
+
    +
  • With -f: don't prompt
  • +
  • with -d: remove directories
  • +
  • with -x: Also remote .gitignored files
  • +
  • with -X: Only remove .gitignore files
  • +
+
+
+
+
+
+
+

Hunks

Git Hunks

A "Hunk" is one git change. This changeset has three hunks:

+
+
+
+
+
+
+
+import matplotlib
++import numpy as np
+
+ from matplotlib import pylab
+ from matplotlib.backends.backend_pdf import PdfPages
+
++def increment_or_add(key,hash,weight=1):
++       if key not in hash:
++               hash[key]=0
++       hash[key]+=weight
++
+ data_path=os.path.join(os.path.dirname(
+                        os.path.abspath(__file__)),
+-regenerate=False
++regenerate=True
+
+
+
+
+
+
+
+

Interactive add

git add and git reset can be used to stage/unstage a whole file, +but you can use interactive mode to stage by hunk, choosing +yes or no for each hunk.

+
+
+
+
+
+
+
git add -p myfile.py
+
+
+
+
+
+
+
+
+import matplotlib
++import numpy as np
+#Stage this hunk [y,n,a,d,/,j,J,g,e,?]?
+
+
+
+
+
+
+
+

GitHub pages

Yaml Frontmatter

GitHub will publish repositories containing markdown as web pages, automatically.

+

You'll need to add this content:

+
+
   ---
+   ---
+
+
+

A pair of lines with three dashes, to the top of each markdown file. This is how GitHub knows which markdown files to make into web pages. +Here's why for the curious.

+
+
+
+
+
+
In [17]:
+
+
+
%%writefile index.md
+---
+title: Github Pages Example
+---
+Mountains and Lakes in the UK
+===================
+
+Engerland is not very mountainous.
+But has some tall hills, and maybe a mountain or two depending on your definition.
+
+
+
+
+
+
+
+
+
+
Overwriting index.md
+
+
+
+
+
+
+
+
+
In [18]:
+
+
+
%%bash
+git commit -am "Add github pages YAML frontmatter"
+
+
+
+
+
+
+
+
+
+
[main ad82f9b] Add github pages YAML frontmatter
+ 1 file changed, 7 insertions(+), 4 deletions(-)
+
+
+
+
+
+
+
+
+
+

The gh-pages branch

GitHub creates github pages when you use a special named branch.

+

This is best used to create documentation for a program you write, but you can use it for anything.

+
+
+
+
+
+
In [19]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [20]:
+
+
+
%%bash
+
+git switch -c gh-pages
+git push -uf origin gh-pages
+
+
+
+
+
+
+
+
+
+
Switched to a new branch 'gh-pages'
+To github.com:UCL/github-example.git
+ + 3f7041e...ad82f9b gh-pages -> gh-pages (forced update)
+
+
+
+
+
+
+
branch 'gh-pages' set up to track 'origin/gh-pages'.
+
+
+
+
+
+
+
+
+
+

The first time you do this, GitHub takes a few minutes to generate your pages.

+

The website will appear at http://username.github.io/repositoryname, for example:

+

http://UCL.github.io/github-example/

+
+
+
+
+
+
+

UCL layout for GitHub pages

You can use GitHub pages to make HTML layouts, here's an example of how to do it, +and how it looks. We won't go into the detail of this now, +but after the class, you might want to try this.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/11Miscellany.ipynb b/ch00git/11Miscellany.ipynb new file mode 100644 index 000000000..ad1266395 --- /dev/null +++ b/ch00git/11Miscellany.ipynb @@ -0,0 +1,649 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9308e351", + "metadata": {}, + "source": [ + "## Git Stash" + ] + }, + { + "cell_type": "markdown", + "id": "a491de2a", + "metadata": {}, + "source": [ + "Before you can `git pull`, you need to have committed any changes you have made. If you find you want to pull, but you're not ready to commit, you have to temporarily \"put aside\" your uncommitted changes.\n", + "For this, you can use the `git stash` command, like in the following example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a1cd4cf", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09c35527", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Wales.md\n", + "Mountains In Wales\n", + "==================\n", + "\n", + "* Pen y Fan\n", + "* Tryfan\n", + "* Snowdon\n", + "* Glyder Fawr\n", + "* Fan y Big\n", + "* Cadair Idris" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce709dab", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git stash\n", + "git pull" + ] + }, + { + "cell_type": "markdown", + "id": "fc5a1cb7", + "metadata": {}, + "source": [ + "By stashing your work first, your repository becomes clean, allowing you to pull. To restore your changes, use `git stash apply`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e28bb21", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "git stash apply" + ] + }, + { + "cell_type": "markdown", + "id": "1279dad1", + "metadata": {}, + "source": [ + "The \"Stash\" is a way of temporarily saving your working area, and can help out in a pinch." + ] + }, + { + "cell_type": "markdown", + "id": "b45d0826", + "metadata": {}, + "source": [ + "## Tagging\n", + "\n", + "Tags are easy to read labels for revisions, and can be used anywhere we would name a commit.\n", + "\n", + "Produce real results *only* with tagged revisions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0bb3496", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git tag -a v1.0 -m \"Release 1.0\"\n", + "git push --tags" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df254893", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Pennines.md\n", + "\n", + "Mountains In the Pennines\n", + "========================\n", + "\n", + "* Cross Fell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2133533", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add Pennines.md\n", + "git commit -am \"Add Pennines\"" + ] + }, + { + "cell_type": "markdown", + "id": "d3b8b865", + "metadata": {}, + "source": [ + "You can also use tag names in the place of commmit hashes, such as to list the history between particular commits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3e51a32", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git log v1.0.. --graph --oneline" + ] + }, + { + "cell_type": "markdown", + "id": "2a894c23", + "metadata": {}, + "source": [ + "If .. is used without a following commit name, HEAD is assumed." + ] + }, + { + "cell_type": "markdown", + "id": "0ebc740d", + "metadata": {}, + "source": [ + "## Working with generated files: gitignore" + ] + }, + { + "cell_type": "markdown", + "id": "72a39878", + "metadata": {}, + "source": [ + "We often end up with files that are generated by our program. It is bad practice to keep these in Git; just keep the sources." + ] + }, + { + "cell_type": "markdown", + "id": "e71b82c2", + "metadata": {}, + "source": [ + "Examples include `.o` and `.x` files for compiled languages, `.pyc` files in Python." + ] + }, + { + "cell_type": "markdown", + "id": "6dbf0a45", + "metadata": {}, + "source": [ + "In our example, we might want to make our .md files into a PDF with pandoc:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "476604ac", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Makefile\n", + "\n", + "MDS=$(wildcard *.md)\n", + "PDFS=$(MDS:.md=.pdf)\n", + "\n", + "default: $(PDFS)\n", + "\n", + "%.pdf: %.md\n", + "\tpandoc $< -o $@" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "274b9bc2", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "make" + ] + }, + { + "cell_type": "markdown", + "id": "7951eed9", + "metadata": {}, + "source": [ + "We now have a bunch of output .pdf files corresponding to each Markdown file." + ] + }, + { + "cell_type": "markdown", + "id": "325abd0e", + "metadata": {}, + "source": [ + "But we don't want those to show up in git:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8b01e0c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "markdown", + "id": "dfe1cce7", + "metadata": {}, + "source": [ + "Use .gitignore files to tell Git not to pay attention to files with certain paths:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df8cbbda", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile .gitignore\n", + "*.pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e59e029b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "497d0d97", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git add Makefile\n", + "git add .gitignore\n", + "git commit -am \"Add a makefile and ignore generated files\"\n", + "git push" + ] + }, + { + "cell_type": "markdown", + "id": "b2090204", + "metadata": {}, + "source": [ + "## Git clean" + ] + }, + { + "cell_type": "markdown", + "id": "62823297", + "metadata": {}, + "source": [ + "Sometimes you end up creating various files that you do not want to include in version control. An easy way of deleting them (if that is what you want) is the `git clean` command, which will remove the files that git is not tracking." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc30f7cb", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git clean -fX" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1aa0c88", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "ls" + ] + }, + { + "cell_type": "markdown", + "id": "5269c465", + "metadata": {}, + "source": [ + "* With -f: don't prompt\n", + "* with -d: remove directories\n", + "* with -x: Also remote .gitignored files\n", + "* with -X: Only remove .gitignore files" + ] + }, + { + "cell_type": "markdown", + "id": "78b8503a", + "metadata": {}, + "source": [ + "## Hunks\n", + "\n", + "### Git Hunks\n", + "\n", + "A \"Hunk\" is one git change. This changeset has three hunks:" + ] + }, + { + "cell_type": "markdown", + "id": "67576957", + "metadata": { + "attributes": { + "classes": [ + " diff" + ], + "id": "" + } + }, + "source": [ + "```diff\n", + "+import matplotlib\n", + "+import numpy as np\n", + "\n", + " from matplotlib import pylab\n", + " from matplotlib.backends.backend_pdf import PdfPages\n", + "\n", + "+def increment_or_add(key,hash,weight=1):\n", + "+ if key not in hash:\n", + "+ hash[key]=0\n", + "+ hash[key]+=weight\n", + "+\n", + " data_path=os.path.join(os.path.dirname(\n", + " os.path.abspath(__file__)),\n", + "-regenerate=False\n", + "+regenerate=True\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "23f37e47", + "metadata": {}, + "source": [ + "### Interactive add\n", + "\n", + "`git add` and `git reset` can be used to stage/unstage a whole file,\n", + "but you can use interactive mode to stage by hunk, choosing\n", + "yes or no for each hunk." + ] + }, + { + "cell_type": "markdown", + "id": "9f05b0e8", + "metadata": {}, + "source": [ + "``` bash\n", + "git add -p myfile.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "735d2751", + "metadata": { + "attributes": { + "classes": [ + " diff" + ], + "id": "" + } + }, + "source": [ + "``` diff\n", + "+import matplotlib\n", + "+import numpy as np\n", + "#Stage this hunk [y,n,a,d,/,j,J,g,e,?]?\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "45d5de48", + "metadata": {}, + "source": [ + "## GitHub pages\n", + "\n", + "### Yaml Frontmatter\n", + "\n", + "GitHub will publish repositories containing markdown as web pages, automatically. \n", + "\n", + "You'll need to add this content:\n", + "\n", + "> ```\n", + "> ---\n", + "> ---\n", + "> ```\n", + "\n", + "A pair of lines with three dashes, to the top of each markdown file. This is how GitHub knows which markdown files to make into web pages.\n", + "[Here's why](https://jekyllrb.com/docs/front-matter/) for the curious. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d66903fd", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile index.md\n", + "---\n", + "title: Github Pages Example\n", + "---\n", + "Mountains and Lakes in the UK\n", + "===================\n", + "\n", + "Engerland is not very mountainous.\n", + "But has some tall hills, and maybe a mountain or two depending on your definition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "821c2c9b", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add github pages YAML frontmatter\"" + ] + }, + { + "cell_type": "markdown", + "id": "06a57d87", + "metadata": {}, + "source": [ + "### The gh-pages branch\n", + "\n", + "GitHub creates github pages when you use a special named branch.\n", + "\n", + "This is best used to create documentation for a program you write, but you can use it for anything." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17798667", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd28d1e8", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "git switch -c gh-pages\n", + "git push -uf origin gh-pages" + ] + }, + { + "cell_type": "markdown", + "id": "227ff03b", + "metadata": {}, + "source": [ + "The first time you do this, GitHub takes a few minutes to generate your pages. \n", + "\n", + "The website will appear at `http://username.github.io/repositoryname`, for example:\n", + "\n", + "http://UCL.github.io/github-example/" + ] + }, + { + "cell_type": "markdown", + "id": "fe5a0e1e", + "metadata": {}, + "source": [ + "### UCL layout for GitHub pages\n", + "\n", + "You can use GitHub pages to make HTML layouts, here's an [example of how to do it](http://github.com/UCL/ucl-github-pages-example), \n", + "and [how it looks](http://github-pages.ucl.ac.uk/ucl-github-pages-example). We won't go into the detail of this now,\n", + "but after the class, you might want to try this." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Git miscellany" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/11Miscellany.ipynb.py b/ch00git/11Miscellany.ipynb.py new file mode 100644 index 000000000..985562877 --- /dev/null +++ b/ch00git/11Miscellany.ipynb.py @@ -0,0 +1,257 @@ +# --- +# jupyter: +# jekyll: +# display_name: Git miscellany +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Git Stash + +# %% [markdown] +# Before you can `git pull`, you need to have committed any changes you have made. If you find you want to pull, but you're not ready to commit, you have to temporarily "put aside" your uncommitted changes. +# For this, you can use the `git stash` command, like in the following example: + +# %% jupyter={"outputs_hidden": true} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} +# %%writefile Wales.md +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr +* Fan y Big +* Cadair Idris + +# %% jupyter={"outputs_hidden": false} language="bash" +# git stash +# git pull + +# %% [markdown] +# By stashing your work first, your repository becomes clean, allowing you to pull. To restore your changes, use `git stash apply`. + +# %% jupyter={"outputs_hidden": false} magic_args="--no-raise-error" language="bash" +# git stash apply + +# %% [markdown] +# The "Stash" is a way of temporarily saving your working area, and can help out in a pinch. + +# %% [markdown] +# ## Tagging +# +# Tags are easy to read labels for revisions, and can be used anywhere we would name a commit. +# +# Produce real results *only* with tagged revisions + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git tag -a v1.0 -m "Release 1.0" +# git push --tags + +# %% jupyter={"outputs_hidden": false} +# %%writefile Pennines.md + +Mountains In the Pennines +======================== + +* Cross Fell + +# %% jupyter={"outputs_hidden": false} language="bash" +# git add Pennines.md +# git commit -am "Add Pennines" + +# %% [markdown] +# You can also use tag names in the place of commmit hashes, such as to list the history between particular commits: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git log v1.0.. --graph --oneline + +# %% [markdown] +# If .. is used without a following commit name, HEAD is assumed. + +# %% [markdown] +# ## Working with generated files: gitignore + +# %% [markdown] +# We often end up with files that are generated by our program. It is bad practice to keep these in Git; just keep the sources. + +# %% [markdown] +# Examples include `.o` and `.x` files for compiled languages, `.pyc` files in Python. + +# %% [markdown] +# In our example, we might want to make our .md files into a PDF with pandoc: + +# %% jupyter={"outputs_hidden": false} +# %%writefile Makefile + +MDS=$(wildcard *.md) +PDFS=$(MDS:.md=.pdf) + +default: $(PDFS) + +%.pdf: %.md + pandoc $< -o $@ + +# %% jupyter={"outputs_hidden": false} language="bash" +# make + +# %% [markdown] +# We now have a bunch of output .pdf files corresponding to each Markdown file. + +# %% [markdown] +# But we don't want those to show up in git: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% [markdown] +# Use .gitignore files to tell Git not to pay attention to files with certain paths: + +# %% jupyter={"outputs_hidden": false} +# %%writefile .gitignore +*.pdf + +# %% jupyter={"outputs_hidden": false} language="bash" +# git status + +# %% jupyter={"outputs_hidden": false} language="bash" +# git add Makefile +# git add .gitignore +# git commit -am "Add a makefile and ignore generated files" +# git push + +# %% [markdown] +# ## Git clean + +# %% [markdown] +# Sometimes you end up creating various files that you do not want to include in version control. An easy way of deleting them (if that is what you want) is the `git clean` command, which will remove the files that git is not tracking. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git clean -fX + +# %% jupyter={"outputs_hidden": false} language="bash" +# ls + +# %% [markdown] +# * With -f: don't prompt +# * with -d: remove directories +# * with -x: Also remote .gitignored files +# * with -X: Only remove .gitignore files + +# %% [markdown] +# ## Hunks +# +# ### Git Hunks +# +# A "Hunk" is one git change. This changeset has three hunks: + +# %% [markdown] attributes={"classes": [" diff"], "id": ""} +# ```diff +# +import matplotlib +# +import numpy as np +# +# from matplotlib import pylab +# from matplotlib.backends.backend_pdf import PdfPages +# +# +def increment_or_add(key,hash,weight=1): +# + if key not in hash: +# + hash[key]=0 +# + hash[key]+=weight +# + +# data_path=os.path.join(os.path.dirname( +# os.path.abspath(__file__)), +# -regenerate=False +# +regenerate=True +# ``` + +# %% [markdown] +# ### Interactive add +# +# `git add` and `git reset` can be used to stage/unstage a whole file, +# but you can use interactive mode to stage by hunk, choosing +# yes or no for each hunk. + +# %% [markdown] +# ``` bash +# git add -p myfile.py +# ``` + +# %% [markdown] attributes={"classes": [" diff"], "id": ""} +# ``` diff +# +import matplotlib +# +import numpy as np +# #Stage this hunk [y,n,a,d,/,j,J,g,e,?]? +# ``` + +# %% [markdown] +# ## GitHub pages +# +# ### Yaml Frontmatter +# +# GitHub will publish repositories containing markdown as web pages, automatically. +# +# You'll need to add this content: +# +# > ``` +# > --- +# > --- +# > ``` +# +# A pair of lines with three dashes, to the top of each markdown file. This is how GitHub knows which markdown files to make into web pages. +# [Here's why](https://jekyllrb.com/docs/front-matter/) for the curious. + +# %% jupyter={"outputs_hidden": false} +# %%writefile index.md +--- +title: Github Pages Example +--- +Mountains and Lakes in the UK +=================== + +Engerland is not very mountainous. +But has some tall hills, and maybe a mountain or two depending on your definition. + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add github pages YAML frontmatter" + +# %% [markdown] +# ### The gh-pages branch +# +# GitHub creates github pages when you use a special named branch. +# +# This is best used to create documentation for a program you write, but you can use it for anything. + +# %% jupyter={"outputs_hidden": false} +os.chdir(working_dir) + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# +# git switch -c gh-pages +# git push -uf origin gh-pages + +# %% [markdown] +# The first time you do this, GitHub takes a few minutes to generate your pages. +# +# The website will appear at `http://username.github.io/repositoryname`, for example: +# +# http://UCL.github.io/github-example/ + +# %% [markdown] +# ### UCL layout for GitHub pages +# +# You can use GitHub pages to make HTML layouts, here's an [example of how to do it](http://github.com/UCL/ucl-github-pages-example), +# and [how it looks](http://github-pages.ucl.ac.uk/ucl-github-pages-example). We won't go into the detail of this now, +# but after the class, you might want to try this. diff --git a/ch00git/12Remotes.html b/ch00git/12Remotes.html new file mode 100644 index 000000000..e24332e0f --- /dev/null +++ b/ch00git/12Remotes.html @@ -0,0 +1,751 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remotes + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Working with multiple remotes

Distributed versus centralised

Older version control systems (cvs, svn) were "centralised"; the history was kept only on a server, +and all commits required an internet.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CentralisedDistributed
Server has historyEvery user has full history
Your computer has one snapshotMany local branches
To access history, need internetHistory always available
You commit to remote serverUsers synchronise histories
cvs, subversion(svn)git, mercurial (hg), bazaar (bzr)
+
+
+
+
+
+
+

With modern distributed systems, we can add a second remote. This might be a personal fork on github:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+working_dir = os.path.join(git_dir, 'git_example')
+os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+git switch main
+git remote add arc git@github.com:UCL-ARC-RSEing-with-Python/github-example.git
+git remote -v
+
+
+
+
+
+
+
+
+
+
Switched to branch 'main'
+
+
+
+
+
+
+
Your branch is ahead of 'origin/main' by 1 commit.
+  (use "git push" to publish your local commits)
+arc	git@github.com:UCL-ARC-RSEing-with-Python/github-example.git (fetch)
+arc	git@github.com:UCL-ARC-RSEing-with-Python/github-example.git (push)
+origin	git@github.com:UCL/github-example.git (fetch)
+origin	git@github.com:UCL/github-example.git (push)
+
+
+
+
+
+
+
+
+
+

We can push to a named remote:

+
+
+
+
+
+
In [3]:
+
+
+
%%writefile Pennines.md
+
+Mountains In the Pennines
+========================
+
+* Cross Fell
+* Whernside
+
+
+
+
+
+
+
+
+
+
Overwriting Pennines.md
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+git commit -am "Add Whernside"
+
+
+
+
+
+
+
+
+
+
[main b202e90] Add Whernside
+ 1 file changed, 1 insertion(+)
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+git push -uf arc main
+
+
+
+
+
+
+
+
+
+
To github.com:UCL-ARC-RSEing-with-Python/github-example.git
+ + db58c36...b202e90 main -> main (forced update)
+
+
+
+
+
+
+
branch 'main' set up to track 'arc/main'.
+
+
+
+
+
+
+
+
+
+

Referencing remotes

You can always refer to commits on a remote like this:

+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+git fetch
+git log --oneline --left-right arc/main...origin/main
+
+
+
+
+
+
+
+
+
+
From github.com:UCL-ARC-RSEing-with-Python/github-example
+ * [new branch]      gh-pages   -> arc/gh-pages
+
+
+
+
+
+
+
< b202e90 Add Whernside
+< ad82f9b Add github pages YAML frontmatter
+
+
+
+
+
+
+
+
+
+

To see the differences between remotes, for example.

+

To see what files you have changed that aren't updated on a particular remote, for example:

+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git diff --name-only origin/main
+
+
+
+
+
+
+
+
+
+
Pennines.md
+index.md
+
+
+
+
+
+
+
+
+
+

When you reference remotes like this, you're working with a cached copy of the last time you interacted with the remote. You can do git fetch to update local data with the remotes without actually pulling. You can also get useful information about whether tracking branches are ahead or behind the remote branches they track:

+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git branch -vv
+
+
+
+
+
+
+
+
+
+
  gh-pages ad82f9b [origin/gh-pages] Add github pages YAML frontmatter
+* main     b202e90 [arc/main] Add Whernside
+
+
+
+
+
+
+
+
+
+

Hosting Servers

Hosting a local server

    +
  • Any repository can be a remote for pulls
  • +
  • Can pull/push over shared folders or ssh
  • +
  • Pushing to someone's working copy is dangerous
  • +
  • Use git init --bare to make a copy for pushing
  • +
  • You don't need to create a "server" as such, any 'bare' git repo will do.
  • +
+
+
+
+
+
+
In [9]:
+
+
+
bare_dir = os.path.join(git_dir, 'bare_repo')
+os.chdir(git_dir)
+
+
+
+
+
+
+
+
In [10]:
+
+
+
%%bash
+mkdir -p bare_repo
+cd bare_repo
+git init --bare
+
+
+
+
+
+
+
+
+
+
Initialized empty Git repository in /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch00git/learning_git/bare_repo/
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
os.chdir(working_dir)
+
+
+
+
+
+
+
+
In [12]:
+
+
+
%%bash
+git remote add local_bare ../bare_repo
+git push -u local_bare main
+
+
+
+
+
+
+
+
+
+
To ../bare_repo
+ * [new branch]      main -> main
+
+
+
+
+
+
+
branch 'main' set up to track 'local_bare/main'.
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+git remote -v
+
+
+
+
+
+
+
+
+
+
arc	git@github.com:UCL-ARC-RSEing-with-Python/github-example.git (fetch)
+arc	git@github.com:UCL-ARC-RSEing-with-Python/github-example.git (push)
+local_bare	../bare_repo (fetch)
+local_bare	../bare_repo (push)
+origin	git@github.com:UCL/github-example.git (fetch)
+origin	git@github.com:UCL/github-example.git (push)
+
+
+
+
+
+
+
+
+
+

You can now work with this local repository, just as with any other git server. +If you have a colleague on a shared file system, you can use this approach to collaborate through that file system.

+
+
+
+
+
+
+

Home-made SSH servers

Classroom exercise: Try creating a server for yourself using a machine you can SSH to:

+
+
+
+
+
+
+
ssh <mymachine>
+mkdir mygitserver
+cd mygitserver
+git init --bare
+exit
+git remote add <somename> ssh://user@host/mygitserver
+git push -u <somename> main
+
+
+
+
+
+
+
+

SSH keys and GitHub

Classroom exercise: If you haven't already, you should set things up so that you don't have to keep typing in your +password whenever you interact with GitHub via the command line.

+

You can do this with an "ssh keypair". You may have created a keypair in the +Software Carpentry shell training. Go to the ssh settings +page on GitHub and upload your public key by +copying the content from your computer. (Probably at .ssh/id_rsa.pub)

+

If you have difficulties, the instructions for this are on the GitHub +website.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/12Remotes.ipynb b/ch00git/12Remotes.ipynb new file mode 100644 index 000000000..3781957c7 --- /dev/null +++ b/ch00git/12Remotes.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cee8822b", + "metadata": {}, + "source": [ + "## Working with multiple remotes\n", + "\n", + "### Distributed versus centralised\n", + "\n", + "Older version control systems (cvs, svn) were \"centralised\"; the history was kept only on a server,\n", + "and all commits required an internet.\n", + "\n", + "Centralised | Distributed\n", + "-------------------------------|--------------------------\n", + "Server has history |Every user has full history\n", + "Your computer has one snapshot | Many local branches\n", + "To access history, need internet| History always available\n", + "You commit to remote server | Users synchronise histories\n", + "cvs, subversion(svn) | git, mercurial (hg), bazaar (bzr)" + ] + }, + { + "cell_type": "markdown", + "id": "f95a67d3", + "metadata": {}, + "source": [ + "With modern distributed systems, we can add a second remote. This might be a personal *fork* on github:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee97d898", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "working_dir = os.path.join(git_dir, 'git_example')\n", + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4de0ef6f", + "metadata": { + "attributes": { + "classes": [ + " Bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git switch main\n", + "git remote add arc git@github.com:UCL-ARC-RSEing-with-Python/github-example.git\n", + "git remote -v" + ] + }, + { + "cell_type": "markdown", + "id": "dabb4d38", + "metadata": {}, + "source": [ + "We can push to a named remote:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "542162f8", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile Pennines.md\n", + "\n", + "Mountains In the Pennines\n", + "========================\n", + "\n", + "* Cross Fell\n", + "* Whernside" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3314c8e2", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git commit -am \"Add Whernside\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc30908c", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git push -uf arc main" + ] + }, + { + "cell_type": "markdown", + "id": "585b2dea", + "metadata": {}, + "source": [ + "### Referencing remotes\n", + "\n", + "You can always refer to commits on a remote like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1319069", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git fetch\n", + "git log --oneline --left-right arc/main...origin/main" + ] + }, + { + "cell_type": "markdown", + "id": "328a44b3", + "metadata": {}, + "source": [ + "To see the differences between remotes, for example.\n", + "\n", + "To see what files you have changed that aren't updated on a particular remote, for example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4afc5928", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git diff --name-only origin/main" + ] + }, + { + "cell_type": "markdown", + "id": "176190fc", + "metadata": {}, + "source": [ + "When you reference remotes like this, you're working with a cached copy of the last time you interacted with the remote. You can do `git fetch` to update local data with the remotes without actually pulling. You can also get useful information about whether tracking branches are ahead or behind the remote branches they track:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d651101", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git branch -vv" + ] + }, + { + "cell_type": "markdown", + "id": "d61ebfce", + "metadata": {}, + "source": [ + "## Hosting Servers\n", + "\n", + "### Hosting a local server\n", + "\n", + "* Any repository can be a remote for pulls\n", + "* Can pull/push over shared folders or ssh\n", + "* Pushing to someone's working copy is dangerous\n", + "* Use `git init --bare` to make a copy for pushing\n", + "* You don't need to create a \"server\" as such, any 'bare' git repo will do." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4effc34d", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "bare_dir = os.path.join(git_dir, 'bare_repo')\n", + "os.chdir(git_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f12daf56", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p bare_repo\n", + "cd bare_repo\n", + "git init --bare" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e95bead3", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "os.chdir(working_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e70cf935", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git remote add local_bare ../bare_repo\n", + "git push -u local_bare main" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5bd37be", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git remote -v" + ] + }, + { + "cell_type": "markdown", + "id": "e55856a5", + "metadata": {}, + "source": [ + "You can now work with this local repository, just as with any other git server.\n", + "If you have a colleague on a shared file system, you can use this approach to collaborate through that file system." + ] + }, + { + "cell_type": "markdown", + "id": "c375191e", + "metadata": {}, + "source": [ + "### Home-made SSH servers\n", + "\n", + "Classroom exercise: Try creating a server for yourself using a machine you can SSH to:" + ] + }, + { + "cell_type": "markdown", + "id": "0efc28a4", + "metadata": {}, + "source": [ + "``` bash\n", + "ssh \n", + "mkdir mygitserver\n", + "cd mygitserver\n", + "git init --bare\n", + "exit\n", + "git remote add ssh://user@host/mygitserver\n", + "git push -u main\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "6a492ba2", + "metadata": {}, + "source": [ + "## SSH keys and GitHub\n", + "\n", + "Classroom exercise: If you haven't already, you should set things up so that you don't have to keep typing in your\n", + "password whenever you interact with GitHub via the command line.\n", + "\n", + "You can do this with an \"ssh keypair\". You may have created a keypair in the\n", + "Software Carpentry shell training. Go to the [ssh settings\n", + "page](https://github.com/settings/ssh) on GitHub and upload your public key by\n", + "copying the content from your computer. (Probably at .ssh/id_rsa.pub)\n", + "\n", + "If you have difficulties, the instructions for this are [on the GitHub\n", + "website](https://docs.github.com/en/authentication/connecting-to-github-with-ssh)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Remotes" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/12Remotes.ipynb.py b/ch00git/12Remotes.ipynb.py new file mode 100644 index 000000000..58853db38 --- /dev/null +++ b/ch00git/12Remotes.ipynb.py @@ -0,0 +1,148 @@ +# --- +# jupyter: +# jekyll: +# display_name: Remotes +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Working with multiple remotes +# +# ### Distributed versus centralised +# +# Older version control systems (cvs, svn) were "centralised"; the history was kept only on a server, +# and all commits required an internet. +# +# Centralised | Distributed +# -------------------------------|-------------------------- +# Server has history |Every user has full history +# Your computer has one snapshot | Many local branches +# To access history, need internet| History always available +# You commit to remote server | Users synchronise histories +# cvs, subversion(svn) | git, mercurial (hg), bazaar (bzr) + +# %% [markdown] +# With modern distributed systems, we can add a second remote. This might be a personal *fork* on github: + +# %% jupyter={"outputs_hidden": false} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +working_dir = os.path.join(git_dir, 'git_example') +os.chdir(working_dir) + +# %% attributes={"classes": [" Bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git switch main +# git remote add arc git@github.com:UCL-ARC-RSEing-with-Python/github-example.git +# git remote -v + +# %% [markdown] +# We can push to a named remote: + +# %% jupyter={"outputs_hidden": false} +# %%writefile Pennines.md + +Mountains In the Pennines +======================== + +* Cross Fell +* Whernside + +# %% jupyter={"outputs_hidden": false} language="bash" +# git commit -am "Add Whernside" + +# %% jupyter={"outputs_hidden": false} language="bash" +# git push -uf arc main + +# %% [markdown] +# ### Referencing remotes +# +# You can always refer to commits on a remote like this: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git fetch +# git log --oneline --left-right arc/main...origin/main + +# %% [markdown] +# To see the differences between remotes, for example. +# +# To see what files you have changed that aren't updated on a particular remote, for example: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git diff --name-only origin/main + +# %% [markdown] +# When you reference remotes like this, you're working with a cached copy of the last time you interacted with the remote. You can do `git fetch` to update local data with the remotes without actually pulling. You can also get useful information about whether tracking branches are ahead or behind the remote branches they track: + +# %% jupyter={"outputs_hidden": false} language="bash" +# git branch -vv + +# %% [markdown] +# ## Hosting Servers +# +# ### Hosting a local server +# +# * Any repository can be a remote for pulls +# * Can pull/push over shared folders or ssh +# * Pushing to someone's working copy is dangerous +# * Use `git init --bare` to make a copy for pushing +# * You don't need to create a "server" as such, any 'bare' git repo will do. + +# %% jupyter={"outputs_hidden": false} +bare_dir = os.path.join(git_dir, 'bare_repo') +os.chdir(git_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# mkdir -p bare_repo +# cd bare_repo +# git init --bare + +# %% jupyter={"outputs_hidden": false} +os.chdir(working_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# git remote add local_bare ../bare_repo +# git push -u local_bare main + +# %% jupyter={"outputs_hidden": false} language="bash" +# git remote -v + +# %% [markdown] +# You can now work with this local repository, just as with any other git server. +# If you have a colleague on a shared file system, you can use this approach to collaborate through that file system. + +# %% [markdown] +# ### Home-made SSH servers +# +# Classroom exercise: Try creating a server for yourself using a machine you can SSH to: + +# %% [markdown] +# ``` bash +# ssh +# mkdir mygitserver +# cd mygitserver +# git init --bare +# exit +# git remote add ssh://user@host/mygitserver +# git push -u main +# ``` + +# %% [markdown] +# ## SSH keys and GitHub +# +# Classroom exercise: If you haven't already, you should set things up so that you don't have to keep typing in your +# password whenever you interact with GitHub via the command line. +# +# You can do this with an "ssh keypair". You may have created a keypair in the +# Software Carpentry shell training. Go to the [ssh settings +# page](https://github.com/settings/ssh) on GitHub and upload your public key by +# copying the content from your computer. (Probably at .ssh/id_rsa.pub) +# +# If you have difficulties, the instructions for this are [on the GitHub +# website](https://docs.github.com/en/authentication/connecting-to-github-with-ssh). diff --git a/ch00git/13Rebase.html b/ch00git/13Rebase.html new file mode 100644 index 000000000..e401ffc28 --- /dev/null +++ b/ch00git/13Rebase.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rebase + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Rebasing

Rebase vs merge

A git merge is only one of two ways to get someone else's work into yours. +The other is called a rebase.

+

In a merge, a revision is added, which brings the branches together. Both histories are retained. +In a rebase, git tries to work out

+
+

What would you need to have done, to make your changes, if your colleague had already made theirs?

+
+

Git will invent some new revisions, and the result will be a repository with an apparently linear history. This can be useful if you want a cleaner, non-branching history, but it has the risk of creating inconsistencies, since you are, in a way, "rewriting" history.

+

An example rebase

We've built a repository to help visualise the difference between a merge and a rebase.

+
+
+
+
+
+
+

The initial state of both collaborators is a text file, wocky.md:

+
It was clear and cold,
+and the slimy monsters
+
+
+
+
+
+
+
+

On the main branch, a second commit ('Dancing') has been added:

+
+
+
+
+
+
+
It was clear and cold,
+and the slimy monsters
+danced and spun in the waves
+
+
+
+
+
+
+
+

On the "Carollian" branch, a commit has been added translating the initial state into Lewis Caroll's language:

+
'Twas brillig,
+and the slithy toves
+
+
+
+
+
+
+
+

So the logs look like this:

+
+
+
+
+
+
+
git log --oneline --graph main
+
+
* 2a74d89 Dancing
+* 6a4834d Initial state
+
+
git log --oneline --graph carollian
+
+
* 2232bf3 Translate into Caroll's language
+* 6a4834d Initial state
+
+
+
+
+
+
+
+

If we now merge carollian into main, the final state will include both changes:

+
+
+
+
+
+
+
'Twas brillig,
+and the slithy toves
+danced and spun in the waves
+
+
+
+
+
+
+
+

But the graph shows a divergence and then a convergence:

+
+
+
+
+
+
+
git log --oneline --graph
+
+
*   b41f869 Merge branch 'carollian' into main_merge_carollian
+|\
+| * 2232bf3 Translate into Caroll's language
+* | 2a74d89 Dancing
+|/
+* 6a4834d Initial state
+
+
+
+
+
+
+
+

But if we rebase, the final content of the file is still the same, but the graph is different:

+
+
+
+
+
+
+
git log --oneline --graph main_rebase_carollian
+
+
* df618e0 Dancing
+* 2232bf3 Translate into Caroll's language
+* 6a4834d Initial state
+
+
+
+
+
+
+
+

We have essentially created a new history, in which our changes come after the ones in the carollian branch. Note that, in this case, the hash for our "Dancing" commit has changed (from 2a74d89 to df618e0)!

+

To trigger the rebase, we did:

+
git switch main
+git rebase carollian
+
+

If this had been a remote, we would merge it with:

+
git pull --rebase
+
+
+
+
+
+
+
+

Fast Forwards

If we want to continue with the translation, and now want to merge the rebased branch into the carollian branch, +we get:

+
git switch carollian
+git merge main
+
+
Updating 2232bf3..df618e0
+Fast-forward
+ wocky.md | 1 +
+ 1 file changed, 1 insertion(+)
+
+

The main branch was already rebased on the carollian branch, so this merge was just a question of updating metadata (moving the label for the carollian branch so that it points to the same commit main does): a "fast forward".

+

Rebasing pros and cons

Some people like the clean, apparently linear history that rebase provides.

+

But rebase rewrites history.

+

If you've already pushed, or anyone else has got your changes, things will get screwed up.

+

If you know your changes are still secret, it might be better to rebase to keep the history clean. +If in doubt, just merge.

+

Squashing

A second way to use the git rebase command is to rebase your work on top of one of your own earlier commits, +in interactive mode (-i). A common use of this is to "squash" several commits that should really be one, i.e. combine them into a single commit that contains all their changes:

+
+
+
+
+
+
+
git log
+
+
ea15 Some good work
+ll54 Fix another typo
+de73 Fix a typo
+ab11 A great piece of work
+cd27 Initial commit
+
+
+
+
+
+
+
+

Using rebase to squash

+
+
+
+
+
+
+

If we type

+
git rebase -i ab11 #OR HEAD^^
+
+

an edit window pops up with:

+
+
+
+
+
+
+
pick cd27 Initial commit
+pick ab11 A great piece of work
+pick de73 Fix a typo
+pick ll54 Fix another typo
+pick ea15 Some good work
+
+# Rebase 60709da..30e0ccb onto 60709da
+#
+# Commands:
+#  p, pick = use commit
+#  e, edit = use commit, but stop for amending
+#  s, squash = use commit, but meld into previous commit
+
+
+
+
+
+
+
+

We can rewrite select commits to be merged, so that the history is neater before we push. +This is a great idea if you have lots of trivial typo commits.

+
+
+
+
+
+
+
pick cd27 Initial commit
+pick ab11 A great piece of work
+squash de73 Fix a typo
+squash ll54 Fix another typo
+pick ea15 Some good work
+
+

save the interactive rebase config file, and rebase will build a new history:

+
git log
+
+
de82 Some good work
+fc52 A great piece of work
+cd27 Initial commit
+
+
+
+
+
+
+
+

Note the commit hash codes for 'Some good work' and 'A great piece of work' have changed, +as the change they represent has changed.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/13Rebase.ipynb b/ch00git/13Rebase.ipynb new file mode 100644 index 000000000..ff9bad537 --- /dev/null +++ b/ch00git/13Rebase.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0979b934", + "metadata": {}, + "source": [ + "## Rebasing\n", + "\n", + "### Rebase vs merge\n", + "\n", + "A git *merge* is only one of two ways to get someone else's work into yours.\n", + "The other is called a rebase.\n", + "\n", + "In a merge, a revision is added, which brings the branches together. Both histories are retained.\n", + "In a rebase, git tries to work out\n", + "\n", + "> What would you need to have done, to make your changes, if your colleague had already made theirs?\n", + "\n", + "Git will invent some new revisions, and the result will be a repository with an apparently linear history. This can be useful if you want a cleaner, non-branching history, but it has the risk of creating inconsistencies, since you are, in a way, \"rewriting\" history.\n", + "\n", + "### An example rebase\n", + "\n", + "We've built a [repository to help visualise the difference between a merge and a rebase](https://github.com/UCL-ARC-RSEing-with-Python/wocky_rebase/blob/main/wocky.md)." + ] + }, + { + "cell_type": "markdown", + "id": "d1c60d8b", + "metadata": {}, + "source": [ + "The initial state of both collaborators is a text file, wocky.md:\n", + "\n", + "```\n", + "It was clear and cold,\n", + "and the slimy monsters\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "68673414", + "metadata": {}, + "source": [ + "On the `main` branch, a second commit ('Dancing') has been added:" + ] + }, + { + "cell_type": "markdown", + "id": "37b0100f", + "metadata": {}, + "source": [ + "```\n", + "It was clear and cold,\n", + "and the slimy monsters\n", + "danced and spun in the waves\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "69275c40", + "metadata": {}, + "source": [ + "On the \"Carollian\" branch, a commit has been added translating the initial state into Lewis Caroll's language:\n", + "\n", + "```\n", + "'Twas brillig,\n", + "and the slithy toves\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "42e9022c", + "metadata": {}, + "source": [ + "So the logs look like this:" + ] + }, + { + "cell_type": "markdown", + "id": "a9addbb4", + "metadata": {}, + "source": [ + "```bash\n", + "git log --oneline --graph main\n", + "```\n", + "\n", + "```\n", + "* 2a74d89 Dancing\n", + "* 6a4834d Initial state\n", + "```\n", + "\n", + "```bash\n", + "git log --oneline --graph carollian\n", + "```\n", + "\n", + "```\n", + "* 2232bf3 Translate into Caroll's language\n", + "* 6a4834d Initial state\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "b76e0fa6", + "metadata": {}, + "source": [ + "If we now **merge** carollian into main, the final state will include both changes:" + ] + }, + { + "cell_type": "markdown", + "id": "461355bb", + "metadata": {}, + "source": [ + "```\n", + "'Twas brillig,\n", + "and the slithy toves\n", + "danced and spun in the waves\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "b76603fe", + "metadata": {}, + "source": [ + "But the graph shows a divergence and then a convergence:" + ] + }, + { + "cell_type": "markdown", + "id": "576433ca", + "metadata": {}, + "source": [ + "```\n", + "git log --oneline --graph\n", + "```\n", + "\n", + "```\n", + "* b41f869 Merge branch 'carollian' into main_merge_carollian\n", + "|\\\n", + "| * 2232bf3 Translate into Caroll's language\n", + "* | 2a74d89 Dancing\n", + "|/\n", + "* 6a4834d Initial state\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "2a023eb4", + "metadata": {}, + "source": [ + "But if we **rebase**, the final content of the file is still the same, but the graph is different:" + ] + }, + { + "cell_type": "markdown", + "id": "9fb7f105", + "metadata": {}, + "source": [ + "``` bash\n", + "git log --oneline --graph main_rebase_carollian\n", + "```\n", + "\n", + "```\n", + "* df618e0 Dancing\n", + "* 2232bf3 Translate into Caroll's language\n", + "* 6a4834d Initial state\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "30fe9821", + "metadata": {}, + "source": [ + "We have essentially created a new history, in which our changes come after the ones in the carollian branch. Note that, in this case, the hash for our \"Dancing\" commit has changed (from `2a74d89` to `df618e0`)!\n", + "\n", + "To trigger the rebase, we did:\n", + " \n", + "``` bash\n", + "git switch main\n", + "git rebase carollian\n", + "```\n", + "\n", + "If this had been a remote, we would merge it with:\n", + " \n", + "``` bash\n", + "git pull --rebase\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "13543891", + "metadata": {}, + "source": [ + "### Fast Forwards\n", + "\n", + "If we want to continue with the translation, and now want to merge the rebased branch into the carollian branch, \n", + "we get:\n", + "\n", + "```bash\n", + "git switch carollian\n", + "git merge main\n", + "```\n", + "\n", + "\n", + "``` bash\n", + "Updating 2232bf3..df618e0\n", + "Fast-forward\n", + " wocky.md | 1 +\n", + " 1 file changed, 1 insertion(+)\n", + "```\n", + "\n", + "The main branch was already **rebased on** the carollian branch, so this merge was just a question of updating *metadata* (moving the label for the carollian branch so that it points to the same commit main does): a \"fast forward\".\n", + "\n", + "### Rebasing pros and cons\n", + "\n", + "Some people like the clean, apparently linear history that rebase provides.\n", + "\n", + "But *rebase rewrites history*.\n", + "\n", + "If you've already pushed, or anyone else has got your changes, things will get screwed up.\n", + "\n", + "If you know your changes are still secret, it might be better to rebase to keep the history clean.\n", + "If in doubt, just merge.\n", + "\n", + "## Squashing\n", + "\n", + "A second way to use the `git rebase` command is to rebase your work on top of one of *your own* earlier commits,\n", + "in interactive mode (`-i`). A common use of this is to \"squash\" several commits that should really be one, i.e. combine them into a single commit that contains all their changes:" + ] + }, + { + "cell_type": "markdown", + "id": "a1142f7b", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "source": [ + "``` bash\n", + "git log\n", + "```\n", + "\n", + "```\n", + "ea15 Some good work\n", + "ll54 Fix another typo\n", + "de73 Fix a typo\n", + "ab11 A great piece of work\n", + "cd27 Initial commit\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5fc8a4e5", + "metadata": {}, + "source": [ + "### Using rebase to squash" + ] + }, + { + "cell_type": "markdown", + "id": "62d414e7", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "source": [ + "If we type \n", + "\n", + "``` bash\n", + "git rebase -i ab11 #OR HEAD^^\n", + "```\n", + "\n", + "an edit window pops up with:" + ] + }, + { + "cell_type": "markdown", + "id": "a77c7fe4", + "metadata": {}, + "source": [ + "```\n", + "pick cd27 Initial commit\n", + "pick ab11 A great piece of work\n", + "pick de73 Fix a typo\n", + "pick ll54 Fix another typo\n", + "pick ea15 Some good work\n", + "\n", + "# Rebase 60709da..30e0ccb onto 60709da\n", + "#\n", + "# Commands:\n", + "# p, pick = use commit\n", + "# e, edit = use commit, but stop for amending\n", + "# s, squash = use commit, but meld into previous commit\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "846d86c3", + "metadata": {}, + "source": [ + "We can rewrite select commits to be merged, so that the history is neater before we push.\n", + "This is a great idea if you have lots of trivial typo commits." + ] + }, + { + "cell_type": "markdown", + "id": "7dad79e7", + "metadata": {}, + "source": [ + "```\n", + "pick cd27 Initial commit\n", + "pick ab11 A great piece of work\n", + "squash de73 Fix a typo\n", + "squash ll54 Fix another typo\n", + "pick ea15 Some good work\n", + "```\n", + "\n", + "save the interactive rebase config file, and rebase will build a new history:\n", + "\n", + "``` bash\n", + "git log\n", + "```\n", + "\n", + "```\n", + "de82 Some good work\n", + "fc52 A great piece of work\n", + "cd27 Initial commit\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "71a17fa0", + "metadata": {}, + "source": [ + "Note the commit hash codes for 'Some good work' and 'A great piece of work' have changed, \n", + "as the change they represent has changed." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Rebase" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/13Rebase.ipynb.py b/ch00git/13Rebase.ipynb.py new file mode 100644 index 000000000..7b38c82a2 --- /dev/null +++ b/ch00git/13Rebase.ipynb.py @@ -0,0 +1,244 @@ +# --- +# jupyter: +# jekyll: +# display_name: Rebase +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Rebasing +# +# ### Rebase vs merge +# +# A git *merge* is only one of two ways to get someone else's work into yours. +# The other is called a rebase. +# +# In a merge, a revision is added, which brings the branches together. Both histories are retained. +# In a rebase, git tries to work out +# +# > What would you need to have done, to make your changes, if your colleague had already made theirs? +# +# Git will invent some new revisions, and the result will be a repository with an apparently linear history. This can be useful if you want a cleaner, non-branching history, but it has the risk of creating inconsistencies, since you are, in a way, "rewriting" history. +# +# ### An example rebase +# +# We've built a [repository to help visualise the difference between a merge and a rebase](https://github.com/UCL-ARC-RSEing-with-Python/wocky_rebase/blob/main/wocky.md). + +# %% [markdown] +# The initial state of both collaborators is a text file, wocky.md: +# +# ``` +# It was clear and cold, +# and the slimy monsters +# ``` + +# %% [markdown] +# On the `main` branch, a second commit ('Dancing') has been added: + +# %% [markdown] +# ``` +# It was clear and cold, +# and the slimy monsters +# danced and spun in the waves +# ``` + +# %% [markdown] +# On the "Carollian" branch, a commit has been added translating the initial state into Lewis Caroll's language: +# +# ``` +# 'Twas brillig, +# and the slithy toves +# ``` + +# %% [markdown] +# So the logs look like this: + +# %% [markdown] +# ```bash +# git log --oneline --graph main +# ``` +# +# ``` +# * 2a74d89 Dancing +# * 6a4834d Initial state +# ``` +# +# ```bash +# git log --oneline --graph carollian +# ``` +# +# ``` +# * 2232bf3 Translate into Caroll's language +# * 6a4834d Initial state +# ``` + +# %% [markdown] +# If we now **merge** carollian into main, the final state will include both changes: + +# %% [markdown] +# ``` +# 'Twas brillig, +# and the slithy toves +# danced and spun in the waves +# ``` +# + +# %% [markdown] +# But the graph shows a divergence and then a convergence: + +# %% [markdown] +# ``` +# git log --oneline --graph +# ``` +# +# ``` +# * b41f869 Merge branch 'carollian' into main_merge_carollian +# |\ +# | * 2232bf3 Translate into Caroll's language +# * | 2a74d89 Dancing +# |/ +# * 6a4834d Initial state +# ``` + +# %% [markdown] +# But if we **rebase**, the final content of the file is still the same, but the graph is different: + +# %% [markdown] +# ``` bash +# git log --oneline --graph main_rebase_carollian +# ``` +# +# ``` +# * df618e0 Dancing +# * 2232bf3 Translate into Caroll's language +# * 6a4834d Initial state +# ``` + +# %% [markdown] +# We have essentially created a new history, in which our changes come after the ones in the carollian branch. Note that, in this case, the hash for our "Dancing" commit has changed (from `2a74d89` to `df618e0`)! +# +# To trigger the rebase, we did: +# +# ``` bash +# git switch main +# git rebase carollian +# ``` +# +# If this had been a remote, we would merge it with: +# +# ``` bash +# git pull --rebase +# ``` + +# %% [markdown] +# ### Fast Forwards +# +# If we want to continue with the translation, and now want to merge the rebased branch into the carollian branch, +# we get: +# +# ```bash +# git switch carollian +# git merge main +# ``` +# +# +# ``` bash +# Updating 2232bf3..df618e0 +# Fast-forward +# wocky.md | 1 + +# 1 file changed, 1 insertion(+) +# ``` +# +# The main branch was already **rebased on** the carollian branch, so this merge was just a question of updating *metadata* (moving the label for the carollian branch so that it points to the same commit main does): a "fast forward". +# +# ### Rebasing pros and cons +# +# Some people like the clean, apparently linear history that rebase provides. +# +# But *rebase rewrites history*. +# +# If you've already pushed, or anyone else has got your changes, things will get screwed up. +# +# If you know your changes are still secret, it might be better to rebase to keep the history clean. +# If in doubt, just merge. +# +# ## Squashing +# +# A second way to use the `git rebase` command is to rebase your work on top of one of *your own* earlier commits, +# in interactive mode (`-i`). A common use of this is to "squash" several commits that should really be one, i.e. combine them into a single commit that contains all their changes: + +# %% [markdown] attributes={"classes": [" bash"], "id": ""} +# ``` bash +# git log +# ``` +# +# ``` +# ea15 Some good work +# ll54 Fix another typo +# de73 Fix a typo +# ab11 A great piece of work +# cd27 Initial commit +# ``` + +# %% [markdown] +# ### Using rebase to squash + +# %% [markdown] attributes={"classes": [" bash"], "id": ""} +# If we type +# +# ``` bash +# git rebase -i ab11 #OR HEAD^^ +# ``` +# +# an edit window pops up with: + +# %% [markdown] +# ``` +# pick cd27 Initial commit +# pick ab11 A great piece of work +# pick de73 Fix a typo +# pick ll54 Fix another typo +# pick ea15 Some good work +# +# # Rebase 60709da..30e0ccb onto 60709da +# # +# # Commands: +# # p, pick = use commit +# # e, edit = use commit, but stop for amending +# # s, squash = use commit, but meld into previous commit +# ``` + +# %% [markdown] +# We can rewrite select commits to be merged, so that the history is neater before we push. +# This is a great idea if you have lots of trivial typo commits. + +# %% [markdown] +# ``` +# pick cd27 Initial commit +# pick ab11 A great piece of work +# squash de73 Fix a typo +# squash ll54 Fix another typo +# pick ea15 Some good work +# ``` +# +# save the interactive rebase config file, and rebase will build a new history: +# +# ``` bash +# git log +# ``` +# +# ``` +# de82 Some good work +# fc52 A great piece of work +# cd27 Initial commit +# ``` + +# %% [markdown] +# Note the commit hash codes for 'Some good work' and 'A great piece of work' have changed, +# as the change they represent has changed. diff --git a/ch00git/14Bisect.html b/ch00git/14Bisect.html new file mode 100644 index 000000000..d3cffcbcf --- /dev/null +++ b/ch00git/14Bisect.html @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bisect + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Debugging With Git Bisect

You can use

+
git bisect
+
+

to find out which commit caused a bug.

+
+
+
+
+
+
+

An example repository

In a nice open source example, I found an arbitrary exemplar on github

+
+
+
+
+
+
In [1]:
+
+
+
import os
+top_dir = os.getcwd()
+git_dir = os.path.join(top_dir, 'learning_git')
+os.chdir(git_dir)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+rm -rf bisectdemo
+git clone https://github.com/UCL-ARC-RSEing-with-Python/bisectdemo.git
+
+
+
+
+
+
+
+
+
+
Cloning into 'bisectdemo'...
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
bisect_dir=os.path.join(git_dir,'bisectdemo')
+os.chdir(bisect_dir)
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+python squares.py 2 # 4
+
+
+
+
+
+
+
+
+
+
4
+
+
+
+
+
+
+
+
+
+

This has been set up to break itself at a random commit, and leave you to use +bisect to work out where it has broken:

+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+./breakme.sh > break_output
+
+
+
+
+
+
+
+
+
+
Switched to a new branch 'buggy'
+
+
+
+
+
+
+
+
+
+

Which will make a bunch of commits, of which one is broken, and leave you in the broken final state

+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+python squares.py 2 # Error message
+
+
+
+
+
+
+
+
+
+
Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[6], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'python squares.py 2 #\xa0Error message\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'python squares.py 2 #\xc2\xa0Error message\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

Bisecting manually

+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+git bisect start
+git bisect bad # We know the current state is broken
+git switch main
+git bisect good # We know the main branch state is OK
+
+
+
+
+
+
+
+
+
+
status: waiting for both good and bad commits
+status: waiting for good commit(s), bad commit known
+
+
+
+
+
+
+
warning: you are switching branch while bisecting
+Switched to branch 'main'
+
+
+
+
+
+
+
Your branch is up to date with 'origin/main'.
+Bisecting: 500 revisions left to test after this (roughly 9 steps)
+[d3d5c7b6b99b4e290499e987492372c0cec51d80] Comment 499
+
+
+
+
+
+
+
+
+
+

Bisect needs one known good and one known bad commit to get started

+
+
+
+
+
+
+

Solving Manually

+
+
+
+
+
+
+
python squares.py 2 # 4
+git bisect good
+python squares.py 2 # 4
+git bisect good
+python squares.py 2 # 4
+git bisect good
+python squares.py 2 # Crash
+git bisect bad
+python squares.py 2 # Crash
+git bisect bad
+python squares.py 2 # Crash
+git bisect bad
+python squares.py 2 #Crash
+git bisect bad
+python squares.py 2 # 4
+git bisect good
+python squares.py 2 # 4
+git bisect good
+python squares.py 2 # 4
+git bisect good
+
+
+
+
+
+
+
+

And eventually:

+
+
+
+
+
+
+
git bisect good
+    Bisecting: 0 revisions left to test after this (roughly 0 steps)
+
+python squares.py 2
+    4
+
+git bisect good
+2777975a2334c2396ccb9faf98ab149824ec465b is the first bad commit
+commit 2777975a2334c2396ccb9faf98ab149824ec465b
+Author: Shawn Siefkas <shawn.siefkas@meredith.com>
+Date:   Thu Nov 14 09:23:55 2013 -0600
+
+    Breaking argument type
+
+
+
+
+
+
+
+

Stop the bisect process with:

+
git bisect reset
+
+
+
+
+
+
+
+

Solving automatically

If we have an appropriate unit test, we can do all this automatically:

+

(NOTE: You don't need to redirect the stderr and stdout (with &>) of git bisect run to a file when running these commands outside a jupyter notebook (i.e., on a shell). This is done here so the errors appears with the right commits)

+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+git bisect start
+git bisect bad HEAD # We know the current state is broken
+git bisect good main # We know main is good
+git bisect run python squares.py 2 &> gitbisect.out
+cat gitbisect.out
+
+
+
+
+
+
+
+
+
+
Previous HEAD position was d3d5c7b Comment 499
+Switched to branch 'buggy'
+
+
+
+
+
+
+
status: waiting for both good and bad commits
+status: waiting for good commit(s), bad commit known
+Bisecting: 500 revisions left to test after this (roughly 9 steps)
+[d3d5c7b6b99b4e290499e987492372c0cec51d80] Comment 499
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 249 revisions left to test after this (roughly 8 steps)
+[0ad67c82d7913110ab7b4a041bdfb3e57230be1a] Comment 250
+running 'python' 'squares.py' '2'
+4
+Bisecting: 124 revisions left to test after this (roughly 7 steps)
+[d3fdbc4533c5e312222427be427d17ee206344c2] Comment 374
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 62 revisions left to test after this (roughly 6 steps)
+[2c88f27bdac158fa6fe45e747db29d084a9d06a8] Comment 312
+running 'python' 'squares.py' '2'
+4
+Bisecting: 31 revisions left to test after this (roughly 5 steps)
+[0c034989a425904cda9481d07a02b3e363563338] Comment 342
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 15 revisions left to test after this (roughly 4 steps)
+[4189ae93787e6b79eb56047dfe6149715e2843fe] Comment 326
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 7 revisions left to test after this (roughly 3 steps)
+[26346951d7fc0fd2d008fe23bcf1a2b702aae112] Comment 318
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 3 revisions left to test after this (roughly 2 steps)
+[6a9ff4af9516543c6f62c04327fb83f39ed26925] Comment 314
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 0 revisions left to test after this (roughly 1 step)
+[17604547f1da4c33937335221d6753e2d18388b8] Comment 313
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+Bisecting: 0 revisions left to test after this (roughly 0 steps)
+[fd03a1bcc29411c50223547ff7f86570a0e91953] Breaking argument type
+running 'python' 'squares.py' '2'
+Traceback (most recent call last):
+  File "squares.py", line 9, in <module>
+    print(integer**2)
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+fd03a1bcc29411c50223547ff7f86570a0e91953 is the first bad commit
+commit fd03a1bcc29411c50223547ff7f86570a0e91953
+Author: Shawn Siefkas <shawn.siefkas@meredith.com>
+Date:   Thu Nov 14 09:23:55 2013 -0600
+
+    Breaking argument type
+
+ squares.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+bisect found first bad commit
+
+
+
+
+
+
+
+
+
+

Boom!

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/14Bisect.ipynb b/ch00git/14Bisect.ipynb new file mode 100644 index 000000000..8aa3544a2 --- /dev/null +++ b/ch00git/14Bisect.ipynb @@ -0,0 +1,342 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7396638b", + "metadata": {}, + "source": [ + "## Debugging With Git Bisect\n", + "\n", + "You can use\n", + "\n", + "``` bash\n", + "git bisect\n", + "```\n", + "\n", + "to find out which commit caused a bug." + ] + }, + { + "cell_type": "markdown", + "id": "a844cc05", + "metadata": {}, + "source": [ + "### An example repository\n", + "\n", + "In a nice open source example, I found an arbitrary exemplar on github" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e4bbb59", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "import os\n", + "top_dir = os.getcwd()\n", + "git_dir = os.path.join(top_dir, 'learning_git')\n", + "os.chdir(git_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37564630", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "rm -rf bisectdemo\n", + "git clone https://github.com/UCL-ARC-RSEing-with-Python/bisectdemo.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02a4a269", + "metadata": { + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "bisect_dir=os.path.join(git_dir,'bisectdemo')\n", + "os.chdir(bisect_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc4940d7", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "python squares.py 2 # 4" + ] + }, + { + "cell_type": "markdown", + "id": "b6530419", + "metadata": {}, + "source": [ + "This has been set up to break itself at a random commit, and leave you to use\n", + "bisect to work out where it has broken:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d1099b1", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "./breakme.sh > break_output" + ] + }, + { + "cell_type": "markdown", + "id": "361e8d80", + "metadata": {}, + "source": [ + "Which will make a bunch of commits, of which one is broken, and leave you in the broken final state" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea2f1103", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "python squares.py 2 # Error message" + ] + }, + { + "cell_type": "markdown", + "id": "baff4309", + "metadata": {}, + "source": [ + "### Bisecting manually" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa01c9a5", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git bisect start\n", + "git bisect bad # We know the current state is broken\n", + "git switch main\n", + "git bisect good # We know the main branch state is OK" + ] + }, + { + "cell_type": "markdown", + "id": "c71a8601", + "metadata": {}, + "source": [ + "Bisect needs one known good and one known bad commit to get started" + ] + }, + { + "cell_type": "markdown", + "id": "972b8111", + "metadata": {}, + "source": [ + "### Solving Manually" + ] + }, + { + "cell_type": "markdown", + "id": "304c6b60", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "source": [ + "``` bash\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "python squares.py 2 # Crash\n", + "git bisect bad\n", + "python squares.py 2 # Crash\n", + "git bisect bad\n", + "python squares.py 2 # Crash\n", + "git bisect bad\n", + "python squares.py 2 #Crash\n", + "git bisect bad\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "python squares.py 2 # 4\n", + "git bisect good\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "0c6186be", + "metadata": {}, + "source": [ + "And eventually:" + ] + }, + { + "cell_type": "markdown", + "id": "aad53004", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "source": [ + "``` bash\n", + "git bisect good\n", + " Bisecting: 0 revisions left to test after this (roughly 0 steps)\n", + "\n", + "python squares.py 2\n", + " 4\n", + "\n", + "git bisect good\n", + "2777975a2334c2396ccb9faf98ab149824ec465b is the first bad commit\n", + "commit 2777975a2334c2396ccb9faf98ab149824ec465b\n", + "Author: Shawn Siefkas \n", + "Date: Thu Nov 14 09:23:55 2013 -0600\n", + "\n", + " Breaking argument type\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "820edf33", + "metadata": {}, + "source": [ + "Stop the bisect process with:\n", + "\n", + "``` bash\n", + "git bisect reset\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8d1ba607", + "metadata": {}, + "source": [ + "### Solving automatically\n", + "\n", + "If we have an appropriate unit test, we can do all this automatically:\n", + "\n", + "(*NOTE*: You don't need [to redirect the `stderr` and `stdout`](https://linuxize.com/post/bash-redirect-stderr-stdout/) (with `&>`) of `git bisect run` to a file when running these commands outside a jupyter notebook (i.e., on a shell). This is done here so the errors appears with the right commits)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9287b69", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "git bisect start\n", + "git bisect bad HEAD # We know the current state is broken\n", + "git bisect good main # We know main is good\n", + "git bisect run python squares.py 2 &> gitbisect.out\n", + "cat gitbisect.out" + ] + }, + { + "cell_type": "markdown", + "id": "e15b6209", + "metadata": {}, + "source": [ + "Boom!" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Bisect" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch00git/14Bisect.ipynb.py b/ch00git/14Bisect.ipynb.py new file mode 100644 index 000000000..e1c8aa228 --- /dev/null +++ b/ch00git/14Bisect.ipynb.py @@ -0,0 +1,143 @@ +# --- +# jupyter: +# jekyll: +# display_name: Bisect +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Debugging With Git Bisect +# +# You can use +# +# ``` bash +# git bisect +# ``` +# +# to find out which commit caused a bug. + +# %% [markdown] +# ### An example repository +# +# In a nice open source example, I found an arbitrary exemplar on github + +# %% jupyter={"outputs_hidden": true} +import os +top_dir = os.getcwd() +git_dir = os.path.join(top_dir, 'learning_git') +os.chdir(git_dir) + +# %% jupyter={"outputs_hidden": false} language="bash" +# rm -rf bisectdemo +# git clone https://github.com/UCL-ARC-RSEing-with-Python/bisectdemo.git + +# %% jupyter={"outputs_hidden": true} +bisect_dir=os.path.join(git_dir,'bisectdemo') +os.chdir(bisect_dir) + +# %% attributes={"classes": [" bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# python squares.py 2 # 4 + +# %% [markdown] +# This has been set up to break itself at a random commit, and leave you to use +# bisect to work out where it has broken: + +# %% attributes={"classes": [" bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# ./breakme.sh > break_output + +# %% [markdown] +# Which will make a bunch of commits, of which one is broken, and leave you in the broken final state + +# %% jupyter={"outputs_hidden": false} language="bash" +# python squares.py 2 # Error message + +# %% [markdown] +# ### Bisecting manually + +# %% attributes={"classes": [" bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git bisect start +# git bisect bad # We know the current state is broken +# git switch main +# git bisect good # We know the main branch state is OK + +# %% [markdown] +# Bisect needs one known good and one known bad commit to get started + +# %% [markdown] +# ### Solving Manually + +# %% [markdown] attributes={"classes": [" bash"], "id": ""} +# ``` bash +# python squares.py 2 # 4 +# git bisect good +# python squares.py 2 # 4 +# git bisect good +# python squares.py 2 # 4 +# git bisect good +# python squares.py 2 # Crash +# git bisect bad +# python squares.py 2 # Crash +# git bisect bad +# python squares.py 2 # Crash +# git bisect bad +# python squares.py 2 #Crash +# git bisect bad +# python squares.py 2 # 4 +# git bisect good +# python squares.py 2 # 4 +# git bisect good +# python squares.py 2 # 4 +# git bisect good +# ``` +# + +# %% [markdown] +# And eventually: + +# %% [markdown] attributes={"classes": [" bash"], "id": ""} +# ``` bash +# git bisect good +# Bisecting: 0 revisions left to test after this (roughly 0 steps) +# +# python squares.py 2 +# 4 +# +# git bisect good +# 2777975a2334c2396ccb9faf98ab149824ec465b is the first bad commit +# commit 2777975a2334c2396ccb9faf98ab149824ec465b +# Author: Shawn Siefkas +# Date: Thu Nov 14 09:23:55 2013 -0600 +# +# Breaking argument type +# +# ``` + +# %% [markdown] +# Stop the bisect process with: +# +# ``` bash +# git bisect reset +# ``` + +# %% [markdown] +# ### Solving automatically +# +# If we have an appropriate unit test, we can do all this automatically: +# +# (*NOTE*: You don't need [to redirect the `stderr` and `stdout`](https://linuxize.com/post/bash-redirect-stderr-stdout/) (with `&>`) of `git bisect run` to a file when running these commands outside a jupyter notebook (i.e., on a shell). This is done here so the errors appears with the right commits) + +# %% attributes={"classes": [" bash"], "id": ""} jupyter={"outputs_hidden": false} language="bash" +# git bisect start +# git bisect bad HEAD # We know the current state is broken +# git bisect good main # We know main is good +# git bisect run python squares.py 2 &> gitbisect.out +# cat gitbisect.out + +# %% [markdown] +# Boom! diff --git a/ch00git/index.html b/ch00git/index.html new file mode 100644 index 000000000..92edfc2fb --- /dev/null +++ b/ch00git/index.html @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version Control + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Why use version control
  • +
  • Solo use of version control
  • +
  • Publishing your code to GitHub
  • +
  • Collaborating with others through Git
  • +
  • Branching
  • +
  • Rebasing and Merging
  • +
  • Debugging with GitBisect
  • +
  • Forks, Pull Requests and the GitHub Flow
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/learning_git/bare_repo/HEAD b/ch00git/learning_git/bare_repo/HEAD new file mode 100644 index 000000000..b870d8262 --- /dev/null +++ b/ch00git/learning_git/bare_repo/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/ch00git/learning_git/bare_repo/config b/ch00git/learning_git/bare_repo/config new file mode 100644 index 000000000..07d359d07 --- /dev/null +++ b/ch00git/learning_git/bare_repo/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/ch00git/learning_git/bare_repo/description b/ch00git/learning_git/bare_repo/description new file mode 100755 index 000000000..498b267a8 --- /dev/null +++ b/ch00git/learning_git/bare_repo/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/ch00git/learning_git/bare_repo/hooks/applypatch-msg.sample b/ch00git/learning_git/bare_repo/hooks/applypatch-msg.sample new file mode 100755 index 000000000..a5d7b84a6 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/ch00git/learning_git/bare_repo/hooks/commit-msg.sample b/ch00git/learning_git/bare_repo/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/ch00git/learning_git/bare_repo/hooks/fsmonitor-watchman.sample b/ch00git/learning_git/bare_repo/hooks/fsmonitor-watchman.sample new file mode 100755 index 000000000..23e856f5d --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/ch00git/learning_git/bare_repo/hooks/post-update.sample b/ch00git/learning_git/bare_repo/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/ch00git/learning_git/bare_repo/hooks/pre-applypatch.sample b/ch00git/learning_git/bare_repo/hooks/pre-applypatch.sample new file mode 100755 index 000000000..4142082bc --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/ch00git/learning_git/bare_repo/hooks/pre-commit.sample b/ch00git/learning_git/bare_repo/hooks/pre-commit.sample new file mode 100755 index 000000000..e144712c8 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/ch00git/learning_git/bare_repo/hooks/pre-merge-commit.sample b/ch00git/learning_git/bare_repo/hooks/pre-merge-commit.sample new file mode 100755 index 000000000..399eab192 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/ch00git/learning_git/bare_repo/hooks/pre-push.sample b/ch00git/learning_git/bare_repo/hooks/pre-push.sample new file mode 100755 index 000000000..4ce688d32 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/ch00git/learning_git/bare_repo/hooks/pre-rebase.sample b/ch00git/learning_git/bare_repo/hooks/pre-rebase.sample new file mode 100755 index 000000000..6cbef5c37 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/ch00git/learning_git/bare_repo/hooks/pre-receive.sample b/ch00git/learning_git/bare_repo/hooks/pre-receive.sample new file mode 100755 index 000000000..a1fd29ec1 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/ch00git/learning_git/bare_repo/hooks/prepare-commit-msg.sample b/ch00git/learning_git/bare_repo/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..10fa14c5a --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/ch00git/learning_git/bare_repo/hooks/push-to-checkout.sample b/ch00git/learning_git/bare_repo/hooks/push-to-checkout.sample new file mode 100755 index 000000000..af5a0c001 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/ch00git/learning_git/bare_repo/hooks/update.sample b/ch00git/learning_git/bare_repo/hooks/update.sample new file mode 100755 index 000000000..c4d426bc6 --- /dev/null +++ b/ch00git/learning_git/bare_repo/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/ch00git/learning_git/bare_repo/info/exclude b/ch00git/learning_git/bare_repo/info/exclude new file mode 100755 index 000000000..a5196d1be --- /dev/null +++ b/ch00git/learning_git/bare_repo/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/ch00git/learning_git/bare_repo/objects/02/2c38d1886bd36d1760ed255a4f6bbd1f43be8e b/ch00git/learning_git/bare_repo/objects/02/2c38d1886bd36d1760ed255a4f6bbd1f43be8e new file mode 100644 index 0000000000000000000000000000000000000000..39bff2ec385a80c7ef888783a61cd357b542ac76 GIT binary patch literal 117 zcmV-*0E+*30V^p=O;s>7G-NO|FfcPQQ3y}WNiEjPO<}lk!osO~v8O)6`_+jIYb~F2 za5ZQ{)nw+Sq*j2`v~f;+%j)cVuu%3+i2MOw=M&5y!k}t$60=it67y0(%4#;8^b~qF Xe|pLN&P6}UsyY{~J^LO2pd>CA<~lXW literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/03/625bb37da8f24bd34e2acfaf716ab0ce23be5d b/ch00git/learning_git/bare_repo/objects/03/625bb37da8f24bd34e2acfaf716ab0ce23be5d new file mode 100644 index 0000000000000000000000000000000000000000..ac5ebc486fbfcce78aca10601bd682e5f1c3950e GIT binary patch literal 160 zcmV;R0AK%j0cDLn3c^4Tg{}7#?*K+bun}wou~5Or15C)63~pvZc2>>pO+;*b<@?@G z8Lh->xtP3HukdUF0zFvt@M>O(3msqWj22;SJ+@4BLN&u-BUV zIK?GF&GF!VK>HoCoEBl19StQUYZ*>4qLCQW{BzRMP%`M(fc5aFq2i9U60Kp1!E!%3 OH&JmhU4%E93`Y!NIZL(x literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/15/06ddb315a81961af669ac4ec0f7b8d39564ea6 b/ch00git/learning_git/bare_repo/objects/15/06ddb315a81961af669ac4ec0f7b8d39564ea6 new file mode 100644 index 0000000000000000000000000000000000000000..ce91916d9783b0d2690472780949990f876a79f8 GIT binary patch literal 117 zcmV-*0E+*30V^p=O;s>7G-NO|FfcPQQ3y}WNiEjPO<_3lT0pC#GTfNaKEZk2#NRT9 z4?a#etNv!~>Y2%#umetw!V4>`t5cvbV&L@~ZghAEhBxa}PB<7`nl+|oF=_&MV X{`8Xjor`{yRdp^}d-gp5!ICbYAo)4+ literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/1b/45884214655a0199ed5b0c59f7c4c4c80b5e0c b/ch00git/learning_git/bare_repo/objects/1b/45884214655a0199ed5b0c59f7c4c4c80b5e0c new file mode 100644 index 0000000000000000000000000000000000000000..7de7718d1105d4a7f5e2215914720546060e6e56 GIT binary patch literal 52 zcmV-40L%Y)0V^p=O;s>9V=y!@Ff%bx$jnPgt=u9k literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/21/c306bbfcd288f6f6e74c8495301159bb1f8ed2 b/ch00git/learning_git/bare_repo/objects/21/c306bbfcd288f6f6e74c8495301159bb1f8ed2 new file mode 100644 index 000000000..a594a8acc --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/21/c306bbfcd288f6f6e74c8495301159bb1f8ed2 @@ -0,0 +1 @@ +xPj0_1 =T3 8MP#$ʶ,SkS":0s썤,%*<ڸUĵȼ5hGŻ/yoy+Qb UMC}]j;3 0mG;ki(=N;ԇrX&lcSt'spJxnЩ,g@ \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/2c/bc3a1a3c145ef2087cef6303e6508feb2e95eb b/ch00git/learning_git/bare_repo/objects/2c/bc3a1a3c145ef2087cef6303e6508feb2e95eb new file mode 100644 index 0000000000000000000000000000000000000000..c00157375fec0e9b2f2848a7849967b03be1ec50 GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9V=y!@Ff%bx$jnPgt^# literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/34/6ff8f9f1e8b276e946a07066c5642cde43659e b/ch00git/learning_git/bare_repo/objects/34/6ff8f9f1e8b276e946a07066c5642cde43659e new file mode 100644 index 0000000000000000000000000000000000000000..6c4862f80f0f8bf32ecfb54ba3235d2dd50eb595 GIT binary patch literal 86 zcmV-c0IC0Y0V^p=O;s>AWiT`_Ff%bx$jnPgt sKZHTmAWiT`_Ff%bx$jnPgt sKZHTmrA}@&QNB zgCCOhESgJwfx=F3i*}MR^1Wp5GAyZ}m literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/4f/db31b5b5aca2f87f78ef5da503cb823bbeab9d b/ch00git/learning_git/bare_repo/objects/4f/db31b5b5aca2f87f78ef5da503cb823bbeab9d new file mode 100644 index 0000000000000000000000000000000000000000..d0cf87235d1a9e27d123df3221b60a598b8b283c GIT binary patch literal 117 zcmV-*0E+*30V^p=O;s>7G-NO|FfcPQQ3y}WNiEjPO<~CLWLs0F=Q%;-a!u%{zq^Z<(^< +5@[29C<2ڒbIcJ RՂL0`+B91\ +{kh9c)wRp'aP{ۊoз\^ f{DC{8=UPלa>M~ \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/5a/a2999dd374d2da5874f5b379646b8f4c6d7a68 b/ch00git/learning_git/bare_repo/objects/5a/a2999dd374d2da5874f5b379646b8f4c6d7a68 new file mode 100644 index 000000000..f6800ed0f --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/5a/a2999dd374d2da5874f5b379646b8f4c6d7a68 @@ -0,0 +1,2 @@ +xAj0 E)/ Gm(u/!2CM{yX 0?ٮ +D,s++MXXL5c+-G"l SK1)ܝv;|VuvWx[e_xz*#4x g\{j wk YA8uɼO#P \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/5b/f472d82a68b889f58165491d712e8f15de0de7 b/ch00git/learning_git/bare_repo/objects/5b/f472d82a68b889f58165491d712e8f15de0de7 new file mode 100644 index 000000000..f121386ba --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/5b/f472d82a68b889f58165491d712e8f15de0de7 @@ -0,0 +1,2 @@ +xK1Eg5$IWMTj[쏴í4]<"{ )[j }.$9j&Kƫ +sέuoL)bO9`(Z ?tXV8\e\t8w'~.ZB}S_8|Ћb^ L \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/69/4c013206739cee29c71040e85a93b975d8e95b b/ch00git/learning_git/bare_repo/objects/69/4c013206739cee29c71040e85a93b975d8e95b new file mode 100644 index 0000000000000000000000000000000000000000..46ca6b08564422cdfdf54c4d7e48b87dd7a50561 GIT binary patch literal 167 zcmV;Y09gNc0i}*X3c@fDMP26m>0q6k~N#>nq! V)qgh3DJ8gS&STc6`2an^OeRhXQU?G4 literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/6a/4906ac762e499014d38c7716fb80ed5ad668a2 b/ch00git/learning_git/bare_repo/objects/6a/4906ac762e499014d38c7716fb80ed5ad668a2 new file mode 100644 index 0000000000000000000000000000000000000000..7a999323feb81f318cebd5718668c448c5facbef GIT binary patch literal 66 zcmV-I0KNZs0ZYosPf{>9VDQZ^%_~XF%qv##%u@(Y%t7HDE9_FfcPQQ3y!Q%gfA5E!N9TVOYfb_OfY}ynAy`UfsEe zzKtF2d)lGO!V_~KYHpmcaH?MHsn76!bt1!B%O@RN4cbsOnRzLx6(DokI48bkb@n}2 zD0?SF{s6D@3FZ%BP&GM;*{L~+c_|=eH5*QP3O$=Yz2tu9q90{dor~6 VD*rgtHD?(3DFaVgeE@!}Omo|*O$Y!0 literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/7c/a7cdd047dece7431e031e62d82311c12db7d65 b/ch00git/learning_git/bare_repo/objects/7c/a7cdd047dece7431e031e62d82311c12db7d65 new file mode 100644 index 0000000000000000000000000000000000000000..f85b40e949085b5a7c62e8742768ea66f7b4529d GIT binary patch literal 213 zcmV;`04o1@0V^p=O;s>5GGQ<@FfcPQQP4}zEXhpI%P&f0SZHQkImIj7_1u;@e4e%q zuP2)xp9NLro0y%NmYI{vF!QAP=__Fm`~(#ibSMUv@EJ6_vP0Dbq~_&i=A{8m^1ke+b$)$e)`oM+`(mMLa)3_CNz6+DDXZCV(o^W!{OKk4I~V;Z PtLj{|_UwBAmUvpZEuw59 literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/7c/b0c94912e69f9774df89a2f8767a89a2adcdef b/ch00git/learning_git/bare_repo/objects/7c/b0c94912e69f9774df89a2f8767a89a2adcdef new file mode 100644 index 0000000000000000000000000000000000000000..54f7891703ce67c5e96f160c49268458dc0c0b76 GIT binary patch literal 99 zcmV-p0G$7L0ZYosPg1ZnW$;PNPR&WoOHokZvV{T#ARCCBOLLQoG7}Xt5{ngz^K(-b z3W`!oN-7mHGIMf@bre8)a)6qP6-x5+xww4uOY=$+GxLhAxU>{JQgg~ub8;&4xBx(1 FAY}=oDa-%> literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/85/bec83768f27da53032a611e29595d5b5f40378 b/ch00git/learning_git/bare_repo/objects/85/bec83768f27da53032a611e29595d5b5f40378 new file mode 100644 index 000000000..8fe29c725 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/85/bec83768f27da53032a611e29595d5b5f40378 @@ -0,0 +1,2 @@ +xj0{S֒(/Z[W>at߶ @jNI=GܘܤviTe\Rp##N\Na*iF7XNjz[OY>7`}+1?yYAR -7M: +=W \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/86/0991ed05434dc1711ddc541fc00d43c803f056 b/ch00git/learning_git/bare_repo/objects/86/0991ed05434dc1711ddc541fc00d43c803f056 new file mode 100644 index 0000000000000000000000000000000000000000..269530efae7fc454dfc4083e151754f161670bfe GIT binary patch literal 137 zcmV;40CxX)0cDLb3c^4TMXmP~{{RvY1S=cCN-aIWgpA4HW=3{qBe}gswDOk^-urE~ z7MtC6b+M{Jq7KpyXS%aP6frQaCje>xzXcz)=aLsk9BP4y%Z%|`v+7Mc)WARvw~-N) rGX}}o*Po}6=9WR}XTS>LY3TUD+KKis#cZ{_$xT#3Oq=uq)6zVuxu8Q# literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/99/c927cbd456e04e1120a0882151740e30834507 b/ch00git/learning_git/bare_repo/objects/99/c927cbd456e04e1120a0882151740e30834507 new file mode 100644 index 0000000000000000000000000000000000000000..b9cab38703f838dc822412e53202b267a7514170 GIT binary patch literal 96 zcmV-m0H6PO0ZYosPg1ZnWZ?333AR9V=y!@Ff%bx$jnPgt9V=y!@Ff%bx$jnPgtK L^1~JYPt+1}E-M#r literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/a1/363379944a5745ceb49c0e493d80eb9335c79a b/ch00git/learning_git/bare_repo/objects/a1/363379944a5745ceb49c0e493d80eb9335c79a new file mode 100644 index 0000000000000000000000000000000000000000..7ad9e4a08fbdb9546fbb5111fc2ebfd3d4aa5e81 GIT binary patch literal 21 ccmb>0cDIa3IZ_@MXmP~{{XUJp`DFjC6*o_A>(EsnUS4|lG|%QZ2aZpzZZ)} z><;_QRhxKVF|fch;dTKq9sf7*S(XwcfeoULC#@azThM0n_?(&}2ZL!Pcqj##OR+76 kSVND5@ROh`{G~|rAW19>RM54pA9815XO($+18sje=H*a31ONa4 literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/a2/0dedd3357a1f47838c6e7ecee14d818887bc87 b/ch00git/learning_git/bare_repo/objects/a2/0dedd3357a1f47838c6e7ecee14d818887bc87 new file mode 100644 index 0000000000000000000000000000000000000000..e18a32ded026050a5948327bb65ba8c3e9c72972 GIT binary patch literal 60 zcmV-C0K@-y0ZYosPf{>5W8m`5FU>1S%*-oR@XS*v$w*ZQNX^U3%u6livc&|U Si}H($72HyDa<~9ULlt9(HXS1X literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/a6/361f3fc86493e1cffc5218e0c5a9312bd9b684 b/ch00git/learning_git/bare_repo/objects/a6/361f3fc86493e1cffc5218e0c5a9312bd9b684 new file mode 100644 index 000000000..15f5c5409 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/a6/361f3fc86493e1cffc5218e0c5a9312bd9b684 @@ -0,0 +1,3 @@ +xI +1D]/4Q^'M=N^Z2M +RC-D ).2B!$! m9X [\AG#{mQZ3.sb'3ýK;ΉƥBn__sVnA8έuH8&~O+9GhZJ \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/a8/4a21b9c40e8bc87c42e88c84b2854dfa1ed652 b/ch00git/learning_git/bare_repo/objects/a8/4a21b9c40e8bc87c42e88c84b2854dfa1ed652 new file mode 100644 index 000000000..8c7fc2ea4 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/a8/4a21b9c40e8bc87c42e88c84b2854dfa1ed652 @@ -0,0 +1,2 @@ +xKOR07aINIKQPಅ Ks22sS +RKJ*22sru@zrJݸ \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/ad/82f9b3ac49e557eca127a56411d4f52d11d772 b/ch00git/learning_git/bare_repo/objects/ad/82f9b3ac49e557eca127a56411d4f52d11d772 new file mode 100644 index 000000000..4e29f32b0 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/ad/82f9b3ac49e557eca127a56411d4f52d11d772 @@ -0,0 +1,5 @@ +xKj1D)zo02{m ˖c  +.((x:]'&X +퐥 +vݚ]/зyґt?FS \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/ae/7410182b095a876b07012ff84c7c11cdffcd9d b/ch00git/learning_git/bare_repo/objects/ae/7410182b095a876b07012ff84c7c11cdffcd9d new file mode 100644 index 000000000..74edc35da --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/ae/7410182b095a876b07012ff84c7c11cdffcd9d @@ -0,0 +1 @@ +xKj1D)z0uK&^V6E3\zTe1FWj-!g7hb*vN%0!Ee2w]J蝕gomLyjpa Gïqkx}ppz˟9o_OpZ0Çew:̥VV (M \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/b2/02e904a72055b1710d61f18e048a5ef425c7ae b/ch00git/learning_git/bare_repo/objects/b2/02e904a72055b1710d61f18e048a5ef425c7ae new file mode 100644 index 000000000..7345744f8 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/b2/02e904a72055b1710d61f18e048a5ef425c7ae @@ -0,0 +1,2 @@ +xN +0ػPvAD={MT4s izf=$uO-]D3K=iGؕ4Dr9SWshHC l,1靍=K;A%Cnⲭ7G-NO|FfcPQQ3y}WNiEjPO=0-_qT7XY^^&tc7R$1(EsD0y zOv*}xs>#etNv!~>Y2%#umetw!V4>`t5cvbV&L@~ZghAEhBxa}PB<7`nl+|oF=_&MV X{`8Xjor`{yRdp^}d-gp52I^ zmaFCBsFQeRF=Xkn=TZzT@PTkT0BF|#7w}fDB@}EBb=;}W7(ZS*Sqjc(atzsEIubmW zf+76dchs2Yj*!11XoWu%J>5upmMc^+>r`LJg@v6}Qq#QqV#nwBb7`aOCRZc3fh)B* ar9+BVu^=+Z{Fm`r_Rw1T)I0$Okyi*XKv4|< literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/bf/be7ced096691b79738a3d5a0b16b6459a8a12c b/ch00git/learning_git/bare_repo/objects/bf/be7ced096691b79738a3d5a0b16b6459a8a12c new file mode 100644 index 0000000000000000000000000000000000000000..31dcc4ff97e0a3c640fde6dc207b6a152cbd5feb GIT binary patch literal 71 zcmV-N0J#5n0ZYosPf{?qVBqr2FU>1S%*-oR@XS*v$w*ZQNX^U3%u6livc&|U di}H($72HyDa)6xhjMSpM;>?s(E&yW+8DlDMBwzpl literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/c0/76f26f388a67ba6f95da979a1330edc957937d b/ch00git/learning_git/bare_repo/objects/c0/76f26f388a67ba6f95da979a1330edc957937d new file mode 100644 index 0000000000000000000000000000000000000000..c61c97ae2df1dd87a5bc6dcb46cb0acb0ba244bc GIT binary patch literal 86 zcmV-c0IC0Y0V^p=O;s>AWiT`_Ff%bx$jnPgtK s^1~LWnw-S!)SSe;6p*qNUWz-9@O7W4aeC3yvZ>Ygm)x}=04)$AYPQ-c*#H0l literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/c4/eb102a88795733013f6043ae91fb1cc3c1e425 b/ch00git/learning_git/bare_repo/objects/c4/eb102a88795733013f6043ae91fb1cc3c1e425 new file mode 100644 index 0000000000000000000000000000000000000000..78643f48e678c08c4f7a190627d065bd517c8a81 GIT binary patch literal 73 zcmV-P0Ji^l0ZYosPg1ZjV(`r`%_~XF%qv##%u@(Y%t6G-UA2FU>1S%*-oR@XS*PPs~Xz=CZ{ATwGcT0jYTkl?rZ& zc|dkZQDqvK5uBG_o{|p|a?hzuNi71(mlpwfKxIJ1PMPUIwsT@iVrG$oXG&3KF&6+i GyCVlW;wQ=g literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/dd/5cf9cab9854226b00c0eaec356bedfcad3e184 b/ch00git/learning_git/bare_repo/objects/dd/5cf9cab9854226b00c0eaec356bedfcad3e184 new file mode 100644 index 0000000000000000000000000000000000000000..65ab3865aad500d4103d4e142ce480482272cb4e GIT binary patch literal 135 zcmV;20C@j+0cDLb3PLduM6LfR<^b}+Lab~AK@dw1up#khA=#B=qvZA)5gVs`%$spZ z9X7k|>Y`1ISSm<7PMlH&3oHg)&j6$+gXn{QyKHP(NmIW&4+RB^USlC!)o?g@^JXUE@Jj(z8 literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/df/5fed820b7fa9834693c535803cefbd777411ca b/ch00git/learning_git/bare_repo/objects/df/5fed820b7fa9834693c535803cefbd777411ca new file mode 100644 index 000000000..ae683dd76 --- /dev/null +++ b/ch00git/learning_git/bare_repo/objects/df/5fed820b7fa9834693c535803cefbd777411ca @@ -0,0 +1 @@ +xj0DW} HP+'F$/l9_׿Y 92㳑9_klGruK>A-bjKRsbC9#xoyܨ=@_+Av_w2A!8O}Did h6zN& oR{ \ No newline at end of file diff --git a/ch00git/learning_git/bare_repo/objects/e7/3f9893bc695e0a7129310f90597bbc6d8296ed b/ch00git/learning_git/bare_repo/objects/e7/3f9893bc695e0a7129310f90597bbc6d8296ed new file mode 100644 index 0000000000000000000000000000000000000000..c67c739550ca95dbf6080f7c5ffc13863f2418f5 GIT binary patch literal 214 zcmV;{04e`?0V^p=O;s>5GGQ<@FfcPQQP4}zEXhpI%P&f0SZHQkImIj7_1u;@e4e%q zuP2)xp9NLro0y%NmYI{vF!QAP=__Fm`~(#ibSMUv@EJ6_vP0Dbq~_&i=A{WLRtYq=TzL8>%KV zFD11CWDavu^ybGDJzY&^P5=M^ literal 0 HcmV?d00001 diff --git a/ch00git/learning_git/bare_repo/objects/f2/bc29cdb9f78cd43ceccbcd25abf66080ee34ba b/ch00git/learning_git/bare_repo/objects/f2/bc29cdb9f78cd43ceccbcd25abf66080ee34ba new file mode 100644 index 0000000000000000000000000000000000000000..9f6eb6476f4e4d5a5f50a42fe70757f4b8b60354 GIT binary patch literal 213 zcmV;`04o1@0V^p=O;s>5GGQ<@FfcPQQP4}zEXhpI%P&f0SZHQkImIj7_1u;@e4e%q zuP2)xp9NLro0y%NmYI{vF!QAP=__Fm`~(#ibSMUv@EJ6_vP0Dbq~_&i=A{5VDQZ^%_~XF%qv##%u@(Y%t + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ch00git/learning_git/bisectdemo/README.md b/ch00git/learning_git/bisectdemo/README.md new file mode 100644 index 000000000..486be8a5e --- /dev/null +++ b/ch00git/learning_git/bisectdemo/README.md @@ -0,0 +1,71 @@ +# Git Bisect Demo + +`squares.py` just spits out the square of an integer passed in as an argument. The function of the script is irrelevant as we are really looking at git. Running `breakme.sh` will create a buggy branch with 1000 dummy commits and 1 commit that introduces a regression in `squares.py`. This environment can be used to demonstrate the value of the git bisect command. + +## Create your buggy branch + +**Keep in mind this will blow away any local branch named buggy.** + +```bash +$ git clone https://github.com/shawnsi/bisectdemo.git +$ ./breakme.sh +``` + +## Bisect + +Running `squares.py` will now show an obvious error. + +```bash +$ python squares.py 2 +Traceback (most recent call last): + File "squares.py", line 7, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +``` + +Now we can use `git bisect` to identify the first commit that introduced the regression. Start this bisection then indicate known good and bad commits. + +```bash +$ git bisect start +$ git bisect good master +$ git bisect bad HEAD +``` + +Test the current commit and tell git whether its good or bad. The order of commands below is just an example. Your individual bisection may proceed differently. + +```bash +$ python squares.py 2 +4 +$ git bisect good +$ python squares.py 2 +Traceback (most recent call last): + File "squares.py", line 7, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +$ git bisect bad +``` + +Eventually git will widdle the commit log down to one commit that introduced the regression. + +```bash +$ git bisect good +15644003d085cf1a35e2c4c4af654d9e386f811a is the first bad commit +commit 15644003d085cf1a35e2c4c4af654d9e386f811a +Author: Shawn Siefkas +Date: Thu Nov 14 09:23:55 2013 -0600 + + Breaking argument type + +:100644 100644 ff0746a9f0ac02303bd87ed7c3d1571776cbf28d ac23d6bb695e1563edbd3a3fcc0079e1be1793ae M squares.py +``` + +### Automatic Bisection + +This example can be further condensed via `git bisect run`: + +```bash +$ git bisect start +$ git bisect good master +$ git bisect bad buggy +$ git bisect run python squares.py 2 +``` diff --git a/ch00git/learning_git/bisectdemo/break_output b/ch00git/learning_git/bisectdemo/break_output new file mode 100644 index 000000000..b5eb14908 --- /dev/null +++ b/ch00git/learning_git/bisectdemo/break_output @@ -0,0 +1,2005 @@ +[buggy daf16f8] Comment 1 + 1 file changed, 1 insertion(+) +[buggy 3a3c3ab] Comment 2 + 1 file changed, 1 insertion(+) +[buggy d291cdf] Comment 3 + 1 file changed, 1 insertion(+) +[buggy d3dccf9] Comment 4 + 1 file changed, 1 insertion(+) +[buggy 970be30] Comment 5 + 1 file changed, 1 insertion(+) +[buggy 8844a39] Comment 6 + 1 file changed, 1 insertion(+) +[buggy 464b8ce] Comment 7 + 1 file changed, 1 insertion(+) +[buggy d4ecf6f] Comment 8 + 1 file changed, 1 insertion(+) +[buggy e8559c4] Comment 9 + 1 file changed, 1 insertion(+) +[buggy 5825596] Comment 10 + 1 file changed, 1 insertion(+) +[buggy 73ba64f] Comment 11 + 1 file changed, 1 insertion(+) +[buggy 912c929] Comment 12 + 1 file changed, 1 insertion(+) +[buggy c09db3e] Comment 13 + 1 file changed, 1 insertion(+) +[buggy 756296b] Comment 14 + 1 file changed, 1 insertion(+) +[buggy 64dc1aa] Comment 15 + 1 file changed, 1 insertion(+) +[buggy 3a25057] Comment 16 + 1 file changed, 1 insertion(+) +[buggy c5913ea] Comment 17 + 1 file changed, 1 insertion(+) +[buggy 5dfe71f] Comment 18 + 1 file changed, 1 insertion(+) +[buggy 02521a1] Comment 19 + 1 file changed, 1 insertion(+) +[buggy 0992cf0] Comment 20 + 1 file changed, 1 insertion(+) +[buggy 7dfe369] Comment 21 + 1 file changed, 1 insertion(+) +[buggy 6890cd2] Comment 22 + 1 file changed, 1 insertion(+) +[buggy 64d7b44] Comment 23 + 1 file changed, 1 insertion(+) +[buggy 8b66a80] Comment 24 + 1 file changed, 1 insertion(+) +[buggy 38b5dbc] Comment 25 + 1 file changed, 1 insertion(+) +[buggy 1fd68b4] Comment 26 + 1 file changed, 1 insertion(+) +[buggy 1c48e51] Comment 27 + 1 file changed, 1 insertion(+) +[buggy 8a04996] Comment 28 + 1 file changed, 1 insertion(+) +[buggy 813b76a] Comment 29 + 1 file changed, 1 insertion(+) +[buggy fe18715] Comment 30 + 1 file changed, 1 insertion(+) +[buggy 63b96bd] Comment 31 + 1 file changed, 1 insertion(+) +[buggy b7e1322] Comment 32 + 1 file changed, 1 insertion(+) +[buggy 9cca309] Comment 33 + 1 file changed, 1 insertion(+) +[buggy d1e86ea] Comment 34 + 1 file changed, 1 insertion(+) +[buggy 739ce52] Comment 35 + 1 file changed, 1 insertion(+) +[buggy 14dafad] Comment 36 + 1 file changed, 1 insertion(+) +[buggy 6800de2] Comment 37 + 1 file changed, 1 insertion(+) +[buggy ce90a4d] Comment 38 + 1 file changed, 1 insertion(+) +[buggy 996fd8f] Comment 39 + 1 file changed, 1 insertion(+) +[buggy 75b8bbf] Comment 40 + 1 file changed, 1 insertion(+) +[buggy 5d0ff29] Comment 41 + 1 file changed, 1 insertion(+) +[buggy 8046042] Comment 42 + 1 file changed, 1 insertion(+) +[buggy a08180e] Comment 43 + 1 file changed, 1 insertion(+) +[buggy 8b41e73] Comment 44 + 1 file changed, 1 insertion(+) +[buggy a0a1582] Comment 45 + 1 file changed, 1 insertion(+) +[buggy 7de2b5c] Comment 46 + 1 file changed, 1 insertion(+) +[buggy f508900] Comment 47 + 1 file changed, 1 insertion(+) +[buggy 0b86a53] Comment 48 + 1 file changed, 1 insertion(+) +[buggy 80b1ffe] Comment 49 + 1 file changed, 1 insertion(+) +[buggy a69d85d] Comment 50 + 1 file changed, 1 insertion(+) +[buggy 2285bc2] Comment 51 + 1 file changed, 1 insertion(+) +[buggy 7f365cb] Comment 52 + 1 file changed, 1 insertion(+) +[buggy 1fedfed] Comment 53 + 1 file changed, 1 insertion(+) +[buggy 804268e] Comment 54 + 1 file changed, 1 insertion(+) +[buggy 0c5e6d1] Comment 55 + 1 file changed, 1 insertion(+) +[buggy 35d4591] Comment 56 + 1 file changed, 1 insertion(+) +[buggy 2c83b96] Comment 57 + 1 file changed, 1 insertion(+) +[buggy 7bbcf7e] Comment 58 + 1 file changed, 1 insertion(+) +[buggy 6c27894] Comment 59 + 1 file changed, 1 insertion(+) +[buggy 33ffd84] Comment 60 + 1 file changed, 1 insertion(+) +[buggy 687537b] Comment 61 + 1 file changed, 1 insertion(+) +[buggy 0bcc262] Comment 62 + 1 file changed, 1 insertion(+) +[buggy 9d53f94] Comment 63 + 1 file changed, 1 insertion(+) +[buggy e98571d] Comment 64 + 1 file changed, 1 insertion(+) +[buggy c328ba8] Comment 65 + 1 file changed, 1 insertion(+) +[buggy 5a49c41] Comment 66 + 1 file changed, 1 insertion(+) +[buggy 714b05e] Comment 67 + 1 file changed, 1 insertion(+) +[buggy bcbc401] Comment 68 + 1 file changed, 1 insertion(+) +[buggy 105073b] Comment 69 + 1 file changed, 1 insertion(+) +[buggy ac6f6f3] Comment 70 + 1 file changed, 1 insertion(+) +[buggy a0ac539] Comment 71 + 1 file changed, 1 insertion(+) +[buggy ffe48fd] Comment 72 + 1 file changed, 1 insertion(+) +[buggy 4df7d24] Comment 73 + 1 file changed, 1 insertion(+) +[buggy d780a0e] Comment 74 + 1 file changed, 1 insertion(+) +[buggy 8cd994e] Comment 75 + 1 file changed, 1 insertion(+) +[buggy 98a3dc9] Comment 76 + 1 file changed, 1 insertion(+) +[buggy 837712f] Comment 77 + 1 file changed, 1 insertion(+) +[buggy 91ea235] Comment 78 + 1 file changed, 1 insertion(+) +[buggy 66eef20] Comment 79 + 1 file changed, 1 insertion(+) +[buggy a368629] Comment 80 + 1 file changed, 1 insertion(+) +[buggy 2cc560f] Comment 81 + 1 file changed, 1 insertion(+) +[buggy ef2e34d] Comment 82 + 1 file changed, 1 insertion(+) +[buggy b74e4e2] Comment 83 + 1 file changed, 1 insertion(+) +[buggy e258a6e] Comment 84 + 1 file changed, 1 insertion(+) +[buggy b53117e] Comment 85 + 1 file changed, 1 insertion(+) +[buggy 293a2ba] Comment 86 + 1 file changed, 1 insertion(+) +[buggy cd8c70e] Comment 87 + 1 file changed, 1 insertion(+) +[buggy 16cda90] Comment 88 + 1 file changed, 1 insertion(+) +[buggy 8e0633f] Comment 89 + 1 file changed, 1 insertion(+) +[buggy 853aa00] Comment 90 + 1 file changed, 1 insertion(+) +[buggy cc86736] Comment 91 + 1 file changed, 1 insertion(+) +[buggy 37fd3bb] Comment 92 + 1 file changed, 1 insertion(+) +[buggy 678ef02] Comment 93 + 1 file changed, 1 insertion(+) +[buggy 01d0834] Comment 94 + 1 file changed, 1 insertion(+) +[buggy fdd03d0] Comment 95 + 1 file changed, 1 insertion(+) +[buggy 026464f] Comment 96 + 1 file changed, 1 insertion(+) +[buggy 0412647] Comment 97 + 1 file changed, 1 insertion(+) +[buggy 73df8d6] Comment 98 + 1 file changed, 1 insertion(+) +[buggy 85a7c13] Comment 99 + 1 file changed, 1 insertion(+) +[buggy c53b8af] Comment 100 + 1 file changed, 1 insertion(+) +[buggy ba566f9] Comment 101 + 1 file changed, 1 insertion(+) +[buggy 075da37] Comment 102 + 1 file changed, 1 insertion(+) +[buggy 92cfcac] Comment 103 + 1 file changed, 1 insertion(+) +[buggy 1f261f5] Comment 104 + 1 file changed, 1 insertion(+) +[buggy 86305b2] Comment 105 + 1 file changed, 1 insertion(+) +[buggy 1d7f9bc] Comment 106 + 1 file changed, 1 insertion(+) +[buggy 630fb38] Comment 107 + 1 file changed, 1 insertion(+) +[buggy 06079fe] Comment 108 + 1 file changed, 1 insertion(+) +[buggy 4e3d3e0] Comment 109 + 1 file changed, 1 insertion(+) +[buggy c7282a4] Comment 110 + 1 file changed, 1 insertion(+) +[buggy 2f309c6] Comment 111 + 1 file changed, 1 insertion(+) +[buggy d9cf3eb] Comment 112 + 1 file changed, 1 insertion(+) +[buggy 684b4e1] Comment 113 + 1 file changed, 1 insertion(+) +[buggy abaabc4] Comment 114 + 1 file changed, 1 insertion(+) +[buggy 7e1026e] Comment 115 + 1 file changed, 1 insertion(+) +[buggy e77f406] Comment 116 + 1 file changed, 1 insertion(+) +[buggy bf76c93] Comment 117 + 1 file changed, 1 insertion(+) +[buggy 53c3131] Comment 118 + 1 file changed, 1 insertion(+) +[buggy 09d5534] Comment 119 + 1 file changed, 1 insertion(+) +[buggy d595f14] Comment 120 + 1 file changed, 1 insertion(+) +[buggy 266d13f] Comment 121 + 1 file changed, 1 insertion(+) +[buggy 8f1e551] Comment 122 + 1 file changed, 1 insertion(+) +[buggy a8d03b9] Comment 123 + 1 file changed, 1 insertion(+) +[buggy 3481a1c] Comment 124 + 1 file changed, 1 insertion(+) +[buggy 8b7acd8] Comment 125 + 1 file changed, 1 insertion(+) +[buggy 6354483] Comment 126 + 1 file changed, 1 insertion(+) +[buggy 8dde26e] Comment 127 + 1 file changed, 1 insertion(+) +[buggy 481f5ce] Comment 128 + 1 file changed, 1 insertion(+) +[buggy 398543d] Comment 129 + 1 file changed, 1 insertion(+) +[buggy 7091e9e] Comment 130 + 1 file changed, 1 insertion(+) +[buggy 9feeb78] Comment 131 + 1 file changed, 1 insertion(+) +[buggy 139f4ee] Comment 132 + 1 file changed, 1 insertion(+) +[buggy 4881687] Comment 133 + 1 file changed, 1 insertion(+) +[buggy 90f71b0] Comment 134 + 1 file changed, 1 insertion(+) +[buggy fac1bb3] Comment 135 + 1 file changed, 1 insertion(+) +[buggy 9892fae] Comment 136 + 1 file changed, 1 insertion(+) +[buggy 3c46dd5] Comment 137 + 1 file changed, 1 insertion(+) +[buggy d49f06c] Comment 138 + 1 file changed, 1 insertion(+) +[buggy 3c24147] Comment 139 + 1 file changed, 1 insertion(+) +[buggy 5348546] Comment 140 + 1 file changed, 1 insertion(+) +[buggy 59fcb48] Comment 141 + 1 file changed, 1 insertion(+) +[buggy ea66be3] Comment 142 + 1 file changed, 1 insertion(+) +[buggy b53c171] Comment 143 + 1 file changed, 1 insertion(+) +[buggy 1f868fd] Comment 144 + 1 file changed, 1 insertion(+) +[buggy 078ff5f] Comment 145 + 1 file changed, 1 insertion(+) +[buggy 5473114] Comment 146 + 1 file changed, 1 insertion(+) +[buggy e26584e] Comment 147 + 1 file changed, 1 insertion(+) +[buggy f593d7e] Comment 148 + 1 file changed, 1 insertion(+) +[buggy bd7d1e6] Comment 149 + 1 file changed, 1 insertion(+) +[buggy dfb0e02] Comment 150 + 1 file changed, 1 insertion(+) +[buggy 9b14733] Comment 151 + 1 file changed, 1 insertion(+) +[buggy 5931ec6] Comment 152 + 1 file changed, 1 insertion(+) +[buggy b3f9f4f] Comment 153 + 1 file changed, 1 insertion(+) +[buggy 2398175] Comment 154 + 1 file changed, 1 insertion(+) +[buggy bc57ab8] Comment 155 + 1 file changed, 1 insertion(+) +[buggy 6ec35b5] Comment 156 + 1 file changed, 1 insertion(+) +[buggy 91ae3b3] Comment 157 + 1 file changed, 1 insertion(+) +[buggy 891a11c] Comment 158 + 1 file changed, 1 insertion(+) +[buggy 900f0f3] Comment 159 + 1 file changed, 1 insertion(+) +[buggy 1c3de9b] Comment 160 + 1 file changed, 1 insertion(+) +[buggy 07e04d3] Comment 161 + 1 file changed, 1 insertion(+) +[buggy 37e398a] Comment 162 + 1 file changed, 1 insertion(+) +[buggy 56e6c1a] Comment 163 + 1 file changed, 1 insertion(+) +[buggy d637fa1] Comment 164 + 1 file changed, 1 insertion(+) +[buggy 5bae44b] Comment 165 + 1 file changed, 1 insertion(+) +[buggy ea05cf7] Comment 166 + 1 file changed, 1 insertion(+) +[buggy be0355d] Comment 167 + 1 file changed, 1 insertion(+) +[buggy 7cc80f6] Comment 168 + 1 file changed, 1 insertion(+) +[buggy 93af628] Comment 169 + 1 file changed, 1 insertion(+) +[buggy cf7cc1e] Comment 170 + 1 file changed, 1 insertion(+) +[buggy a2bbdec] Comment 171 + 1 file changed, 1 insertion(+) +[buggy 6b6c391] Comment 172 + 1 file changed, 1 insertion(+) +[buggy 0f9f875] Comment 173 + 1 file changed, 1 insertion(+) +[buggy 74ad2e9] Comment 174 + 1 file changed, 1 insertion(+) +[buggy 496e36f] Comment 175 + 1 file changed, 1 insertion(+) +[buggy a71f561] Comment 176 + 1 file changed, 1 insertion(+) +[buggy 115cceb] Comment 177 + 1 file changed, 1 insertion(+) +[buggy 2032ad9] Comment 178 + 1 file changed, 1 insertion(+) +[buggy a1a1917] Comment 179 + 1 file changed, 1 insertion(+) +[buggy a94541e] Comment 180 + 1 file changed, 1 insertion(+) +[buggy 48885c9] Comment 181 + 1 file changed, 1 insertion(+) +[buggy 9d683c4] Comment 182 + 1 file changed, 1 insertion(+) +[buggy e126ac5] Comment 183 + 1 file changed, 1 insertion(+) +[buggy a634b8b] Comment 184 + 1 file changed, 1 insertion(+) +[buggy b447991] Comment 185 + 1 file changed, 1 insertion(+) +[buggy d62bb5d] Comment 186 + 1 file changed, 1 insertion(+) +[buggy 0dd41bf] Comment 187 + 1 file changed, 1 insertion(+) +[buggy d529110] Comment 188 + 1 file changed, 1 insertion(+) +[buggy 4023c82] Comment 189 + 1 file changed, 1 insertion(+) +[buggy 074a014] Comment 190 + 1 file changed, 1 insertion(+) +[buggy def8b3f] Comment 191 + 1 file changed, 1 insertion(+) +[buggy 78ce1c1] Comment 192 + 1 file changed, 1 insertion(+) +[buggy 7dc5f1a] Comment 193 + 1 file changed, 1 insertion(+) +[buggy 24acaab] Comment 194 + 1 file changed, 1 insertion(+) +[buggy 515157e] Comment 195 + 1 file changed, 1 insertion(+) +[buggy fc890ea] Comment 196 + 1 file changed, 1 insertion(+) +[buggy 6ed6da6] Comment 197 + 1 file changed, 1 insertion(+) +[buggy 01a137c] Comment 198 + 1 file changed, 1 insertion(+) +[buggy c736662] Comment 199 + 1 file changed, 1 insertion(+) +[buggy 6175dc8] Comment 200 + 1 file changed, 1 insertion(+) +[buggy 5dd14f8] Comment 201 + 1 file changed, 1 insertion(+) +[buggy 0ac50cf] Comment 202 + 1 file changed, 1 insertion(+) +[buggy 72e1a01] Comment 203 + 1 file changed, 1 insertion(+) +[buggy 86b4871] Comment 204 + 1 file changed, 1 insertion(+) +[buggy 58fa24e] Comment 205 + 1 file changed, 1 insertion(+) +[buggy 79bde8c] Comment 206 + 1 file changed, 1 insertion(+) +[buggy 3dc6429] Comment 207 + 1 file changed, 1 insertion(+) +[buggy 5234fb4] Comment 208 + 1 file changed, 1 insertion(+) +[buggy 1dd3017] Comment 209 + 1 file changed, 1 insertion(+) +[buggy 516861b] Comment 210 + 1 file changed, 1 insertion(+) +[buggy 41e5d45] Comment 211 + 1 file changed, 1 insertion(+) +[buggy fd4ab0a] Comment 212 + 1 file changed, 1 insertion(+) +[buggy ab1b66f] Comment 213 + 1 file changed, 1 insertion(+) +[buggy 5302d90] Comment 214 + 1 file changed, 1 insertion(+) +[buggy ba1db02] Comment 215 + 1 file changed, 1 insertion(+) +[buggy ebfd21a] Comment 216 + 1 file changed, 1 insertion(+) +[buggy 5782fc4] Comment 217 + 1 file changed, 1 insertion(+) +[buggy a9e06a3] Comment 218 + 1 file changed, 1 insertion(+) +[buggy e1c5f69] Comment 219 + 1 file changed, 1 insertion(+) +[buggy 48d6cee] Comment 220 + 1 file changed, 1 insertion(+) +[buggy 1d56cac] Comment 221 + 1 file changed, 1 insertion(+) +[buggy 4b15667] Comment 222 + 1 file changed, 1 insertion(+) +[buggy dbd21b3] Comment 223 + 1 file changed, 1 insertion(+) +[buggy 6e377c3] Comment 224 + 1 file changed, 1 insertion(+) +[buggy 4ad32bc] Comment 225 + 1 file changed, 1 insertion(+) +[buggy a616382] Comment 226 + 1 file changed, 1 insertion(+) +[buggy da5bf5d] Comment 227 + 1 file changed, 1 insertion(+) +[buggy dd8c68b] Comment 228 + 1 file changed, 1 insertion(+) +[buggy f8a3d21] Comment 229 + 1 file changed, 1 insertion(+) +[buggy f9a2829] Comment 230 + 1 file changed, 1 insertion(+) +[buggy 82a7ce3] Comment 231 + 1 file changed, 1 insertion(+) +[buggy 99c336a] Comment 232 + 1 file changed, 1 insertion(+) +[buggy 3801a75] Comment 233 + 1 file changed, 1 insertion(+) +[buggy ba22d21] Comment 234 + 1 file changed, 1 insertion(+) +[buggy 9c76402] Comment 235 + 1 file changed, 1 insertion(+) +[buggy 4b7c0a8] Comment 236 + 1 file changed, 1 insertion(+) +[buggy 0dda780] Comment 237 + 1 file changed, 1 insertion(+) +[buggy 0d434e6] Comment 238 + 1 file changed, 1 insertion(+) +[buggy e9a51d6] Comment 239 + 1 file changed, 1 insertion(+) +[buggy 898bbaf] Comment 240 + 1 file changed, 1 insertion(+) +[buggy b41b7ae] Comment 241 + 1 file changed, 1 insertion(+) +[buggy cc3aa6a] Comment 242 + 1 file changed, 1 insertion(+) +[buggy 8e120fb] Comment 243 + 1 file changed, 1 insertion(+) +[buggy 15c01d1] Comment 244 + 1 file changed, 1 insertion(+) +[buggy a238de3] Comment 245 + 1 file changed, 1 insertion(+) +[buggy e6824b9] Comment 246 + 1 file changed, 1 insertion(+) +[buggy 83f4c15] Comment 247 + 1 file changed, 1 insertion(+) +[buggy 51f00f4] Comment 248 + 1 file changed, 1 insertion(+) +[buggy 6a1b00d] Comment 249 + 1 file changed, 1 insertion(+) +[buggy 0ad67c8] Comment 250 + 1 file changed, 1 insertion(+) +[buggy d2ac1ed] Comment 251 + 1 file changed, 1 insertion(+) +[buggy 987bf78] Comment 252 + 1 file changed, 1 insertion(+) +[buggy 3ba6e67] Comment 253 + 1 file changed, 1 insertion(+) +[buggy 0c395ac] Comment 254 + 1 file changed, 1 insertion(+) +[buggy ffc9419] Comment 255 + 1 file changed, 1 insertion(+) +[buggy ae3a3af] Comment 256 + 1 file changed, 1 insertion(+) +[buggy b3a4ef8] Comment 257 + 1 file changed, 1 insertion(+) +[buggy 062e583] Comment 258 + 1 file changed, 1 insertion(+) +[buggy 846a595] Comment 259 + 1 file changed, 1 insertion(+) +[buggy f3b9b2f] Comment 260 + 1 file changed, 1 insertion(+) +[buggy a6e5e6b] Comment 261 + 1 file changed, 1 insertion(+) +[buggy 33c1a1a] Comment 262 + 1 file changed, 1 insertion(+) +[buggy 46e100e] Comment 263 + 1 file changed, 1 insertion(+) +[buggy 5fd71ba] Comment 264 + 1 file changed, 1 insertion(+) +[buggy 65efbc8] Comment 265 + 1 file changed, 1 insertion(+) +[buggy a37f89f] Comment 266 + 1 file changed, 1 insertion(+) +[buggy 1e38300] Comment 267 + 1 file changed, 1 insertion(+) +[buggy 2ccaabf] Comment 268 + 1 file changed, 1 insertion(+) +[buggy 59a570b] Comment 269 + 1 file changed, 1 insertion(+) +[buggy 9d40f7d] Comment 270 + 1 file changed, 1 insertion(+) +[buggy b28dfc3] Comment 271 + 1 file changed, 1 insertion(+) +[buggy c50babe] Comment 272 + 1 file changed, 1 insertion(+) +[buggy 69d37dd] Comment 273 + 1 file changed, 1 insertion(+) +[buggy d847d45] Comment 274 + 1 file changed, 1 insertion(+) +[buggy 35eae03] Comment 275 + 1 file changed, 1 insertion(+) +[buggy ed1319c] Comment 276 + 1 file changed, 1 insertion(+) +[buggy 9943ea9] Comment 277 + 1 file changed, 1 insertion(+) +[buggy 114c833] Comment 278 + 1 file changed, 1 insertion(+) +[buggy 242fdb6] Comment 279 + 1 file changed, 1 insertion(+) +[buggy 85cfbcd] Comment 280 + 1 file changed, 1 insertion(+) +[buggy 21cac1a] Comment 281 + 1 file changed, 1 insertion(+) +[buggy ce47eb4] Comment 282 + 1 file changed, 1 insertion(+) +[buggy dd40162] Comment 283 + 1 file changed, 1 insertion(+) +[buggy cf4ec1d] Comment 284 + 1 file changed, 1 insertion(+) +[buggy 65c31da] Comment 285 + 1 file changed, 1 insertion(+) +[buggy 77a5b4d] Comment 286 + 1 file changed, 1 insertion(+) +[buggy 075e37f] Comment 287 + 1 file changed, 1 insertion(+) +[buggy 1f5245f] Comment 288 + 1 file changed, 1 insertion(+) +[buggy 454bc4e] Comment 289 + 1 file changed, 1 insertion(+) +[buggy 1571b34] Comment 290 + 1 file changed, 1 insertion(+) +[buggy b3c007d] Comment 291 + 1 file changed, 1 insertion(+) +[buggy e438ed5] Comment 292 + 1 file changed, 1 insertion(+) +[buggy ee23d8c] Comment 293 + 1 file changed, 1 insertion(+) +[buggy 0705cc6] Comment 294 + 1 file changed, 1 insertion(+) +[buggy cca0014] Comment 295 + 1 file changed, 1 insertion(+) +[buggy a24bf6f] Comment 296 + 1 file changed, 1 insertion(+) +[buggy 874e61e] Comment 297 + 1 file changed, 1 insertion(+) +[buggy 2de528f] Comment 298 + 1 file changed, 1 insertion(+) +[buggy b0cc1c8] Comment 299 + 1 file changed, 1 insertion(+) +[buggy b1c26ae] Comment 300 + 1 file changed, 1 insertion(+) +[buggy e0dd4dc] Comment 301 + 1 file changed, 1 insertion(+) +[buggy 342a752] Comment 302 + 1 file changed, 1 insertion(+) +[buggy 7923ec4] Comment 303 + 1 file changed, 1 insertion(+) +[buggy 3903869] Comment 304 + 1 file changed, 1 insertion(+) +[buggy caf5e04] Comment 305 + 1 file changed, 1 insertion(+) +[buggy 690f629] Comment 306 + 1 file changed, 1 insertion(+) +[buggy 19f8053] Comment 307 + 1 file changed, 1 insertion(+) +[buggy 416a468] Comment 308 + 1 file changed, 1 insertion(+) +[buggy 8dbc1d0] Comment 309 + 1 file changed, 1 insertion(+) +[buggy f0a59b6] Comment 310 + 1 file changed, 1 insertion(+) +[buggy 0e0201c] Comment 311 + 1 file changed, 1 insertion(+) +[buggy 2c88f27] Comment 312 + 1 file changed, 1 insertion(+) +Auto-merging squares.py +[buggy fd03a1b] Breaking argument type + Author: Shawn Siefkas + Date: Thu Nov 14 09:23:55 2013 -0600 + 1 file changed, 1 insertion(+), 1 deletion(-) +[buggy 1760454] Comment 313 + 1 file changed, 1 insertion(+) +[buggy 6a9ff4a] Comment 314 + 1 file changed, 1 insertion(+) +[buggy 3ce80f5] Comment 315 + 1 file changed, 1 insertion(+) +[buggy 1340f99] Comment 316 + 1 file changed, 1 insertion(+) +[buggy ef83cdb] Comment 317 + 1 file changed, 1 insertion(+) +[buggy 2634695] Comment 318 + 1 file changed, 1 insertion(+) +[buggy 610d973] Comment 319 + 1 file changed, 1 insertion(+) +[buggy 89daa0e] Comment 320 + 1 file changed, 1 insertion(+) +[buggy 2701c6a] Comment 321 + 1 file changed, 1 insertion(+) +[buggy b2b3ad9] Comment 322 + 1 file changed, 1 insertion(+) +[buggy fa5891e] Comment 323 + 1 file changed, 1 insertion(+) +[buggy 4aa122c] Comment 324 + 1 file changed, 1 insertion(+) +[buggy 5da608d] Comment 325 + 1 file changed, 1 insertion(+) +[buggy 4189ae9] Comment 326 + 1 file changed, 1 insertion(+) +[buggy 8249828] Comment 327 + 1 file changed, 1 insertion(+) +[buggy 424f1cc] Comment 328 + 1 file changed, 1 insertion(+) +[buggy 4ee67c9] Comment 329 + 1 file changed, 1 insertion(+) +[buggy 17a2b53] Comment 330 + 1 file changed, 1 insertion(+) +[buggy 6bc153b] Comment 331 + 1 file changed, 1 insertion(+) +[buggy 2cafba9] Comment 332 + 1 file changed, 1 insertion(+) +[buggy c1cfd1e] Comment 333 + 1 file changed, 1 insertion(+) +[buggy eab96cc] Comment 334 + 1 file changed, 1 insertion(+) +[buggy d79fca4] Comment 335 + 1 file changed, 1 insertion(+) +[buggy 0240dbe] Comment 336 + 1 file changed, 1 insertion(+) +[buggy a564b0b] Comment 337 + 1 file changed, 1 insertion(+) +[buggy 55959e9] Comment 338 + 1 file changed, 1 insertion(+) +[buggy d7ead0b] Comment 339 + 1 file changed, 1 insertion(+) +[buggy df820ed] Comment 340 + 1 file changed, 1 insertion(+) +[buggy 80751f1] Comment 341 + 1 file changed, 1 insertion(+) +[buggy 0c03498] Comment 342 + 1 file changed, 1 insertion(+) +[buggy 8a01467] Comment 343 + 1 file changed, 1 insertion(+) +[buggy 9b5e7d6] Comment 344 + 1 file changed, 1 insertion(+) +[buggy 79e2b09] Comment 345 + 1 file changed, 1 insertion(+) +[buggy 0ea5a1f] Comment 346 + 1 file changed, 1 insertion(+) +[buggy 76ee8f6] Comment 347 + 1 file changed, 1 insertion(+) +[buggy 695ab4c] Comment 348 + 1 file changed, 1 insertion(+) +[buggy df3d2d0] Comment 349 + 1 file changed, 1 insertion(+) +[buggy cbbe235] Comment 350 + 1 file changed, 1 insertion(+) +[buggy 74d9067] Comment 351 + 1 file changed, 1 insertion(+) +[buggy 1ae483b] Comment 352 + 1 file changed, 1 insertion(+) +[buggy 0cd5c5c] Comment 353 + 1 file changed, 1 insertion(+) +[buggy fb44584] Comment 354 + 1 file changed, 1 insertion(+) +[buggy 0a44770] Comment 355 + 1 file changed, 1 insertion(+) +[buggy fc8089a] Comment 356 + 1 file changed, 1 insertion(+) +[buggy c40dca8] Comment 357 + 1 file changed, 1 insertion(+) +[buggy 0a12b9b] Comment 358 + 1 file changed, 1 insertion(+) +[buggy b9062ba] Comment 359 + 1 file changed, 1 insertion(+) +[buggy 9f3f84d] Comment 360 + 1 file changed, 1 insertion(+) +[buggy 569c96c] Comment 361 + 1 file changed, 1 insertion(+) +[buggy ed737cd] Comment 362 + 1 file changed, 1 insertion(+) +[buggy c13d04a] Comment 363 + 1 file changed, 1 insertion(+) +[buggy 61c8667] Comment 364 + 1 file changed, 1 insertion(+) +[buggy ec23705] Comment 365 + 1 file changed, 1 insertion(+) +[buggy 32a3819] Comment 366 + 1 file changed, 1 insertion(+) +[buggy 0c9e006] Comment 367 + 1 file changed, 1 insertion(+) +[buggy 9b4da28] Comment 368 + 1 file changed, 1 insertion(+) +[buggy 9010d55] Comment 369 + 1 file changed, 1 insertion(+) +[buggy 730b26b] Comment 370 + 1 file changed, 1 insertion(+) +[buggy c484d91] Comment 371 + 1 file changed, 1 insertion(+) +[buggy feb7e0b] Comment 372 + 1 file changed, 1 insertion(+) +[buggy 1a37c98] Comment 373 + 1 file changed, 1 insertion(+) +[buggy d3fdbc4] Comment 374 + 1 file changed, 1 insertion(+) +[buggy ecda4fd] Comment 375 + 1 file changed, 1 insertion(+) +[buggy 9d1eab9] Comment 376 + 1 file changed, 1 insertion(+) +[buggy 27e31cd] Comment 377 + 1 file changed, 1 insertion(+) +[buggy 1868402] Comment 378 + 1 file changed, 1 insertion(+) +[buggy df5f022] Comment 379 + 1 file changed, 1 insertion(+) +[buggy 2930890] Comment 380 + 1 file changed, 1 insertion(+) +[buggy 2817c03] Comment 381 + 1 file changed, 1 insertion(+) +[buggy 62743d2] Comment 382 + 1 file changed, 1 insertion(+) +[buggy de14729] Comment 383 + 1 file changed, 1 insertion(+) +[buggy 0bbb7d2] Comment 384 + 1 file changed, 1 insertion(+) +[buggy 0d3fa80] Comment 385 + 1 file changed, 1 insertion(+) +[buggy 5b59f8f] Comment 386 + 1 file changed, 1 insertion(+) +[buggy 833f7e4] Comment 387 + 1 file changed, 1 insertion(+) +[buggy 9aad8ef] Comment 388 + 1 file changed, 1 insertion(+) +[buggy 6844450] Comment 389 + 1 file changed, 1 insertion(+) +[buggy 09a8cff] Comment 390 + 1 file changed, 1 insertion(+) +[buggy 80aaba4] Comment 391 + 1 file changed, 1 insertion(+) +[buggy 496deae] Comment 392 + 1 file changed, 1 insertion(+) +[buggy 658ede6] Comment 393 + 1 file changed, 1 insertion(+) +[buggy ec916e4] Comment 394 + 1 file changed, 1 insertion(+) +[buggy d9eecf8] Comment 395 + 1 file changed, 1 insertion(+) +[buggy 0e01196] Comment 396 + 1 file changed, 1 insertion(+) +[buggy 10a7b59] Comment 397 + 1 file changed, 1 insertion(+) +[buggy 51c6b3c] Comment 398 + 1 file changed, 1 insertion(+) +[buggy 928ca99] Comment 399 + 1 file changed, 1 insertion(+) +[buggy f523153] Comment 400 + 1 file changed, 1 insertion(+) +[buggy 875e693] Comment 401 + 1 file changed, 1 insertion(+) +[buggy 48c8f37] Comment 402 + 1 file changed, 1 insertion(+) +[buggy 1b57734] Comment 403 + 1 file changed, 1 insertion(+) +[buggy 4863ba3] Comment 404 + 1 file changed, 1 insertion(+) +[buggy 60ae201] Comment 405 + 1 file changed, 1 insertion(+) +[buggy af1c448] Comment 406 + 1 file changed, 1 insertion(+) +[buggy 118550f] Comment 407 + 1 file changed, 1 insertion(+) +[buggy 53924d2] Comment 408 + 1 file changed, 1 insertion(+) +[buggy ba52a65] Comment 409 + 1 file changed, 1 insertion(+) +[buggy 0739c0d] Comment 410 + 1 file changed, 1 insertion(+) +[buggy 03893b5] Comment 411 + 1 file changed, 1 insertion(+) +[buggy 3b3e104] Comment 412 + 1 file changed, 1 insertion(+) +[buggy 5c22d1e] Comment 413 + 1 file changed, 1 insertion(+) +[buggy e9720c8] Comment 414 + 1 file changed, 1 insertion(+) +[buggy 48425ff] Comment 415 + 1 file changed, 1 insertion(+) +[buggy da88dc7] Comment 416 + 1 file changed, 1 insertion(+) +[buggy a6a19fa] Comment 417 + 1 file changed, 1 insertion(+) +[buggy 56964c0] Comment 418 + 1 file changed, 1 insertion(+) +[buggy d80b46d] Comment 419 + 1 file changed, 1 insertion(+) +[buggy 552222f] Comment 420 + 1 file changed, 1 insertion(+) +[buggy c7f9301] Comment 421 + 1 file changed, 1 insertion(+) +[buggy c6e96aa] Comment 422 + 1 file changed, 1 insertion(+) +[buggy af42fd7] Comment 423 + 1 file changed, 1 insertion(+) +[buggy 134a175] Comment 424 + 1 file changed, 1 insertion(+) +[buggy db76da8] Comment 425 + 1 file changed, 1 insertion(+) +[buggy 9f7f56c] Comment 426 + 1 file changed, 1 insertion(+) +[buggy ed3d616] Comment 427 + 1 file changed, 1 insertion(+) +[buggy d2ce7d4] Comment 428 + 1 file changed, 1 insertion(+) +[buggy f4d7bce] Comment 429 + 1 file changed, 1 insertion(+) +[buggy f10f10c] Comment 430 + 1 file changed, 1 insertion(+) +[buggy 33444d4] Comment 431 + 1 file changed, 1 insertion(+) +[buggy 2bcb6f7] Comment 432 + 1 file changed, 1 insertion(+) +[buggy 5316021] Comment 433 + 1 file changed, 1 insertion(+) +[buggy 0e890e2] Comment 434 + 1 file changed, 1 insertion(+) +[buggy aedd87f] Comment 435 + 1 file changed, 1 insertion(+) +[buggy f5fff4f] Comment 436 + 1 file changed, 1 insertion(+) +[buggy 0501b62] Comment 437 + 1 file changed, 1 insertion(+) +[buggy 8bd514c] Comment 438 + 1 file changed, 1 insertion(+) +[buggy 93e979d] Comment 439 + 1 file changed, 1 insertion(+) +[buggy 216e3b3] Comment 440 + 1 file changed, 1 insertion(+) +[buggy 9e7f05c] Comment 441 + 1 file changed, 1 insertion(+) +[buggy 00627bf] Comment 442 + 1 file changed, 1 insertion(+) +[buggy 6ad6c5c] Comment 443 + 1 file changed, 1 insertion(+) +[buggy b537f17] Comment 444 + 1 file changed, 1 insertion(+) +[buggy fe3ed32] Comment 445 + 1 file changed, 1 insertion(+) +[buggy 9a87aee] Comment 446 + 1 file changed, 1 insertion(+) +[buggy 07a603a] Comment 447 + 1 file changed, 1 insertion(+) +[buggy 96eb36e] Comment 448 + 1 file changed, 1 insertion(+) +[buggy b82136a] Comment 449 + 1 file changed, 1 insertion(+) +[buggy 4fc50d2] Comment 450 + 1 file changed, 1 insertion(+) +[buggy f27ee7a] Comment 451 + 1 file changed, 1 insertion(+) +[buggy 10cad4a] Comment 452 + 1 file changed, 1 insertion(+) +[buggy 5c92682] Comment 453 + 1 file changed, 1 insertion(+) +[buggy f82b811] Comment 454 + 1 file changed, 1 insertion(+) +[buggy 7965072] Comment 455 + 1 file changed, 1 insertion(+) +[buggy b6c5b07] Comment 456 + 1 file changed, 1 insertion(+) +[buggy 87e21d4] Comment 457 + 1 file changed, 1 insertion(+) +[buggy 298ba3c] Comment 458 + 1 file changed, 1 insertion(+) +[buggy 417cf60] Comment 459 + 1 file changed, 1 insertion(+) +[buggy 9ccf8a3] Comment 460 + 1 file changed, 1 insertion(+) +[buggy ead9243] Comment 461 + 1 file changed, 1 insertion(+) +[buggy a231072] Comment 462 + 1 file changed, 1 insertion(+) +[buggy b5e2b47] Comment 463 + 1 file changed, 1 insertion(+) +[buggy 64a0082] Comment 464 + 1 file changed, 1 insertion(+) +[buggy 2728007] Comment 465 + 1 file changed, 1 insertion(+) +[buggy 9e635dc] Comment 466 + 1 file changed, 1 insertion(+) +[buggy 488ba17] Comment 467 + 1 file changed, 1 insertion(+) +[buggy e7aee39] Comment 468 + 1 file changed, 1 insertion(+) +[buggy 2ae3604] Comment 469 + 1 file changed, 1 insertion(+) +[buggy 778b522] Comment 470 + 1 file changed, 1 insertion(+) +[buggy c9f6d52] Comment 471 + 1 file changed, 1 insertion(+) +[buggy 34a09ff] Comment 472 + 1 file changed, 1 insertion(+) +[buggy 2210801] Comment 473 + 1 file changed, 1 insertion(+) +[buggy 4b299bb] Comment 474 + 1 file changed, 1 insertion(+) +[buggy 4bdcb5c] Comment 475 + 1 file changed, 1 insertion(+) +[buggy 025bc29] Comment 476 + 1 file changed, 1 insertion(+) +[buggy 72b2bdc] Comment 477 + 1 file changed, 1 insertion(+) +[buggy 2e33921] Comment 478 + 1 file changed, 1 insertion(+) +[buggy f56e6e8] Comment 479 + 1 file changed, 1 insertion(+) +[buggy 83f2eb1] Comment 480 + 1 file changed, 1 insertion(+) +[buggy d4bd9cc] Comment 481 + 1 file changed, 1 insertion(+) +[buggy 9149bfe] Comment 482 + 1 file changed, 1 insertion(+) +[buggy 630e8cb] Comment 483 + 1 file changed, 1 insertion(+) +[buggy bdf2efe] Comment 484 + 1 file changed, 1 insertion(+) +[buggy c3c439b] Comment 485 + 1 file changed, 1 insertion(+) +[buggy e541017] Comment 486 + 1 file changed, 1 insertion(+) +[buggy f3ca112] Comment 487 + 1 file changed, 1 insertion(+) +[buggy 0936aaf] Comment 488 + 1 file changed, 1 insertion(+) +[buggy 4fe7787] Comment 489 + 1 file changed, 1 insertion(+) +[buggy 49ea60a] Comment 490 + 1 file changed, 1 insertion(+) +[buggy 532428c] Comment 491 + 1 file changed, 1 insertion(+) +[buggy 881fefe] Comment 492 + 1 file changed, 1 insertion(+) +[buggy 589c6d5] Comment 493 + 1 file changed, 1 insertion(+) +[buggy db6ea70] Comment 494 + 1 file changed, 1 insertion(+) +[buggy f56f7ef] Comment 495 + 1 file changed, 1 insertion(+) +[buggy 2faac08] Comment 496 + 1 file changed, 1 insertion(+) +[buggy e70e558] Comment 497 + 1 file changed, 1 insertion(+) +[buggy 51b3526] Comment 498 + 1 file changed, 1 insertion(+) +[buggy d3d5c7b] Comment 499 + 1 file changed, 1 insertion(+) +[buggy 18d5935] Comment 500 + 1 file changed, 1 insertion(+) +[buggy d51ed99] Comment 501 + 1 file changed, 1 insertion(+) +[buggy 7aa5887] Comment 502 + 1 file changed, 1 insertion(+) +[buggy 68d90d6] Comment 503 + 1 file changed, 1 insertion(+) +[buggy 95dc392] Comment 504 + 1 file changed, 1 insertion(+) +[buggy c02a1a8] Comment 505 + 1 file changed, 1 insertion(+) +[buggy a0e14b7] Comment 506 + 1 file changed, 1 insertion(+) +[buggy 1aae135] Comment 507 + 1 file changed, 1 insertion(+) +[buggy 963fe09] Comment 508 + 1 file changed, 1 insertion(+) +[buggy 5f273fe] Comment 509 + 1 file changed, 1 insertion(+) +[buggy 778ed88] Comment 510 + 1 file changed, 1 insertion(+) +[buggy 453df54] Comment 511 + 1 file changed, 1 insertion(+) +[buggy ced0436] Comment 512 + 1 file changed, 1 insertion(+) +[buggy 51503c8] Comment 513 + 1 file changed, 1 insertion(+) +[buggy 54e37da] Comment 514 + 1 file changed, 1 insertion(+) +[buggy cc6278e] Comment 515 + 1 file changed, 1 insertion(+) +[buggy 73675b7] Comment 516 + 1 file changed, 1 insertion(+) +[buggy 96a4b99] Comment 517 + 1 file changed, 1 insertion(+) +[buggy 061146f] Comment 518 + 1 file changed, 1 insertion(+) +[buggy 35204a2] Comment 519 + 1 file changed, 1 insertion(+) +[buggy f630b99] Comment 520 + 1 file changed, 1 insertion(+) +[buggy 192c3c9] Comment 521 + 1 file changed, 1 insertion(+) +[buggy f3d71ba] Comment 522 + 1 file changed, 1 insertion(+) +[buggy 341b6d0] Comment 523 + 1 file changed, 1 insertion(+) +[buggy 2ce0a62] Comment 524 + 1 file changed, 1 insertion(+) +[buggy 62491bd] Comment 525 + 1 file changed, 1 insertion(+) +[buggy 825eda3] Comment 526 + 1 file changed, 1 insertion(+) +[buggy 8ebe078] Comment 527 + 1 file changed, 1 insertion(+) +[buggy 0aefc98] Comment 528 + 1 file changed, 1 insertion(+) +[buggy fb552ac] Comment 529 + 1 file changed, 1 insertion(+) +[buggy 79ff3ee] Comment 530 + 1 file changed, 1 insertion(+) +[buggy dce164e] Comment 531 + 1 file changed, 1 insertion(+) +[buggy 14e8b1f] Comment 532 + 1 file changed, 1 insertion(+) +[buggy 8e0e2d3] Comment 533 + 1 file changed, 1 insertion(+) +[buggy 968640c] Comment 534 + 1 file changed, 1 insertion(+) +[buggy d4e37f7] Comment 535 + 1 file changed, 1 insertion(+) +[buggy 4f57ff2] Comment 536 + 1 file changed, 1 insertion(+) +[buggy 62a8034] Comment 537 + 1 file changed, 1 insertion(+) +[buggy f8248b5] Comment 538 + 1 file changed, 1 insertion(+) +[buggy e0c580c] Comment 539 + 1 file changed, 1 insertion(+) +[buggy a60d0a6] Comment 540 + 1 file changed, 1 insertion(+) +[buggy 00f1833] Comment 541 + 1 file changed, 1 insertion(+) +[buggy 100003c] Comment 542 + 1 file changed, 1 insertion(+) +[buggy 46e6ea8] Comment 543 + 1 file changed, 1 insertion(+) +[buggy f26710b] Comment 544 + 1 file changed, 1 insertion(+) +[buggy c0f3d5a] Comment 545 + 1 file changed, 1 insertion(+) +[buggy d990b48] Comment 546 + 1 file changed, 1 insertion(+) +[buggy ca7bb67] Comment 547 + 1 file changed, 1 insertion(+) +[buggy 87435b3] Comment 548 + 1 file changed, 1 insertion(+) +[buggy 681a496] Comment 549 + 1 file changed, 1 insertion(+) +[buggy 037e547] Comment 550 + 1 file changed, 1 insertion(+) +[buggy c190a11] Comment 551 + 1 file changed, 1 insertion(+) +[buggy 72584cd] Comment 552 + 1 file changed, 1 insertion(+) +[buggy d3cfad1] Comment 553 + 1 file changed, 1 insertion(+) +[buggy 5c563b0] Comment 554 + 1 file changed, 1 insertion(+) +[buggy 7e24ac3] Comment 555 + 1 file changed, 1 insertion(+) +[buggy 04a1aa7] Comment 556 + 1 file changed, 1 insertion(+) +[buggy e352831] Comment 557 + 1 file changed, 1 insertion(+) +[buggy ce224cd] Comment 558 + 1 file changed, 1 insertion(+) +[buggy 8a7ab03] Comment 559 + 1 file changed, 1 insertion(+) +[buggy 30f1c3b] Comment 560 + 1 file changed, 1 insertion(+) +[buggy 17f467a] Comment 561 + 1 file changed, 1 insertion(+) +[buggy 16c491c] Comment 562 + 1 file changed, 1 insertion(+) +[buggy 441cbab] Comment 563 + 1 file changed, 1 insertion(+) +[buggy f69db8c] Comment 564 + 1 file changed, 1 insertion(+) +[buggy e0e915d] Comment 565 + 1 file changed, 1 insertion(+) +[buggy 3c4a7d0] Comment 566 + 1 file changed, 1 insertion(+) +[buggy 62f7c4f] Comment 567 + 1 file changed, 1 insertion(+) +[buggy aac6d39] Comment 568 + 1 file changed, 1 insertion(+) +[buggy 97a793f] Comment 569 + 1 file changed, 1 insertion(+) +[buggy 611a2ad] Comment 570 + 1 file changed, 1 insertion(+) +[buggy a1755dd] Comment 571 + 1 file changed, 1 insertion(+) +[buggy f0a8707] Comment 572 + 1 file changed, 1 insertion(+) +[buggy a7087a6] Comment 573 + 1 file changed, 1 insertion(+) +[buggy 1c37d54] Comment 574 + 1 file changed, 1 insertion(+) +[buggy cb0167b] Comment 575 + 1 file changed, 1 insertion(+) +[buggy 2b847e2] Comment 576 + 1 file changed, 1 insertion(+) +[buggy 48c525f] Comment 577 + 1 file changed, 1 insertion(+) +[buggy d375de2] Comment 578 + 1 file changed, 1 insertion(+) +[buggy 193b29e] Comment 579 + 1 file changed, 1 insertion(+) +[buggy 7f3ae0e] Comment 580 + 1 file changed, 1 insertion(+) +[buggy 7b17125] Comment 581 + 1 file changed, 1 insertion(+) +[buggy e961f7f] Comment 582 + 1 file changed, 1 insertion(+) +[buggy 564612f] Comment 583 + 1 file changed, 1 insertion(+) +[buggy b201a75] Comment 584 + 1 file changed, 1 insertion(+) +[buggy 8b6ddce] Comment 585 + 1 file changed, 1 insertion(+) +[buggy 9763b9a] Comment 586 + 1 file changed, 1 insertion(+) +[buggy 5411cc2] Comment 587 + 1 file changed, 1 insertion(+) +[buggy c1fd49d] Comment 588 + 1 file changed, 1 insertion(+) +[buggy 9b9e38b] Comment 589 + 1 file changed, 1 insertion(+) +[buggy 2ebc2f0] Comment 590 + 1 file changed, 1 insertion(+) +[buggy b310708] Comment 591 + 1 file changed, 1 insertion(+) +[buggy cc0278f] Comment 592 + 1 file changed, 1 insertion(+) +[buggy dc4d6f3] Comment 593 + 1 file changed, 1 insertion(+) +[buggy 953475b] Comment 594 + 1 file changed, 1 insertion(+) +[buggy ca920f0] Comment 595 + 1 file changed, 1 insertion(+) +[buggy ef773e0] Comment 596 + 1 file changed, 1 insertion(+) +[buggy 4f201f3] Comment 597 + 1 file changed, 1 insertion(+) +[buggy 57e636d] Comment 598 + 1 file changed, 1 insertion(+) +[buggy 0eac64e] Comment 599 + 1 file changed, 1 insertion(+) +[buggy e7badbe] Comment 600 + 1 file changed, 1 insertion(+) +[buggy d869ca9] Comment 601 + 1 file changed, 1 insertion(+) +[buggy 82b995b] Comment 602 + 1 file changed, 1 insertion(+) +[buggy e8a1bce] Comment 603 + 1 file changed, 1 insertion(+) +[buggy 3fc1951] Comment 604 + 1 file changed, 1 insertion(+) +[buggy f0dcc58] Comment 605 + 1 file changed, 1 insertion(+) +[buggy 6799d81] Comment 606 + 1 file changed, 1 insertion(+) +[buggy 2282a7c] Comment 607 + 1 file changed, 1 insertion(+) +[buggy f2decd3] Comment 608 + 1 file changed, 1 insertion(+) +[buggy 7f0b6c7] Comment 609 + 1 file changed, 1 insertion(+) +[buggy c8d6b68] Comment 610 + 1 file changed, 1 insertion(+) +[buggy 5d2c6cb] Comment 611 + 1 file changed, 1 insertion(+) +[buggy aed88f9] Comment 612 + 1 file changed, 1 insertion(+) +[buggy 76ce2cd] Comment 613 + 1 file changed, 1 insertion(+) +[buggy 166e845] Comment 614 + 1 file changed, 1 insertion(+) +[buggy 1417565] Comment 615 + 1 file changed, 1 insertion(+) +[buggy 67341f6] Comment 616 + 1 file changed, 1 insertion(+) +[buggy 421130b] Comment 617 + 1 file changed, 1 insertion(+) +[buggy dc66638] Comment 618 + 1 file changed, 1 insertion(+) +[buggy ac1046b] Comment 619 + 1 file changed, 1 insertion(+) +[buggy 0697b69] Comment 620 + 1 file changed, 1 insertion(+) +[buggy 412a013] Comment 621 + 1 file changed, 1 insertion(+) +[buggy 8ce5087] Comment 622 + 1 file changed, 1 insertion(+) +[buggy 76181f7] Comment 623 + 1 file changed, 1 insertion(+) +[buggy 8c84de6] Comment 624 + 1 file changed, 1 insertion(+) +[buggy 227d90c] Comment 625 + 1 file changed, 1 insertion(+) +[buggy c24da47] Comment 626 + 1 file changed, 1 insertion(+) +[buggy 7eabb95] Comment 627 + 1 file changed, 1 insertion(+) +[buggy 4b58b32] Comment 628 + 1 file changed, 1 insertion(+) +[buggy 24fbdfd] Comment 629 + 1 file changed, 1 insertion(+) +[buggy 1b3aa81] Comment 630 + 1 file changed, 1 insertion(+) +[buggy 6d3a6ac] Comment 631 + 1 file changed, 1 insertion(+) +[buggy 67ba90e] Comment 632 + 1 file changed, 1 insertion(+) +[buggy 81a30c4] Comment 633 + 1 file changed, 1 insertion(+) +[buggy 2efa501] Comment 634 + 1 file changed, 1 insertion(+) +[buggy d5d3623] Comment 635 + 1 file changed, 1 insertion(+) +[buggy d58e1a9] Comment 636 + 1 file changed, 1 insertion(+) +[buggy 9f1c5fe] Comment 637 + 1 file changed, 1 insertion(+) +[buggy 86ebad0] Comment 638 + 1 file changed, 1 insertion(+) +[buggy 2d34ee9] Comment 639 + 1 file changed, 1 insertion(+) +[buggy 4183494] Comment 640 + 1 file changed, 1 insertion(+) +[buggy 8dd8186] Comment 641 + 1 file changed, 1 insertion(+) +[buggy b9285bb] Comment 642 + 1 file changed, 1 insertion(+) +[buggy 5734cde] Comment 643 + 1 file changed, 1 insertion(+) +[buggy fc93f89] Comment 644 + 1 file changed, 1 insertion(+) +[buggy e935e21] Comment 645 + 1 file changed, 1 insertion(+) +[buggy eb67ce3] Comment 646 + 1 file changed, 1 insertion(+) +[buggy 0d946d8] Comment 647 + 1 file changed, 1 insertion(+) +[buggy 59efbd0] Comment 648 + 1 file changed, 1 insertion(+) +[buggy bfa4f9b] Comment 649 + 1 file changed, 1 insertion(+) +[buggy 7cd1f9b] Comment 650 + 1 file changed, 1 insertion(+) +[buggy f0c1b03] Comment 651 + 1 file changed, 1 insertion(+) +[buggy 28d081d] Comment 652 + 1 file changed, 1 insertion(+) +[buggy 53497e3] Comment 653 + 1 file changed, 1 insertion(+) +[buggy 2decb34] Comment 654 + 1 file changed, 1 insertion(+) +[buggy faa73e4] Comment 655 + 1 file changed, 1 insertion(+) +[buggy f744d47] Comment 656 + 1 file changed, 1 insertion(+) +[buggy cf4167b] Comment 657 + 1 file changed, 1 insertion(+) +[buggy 7c92707] Comment 658 + 1 file changed, 1 insertion(+) +[buggy f27bf17] Comment 659 + 1 file changed, 1 insertion(+) +[buggy f84e55f] Comment 660 + 1 file changed, 1 insertion(+) +[buggy e8238ab] Comment 661 + 1 file changed, 1 insertion(+) +[buggy 8e58b52] Comment 662 + 1 file changed, 1 insertion(+) +[buggy 9c44e91] Comment 663 + 1 file changed, 1 insertion(+) +[buggy be145cf] Comment 664 + 1 file changed, 1 insertion(+) +[buggy d99db94] Comment 665 + 1 file changed, 1 insertion(+) +[buggy 4212984] Comment 666 + 1 file changed, 1 insertion(+) +[buggy 30b3e9f] Comment 667 + 1 file changed, 1 insertion(+) +[buggy 36ad257] Comment 668 + 1 file changed, 1 insertion(+) +[buggy 544d6cb] Comment 669 + 1 file changed, 1 insertion(+) +[buggy 4834134] Comment 670 + 1 file changed, 1 insertion(+) +[buggy a467a5d] Comment 671 + 1 file changed, 1 insertion(+) +[buggy 4752d25] Comment 672 + 1 file changed, 1 insertion(+) +[buggy a945150] Comment 673 + 1 file changed, 1 insertion(+) +[buggy 2eeb44e] Comment 674 + 1 file changed, 1 insertion(+) +[buggy 33d866a] Comment 675 + 1 file changed, 1 insertion(+) +[buggy dd44095] Comment 676 + 1 file changed, 1 insertion(+) +[buggy 9e5fb0c] Comment 677 + 1 file changed, 1 insertion(+) +[buggy 3524797] Comment 678 + 1 file changed, 1 insertion(+) +[buggy 340c6c8] Comment 679 + 1 file changed, 1 insertion(+) +[buggy 49224bd] Comment 680 + 1 file changed, 1 insertion(+) +[buggy 04e6d39] Comment 681 + 1 file changed, 1 insertion(+) +[buggy 8019a42] Comment 682 + 1 file changed, 1 insertion(+) +[buggy 2e556af] Comment 683 + 1 file changed, 1 insertion(+) +[buggy e7217eb] Comment 684 + 1 file changed, 1 insertion(+) +[buggy 78dc6c0] Comment 685 + 1 file changed, 1 insertion(+) +[buggy 749dcea] Comment 686 + 1 file changed, 1 insertion(+) +[buggy 3bd6803] Comment 687 + 1 file changed, 1 insertion(+) +[buggy b523414] Comment 688 + 1 file changed, 1 insertion(+) +[buggy 9a1e719] Comment 689 + 1 file changed, 1 insertion(+) +[buggy 58f99f6] Comment 690 + 1 file changed, 1 insertion(+) +[buggy 9ec2dad] Comment 691 + 1 file changed, 1 insertion(+) +[buggy 0648b56] Comment 692 + 1 file changed, 1 insertion(+) +[buggy 061eeb1] Comment 693 + 1 file changed, 1 insertion(+) +[buggy ff1a0e2] Comment 694 + 1 file changed, 1 insertion(+) +[buggy 1d91b7e] Comment 695 + 1 file changed, 1 insertion(+) +[buggy 3e1f287] Comment 696 + 1 file changed, 1 insertion(+) +[buggy 2777e87] Comment 697 + 1 file changed, 1 insertion(+) +[buggy 39de608] Comment 698 + 1 file changed, 1 insertion(+) +[buggy 8f1b1a4] Comment 699 + 1 file changed, 1 insertion(+) +[buggy 66014a8] Comment 700 + 1 file changed, 1 insertion(+) +[buggy 8bea7bd] Comment 701 + 1 file changed, 1 insertion(+) +[buggy 7c0117a] Comment 702 + 1 file changed, 1 insertion(+) +[buggy a13015f] Comment 703 + 1 file changed, 1 insertion(+) +[buggy 979b3e1] Comment 704 + 1 file changed, 1 insertion(+) +[buggy 12e431e] Comment 705 + 1 file changed, 1 insertion(+) +[buggy c930c8b] Comment 706 + 1 file changed, 1 insertion(+) +[buggy a776562] Comment 707 + 1 file changed, 1 insertion(+) +[buggy b7bfbba] Comment 708 + 1 file changed, 1 insertion(+) +[buggy e19f3c7] Comment 709 + 1 file changed, 1 insertion(+) +[buggy ab42c62] Comment 710 + 1 file changed, 1 insertion(+) +[buggy 3a35a8f] Comment 711 + 1 file changed, 1 insertion(+) +[buggy eb9a7dc] Comment 712 + 1 file changed, 1 insertion(+) +[buggy a77577a] Comment 713 + 1 file changed, 1 insertion(+) +[buggy a4e07ea] Comment 714 + 1 file changed, 1 insertion(+) +[buggy 52ec749] Comment 715 + 1 file changed, 1 insertion(+) +[buggy d0c9ade] Comment 716 + 1 file changed, 1 insertion(+) +[buggy bbf1584] Comment 717 + 1 file changed, 1 insertion(+) +[buggy c64586a] Comment 718 + 1 file changed, 1 insertion(+) +[buggy 34ab181] Comment 719 + 1 file changed, 1 insertion(+) +[buggy 5a014fd] Comment 720 + 1 file changed, 1 insertion(+) +[buggy 5a0b0ed] Comment 721 + 1 file changed, 1 insertion(+) +[buggy b27db94] Comment 722 + 1 file changed, 1 insertion(+) +[buggy 947355c] Comment 723 + 1 file changed, 1 insertion(+) +[buggy bd6273b] Comment 724 + 1 file changed, 1 insertion(+) +[buggy f2ae139] Comment 725 + 1 file changed, 1 insertion(+) +[buggy 6ebb44c] Comment 726 + 1 file changed, 1 insertion(+) +[buggy a5b6ef5] Comment 727 + 1 file changed, 1 insertion(+) +[buggy ca6ec76] Comment 728 + 1 file changed, 1 insertion(+) +[buggy 7d32914] Comment 729 + 1 file changed, 1 insertion(+) +[buggy 9242980] Comment 730 + 1 file changed, 1 insertion(+) +[buggy e1bc900] Comment 731 + 1 file changed, 1 insertion(+) +[buggy 9621a54] Comment 732 + 1 file changed, 1 insertion(+) +[buggy 625d188] Comment 733 + 1 file changed, 1 insertion(+) +[buggy c725a6d] Comment 734 + 1 file changed, 1 insertion(+) +[buggy 0e08ef4] Comment 735 + 1 file changed, 1 insertion(+) +[buggy 77cbfca] Comment 736 + 1 file changed, 1 insertion(+) +[buggy 034a300] Comment 737 + 1 file changed, 1 insertion(+) +[buggy 6739e79] Comment 738 + 1 file changed, 1 insertion(+) +[buggy 50c9acc] Comment 739 + 1 file changed, 1 insertion(+) +[buggy ac312ab] Comment 740 + 1 file changed, 1 insertion(+) +[buggy 569a8b4] Comment 741 + 1 file changed, 1 insertion(+) +[buggy bd8b62f] Comment 742 + 1 file changed, 1 insertion(+) +[buggy 551e759] Comment 743 + 1 file changed, 1 insertion(+) +[buggy dbcae50] Comment 744 + 1 file changed, 1 insertion(+) +[buggy b47bce0] Comment 745 + 1 file changed, 1 insertion(+) +[buggy 3732e32] Comment 746 + 1 file changed, 1 insertion(+) +[buggy 9535cdd] Comment 747 + 1 file changed, 1 insertion(+) +[buggy 4266f89] Comment 748 + 1 file changed, 1 insertion(+) +[buggy 5b15d84] Comment 749 + 1 file changed, 1 insertion(+) +[buggy 133d3be] Comment 750 + 1 file changed, 1 insertion(+) +[buggy f71c39f] Comment 751 + 1 file changed, 1 insertion(+) +[buggy eadfa49] Comment 752 + 1 file changed, 1 insertion(+) +[buggy 6bd1c9a] Comment 753 + 1 file changed, 1 insertion(+) +[buggy 29d786b] Comment 754 + 1 file changed, 1 insertion(+) +[buggy f889c67] Comment 755 + 1 file changed, 1 insertion(+) +[buggy 2eb7839] Comment 756 + 1 file changed, 1 insertion(+) +[buggy f16e175] Comment 757 + 1 file changed, 1 insertion(+) +[buggy 55bff6b] Comment 758 + 1 file changed, 1 insertion(+) +[buggy 1395e44] Comment 759 + 1 file changed, 1 insertion(+) +[buggy 3666a53] Comment 760 + 1 file changed, 1 insertion(+) +[buggy ce1c608] Comment 761 + 1 file changed, 1 insertion(+) +[buggy 8b76689] Comment 762 + 1 file changed, 1 insertion(+) +[buggy b313627] Comment 763 + 1 file changed, 1 insertion(+) +[buggy 394c850] Comment 764 + 1 file changed, 1 insertion(+) +[buggy 6770ec8] Comment 765 + 1 file changed, 1 insertion(+) +[buggy fa112a5] Comment 766 + 1 file changed, 1 insertion(+) +[buggy 016fa2b] Comment 767 + 1 file changed, 1 insertion(+) +[buggy b2f042b] Comment 768 + 1 file changed, 1 insertion(+) +[buggy 39be5a2] Comment 769 + 1 file changed, 1 insertion(+) +[buggy 20f0940] Comment 770 + 1 file changed, 1 insertion(+) +[buggy 4e280a6] Comment 771 + 1 file changed, 1 insertion(+) +[buggy f9e08f5] Comment 772 + 1 file changed, 1 insertion(+) +[buggy 997a2b9] Comment 773 + 1 file changed, 1 insertion(+) +[buggy dbc20b6] Comment 774 + 1 file changed, 1 insertion(+) +[buggy 48ea692] Comment 775 + 1 file changed, 1 insertion(+) +[buggy 7803851] Comment 776 + 1 file changed, 1 insertion(+) +[buggy 330a962] Comment 777 + 1 file changed, 1 insertion(+) +[buggy 9d67666] Comment 778 + 1 file changed, 1 insertion(+) +[buggy 158e36e] Comment 779 + 1 file changed, 1 insertion(+) +[buggy 4a2eae6] Comment 780 + 1 file changed, 1 insertion(+) +[buggy 8d564ce] Comment 781 + 1 file changed, 1 insertion(+) +[buggy 45e7561] Comment 782 + 1 file changed, 1 insertion(+) +[buggy 199eea0] Comment 783 + 1 file changed, 1 insertion(+) +[buggy bccfd09] Comment 784 + 1 file changed, 1 insertion(+) +[buggy 9c08dd7] Comment 785 + 1 file changed, 1 insertion(+) +[buggy 8bf3025] Comment 786 + 1 file changed, 1 insertion(+) +[buggy 08b89f8] Comment 787 + 1 file changed, 1 insertion(+) +[buggy f9ac352] Comment 788 + 1 file changed, 1 insertion(+) +[buggy 959a26e] Comment 789 + 1 file changed, 1 insertion(+) +[buggy 240901a] Comment 790 + 1 file changed, 1 insertion(+) +[buggy ae8cfb8] Comment 791 + 1 file changed, 1 insertion(+) +[buggy 4b33093] Comment 792 + 1 file changed, 1 insertion(+) +[buggy 0db3773] Comment 793 + 1 file changed, 1 insertion(+) +[buggy 3338efd] Comment 794 + 1 file changed, 1 insertion(+) +[buggy 3245356] Comment 795 + 1 file changed, 1 insertion(+) +[buggy 5d42859] Comment 796 + 1 file changed, 1 insertion(+) +[buggy 1eb7f92] Comment 797 + 1 file changed, 1 insertion(+) +[buggy 115af13] Comment 798 + 1 file changed, 1 insertion(+) +[buggy e8c579b] Comment 799 + 1 file changed, 1 insertion(+) +[buggy f7a751c] Comment 800 + 1 file changed, 1 insertion(+) +[buggy 8af6fd0] Comment 801 + 1 file changed, 1 insertion(+) +[buggy 527beb2] Comment 802 + 1 file changed, 1 insertion(+) +[buggy 08f0bd8] Comment 803 + 1 file changed, 1 insertion(+) +[buggy 5e026b1] Comment 804 + 1 file changed, 1 insertion(+) +[buggy 0419910] Comment 805 + 1 file changed, 1 insertion(+) +[buggy 87bc273] Comment 806 + 1 file changed, 1 insertion(+) +[buggy ec77adc] Comment 807 + 1 file changed, 1 insertion(+) +[buggy 02cc37c] Comment 808 + 1 file changed, 1 insertion(+) +[buggy 4854063] Comment 809 + 1 file changed, 1 insertion(+) +[buggy a656a50] Comment 810 + 1 file changed, 1 insertion(+) +[buggy 7a6a683] Comment 811 + 1 file changed, 1 insertion(+) +[buggy 56f5e6a] Comment 812 + 1 file changed, 1 insertion(+) +[buggy 11bf4d5] Comment 813 + 1 file changed, 1 insertion(+) +[buggy 2b5f954] Comment 814 + 1 file changed, 1 insertion(+) +[buggy 73e1acc] Comment 815 + 1 file changed, 1 insertion(+) +[buggy cc5a79d] Comment 816 + 1 file changed, 1 insertion(+) +[buggy ccfaf3a] Comment 817 + 1 file changed, 1 insertion(+) +[buggy 9a67456] Comment 818 + 1 file changed, 1 insertion(+) +[buggy 6f41f9a] Comment 819 + 1 file changed, 1 insertion(+) +[buggy bfac78e] Comment 820 + 1 file changed, 1 insertion(+) +[buggy 18004c0] Comment 821 + 1 file changed, 1 insertion(+) +[buggy 598d9d3] Comment 822 + 1 file changed, 1 insertion(+) +[buggy db01ba5] Comment 823 + 1 file changed, 1 insertion(+) +[buggy eddeead] Comment 824 + 1 file changed, 1 insertion(+) +[buggy e457d4a] Comment 825 + 1 file changed, 1 insertion(+) +[buggy d56f18b] Comment 826 + 1 file changed, 1 insertion(+) +[buggy ca4d7b6] Comment 827 + 1 file changed, 1 insertion(+) +[buggy 49b29d9] Comment 828 + 1 file changed, 1 insertion(+) +[buggy 0fb2f83] Comment 829 + 1 file changed, 1 insertion(+) +[buggy 5c9b4ca] Comment 830 + 1 file changed, 1 insertion(+) +[buggy 99e26c8] Comment 831 + 1 file changed, 1 insertion(+) +[buggy d1d6cd0] Comment 832 + 1 file changed, 1 insertion(+) +[buggy 8d92e77] Comment 833 + 1 file changed, 1 insertion(+) +[buggy c159e5f] Comment 834 + 1 file changed, 1 insertion(+) +[buggy 2fc317a] Comment 835 + 1 file changed, 1 insertion(+) +[buggy e1c42a0] Comment 836 + 1 file changed, 1 insertion(+) +[buggy 702b238] Comment 837 + 1 file changed, 1 insertion(+) +[buggy ed656e2] Comment 838 + 1 file changed, 1 insertion(+) +[buggy 2780510] Comment 839 + 1 file changed, 1 insertion(+) +[buggy 37d9e4c] Comment 840 + 1 file changed, 1 insertion(+) +[buggy 51f5cd3] Comment 841 + 1 file changed, 1 insertion(+) +[buggy d35ad5d] Comment 842 + 1 file changed, 1 insertion(+) +[buggy a97d4e0] Comment 843 + 1 file changed, 1 insertion(+) +[buggy 2498019] Comment 844 + 1 file changed, 1 insertion(+) +[buggy 6803714] Comment 845 + 1 file changed, 1 insertion(+) +[buggy 2c3c76e] Comment 846 + 1 file changed, 1 insertion(+) +[buggy 9d632f5] Comment 847 + 1 file changed, 1 insertion(+) +[buggy c1b21d8] Comment 848 + 1 file changed, 1 insertion(+) +[buggy 1ab6681] Comment 849 + 1 file changed, 1 insertion(+) +[buggy b29c6cf] Comment 850 + 1 file changed, 1 insertion(+) +[buggy 45b9ee0] Comment 851 + 1 file changed, 1 insertion(+) +[buggy 40a74ec] Comment 852 + 1 file changed, 1 insertion(+) +[buggy 9c5af81] Comment 853 + 1 file changed, 1 insertion(+) +[buggy 5764a77] Comment 854 + 1 file changed, 1 insertion(+) +[buggy e1b2900] Comment 855 + 1 file changed, 1 insertion(+) +[buggy 000bb98] Comment 856 + 1 file changed, 1 insertion(+) +[buggy dbc1e56] Comment 857 + 1 file changed, 1 insertion(+) +[buggy b1b4492] Comment 858 + 1 file changed, 1 insertion(+) +[buggy f8d9e96] Comment 859 + 1 file changed, 1 insertion(+) +[buggy fd4f03e] Comment 860 + 1 file changed, 1 insertion(+) +[buggy a49434a] Comment 861 + 1 file changed, 1 insertion(+) +[buggy f9868ee] Comment 862 + 1 file changed, 1 insertion(+) +[buggy c391990] Comment 863 + 1 file changed, 1 insertion(+) +[buggy da7310c] Comment 864 + 1 file changed, 1 insertion(+) +[buggy 1a3787c] Comment 865 + 1 file changed, 1 insertion(+) +[buggy 0d040c9] Comment 866 + 1 file changed, 1 insertion(+) +[buggy 65912a6] Comment 867 + 1 file changed, 1 insertion(+) +[buggy 47bb158] Comment 868 + 1 file changed, 1 insertion(+) +[buggy 3473c66] Comment 869 + 1 file changed, 1 insertion(+) +[buggy 236d44f] Comment 870 + 1 file changed, 1 insertion(+) +[buggy 20abe4c] Comment 871 + 1 file changed, 1 insertion(+) +[buggy aea5ba0] Comment 872 + 1 file changed, 1 insertion(+) +[buggy 811dd72] Comment 873 + 1 file changed, 1 insertion(+) +[buggy b3ed0e3] Comment 874 + 1 file changed, 1 insertion(+) +[buggy c696c6e] Comment 875 + 1 file changed, 1 insertion(+) +[buggy 8b5f99e] Comment 876 + 1 file changed, 1 insertion(+) +[buggy e9d67b8] Comment 877 + 1 file changed, 1 insertion(+) +[buggy 444b8b8] Comment 878 + 1 file changed, 1 insertion(+) +[buggy ded792d] Comment 879 + 1 file changed, 1 insertion(+) +[buggy e1d1325] Comment 880 + 1 file changed, 1 insertion(+) +[buggy 8894b69] Comment 881 + 1 file changed, 1 insertion(+) +[buggy 7fa14a7] Comment 882 + 1 file changed, 1 insertion(+) +[buggy 6dd5f73] Comment 883 + 1 file changed, 1 insertion(+) +[buggy ab1a2b1] Comment 884 + 1 file changed, 1 insertion(+) +[buggy ef1e77f] Comment 885 + 1 file changed, 1 insertion(+) +[buggy 5dd025f] Comment 886 + 1 file changed, 1 insertion(+) +[buggy 0fd12e8] Comment 887 + 1 file changed, 1 insertion(+) +[buggy 0499609] Comment 888 + 1 file changed, 1 insertion(+) +[buggy 2f9e859] Comment 889 + 1 file changed, 1 insertion(+) +[buggy 86c150c] Comment 890 + 1 file changed, 1 insertion(+) +[buggy c6e4944] Comment 891 + 1 file changed, 1 insertion(+) +[buggy 1f2094b] Comment 892 + 1 file changed, 1 insertion(+) +[buggy d791027] Comment 893 + 1 file changed, 1 insertion(+) +[buggy 2a38939] Comment 894 + 1 file changed, 1 insertion(+) +[buggy 6196d1e] Comment 895 + 1 file changed, 1 insertion(+) +[buggy 01404f3] Comment 896 + 1 file changed, 1 insertion(+) +[buggy 15acef1] Comment 897 + 1 file changed, 1 insertion(+) +[buggy 1a95f02] Comment 898 + 1 file changed, 1 insertion(+) +[buggy 4cc4848] Comment 899 + 1 file changed, 1 insertion(+) +[buggy 04cd2eb] Comment 900 + 1 file changed, 1 insertion(+) +[buggy 3c89d4f] Comment 901 + 1 file changed, 1 insertion(+) +[buggy 253f4c4] Comment 902 + 1 file changed, 1 insertion(+) +[buggy 115a04b] Comment 903 + 1 file changed, 1 insertion(+) +[buggy e145f3b] Comment 904 + 1 file changed, 1 insertion(+) +[buggy e4138f8] Comment 905 + 1 file changed, 1 insertion(+) +[buggy 8853153] Comment 906 + 1 file changed, 1 insertion(+) +[buggy eddee3b] Comment 907 + 1 file changed, 1 insertion(+) +[buggy 9f9d2de] Comment 908 + 1 file changed, 1 insertion(+) +[buggy 8cfd6dc] Comment 909 + 1 file changed, 1 insertion(+) +[buggy d5b67c1] Comment 910 + 1 file changed, 1 insertion(+) +[buggy dedbd4b] Comment 911 + 1 file changed, 1 insertion(+) +[buggy d522ad3] Comment 912 + 1 file changed, 1 insertion(+) +[buggy 7801e7f] Comment 913 + 1 file changed, 1 insertion(+) +[buggy 0fda570] Comment 914 + 1 file changed, 1 insertion(+) +[buggy c2ea1eb] Comment 915 + 1 file changed, 1 insertion(+) +[buggy 4202873] Comment 916 + 1 file changed, 1 insertion(+) +[buggy bbd3a15] Comment 917 + 1 file changed, 1 insertion(+) +[buggy 6f79ba1] Comment 918 + 1 file changed, 1 insertion(+) +[buggy d38be0d] Comment 919 + 1 file changed, 1 insertion(+) +[buggy 3b69cc3] Comment 920 + 1 file changed, 1 insertion(+) +[buggy 196adf0] Comment 921 + 1 file changed, 1 insertion(+) +[buggy 4b73c49] Comment 922 + 1 file changed, 1 insertion(+) +[buggy 6f881e3] Comment 923 + 1 file changed, 1 insertion(+) +[buggy ea5b660] Comment 924 + 1 file changed, 1 insertion(+) +[buggy 11bdb7c] Comment 925 + 1 file changed, 1 insertion(+) +[buggy b6f0e94] Comment 926 + 1 file changed, 1 insertion(+) +[buggy 0c36a9f] Comment 927 + 1 file changed, 1 insertion(+) +[buggy 5e0ba5b] Comment 928 + 1 file changed, 1 insertion(+) +[buggy 3e1eb41] Comment 929 + 1 file changed, 1 insertion(+) +[buggy 3e35c32] Comment 930 + 1 file changed, 1 insertion(+) +[buggy fbc395c] Comment 931 + 1 file changed, 1 insertion(+) +[buggy b6b7dae] Comment 932 + 1 file changed, 1 insertion(+) +[buggy e588e1e] Comment 933 + 1 file changed, 1 insertion(+) +[buggy 17c4e21] Comment 934 + 1 file changed, 1 insertion(+) +[buggy df1bcff] Comment 935 + 1 file changed, 1 insertion(+) +[buggy 0b3fbf3] Comment 936 + 1 file changed, 1 insertion(+) +[buggy 1b3c24a] Comment 937 + 1 file changed, 1 insertion(+) +[buggy 2e88b3c] Comment 938 + 1 file changed, 1 insertion(+) +[buggy 2d93520] Comment 939 + 1 file changed, 1 insertion(+) +[buggy 6617e22] Comment 940 + 1 file changed, 1 insertion(+) +[buggy fe1f6af] Comment 941 + 1 file changed, 1 insertion(+) +[buggy 7d37bd4] Comment 942 + 1 file changed, 1 insertion(+) +[buggy 284d0a9] Comment 943 + 1 file changed, 1 insertion(+) +[buggy e53373c] Comment 944 + 1 file changed, 1 insertion(+) +[buggy dab1fe6] Comment 945 + 1 file changed, 1 insertion(+) +[buggy 5201a32] Comment 946 + 1 file changed, 1 insertion(+) +[buggy c76baec] Comment 947 + 1 file changed, 1 insertion(+) +[buggy df9e925] Comment 948 + 1 file changed, 1 insertion(+) +[buggy a05a4ae] Comment 949 + 1 file changed, 1 insertion(+) +[buggy 08d41d5] Comment 950 + 1 file changed, 1 insertion(+) +[buggy cb6c0c8] Comment 951 + 1 file changed, 1 insertion(+) +[buggy 5a5ca20] Comment 952 + 1 file changed, 1 insertion(+) +[buggy ec9d366] Comment 953 + 1 file changed, 1 insertion(+) +[buggy d5be58c] Comment 954 + 1 file changed, 1 insertion(+) +[buggy b5e5f6a] Comment 955 + 1 file changed, 1 insertion(+) +[buggy fad0e5b] Comment 956 + 1 file changed, 1 insertion(+) +[buggy 82f66be] Comment 957 + 1 file changed, 1 insertion(+) +[buggy 7ddec55] Comment 958 + 1 file changed, 1 insertion(+) +[buggy b6d769f] Comment 959 + 1 file changed, 1 insertion(+) +[buggy bcc711c] Comment 960 + 1 file changed, 1 insertion(+) +[buggy 8ca7743] Comment 961 + 1 file changed, 1 insertion(+) +[buggy 03b813f] Comment 962 + 1 file changed, 1 insertion(+) +[buggy aad0005] Comment 963 + 1 file changed, 1 insertion(+) +[buggy 50c6107] Comment 964 + 1 file changed, 1 insertion(+) +[buggy 14a6670] Comment 965 + 1 file changed, 1 insertion(+) +[buggy aef6d79] Comment 966 + 1 file changed, 1 insertion(+) +[buggy 4dcd2cb] Comment 967 + 1 file changed, 1 insertion(+) +[buggy 8797ec9] Comment 968 + 1 file changed, 1 insertion(+) +[buggy 5abaf07] Comment 969 + 1 file changed, 1 insertion(+) +[buggy 7963766] Comment 970 + 1 file changed, 1 insertion(+) +[buggy 7bd58e8] Comment 971 + 1 file changed, 1 insertion(+) +[buggy 1e1b466] Comment 972 + 1 file changed, 1 insertion(+) +[buggy ad4fd47] Comment 973 + 1 file changed, 1 insertion(+) +[buggy 29062ef] Comment 974 + 1 file changed, 1 insertion(+) +[buggy 5db5325] Comment 975 + 1 file changed, 1 insertion(+) +[buggy feca313] Comment 976 + 1 file changed, 1 insertion(+) +[buggy a68e00a] Comment 977 + 1 file changed, 1 insertion(+) +[buggy 58e61ed] Comment 978 + 1 file changed, 1 insertion(+) +[buggy be903ec] Comment 979 + 1 file changed, 1 insertion(+) +[buggy 2decd48] Comment 980 + 1 file changed, 1 insertion(+) +[buggy b52498e] Comment 981 + 1 file changed, 1 insertion(+) +[buggy e7fdda0] Comment 982 + 1 file changed, 1 insertion(+) +[buggy c2849d2] Comment 983 + 1 file changed, 1 insertion(+) +[buggy 71fa12e] Comment 984 + 1 file changed, 1 insertion(+) +[buggy d0f4500] Comment 985 + 1 file changed, 1 insertion(+) +[buggy 1bcf4ea] Comment 986 + 1 file changed, 1 insertion(+) +[buggy b39c4ec] Comment 987 + 1 file changed, 1 insertion(+) +[buggy d0426ca] Comment 988 + 1 file changed, 1 insertion(+) +[buggy c373e82] Comment 989 + 1 file changed, 1 insertion(+) +[buggy ba26ad7] Comment 990 + 1 file changed, 1 insertion(+) +[buggy 37eb433] Comment 991 + 1 file changed, 1 insertion(+) +[buggy 27d80dd] Comment 992 + 1 file changed, 1 insertion(+) +[buggy 2ff4c6d] Comment 993 + 1 file changed, 1 insertion(+) +[buggy bc5c120] Comment 994 + 1 file changed, 1 insertion(+) +[buggy 607d66d] Comment 995 + 1 file changed, 1 insertion(+) +[buggy ca1540f] Comment 996 + 1 file changed, 1 insertion(+) +[buggy 18ceba8] Comment 997 + 1 file changed, 1 insertion(+) +[buggy 9f2845a] Comment 998 + 1 file changed, 1 insertion(+) +[buggy a308578] Comment 999 + 1 file changed, 1 insertion(+) +[buggy 020885a] Comment 1000 + 1 file changed, 1 insertion(+) diff --git a/ch00git/learning_git/bisectdemo/breakme.sh b/ch00git/learning_git/bisectdemo/breakme.sh new file mode 100755 index 000000000..5b42b2f18 --- /dev/null +++ b/ch00git/learning_git/bisectdemo/breakme.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# +# Creates 1000 dummy comment commits and randomly inserts a bug among those. +# This can be used to demo the value of the git bisect command. + +function comment { + COMMENT="Comment $1" + echo "#$COMMENT" >> squares.py + git add squares.py + git commit -m "$COMMENT" +} + +BREAKINDEX=$((RANDOM%1000)) + +if git show-ref -q --heads buggy; then + git branch -D buggy +fi +git checkout -b buggy + +for i in $(seq 1 $BREAKINDEX); do + comment $i +done + +git cherry-pick origin/broken + +for i in $(seq $((BREAKINDEX+1)) 1000); do + comment $i +done + diff --git a/ch00git/learning_git/bisectdemo/gitbisect.out b/ch00git/learning_git/bisectdemo/gitbisect.out new file mode 100644 index 000000000..5d1b7ef55 --- /dev/null +++ b/ch00git/learning_git/bisectdemo/gitbisect.out @@ -0,0 +1,72 @@ +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 249 revisions left to test after this (roughly 8 steps) +[0ad67c82d7913110ab7b4a041bdfb3e57230be1a] Comment 250 +running 'python' 'squares.py' '2' +4 +Bisecting: 124 revisions left to test after this (roughly 7 steps) +[d3fdbc4533c5e312222427be427d17ee206344c2] Comment 374 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 62 revisions left to test after this (roughly 6 steps) +[2c88f27bdac158fa6fe45e747db29d084a9d06a8] Comment 312 +running 'python' 'squares.py' '2' +4 +Bisecting: 31 revisions left to test after this (roughly 5 steps) +[0c034989a425904cda9481d07a02b3e363563338] Comment 342 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 15 revisions left to test after this (roughly 4 steps) +[4189ae93787e6b79eb56047dfe6149715e2843fe] Comment 326 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 7 revisions left to test after this (roughly 3 steps) +[26346951d7fc0fd2d008fe23bcf1a2b702aae112] Comment 318 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 3 revisions left to test after this (roughly 2 steps) +[6a9ff4af9516543c6f62c04327fb83f39ed26925] Comment 314 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 0 revisions left to test after this (roughly 1 step) +[17604547f1da4c33937335221d6753e2d18388b8] Comment 313 +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +Bisecting: 0 revisions left to test after this (roughly 0 steps) +[fd03a1bcc29411c50223547ff7f86570a0e91953] Breaking argument type +running 'python' 'squares.py' '2' +Traceback (most recent call last): + File "squares.py", line 9, in + print(integer**2) +TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int' +fd03a1bcc29411c50223547ff7f86570a0e91953 is the first bad commit +commit fd03a1bcc29411c50223547ff7f86570a0e91953 +Author: Shawn Siefkas +Date: Thu Nov 14 09:23:55 2013 -0600 + + Breaking argument type + + squares.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +bisect found first bad commit diff --git a/ch00git/learning_git/bisectdemo/squares.py b/ch00git/learning_git/bisectdemo/squares.py new file mode 100644 index 000000000..ce687dd77 --- /dev/null +++ b/ch00git/learning_git/bisectdemo/squares.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import sys + +integer = sys.argv[1] + +print(integer**2) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +#Comment 1 +#Comment 2 +#Comment 3 +#Comment 4 +#Comment 5 +#Comment 6 +#Comment 7 +#Comment 8 +#Comment 9 +#Comment 10 +#Comment 11 +#Comment 12 +#Comment 13 +#Comment 14 +#Comment 15 +#Comment 16 +#Comment 17 +#Comment 18 +#Comment 19 +#Comment 20 +#Comment 21 +#Comment 22 +#Comment 23 +#Comment 24 +#Comment 25 +#Comment 26 +#Comment 27 +#Comment 28 +#Comment 29 +#Comment 30 +#Comment 31 +#Comment 32 +#Comment 33 +#Comment 34 +#Comment 35 +#Comment 36 +#Comment 37 +#Comment 38 +#Comment 39 +#Comment 40 +#Comment 41 +#Comment 42 +#Comment 43 +#Comment 44 +#Comment 45 +#Comment 46 +#Comment 47 +#Comment 48 +#Comment 49 +#Comment 50 +#Comment 51 +#Comment 52 +#Comment 53 +#Comment 54 +#Comment 55 +#Comment 56 +#Comment 57 +#Comment 58 +#Comment 59 +#Comment 60 +#Comment 61 +#Comment 62 +#Comment 63 +#Comment 64 +#Comment 65 +#Comment 66 +#Comment 67 +#Comment 68 +#Comment 69 +#Comment 70 +#Comment 71 +#Comment 72 +#Comment 73 +#Comment 74 +#Comment 75 +#Comment 76 +#Comment 77 +#Comment 78 +#Comment 79 +#Comment 80 +#Comment 81 +#Comment 82 +#Comment 83 +#Comment 84 +#Comment 85 +#Comment 86 +#Comment 87 +#Comment 88 +#Comment 89 +#Comment 90 +#Comment 91 +#Comment 92 +#Comment 93 +#Comment 94 +#Comment 95 +#Comment 96 +#Comment 97 +#Comment 98 +#Comment 99 +#Comment 100 +#Comment 101 +#Comment 102 +#Comment 103 +#Comment 104 +#Comment 105 +#Comment 106 +#Comment 107 +#Comment 108 +#Comment 109 +#Comment 110 +#Comment 111 +#Comment 112 +#Comment 113 +#Comment 114 +#Comment 115 +#Comment 116 +#Comment 117 +#Comment 118 +#Comment 119 +#Comment 120 +#Comment 121 +#Comment 122 +#Comment 123 +#Comment 124 +#Comment 125 +#Comment 126 +#Comment 127 +#Comment 128 +#Comment 129 +#Comment 130 +#Comment 131 +#Comment 132 +#Comment 133 +#Comment 134 +#Comment 135 +#Comment 136 +#Comment 137 +#Comment 138 +#Comment 139 +#Comment 140 +#Comment 141 +#Comment 142 +#Comment 143 +#Comment 144 +#Comment 145 +#Comment 146 +#Comment 147 +#Comment 148 +#Comment 149 +#Comment 150 +#Comment 151 +#Comment 152 +#Comment 153 +#Comment 154 +#Comment 155 +#Comment 156 +#Comment 157 +#Comment 158 +#Comment 159 +#Comment 160 +#Comment 161 +#Comment 162 +#Comment 163 +#Comment 164 +#Comment 165 +#Comment 166 +#Comment 167 +#Comment 168 +#Comment 169 +#Comment 170 +#Comment 171 +#Comment 172 +#Comment 173 +#Comment 174 +#Comment 175 +#Comment 176 +#Comment 177 +#Comment 178 +#Comment 179 +#Comment 180 +#Comment 181 +#Comment 182 +#Comment 183 +#Comment 184 +#Comment 185 +#Comment 186 +#Comment 187 +#Comment 188 +#Comment 189 +#Comment 190 +#Comment 191 +#Comment 192 +#Comment 193 +#Comment 194 +#Comment 195 +#Comment 196 +#Comment 197 +#Comment 198 +#Comment 199 +#Comment 200 +#Comment 201 +#Comment 202 +#Comment 203 +#Comment 204 +#Comment 205 +#Comment 206 +#Comment 207 +#Comment 208 +#Comment 209 +#Comment 210 +#Comment 211 +#Comment 212 +#Comment 213 +#Comment 214 +#Comment 215 +#Comment 216 +#Comment 217 +#Comment 218 +#Comment 219 +#Comment 220 +#Comment 221 +#Comment 222 +#Comment 223 +#Comment 224 +#Comment 225 +#Comment 226 +#Comment 227 +#Comment 228 +#Comment 229 +#Comment 230 +#Comment 231 +#Comment 232 +#Comment 233 +#Comment 234 +#Comment 235 +#Comment 236 +#Comment 237 +#Comment 238 +#Comment 239 +#Comment 240 +#Comment 241 +#Comment 242 +#Comment 243 +#Comment 244 +#Comment 245 +#Comment 246 +#Comment 247 +#Comment 248 +#Comment 249 +#Comment 250 +#Comment 251 +#Comment 252 +#Comment 253 +#Comment 254 +#Comment 255 +#Comment 256 +#Comment 257 +#Comment 258 +#Comment 259 +#Comment 260 +#Comment 261 +#Comment 262 +#Comment 263 +#Comment 264 +#Comment 265 +#Comment 266 +#Comment 267 +#Comment 268 +#Comment 269 +#Comment 270 +#Comment 271 +#Comment 272 +#Comment 273 +#Comment 274 +#Comment 275 +#Comment 276 +#Comment 277 +#Comment 278 +#Comment 279 +#Comment 280 +#Comment 281 +#Comment 282 +#Comment 283 +#Comment 284 +#Comment 285 +#Comment 286 +#Comment 287 +#Comment 288 +#Comment 289 +#Comment 290 +#Comment 291 +#Comment 292 +#Comment 293 +#Comment 294 +#Comment 295 +#Comment 296 +#Comment 297 +#Comment 298 +#Comment 299 +#Comment 300 +#Comment 301 +#Comment 302 +#Comment 303 +#Comment 304 +#Comment 305 +#Comment 306 +#Comment 307 +#Comment 308 +#Comment 309 +#Comment 310 +#Comment 311 +#Comment 312 diff --git a/ch00git/learning_git/git_example/Makefile b/ch00git/learning_git/git_example/Makefile new file mode 100644 index 000000000..99c927cbd --- /dev/null +++ b/ch00git/learning_git/git_example/Makefile @@ -0,0 +1,8 @@ + +MDS=$(wildcard *.md) +PDFS=$(MDS:.md=.pdf) + +default: $(PDFS) + +%.pdf: %.md + pandoc $< -o $@ diff --git a/ch00git/learning_git/git_example/Pennines.md b/ch00git/learning_git/git_example/Pennines.md new file mode 100644 index 000000000..bfbe7ced0 --- /dev/null +++ b/ch00git/learning_git/git_example/Pennines.md @@ -0,0 +1,6 @@ + +Mountains In the Pennines +======================== + +* Cross Fell +* Whernside diff --git a/ch00git/learning_git/git_example/Scotland.md b/ch00git/learning_git/git_example/Scotland.md new file mode 100644 index 000000000..bf5c6439c --- /dev/null +++ b/ch00git/learning_git/git_example/Scotland.md @@ -0,0 +1,6 @@ +Mountains In Scotland +================== + +* Ben Eighe +* Cairngorm +* Aonach Eagach diff --git a/ch00git/learning_git/git_example/Wales.md b/ch00git/learning_git/git_example/Wales.md new file mode 100644 index 000000000..d8c838427 --- /dev/null +++ b/ch00git/learning_git/git_example/Wales.md @@ -0,0 +1,9 @@ +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr +* Fan y Big +* Cadair Idris diff --git a/ch00git/learning_git/git_example/index.html b/ch00git/learning_git/git_example/index.html new file mode 100644 index 000000000..7d94891ac --- /dev/null +++ b/ch00git/learning_git/git_example/index.html @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Github Pages Example + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Mountains and Lakes in the UK

+ +

Engerland is not very mountainous. +But has some tall hills, and maybe a mountain or two depending on your definition.

+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch00git/learning_git/git_example/lakeland.md b/ch00git/learning_git/git_example/lakeland.md new file mode 100644 index 000000000..7cb0c9491 --- /dev/null +++ b/ch00git/learning_git/git_example/lakeland.md @@ -0,0 +1,7 @@ +Lakeland +======== + +Cumbria has some pretty hills, and lakes too + +Mountains: +* Helvellyn diff --git a/ch00git/learning_git/git_example/wsd.py b/ch00git/learning_git/git_example/wsd.py new file mode 100644 index 000000000..400078605 --- /dev/null +++ b/ch00git/learning_git/git_example/wsd.py @@ -0,0 +1,17 @@ +import requests +import re +import IPython + +def wsd(code): + response = requests.post("http://www.websequencediagrams.com/index.php", data={ + 'message': code, + 'apiVersion': 1, + }) + expr = re.compile("(\?(img|pdf|png|svg)=[a-zA-Z0-9]+)") + m = expr.search(response.text) + if m == None: + print("Invalid response from server.") + return False + + image=requests.get("http://www.websequencediagrams.com/" + m.group(0)) + return IPython.core.display.Image(image.content) diff --git a/ch00git/learning_git/partner_repo/Scotland.md b/ch00git/learning_git/partner_repo/Scotland.md new file mode 100644 index 000000000..9613dda6c --- /dev/null +++ b/ch00git/learning_git/partner_repo/Scotland.md @@ -0,0 +1,5 @@ +Mountains In Scotland +================== + +* Ben Eighe +* Cairngorm diff --git a/ch00git/learning_git/partner_repo/Wales.md b/ch00git/learning_git/partner_repo/Wales.md new file mode 100644 index 000000000..21daae21f --- /dev/null +++ b/ch00git/learning_git/partner_repo/Wales.md @@ -0,0 +1,8 @@ +Mountains In Wales +================== + +* Pen y Fan +* Tryfan +* Snowdon +* Glyder Fawr +* Fan y Big diff --git a/ch00git/learning_git/partner_repo/index.md b/ch00git/learning_git/partner_repo/index.md new file mode 100644 index 000000000..860991ed0 --- /dev/null +++ b/ch00git/learning_git/partner_repo/index.md @@ -0,0 +1,5 @@ +Mountains and Lakes in the UK +=================== +Engerland is not very mountainous. +But has some tall hills, and maybe a +mountain or two depending on your definition. diff --git a/ch00git/learning_git/partner_repo/lakeland.md b/ch00git/learning_git/partner_repo/lakeland.md new file mode 100644 index 000000000..7cb0c9491 --- /dev/null +++ b/ch00git/learning_git/partner_repo/lakeland.md @@ -0,0 +1,7 @@ +Lakeland +======== + +Cumbria has some pretty hills, and lakes too + +Mountains: +* Helvellyn diff --git a/ch00git/somefile.md b/ch00git/somefile.md new file mode 100644 index 000000000..b1233f273 --- /dev/null +++ b/ch00git/somefile.md @@ -0,0 +1 @@ +Some content here diff --git a/ch01python/00pythons.html b/ch01python/00pythons.html new file mode 100644 index 000000000..8b1b0ca67 --- /dev/null +++ b/ch01python/00pythons.html @@ -0,0 +1,804 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Many kinds of Python + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Introduction to Python

+
+
+
+
+
+
+

Introduction

+
+
+
+
+
+
+

Why teach Python?

+
+
+
+
+
+
+
    +
  • In this first session, we will introduce Python.
  • +
  • This course is about programming for data analysis and visualisation in research.
  • +
  • It's not mainly about Python.
  • +
  • But we have to use some language.
  • +
+
+
+
+
+
+
+

Why Python?

+
+
+
+
+
+
+
    +
  • Python is quick to program in
  • +
  • Python is popular in research, and has lots of libraries for science
  • +
  • Python interfaces well with faster languages
  • +
  • Python is free, so you'll never have a problem getting hold of it, wherever you go.
  • +
+
+
+
+
+
+
+

Why write programs for research?

+
+
+
+
+
+
+
    +
  • Not just labour saving
  • +
  • Scripted research can be tested and reproduced
  • +
+
+
+
+
+
+
+

Sensible Input - Reasonable Output

+
+
+
+
+
+
+

Programs are a rigorous way of describing data analysis for other researchers, as well as for computers.

+

Computational research suffers from people assuming each other's data manipulation is correct. By sharing codes, +which are much more easy for a non-author to understand than spreadsheets, we can avoid the "SIRO" problem. The old saw "Garbage in Garbage out" is not the real problem for science:

+
    +
  • Sensible input
  • +
  • Reasonable output
  • +
+
+
+
+
+
+
+

Many kinds of Python

+
+
+
+
+
+
+

The Jupyter Notebook

+
+
+
+
+
+
+

The easiest way to get started using Python, and one of the best for research data work, is the Jupyter Notebook.

+
+
+
+
+
+
+

In the notebook, you can easily mix code with discussion and commentary, and mix code with the results of that code; +including graphs and other data visualisations.

+
+
+
+
+
+
In [1]:
+
+
+
### Make plot
+%matplotlib inline
+import math
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+theta = np.arange(0, 4 * math.pi, 0.1)
+eight = plt.figure()
+axes = eight.add_axes([0, 0, 1, 1])
+axes.plot(0.5 * np.sin(theta), np.cos(theta / 2))
+
+
+
+
+
+
+
+
Out[1]:
+
+
[<matplotlib.lines.Line2D at 0x7f58a83c8490>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

These notes are created using Jupyter notebooks and you may want to use it during the course. However, Jupyter notebooks won't be used for most of the activities and exercises done in class. To get hold of a copy of the notebook, follow the setup instructions shown on the course website, use the installation in Desktop@UCL (available in the teaching cluster rooms or anywhere), or go clone the repository on GitHub.

+
+
+
+
+
+
+

Jupyter notebooks consist of discussion cells, referred to as "markdown cells", and "code cells", which contain Python. This document has been created using Jupyter notebook, and this very cell is a Markdown Cell.

+
+
+
+
+
+
In [2]:
+
+
+
print("This cell is a code cell")
+
+
+
+
+
+
+
+
+
+
This cell is a code cell
+
+
+
+
+
+
+
+
+
+

Code cell inputs are numbered, and show the output below.

+
+
+
+
+
+
+

Markdown cells contain text which uses a simple format to achive pretty layout, +for example, to obtain:

+

bold, italic

+
    +
  • Bullet
  • +
+
+

Quote

+
+

We write:

+
**bold**, *italic*
+
+* Bullet
+
+> Quote
+

See the Markdown documentation at This Hyperlink

+
+
+
+
+
+
+

Typing code in the notebook

+
+
+
+
+
+
+

When working with the notebook, you can either be in a cell, typing its contents, or outside cells, moving around the notebook.

+
    +
  • When in a cell, press escape to leave it. When moving around outside cells, press return to enter.
  • +
  • Outside a cell:
      +
    • Use arrow keys to move around.
    • +
    • Press b to add a new cell below the cursor.
    • +
    • Press m to turn a cell from code mode to markdown mode.
    • +
    • Press shift+enter to calculate the code in the block.
    • +
    • Press h to see a list of useful keys in the notebook.
    • +
    +
  • +
  • Inside a cell:
      +
    • Press tab to suggest completions of variables. (Try it!)
    • +
    +
  • +
+
+
+
+
+
+
+

Supplementary material: Learn more about Jupyter notebooks.

+
+
+
+
+
+
+

The %% at the beginning of a cell is called magics. There's a large list of them available and you can create your own.

+
+
+
+
+
+
+

Python at the command line

+
+
+
+
+
+
+

Data science experts tend to use a "command line environment" to work. You'll be able to learn this at our "Software Carpentry" workshops, which cover other skills for computationally based research.

+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+# Above line tells Python to execute this cell as *shell code*
+# not Python, as if we were in a command line
+
+python -c "print(2 * 4)"
+
+
+
+
+
+
+
+
+
+
8
+
+
+
+
+
+
+
+
+
+

Python scripts

+
+
+
+
+
+
+

Once you get good at programming, you'll want to be able to write your own full programs in Python, which work just +like any other program on your computer. Here are some examples:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+echo "print(2 * 4)" > eight.py
+python eight.py
+
+
+
+
+
+
+
+
+
+
8
+
+
+
+
+
+
+
+
+
+

We can make the script directly executable (on Linux or Mac) by inserting a hashbang) and setting the permissions to execute.

+

Note, the %%writefile cell magic will write the contents of the cell to the file fourteen.py.

+
+
+
+
+
+
In [5]:
+
+
+
%%writefile fourteen.py
+#! /usr/bin/env python
+print(2 * 7)
+
+
+
+
+
+
+
+
+
+
Writing fourteen.py
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+chmod u+x fourteen.py
+./fourteen.py
+
+
+
+
+
+
+
+
+
+
14
+
+
+
+
+
+
+
+
+
+

Python Modules

+
+
+
+
+
+
+

A Python module is a file that contains a set of related functions or other code. The filename must have a .py extension.

+

We can write our own Python modules that we can import and use in other scripts or even in this notebook:

+
+
+
+
+
+
In [7]:
+
+
+
%%writefile draw_eight.py 
+# Above line tells the notebook to treat the rest of this
+# cell as content for a file on disk.
+import math
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+def make_figure():
+    """Plot a figure of eight."""
+
+    theta = np.arange(0, 4 * math.pi, 0.1)
+    eight = plt.figure()
+    axes = eight.add_axes([0, 0, 1, 1])
+    axes.plot(0.5 * np.sin(theta), np.cos(theta / 2))
+
+    return eight
+
+
+
+
+
+
+
+
+
+
Writing draw_eight.py
+
+
+
+
+
+
+
+
+
+

In a real example, we could edit the file on disk +using a code editor such as VS code.

+
+
+
+
+
+
In [8]:
+
+
+
import draw_eight # Load the library file we just wrote to disk
+
+
+
+
+
+
+
+
In [9]:
+
+
+
image = draw_eight.make_figure()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Note, we can import our draw_eight module in this notebook only if the file is in our current working directory (i.e. the folder this notebook is in).

+

To allow us to import our module from anywhere on our computer, or to allow other people to reuse it on their own computer, we can create a Python package.

+
+
+
+
+
+
+

Python packages

A package is a collection of modules that can be installed on our computer and easily shared with others. We will learn how to create packages later on in this course.

+

There is a huge variety of available packages to do pretty much anything. For instance, try import antigravity or import this.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/00pythons.ipynb b/ch01python/00pythons.ipynb new file mode 100644 index 000000000..11286bc80 --- /dev/null +++ b/ch01python/00pythons.ipynb @@ -0,0 +1,455 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "83cefb36", + "metadata": {}, + "source": [ + "# Introduction to Python" + ] + }, + { + "cell_type": "markdown", + "id": "01ecff5d", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "id": "3f61b52e", + "metadata": {}, + "source": [ + "### Why teach Python?" + ] + }, + { + "cell_type": "markdown", + "id": "1addc65c", + "metadata": {}, + "source": [ + "\n", + "* In this first session, we will introduce [Python](http://www.python.org).\n", + "* This course is about programming for data analysis and visualisation in research.\n", + "* It's not mainly about Python.\n", + "* But we have to use some language.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8297a3e0", + "metadata": {}, + "source": [ + "### Why Python?" + ] + }, + { + "cell_type": "markdown", + "id": "5a42c73e", + "metadata": {}, + "source": [ + "\n", + "* Python is quick to program in\n", + "* Python is popular in research, and has lots of libraries for science\n", + "* Python interfaces well with faster languages\n", + "* Python is free, so you'll never have a problem getting hold of it, wherever you go.\n" + ] + }, + { + "cell_type": "markdown", + "id": "67471468", + "metadata": {}, + "source": [ + "### Why write programs for research?" + ] + }, + { + "cell_type": "markdown", + "id": "bbce1caf", + "metadata": {}, + "source": [ + "\n", + "* Not just labour saving\n", + "* Scripted research can be tested and reproduced\n" + ] + }, + { + "cell_type": "markdown", + "id": "ffc7fdfa", + "metadata": {}, + "source": [ + "### Sensible Input - Reasonable Output" + ] + }, + { + "cell_type": "markdown", + "id": "28657818", + "metadata": {}, + "source": [ + "Programs are a rigorous way of describing data analysis for other researchers, as well as for computers.\n", + "\n", + "Computational research suffers from people assuming each other's data manipulation is correct. By sharing codes,\n", + "which are much more easy for a non-author to understand than spreadsheets, we can avoid the \"SIRO\" problem. The old saw \"Garbage in Garbage out\" is not the real problem for science:\n", + "\n", + "* Sensible input\n", + "* Reasonable output\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4dbeb74b", + "metadata": {}, + "source": [ + "## Many kinds of Python" + ] + }, + { + "cell_type": "markdown", + "id": "bf9da316", + "metadata": {}, + "source": [ + "### The Jupyter Notebook" + ] + }, + { + "cell_type": "markdown", + "id": "1e1f45f6", + "metadata": {}, + "source": [ + "The easiest way to get started using Python, and one of the best for research data work, is the Jupyter Notebook." + ] + }, + { + "cell_type": "markdown", + "id": "143a4a7a", + "metadata": {}, + "source": [ + "In the notebook, you can easily mix code with discussion and commentary, and mix code with the results of that code;\n", + "including graphs and other data visualisations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b8195ea", + "metadata": {}, + "outputs": [], + "source": [ + "### Make plot\n", + "%matplotlib inline\n", + "import math\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "theta = np.arange(0, 4 * math.pi, 0.1)\n", + "eight = plt.figure()\n", + "axes = eight.add_axes([0, 0, 1, 1])\n", + "axes.plot(0.5 * np.sin(theta), np.cos(theta / 2))" + ] + }, + { + "cell_type": "markdown", + "id": "f0dba2f5", + "metadata": {}, + "source": [ + "These notes are created using Jupyter notebooks and you may want to use it during the course. However, Jupyter notebooks won't be used for most of the activities and exercises done in class. To get hold of a copy of the notebook, follow the setup instructions shown on the course website, use the installation in Desktop@UCL (available in the teaching cluster rooms or [anywhere](https://www.ucl.ac.uk/isd/services/computers/remote-access/desktopucl-anywhere)), or go clone the [repository](https://github.com/UCL/rsd-engineeringcourse) on GitHub." + ] + }, + { + "cell_type": "markdown", + "id": "fbab42ce", + "metadata": {}, + "source": [ + "Jupyter notebooks consist of discussion cells, referred to as \"markdown cells\", and \"code cells\", which contain Python. This document has been created using Jupyter notebook, and this very cell is a **Markdown Cell**. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3067290c", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"This cell is a code cell\")" + ] + }, + { + "cell_type": "markdown", + "id": "3868956b", + "metadata": {}, + "source": [ + "Code cell inputs are numbered, and show the output below." + ] + }, + { + "cell_type": "markdown", + "id": "c6c520f4", + "metadata": {}, + "source": [ + "Markdown cells contain text which uses a simple format to achive pretty layout, \n", + "for example, to obtain:\n", + "\n", + "**bold**, *italic*\n", + "\n", + "* Bullet\n", + "\n", + "> Quote\n", + "\n", + "We write:\n", + "\n", + " **bold**, *italic*\n", + "\n", + " * Bullet\n", + "\n", + " > Quote\n", + "\n", + "See the Markdown documentation at [This Hyperlink](http://daringfireball.net/projects/markdown/)" + ] + }, + { + "cell_type": "markdown", + "id": "29e90f7c", + "metadata": {}, + "source": [ + "### Typing code in the notebook" + ] + }, + { + "cell_type": "markdown", + "id": "f7248a79", + "metadata": {}, + "source": [ + "When working with the notebook, you can either be in a cell, typing its contents, or outside cells, moving around the notebook.\n", + "\n", + "* When in a cell, press escape to leave it. When moving around outside cells, press return to enter.\n", + "* Outside a cell:\n", + " * Use arrow keys to move around.\n", + " * Press `b` to add a new cell below the cursor.\n", + " * Press `m` to turn a cell from code mode to markdown mode.\n", + " * Press `shift`+`enter` to calculate the code in the block.\n", + " * Press `h` to see a list of useful keys in the notebook.\n", + "* Inside a cell:\n", + " * Press `tab` to suggest completions of variables. (Try it!)" + ] + }, + { + "cell_type": "markdown", + "id": "72cb98be", + "metadata": {}, + "source": [ + "*Supplementary material*: Learn more about [Jupyter notebooks](https://jupyter.org/)." + ] + }, + { + "cell_type": "markdown", + "id": "bc760daa", + "metadata": {}, + "source": [ + "The `%%` at the beginning of a cell is called *magics*. There's a [large list of them available](https://ipython.readthedocs.io/en/stable/interactive/magics.html) and you can [create your own](http://ipython.readthedocs.io/en/stable/config/custommagics.html).\n" + ] + }, + { + "cell_type": "markdown", + "id": "c21c5eb7", + "metadata": {}, + "source": [ + "### Python at the command line" + ] + }, + { + "cell_type": "markdown", + "id": "6124978a", + "metadata": {}, + "source": [ + "Data science experts tend to use a \"command line environment\" to work. You'll be able to learn this at our [\"Software Carpentry\" workshops](http://github-pages.arc.ucl.ac.uk/software-carpentry/), which cover other skills for computationally based research." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52b8401b", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# Above line tells Python to execute this cell as *shell code*\n", + "# not Python, as if we were in a command line\n", + "\n", + "python -c \"print(2 * 4)\"" + ] + }, + { + "cell_type": "markdown", + "id": "6b70c925", + "metadata": {}, + "source": [ + "### Python scripts" + ] + }, + { + "cell_type": "markdown", + "id": "3d476e2b", + "metadata": {}, + "source": [ + "Once you get good at programming, you'll want to be able to write your own full programs in Python, which work just\n", + "like any other program on your computer. Here are some examples:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ab65fe6", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "echo \"print(2 * 4)\" > eight.py\n", + "python eight.py" + ] + }, + { + "cell_type": "markdown", + "id": "d2935df5", + "metadata": {}, + "source": [ + "We can make the script directly executable (on Linux or Mac) by inserting a [hashbang](https://en.wikipedia.org/wiki/Shebang_(Unix%29)) and [setting the permissions](http://v4.software-carpentry.org/shell/perm.html) to execute.\n", + "\n", + "Note, the `%%writefile` cell magic will write the contents of the cell to the file `fourteen.py`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2318afb7", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile fourteen.py\n", + "#! /usr/bin/env python\n", + "print(2 * 7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b012ff17", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "chmod u+x fourteen.py\n", + "./fourteen.py" + ] + }, + { + "cell_type": "markdown", + "id": "c6f5687e", + "metadata": {}, + "source": [ + "### Python Modules" + ] + }, + { + "cell_type": "markdown", + "id": "c4686b2e", + "metadata": {}, + "source": [ + "A Python module is a file that contains a set of related functions or other code. The filename must have a `.py` extension.\n", + "\n", + "We can write our own Python modules that we can import and use in other scripts or even in this notebook:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a0279c1", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile draw_eight.py \n", + "# Above line tells the notebook to treat the rest of this\n", + "# cell as content for a file on disk.\n", + "import math\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "def make_figure():\n", + " \"\"\"Plot a figure of eight.\"\"\"\n", + "\n", + " theta = np.arange(0, 4 * math.pi, 0.1)\n", + " eight = plt.figure()\n", + " axes = eight.add_axes([0, 0, 1, 1])\n", + " axes.plot(0.5 * np.sin(theta), np.cos(theta / 2))\n", + "\n", + " return eight\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "7ad28c98", + "metadata": {}, + "source": [ + "In a real example, we could edit the file on disk\n", + "using a code editor such as [VS code](https://code.visualstudio.com/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90bb8d2f", + "metadata": {}, + "outputs": [], + "source": [ + "import draw_eight # Load the library file we just wrote to disk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee7d2281", + "metadata": {}, + "outputs": [], + "source": [ + "image = draw_eight.make_figure()" + ] + }, + { + "cell_type": "markdown", + "id": "d048bd8e", + "metadata": {}, + "source": [ + "Note, we can import our `draw_eight` module in this notebook only if the file is in our current working directory (i.e. the folder this notebook is in).\n", + "\n", + "To allow us to import our module from anywhere on our computer, or to allow other people to reuse it on their own computer, we can create a [Python package](https://packaging.python.org/en/latest/).\n" + ] + }, + { + "cell_type": "markdown", + "id": "8dcbc654", + "metadata": {}, + "source": [ + "### Python packages\n", + "\n", + "A package is a collection of modules that can be installed on our computer and easily shared with others. We will learn how to create packages later on in this course.\n", + "\n", + "There is a huge variety of available packages to do pretty much anything. For instance, try `import antigravity` or `import this`.\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Many kinds of Python" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/00pythons.ipynb.py b/ch01python/00pythons.ipynb.py new file mode 100644 index 000000000..8d0d63dea --- /dev/null +++ b/ch01python/00pythons.ipynb.py @@ -0,0 +1,235 @@ +# --- +# jupyter: +# jekyll: +# display_name: Many kinds of Python +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Introduction to Python + +# %% [markdown] +# ## Introduction + +# %% [markdown] +# ### Why teach Python? + +# %% [markdown] +# +# * In this first session, we will introduce [Python](http://www.python.org). +# * This course is about programming for data analysis and visualisation in research. +# * It's not mainly about Python. +# * But we have to use some language. +# + +# %% [markdown] +# ### Why Python? + +# %% [markdown] +# +# * Python is quick to program in +# * Python is popular in research, and has lots of libraries for science +# * Python interfaces well with faster languages +# * Python is free, so you'll never have a problem getting hold of it, wherever you go. +# + +# %% [markdown] +# ### Why write programs for research? + +# %% [markdown] +# +# * Not just labour saving +# * Scripted research can be tested and reproduced +# + +# %% [markdown] +# ### Sensible Input - Reasonable Output + +# %% [markdown] +# Programs are a rigorous way of describing data analysis for other researchers, as well as for computers. +# +# Computational research suffers from people assuming each other's data manipulation is correct. By sharing codes, +# which are much more easy for a non-author to understand than spreadsheets, we can avoid the "SIRO" problem. The old saw "Garbage in Garbage out" is not the real problem for science: +# +# * Sensible input +# * Reasonable output +# +# + +# %% [markdown] +# ## Many kinds of Python + +# %% [markdown] +# ### The Jupyter Notebook + +# %% [markdown] +# The easiest way to get started using Python, and one of the best for research data work, is the Jupyter Notebook. + +# %% [markdown] +# In the notebook, you can easily mix code with discussion and commentary, and mix code with the results of that code; +# including graphs and other data visualisations. + +# %% +### Make plot +# %matplotlib inline +import math + +import numpy as np +import matplotlib.pyplot as plt + +theta = np.arange(0, 4 * math.pi, 0.1) +eight = plt.figure() +axes = eight.add_axes([0, 0, 1, 1]) +axes.plot(0.5 * np.sin(theta), np.cos(theta / 2)) + +# %% [markdown] +# These notes are created using Jupyter notebooks and you may want to use it during the course. However, Jupyter notebooks won't be used for most of the activities and exercises done in class. To get hold of a copy of the notebook, follow the setup instructions shown on the course website, use the installation in Desktop@UCL (available in the teaching cluster rooms or [anywhere](https://www.ucl.ac.uk/isd/services/computers/remote-access/desktopucl-anywhere)), or go clone the [repository](https://github.com/UCL/rsd-engineeringcourse) on GitHub. + +# %% [markdown] +# Jupyter notebooks consist of discussion cells, referred to as "markdown cells", and "code cells", which contain Python. This document has been created using Jupyter notebook, and this very cell is a **Markdown Cell**. + +# %% +print("This cell is a code cell") + +# %% [markdown] +# Code cell inputs are numbered, and show the output below. + +# %% [markdown] +# Markdown cells contain text which uses a simple format to achive pretty layout, +# for example, to obtain: +# +# **bold**, *italic* +# +# * Bullet +# +# > Quote +# +# We write: +# +# **bold**, *italic* +# +# * Bullet +# +# > Quote +# +# See the Markdown documentation at [This Hyperlink](http://daringfireball.net/projects/markdown/) + +# %% [markdown] +# ### Typing code in the notebook + +# %% [markdown] +# When working with the notebook, you can either be in a cell, typing its contents, or outside cells, moving around the notebook. +# +# * When in a cell, press escape to leave it. When moving around outside cells, press return to enter. +# * Outside a cell: +# * Use arrow keys to move around. +# * Press `b` to add a new cell below the cursor. +# * Press `m` to turn a cell from code mode to markdown mode. +# * Press `shift`+`enter` to calculate the code in the block. +# * Press `h` to see a list of useful keys in the notebook. +# * Inside a cell: +# * Press `tab` to suggest completions of variables. (Try it!) + +# %% [markdown] +# *Supplementary material*: Learn more about [Jupyter notebooks](https://jupyter.org/). + +# %% [markdown] +# The `%%` at the beginning of a cell is called *magics*. There's a [large list of them available](https://ipython.readthedocs.io/en/stable/interactive/magics.html) and you can [create your own](http://ipython.readthedocs.io/en/stable/config/custommagics.html). +# + +# %% [markdown] +# ### Python at the command line + +# %% [markdown] +# Data science experts tend to use a "command line environment" to work. You'll be able to learn this at our ["Software Carpentry" workshops](http://github-pages.arc.ucl.ac.uk/software-carpentry/), which cover other skills for computationally based research. + +# %% language="bash" +# # Above line tells Python to execute this cell as *shell code* +# # not Python, as if we were in a command line +# +# python -c "print(2 * 4)" + +# %% [markdown] +# ### Python scripts + +# %% [markdown] +# Once you get good at programming, you'll want to be able to write your own full programs in Python, which work just +# like any other program on your computer. Here are some examples: + +# %% language="bash" +# echo "print(2 * 4)" > eight.py +# python eight.py + +# %% [markdown] +# We can make the script directly executable (on Linux or Mac) by inserting a [hashbang](https://en.wikipedia.org/wiki/Shebang_(Unix%29)) and [setting the permissions](http://v4.software-carpentry.org/shell/perm.html) to execute. +# +# Note, the `%%writefile` cell magic will write the contents of the cell to the file `fourteen.py`. + +# %% +# %%writefile fourteen.py +# #! /usr/bin/env python +print(2 * 7) + +# %% language="bash" +# chmod u+x fourteen.py +# ./fourteen.py + +# %% [markdown] +# ### Python Modules + +# %% [markdown] +# A Python module is a file that contains a set of related functions or other code. The filename must have a `.py` extension. +# +# We can write our own Python modules that we can import and use in other scripts or even in this notebook: +# + +# %% +# %%writefile draw_eight.py +# Above line tells the notebook to treat the rest of this +# cell as content for a file on disk. +import math + +import numpy as np +import matplotlib.pyplot as plt + +def make_figure(): + """Plot a figure of eight.""" + + theta = np.arange(0, 4 * math.pi, 0.1) + eight = plt.figure() + axes = eight.add_axes([0, 0, 1, 1]) + axes.plot(0.5 * np.sin(theta), np.cos(theta / 2)) + + return eight + + + +# %% [markdown] +# In a real example, we could edit the file on disk +# using a code editor such as [VS code](https://code.visualstudio.com/). + +# %% +import draw_eight # Load the library file we just wrote to disk + +# %% +image = draw_eight.make_figure() + +# %% [markdown] +# Note, we can import our `draw_eight` module in this notebook only if the file is in our current working directory (i.e. the folder this notebook is in). +# +# To allow us to import our module from anywhere on our computer, or to allow other people to reuse it on their own computer, we can create a [Python package](https://packaging.python.org/en/latest/). +# + +# %% [markdown] +# ### Python packages +# +# A package is a collection of modules that can be installed on our computer and easily shared with others. We will learn how to create packages later on in this course. +# +# There is a huge variety of available packages to do pretty much anything. For instance, try `import antigravity` or `import this`. +# diff --git a/ch01python/010exemplar.html b/ch01python/010exemplar.html new file mode 100644 index 000000000..b9a16f892 --- /dev/null +++ b/ch01python/010exemplar.html @@ -0,0 +1,1487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An example program + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

An example Python data analysis notebook

+
+
+
+
+
+
+

This page illustrates how to use Python to perform a simple but complete analysis: retrieve data, do some computations based on it, and visualise the results.

+

Don't worry if you don't understand everything on this page! Its purpose is to give you an example of things you can do and how to go about doing them - you are not expected to be able to reproduce an analysis like this in Python at this stage! We will be looking at the concepts and practices introduced on this page as we go along the course.

+

As we show the code for different parts of the work, we will be touching on various aspects you may want to keep in mind, either related to Python specifically, or to research programming more generally.

+
+
+
+
+
+
+

Why write software to manage your data and plots?

+
+
+
+
+
+
+

We can use programs for our entire research pipeline. Not just big scientific simulation codes, but also the small scripts which we use to tidy up data and produce plots. This should be code, so that the whole research pipeline +is recorded for reproducibility. Data manipulation in spreadsheets is much harder to share or +check.

+
+
+
+
+
+
+

You can see another similar demonstration on the software carpentry site. +We'll try to give links to other sources of Python training along the way. +Part of our approach is that we assume you know how to use the internet! If you +find something confusing out there, please bring it along to the next session. In this course, we'll always try to draw your attention to other sources of information about what we're learning. Paying attention to as many of these as you need to, is just as important as these core notes.

+
+
+
+
+
+
+

Importing Libraries

+
+
+
+
+
+
+

Research programming is all about using libraries: tools other people have provided programs that do many cool things. +By combining them we can feel really powerful but doing minimum work ourselves. The python syntax to import someone else's library is "import".

+
+
+
+
+
+
In [1]:
+
+
+
import geopy # A python library for investigating geographic information.
+# https://pypi.org/project/geopy/
+
+
+
+
+
+
+
+
+

Now, if you try to follow along on this example in an Jupyter notebook, you'll probably find that +you just got an error message.

+

You'll need to wait until we've covered installation of additional python libraries later in the course, then come +back to this and try again. For now, just follow along and try get the feel for how programming for data-focused +research works.

+
+
+
+
+
+
In [2]:
+
+
+
# Select geocoding service provided by OpenStreetMap's Nominatim - https://wiki.openstreetmap.org/wiki/Nominatim
+geocoder = geopy.geocoders.Nominatim(user_agent="comp0023") 
+geocoder.geocode('Cambridge', exactly_one=False)
+
+
+
+
+
+
+
+
Out[2]:
+
+
[Location(Cambridge, Cambridgeshire, Cambridgeshire and Peterborough, England, United Kingdom, (52.2055314, 0.1186637, 0.0)),
+ Location(Cambridge, Middlesex County, Massachusetts, United States, (42.3655767, -71.1040018, 0.0)),
+ Location(Cambridge, Region of Waterloo, Ontario, Canada, (43.3600536, -80.3123023, 0.0)),
+ Location(Cambridge, Henry County, Illinois, United States, (41.3025257, -90.1962861, 0.0)),
+ Location(Cambridge, Isanti County, Minnesota, 55008, United States, (45.5727408, -93.2243921, 0.0)),
+ Location(Cambridge, Story County, Iowa, United States, (41.8990768, -93.5294029, 0.0)),
+ Location(Cambridge, Dorchester County, Maryland, 21613, United States, (38.5714624, -76.0763177, 0.0)),
+ Location(Cambridge, Guernsey County, Ohio, 43725, United States, (40.031183, -81.5884561, 0.0)),
+ Location(Cambridge, Jefferson County, Kentucky, United States, (38.2217369, -85.616627, 0.0)),
+ Location(Cambridge, Cowley County, Kansas, United States, (37.316988, -96.66633224663678, 0.0))]
+
+
+
+
+
+
+
+
+

The results come out as a list inside a list: [Name, [Latitude, Longitude]]. +Programs represent data in a variety of different containers like this.

+
+
+
+
+
+
+

Comments

+
+
+
+
+
+
+

Code after a # symbol doesn't get run.

+
+
+
+
+
+
In [3]:
+
+
+
print("This runs") # print("This doesn't")
+# print("This doesn't either")
+
+
+
+
+
+
+
+
+
+
This runs
+
+
+
+
+
+
+
+
+
+

Functions

+
+
+
+
+
+
+

We can wrap code up in a function, so that we can repeatedly get just the information we want.

+
+
+
+
+
+
In [4]:
+
+
+
def geolocate(city):
+    """Get the latitude and longitude of a specific location."""
+    
+    full_name, coordinates = geocoder.geocode(city)
+    return coordinates
+
+
+
+
+
+
+
+
+

Defining functions which put together code to make a more complex task seem simple from the outside is the most important thing in programming. The output of the function is specified using the return keyword. The input to the function is put inside brackets after the function name:

+
+
+
+
+
+
In [5]:
+
+
+
geolocate(city='Cambridge')
+
+
+
+
+
+
+
+
Out[5]:
+
+
(52.2055314, 0.1186637)
+
+
+
+
+
+
+
+
+

Variables

+
+
+
+
+
+
+

We can store a result in a variable:

+
+
+
+
+
+
In [6]:
+
+
+
london_location = geolocate("London")
+print(london_location)
+
+
+
+
+
+
+
+
+
+
(51.4893335, -0.14405508452768728)
+
+
+
+
+
+
+
+
+
+

More complex functions

+
+
+
+
+
+
+

We'll fetch a map of a place from the Google Maps server, given a longitude and latitude. +The URLs look like: https://mt0.google.com/vt?x=658&y=340&z=10&lyrs=s. Since we'll frequently be generating these URLs, we will create two helper functions to make our life easier.

+

The first is a function to convert our latitude and longitude into the coordinate tiles system used by Google Maps. +We will then create a second function to build up a web request from the URL given our parameters.

+
+
+
+
+
+
In [7]:
+
+
+
import os
+import math
+import requests
+
+def deg2num(lat_deg, lon_deg, zoom):
+    """Convert latitude and longitude to XY tiles coordinates."""
+
+    lat_rad = math.radians(lat_deg)
+    n = 2.0 ** zoom
+    x_tiles_coord = int((lon_deg + 180.0) / 360.0 * n)
+    y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
+
+    return (x_tiles_coord, y_tiles_coord)
+
+def request_map_at(latitude, longitude, zoom=10, satellite=True):
+    """Retrieve a map from Google at a given location."""
+
+    base_url = "https://mt0.google.com/vt?"
+    x_coord, y_coord = deg2num(latitude, longitude, zoom)
+
+    params = dict(
+        x=x_coord,
+        y=y_coord,
+        z=zoom,
+    )
+    if satellite:
+        params['lyrs'] = 's'
+    
+    return requests.get(base_url, params=params)
+
+
+
+
+
+
+
+
In [8]:
+
+
+
london_latitude, london_longitude = london_location
+map_response = request_map_at(london_latitude, london_longitude)
+
+
+
+
+
+
+
+
+

Checking our work

+
+
+
+
+
+
+

Let's see what URL we ended up with.

+

Firsty we will define two constants so that we can split the returned URL into the base URL and the part of the URL that corresponds to the location we requested:

+
+
+
+
+
+
In [9]:
+
+
+
url = map_response.url
+
+first_25s = slice(0, 25)
+from_25th = slice(25, None)
+
+print(url)
+print(url[first_25s])
+print(url[from_25th])
+
+
+
+
+
+
+
+
+
+
https://mt0.google.com/vt?x=511&y=340&z=10&lyrs=s
+https://mt0.google.com/vt
+?x=511&y=340&z=10&lyrs=s
+
+
+
+
+
+
+
+
+
+

url is a string and we can select parts of this string using the slices we defined above. first_25s will select characters 0 to 24 of the string and from_25th will select all characters from the 25th onwards.

+
+
+
+
+
+
+

We can write tests so that if we change our code later we can check the results are still valid. We will do this here using assert statements. If any of those assert statements are False we will get an error. If we receive an error from our tests we know we need to fix something in our code.

+
+
+
+
+
+
In [10]:
+
+
+
assert "https://mt0.google.com/vt?" in url
+assert "z=10" in url
+assert "lyrs=s" in url
+
+
+
+
+
+
+
+
+

Our previous function comes back with an Object representing the web request. In Python, we can use the . operator to get access to a particular attribute of the object. In this case, the image at the requested URL is stored in the content attribute. It's a big file, so let's just get look at first few bytes:

+
+
+
+
+
+
In [11]:
+
+
+
map_response.content[0:20]
+
+
+
+
+
+
+
+
Out[11]:
+
+
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
+
+
+
+
+
+
+
+
+

Displaying results

+
+
+
+
+
+
+

We'll need to do this a lot, so we can wrap up our previous function in another function to save on typing.

+
+
+
+
+
+
In [12]:
+
+
+
def map_content_at(latitude, longitude, zoom=10, satellite=True):
+    """Retrieve a map image from Google at a given location."""
+
+    return request_map_at(latitude, longitude, zoom=10, satellite=True).content
+
+
+
+
+
+
+
+
+

We can use a library that comes with Jupyter notebook to display the image. This is one of the most powerful things about modern programming languages like Python - being able to work with images, documents, or any other kind of data just as easily as we can with numbers or strings.

+
+
+
+
+
+
In [13]:
+
+
+
import IPython
+
+map_png = map_content_at(london_latitude, london_longitude)
+
+
+
+
+
+
+
+
In [14]:
+
+
+
print("The type of our map result is actually a: ", type(map_png))
+
+
+
+
+
+
+
+
+
+
The type of our map result is actually a:  <class 'bytes'>
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
IPython.display.Image(map_png)
+
+
+
+
+
+
+
+
Out[15]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [16]:
+
+
+
IPython.display.Image(map_content_at(*geolocate("New Delhi")))
+
+
+
+
+
+
+
+
Out[16]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Manipulating Numbers

+
+
+
+
+
+
+

Now we get to our research project: we want to use satellite imagery to find out how urbanised the world is along a line between two cites. We expect the satellite image to be greener in the countryside.

+
+
+
+
+
+
+

We'll need to import a few more libraries to count how much green there is in an image.

+
+
+
+
+
+
In [17]:
+
+
+
from io import BytesIO  # A library to convert between files and strings
+import numpy as np  # A library to deal with matrices
+import imageio.v3 as iio  # A library to deal with images
+
+
+
+
+
+
+
+
+

Let's define what we count as green:

+
+
+
+
+
+
In [18]:
+
+
+
def is_green(pixels):
+    """Determine if each pixel in an image array is green."""
+    
+    # RGB indices
+    red, green, blue = range(3)
+
+    threshold = 1.1
+    greener_than_red = pixels[:, :, green] > threshold * pixels[:, :, red]
+    greener_than_blue = pixels[:, :, green] > threshold * pixels[:, :, blue]
+    green = np.logical_and(greener_than_red, greener_than_blue) 
+
+    return green
+
+
+
+
+
+
+
+
+

This code has assumed we have our pixel data for the image as a $256 \times 256 \times 3$ 3-d matrix, +with each of the three layers being red, green, and blue pixels.

+

We find out which pixels are green by comparing, element-by-element, the middle (green, number 1) layer to the top (red, zero) and bottom (blue, 2)

+
+
+
+
+
+
+

Now we just need to parse in our data, which is a PNG image, and turn it into our matrix format:

+
+
+
+
+
+
In [19]:
+
+
+
def count_green_in_png(data):
+    """Determine the total number of green pixels in an image."""
+
+    f = BytesIO(data)
+    pixels = iio.imread(f) # Get our PNG image as a numpy array
+
+    return np.sum(is_green(pixels))
+
+
+
+
+
+
+
+
In [20]:
+
+
+
london_map = map_content_at(london_latitude, london_longitude)
+green_count_london = count_green_in_png(london_map)
+print(green_count_london)
+
+
+
+
+
+
+
+
+
+
31418
+
+
+
+
+
+
+
+
+
In [21]:
+
+
+
iio.imread(BytesIO(london_map)).shape
+
+
+
+
+
+
+
+
Out[21]:
+
+
(256, 256, 3)
+
+
+
+
+
+
+
+
+

We'll also need a function to get an evenly spaced set of places between two endpoints:

+
+
+
+
+
+
In [22]:
+
+
+
def location_sequence(start, end, steps):
+    """Generate a sequence of evenly spaced locations between two sets of coordinates."""
+
+    start_latitude, start_longitude = start
+    end_latitude, end_longitude = end
+    
+    latitudes = np.linspace(start_latitude, end_latitude, steps)
+    longitudes = np.linspace(start_longitude, end_longitude, steps)
+
+    path = np.vstack([latitudes, longitudes]).transpose()
+    
+    return path
+
+
+
+
+
+
+
+
In [23]:
+
+
+
london_to_cambridge = location_sequence(
+    start=geolocate("London"),
+    end=geolocate("Cambridge"),
+    steps=5,
+)
+print(london_to_cambridge)
+
+
+
+
+
+
+
+
+
+
[[ 5.14893335e+01 -1.44055085e-01]
+ [ 5.16683830e+01 -7.83753884e-02]
+ [ 5.18474324e+01 -1.26956923e-02]
+ [ 5.20264819e+01  5.29840039e-02]
+ [ 5.22055314e+01  1.18663700e-01]]
+
+
+
+
+
+
+
+
+
+

Creating Images

+
+
+
+
+
+
+

We should display the green content to check our work:

+
+
+
+
+
+
In [24]:
+
+
+
def show_green_in_png(data):
+    """Convert all non-green pixels in an RGB image to black.
+
+    Red and blue channel are set to 0 for all pixels.
+    Pixels that are green will have the green channel set to its max value.
+    Pixels that are non-green will have the green channel set to 0.
+    """
+
+    f = BytesIO(data)
+    pixels = iio.imread(f) # Get our PNG image as a numpy array
+    green_pixels = is_green(pixels)
+
+    green_channel = 1
+    binary_pixels = np.zeros_like(pixels, dtype=np.uint8)
+    max_possible_value =  np.iinfo(binary_pixels.dtype).max
+    binary_pixels[green_pixels, green_channel] = max_possible_value
+
+    buffer = BytesIO()
+    binary_image = iio.imwrite(buffer, binary_pixels, extension='.png')
+
+    return buffer.getvalue()
+
+
+
+
+
+
+
+
In [25]:
+
+
+
london_location
+
+
+
+
+
+
+
+
Out[25]:
+
+
(51.4893335, -0.14405508452768728)
+
+
+
+
+
+
+
+
In [26]:
+
+
+
IPython.display.Image(
+    map_content_at(london_latitude, london_longitude, satellite=True)
+)
+
+
+
+
+
+
+
+
Out[26]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [27]:
+
+
+
IPython.display.Image(
+    show_green_in_png(
+        map_content_at(
+            london_latitude,
+            london_longitude,
+            satellite=True,
+        )
+    )
+)
+
+
+
+
+
+
+
+
Out[27]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Looping

+
+
+
+
+
+
+

We can loop over each element in out list of coordinates and get a map for that place:

+
+
+
+
+
+
In [28]:
+
+
+
london_to_birmingham = location_sequence(
+    start=geolocate("London"),
+    end=geolocate("Birmingham"),
+    steps=10,
+)
+
+london_to_birmingham_maps = []
+
+for latitude, longitude in london_to_birmingham:
+
+    current_map = map_content_at(latitude, longitude)
+    london_to_birmingham_maps.append(current_map)
+    
+    IPython.display.display(
+        IPython.display.Image(
+            current_map,
+        )
+    )
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

So now we can count the green from London to Birmingham!

+
+
+
+
+
+
In [29]:
+
+
+
green_at_each_location = [count_green_in_png(current_map) for current_map in london_to_birmingham_maps]
+print(green_at_each_location)
+
+
+
+
+
+
+
+
+
+
[31418, 31418, 62394, 63370, 63407, 63475, 63371, 62909, 59826, 60063]
+
+
+
+
+
+
+
+
+
+

Plotting graphs

+
+
+
+
+
+
+

Let's plot a graph.

+
+
+
+
+
+
In [30]:
+
+
+
import matplotlib.pyplot as plt
+%matplotlib inline
+
+
+
+
+
+
+
+
In [31]:
+
+
+
plt.plot(green_at_each_location)
+
+plt.xticks(range(10))
+plt.xlabel("Sequence step")
+plt.ylabel(r"$N_{green}$")
+
+
+
+
+
+
+
+
Out[31]:
+
+
Text(0, 0.5, '$N_{green}$')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

From a research perspective, of course, this code needs a lot of work. But I hope the power of using programming is clear.

+
+
+
+
+
+
+

Composing Program Elements

+
+
+
+
+
+
+

We built little pieces of useful code, to:

+
    +
  • Find latitude and longitude of a place
  • +
  • Get a map at a given latitude and longitude
  • +
  • Decide whether a (red,green,blue) triple is mainly green
  • +
  • Decide whether each pixel is mainly green
  • +
  • Plot a new image showing the green places
  • +
  • Find evenly spaced points between two places
  • +
+
+
+
+
+
+
+

By putting these together, we can make a function which can plot this graph automatically for any two places:

+
+
+
+
+
+
In [32]:
+
+
+
def green_between(start, end, steps):
+    """Count the amount of green space along a linear path between two locations."""
+
+    sequence = location_sequence(
+        start=geolocate(start),
+        end=geolocate(end),
+        steps=steps,
+    )
+    maps = [map_content_at(latitude, longitude) for latitude, longitude in sequence]
+    green_at_each_location = [count_green_in_png(current_map) for current_map in maps]
+    
+    return green_at_each_location
+
+
+
+
+
+
+
+
In [33]:
+
+
+
plt.plot(green_between('New York', 'Chicago', 20))
+
+
+
+
+
+
+
+
Out[33]:
+
+
[<matplotlib.lines.Line2D at 0x7f230cf060a0>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We can also put the plotting command into a function, to make it more general:

+
+
+
+
+
+
In [34]:
+
+
+
def plot_green_between(start, end, steps):
+    """ount the amount of green space along a linear path between two locations"""
+    green_between_locations = green_between(start, end, steps)
+    plt.plot(green_between_locations)
+    xticks_steps = 5 if steps > 10 else 1
+    plt.xticks(range(0, steps, xticks_steps))
+    plt.xlabel("Sequence step")
+    plt.ylabel(r"$N_{green}$")
+    plt.title(f"{start} -- {end}")
+
+
+
+
+
+
+
+
In [35]:
+
+
+
plot_green_between('New York', 'Chicago', 20)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

And that's it! We've covered - very very quickly - a lot of the Python language, and have introduced some of the most important concepts in modern software engineering.

+
+
+
+
+
+
+

Now we'll go back, carefully, through all the concepts we touched on, and learn how to use them properly ourselves.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/010exemplar.ipynb b/ch01python/010exemplar.ipynb new file mode 100644 index 000000000..2200bbd2b --- /dev/null +++ b/ch01python/010exemplar.ipynb @@ -0,0 +1,961 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c3ea7db2", + "metadata": {}, + "source": [ + "## An example Python data analysis notebook" + ] + }, + { + "cell_type": "markdown", + "id": "00c27b3a", + "metadata": {}, + "source": [ + "This page illustrates how to use Python to perform a simple but complete analysis: retrieve data, do some computations based on it, and visualise the results.\n", + "\n", + "**Don't worry if you don't understand everything on this page!** Its purpose is to give you an example of things you can do and how to go about doing them - you are not expected to be able to reproduce an analysis like this in Python at this stage! We will be looking at the concepts and practices introduced on this page as we go along the course.\n", + "\n", + "As we show the code for different parts of the work, we will be touching on various aspects you may want to keep in mind, either related to Python specifically, or to research programming more generally." + ] + }, + { + "cell_type": "markdown", + "id": "01ad2fc3", + "metadata": {}, + "source": [ + "### Why write software to manage your data and plots? " + ] + }, + { + "cell_type": "markdown", + "id": "ffc0b1e1", + "metadata": {}, + "source": [ + "We can use programs for our entire research pipeline. Not just big scientific simulation codes, but also the small scripts which we use to tidy up data and produce plots. This should be code, so that the whole research pipeline\n", + "is recorded for reproducibility. Data manipulation in spreadsheets is much harder to share or \n", + "check. " + ] + }, + { + "cell_type": "markdown", + "id": "f5cdd192", + "metadata": {}, + "source": [ + "You can see another similar demonstration on the [software carpentry site](https://swcarpentry.github.io/python-novice-inflammation/02-numpy.html).\n", + "We'll try to give links to other sources of Python training along the way.\n", + "Part of our approach is that we assume you know how to use the internet! If you\n", + "find something confusing out there, please bring it along to the next session. In this course, we'll always try to draw your attention to other sources of information about what we're learning. Paying attention to as many of these as you need to, is just as important as these core notes." + ] + }, + { + "cell_type": "markdown", + "id": "3a6b14ff", + "metadata": {}, + "source": [ + "### Importing Libraries" + ] + }, + { + "cell_type": "markdown", + "id": "a90eb1cd", + "metadata": {}, + "source": [ + "Research programming is all about using libraries: tools other people have provided programs that do many cool things.\n", + "By combining them we can feel really powerful but doing minimum work ourselves. The python syntax to import someone else's library is \"import\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3284373", + "metadata": {}, + "outputs": [], + "source": [ + "import geopy # A python library for investigating geographic information.\n", + "# https://pypi.org/project/geopy/" + ] + }, + { + "cell_type": "markdown", + "id": "87ee10c2", + "metadata": {}, + "source": [ + "Now, if you try to follow along on this example in an Jupyter notebook, you'll probably find that \n", + "you just got an error message.\n", + "\n", + "You'll need to wait until we've covered installation of additional python libraries later in the course, then come\n", + "back to this and try again. For now, just follow along and try get the feel for how programming for data-focused\n", + "research works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fac1339", + "metadata": {}, + "outputs": [], + "source": [ + "# Select geocoding service provided by OpenStreetMap's Nominatim - https://wiki.openstreetmap.org/wiki/Nominatim\n", + "geocoder = geopy.geocoders.Nominatim(user_agent=\"comp0023\") \n", + "geocoder.geocode('Cambridge', exactly_one=False)" + ] + }, + { + "cell_type": "markdown", + "id": "2b9c55ba", + "metadata": {}, + "source": [ + "The results come out as a **list** inside a list: `[Name, [Latitude, Longitude]]`. \n", + "Programs represent data in a variety of different **containers** like this." + ] + }, + { + "cell_type": "markdown", + "id": "bb23137a", + "metadata": {}, + "source": [ + "### Comments" + ] + }, + { + "cell_type": "markdown", + "id": "01ceb4f9", + "metadata": {}, + "source": [ + "Code after a `#` symbol doesn't get run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d000796", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"This runs\") # print(\"This doesn't\")\n", + "# print(\"This doesn't either\")" + ] + }, + { + "cell_type": "markdown", + "id": "e8ad885a", + "metadata": {}, + "source": [ + "### Functions" + ] + }, + { + "cell_type": "markdown", + "id": "27d79fca", + "metadata": {}, + "source": [ + "We can wrap code up in a **function**, so that we can repeatedly get just the information we want.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a4850b7", + "metadata": {}, + "outputs": [], + "source": [ + "def geolocate(city):\n", + " \"\"\"Get the latitude and longitude of a specific location.\"\"\"\n", + " \n", + " full_name, coordinates = geocoder.geocode(city)\n", + " return coordinates" + ] + }, + { + "cell_type": "markdown", + "id": "ec214676", + "metadata": {}, + "source": [ + "Defining **functions** which put together code to make a more complex task seem simple from the outside is the most important thing in programming. The output of the function is specified using the `return` keyword. The input to the function is put inside brackets after the function name:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "813610ca", + "metadata": {}, + "outputs": [], + "source": [ + "geolocate(city='Cambridge')" + ] + }, + { + "cell_type": "markdown", + "id": "4f18c937", + "metadata": {}, + "source": [ + "### Variables" + ] + }, + { + "cell_type": "markdown", + "id": "f27a7e7e", + "metadata": {}, + "source": [ + "We can store a result in a variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c03e321", + "metadata": {}, + "outputs": [], + "source": [ + "london_location = geolocate(\"London\")\n", + "print(london_location)" + ] + }, + { + "cell_type": "markdown", + "id": "fdad05a9", + "metadata": {}, + "source": [ + "### More complex functions" + ] + }, + { + "cell_type": "markdown", + "id": "25f76555", + "metadata": {}, + "source": [ + "We'll fetch a map of a place from the Google Maps server, given a longitude and latitude.\n", + "The URLs look like: `https://mt0.google.com/vt?x=658&y=340&z=10&lyrs=s`. Since we'll frequently be generating these URLs, we will create two helper functions to make our life easier.\n", + "\n", + "The first is a function to [convert our latitude and longitude into the coordinate tiles system used by Google Maps](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#X_and_Y).\n", + "We will then create a second function to build up a web request from the URL given our parameters.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66b91ed5", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import math\n", + "import requests\n", + "\n", + "def deg2num(lat_deg, lon_deg, zoom):\n", + " \"\"\"Convert latitude and longitude to XY tiles coordinates.\"\"\"\n", + "\n", + " lat_rad = math.radians(lat_deg)\n", + " n = 2.0 ** zoom\n", + " x_tiles_coord = int((lon_deg + 180.0) / 360.0 * n)\n", + " y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)\n", + "\n", + " return (x_tiles_coord, y_tiles_coord)\n", + "\n", + "def request_map_at(latitude, longitude, zoom=10, satellite=True):\n", + " \"\"\"Retrieve a map from Google at a given location.\"\"\"\n", + "\n", + " base_url = \"https://mt0.google.com/vt?\"\n", + " x_coord, y_coord = deg2num(latitude, longitude, zoom)\n", + "\n", + " params = dict(\n", + " x=x_coord,\n", + " y=y_coord,\n", + " z=zoom,\n", + " )\n", + " if satellite:\n", + " params['lyrs'] = 's'\n", + " \n", + " return requests.get(base_url, params=params)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a147b63f", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "london_latitude, london_longitude = london_location\n", + "map_response = request_map_at(london_latitude, london_longitude)" + ] + }, + { + "cell_type": "markdown", + "id": "356c88b1", + "metadata": {}, + "source": [ + "### Checking our work" + ] + }, + { + "cell_type": "markdown", + "id": "87703c3b", + "metadata": {}, + "source": [ + "Let's see what URL we ended up with.\n", + "\n", + "Firsty we will define two constants so that we can split the returned URL into the base URL and the part of the URL that corresponds to the location we requested:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b9e6a82", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "url = map_response.url\n", + "\n", + "first_25s = slice(0, 25)\n", + "from_25th = slice(25, None)\n", + "\n", + "print(url)\n", + "print(url[first_25s])\n", + "print(url[from_25th])" + ] + }, + { + "cell_type": "markdown", + "id": "53687c4a", + "metadata": {}, + "source": [ + "`url` is a string and we can select parts of this string using the `slice`s we defined above. `first_25s` will select characters 0 to 24 of the string and `from_25th` will select all characters from the 25th onwards.\n" + ] + }, + { + "cell_type": "markdown", + "id": "2fe4e132", + "metadata": {}, + "source": [ + "We can write **tests** so that if we change our code later we can check the results are still valid. We will do this here using `assert` statements. If any of those `assert` statements are `False` we will get an error. If we receive an error from our tests we know we need to fix something in our code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48f0754b", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "assert \"https://mt0.google.com/vt?\" in url\n", + "assert \"z=10\" in url\n", + "assert \"lyrs=s\" in url" + ] + }, + { + "cell_type": "markdown", + "id": "366d403c", + "metadata": {}, + "source": [ + "Our previous function comes back with an Object representing the web request. In Python, we can use the `.\n", + "operator` to get access to a particular **attribute** of the object. In this case, the image at the requested URL is stored in the `content` attribute. It's a big file, so let's just get look at first few bytes:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88b959a0", + "metadata": {}, + "outputs": [], + "source": [ + "map_response.content[0:20]" + ] + }, + { + "cell_type": "markdown", + "id": "7a3e659f", + "metadata": {}, + "source": [ + "### Displaying results" + ] + }, + { + "cell_type": "markdown", + "id": "5d3c88f6", + "metadata": {}, + "source": [ + "We'll need to do this a lot, so we can wrap up our previous function in another function to save on typing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b95059d", + "metadata": {}, + "outputs": [], + "source": [ + "def map_content_at(latitude, longitude, zoom=10, satellite=True):\n", + " \"\"\"Retrieve a map image from Google at a given location.\"\"\"\n", + "\n", + " return request_map_at(latitude, longitude, zoom=10, satellite=True).content\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "83af920a", + "metadata": {}, + "source": [ + "We can use a library that comes with Jupyter notebook to display the image. This is one of the most powerful things about modern programming languages like Python - being able to work with images, documents, or any other kind of data just as easily as we can with numbers or strings.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a74e60c", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "map_png = map_content_at(london_latitude, london_longitude)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de7b302e", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "print(\"The type of our map result is actually a: \", type(map_png))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "271320ce", + "metadata": {}, + "outputs": [], + "source": [ + "IPython.display.Image(map_png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89661ebb", + "metadata": {}, + "outputs": [], + "source": [ + "IPython.display.Image(map_content_at(*geolocate(\"New Delhi\")))" + ] + }, + { + "cell_type": "markdown", + "id": "fd03d8e2", + "metadata": {}, + "source": [ + "### Manipulating Numbers" + ] + }, + { + "cell_type": "markdown", + "id": "4b0115d2", + "metadata": {}, + "source": [ + "Now we get to our research project: we want to use satellite imagery to find out how urbanised the world is along a line between two cites. We expect the satellite image to be greener in the countryside.\n" + ] + }, + { + "cell_type": "markdown", + "id": "21ff31c6", + "metadata": {}, + "source": [ + "We'll need to import a few more libraries to count how much green there is in an image.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6e7d88b", + "metadata": {}, + "outputs": [], + "source": [ + "from io import BytesIO # A library to convert between files and strings\n", + "import numpy as np # A library to deal with matrices\n", + "import imageio.v3 as iio # A library to deal with images" + ] + }, + { + "cell_type": "markdown", + "id": "d195374d", + "metadata": {}, + "source": [ + "Let's define what we count as green:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bdd5167", + "metadata": {}, + "outputs": [], + "source": [ + "def is_green(pixels):\n", + " \"\"\"Determine if each pixel in an image array is green.\"\"\"\n", + " \n", + " # RGB indices\n", + " red, green, blue = range(3)\n", + "\n", + " threshold = 1.1\n", + " greener_than_red = pixels[:, :, green] > threshold * pixels[:, :, red]\n", + " greener_than_blue = pixels[:, :, green] > threshold * pixels[:, :, blue]\n", + " green = np.logical_and(greener_than_red, greener_than_blue) \n", + "\n", + " return green\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "009931f2", + "metadata": {}, + "source": [ + "This code has assumed we have our pixel data for the image as a $256 \\times 256 \\times 3$ 3-d matrix,\n", + "with each of the three layers being red, green, and blue pixels.\n", + "\n", + "We find out which pixels are green by comparing, element-by-element, the middle (green, number 1) layer to the top (red, zero) and bottom (blue, 2)" + ] + }, + { + "cell_type": "markdown", + "id": "97396c81", + "metadata": {}, + "source": [ + "Now we just need to parse in our data, which is a PNG image, and turn it into our matrix format:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dea02afb", + "metadata": {}, + "outputs": [], + "source": [ + "def count_green_in_png(data):\n", + " \"\"\"Determine the total number of green pixels in an image.\"\"\"\n", + "\n", + " f = BytesIO(data)\n", + " pixels = iio.imread(f) # Get our PNG image as a numpy array\n", + "\n", + " return np.sum(is_green(pixels))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45fa5e4a", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "london_map = map_content_at(london_latitude, london_longitude)\n", + "green_count_london = count_green_in_png(london_map)\n", + "print(green_count_london)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd302a6d", + "metadata": {}, + "outputs": [], + "source": [ + "iio.imread(BytesIO(london_map)).shape" + ] + }, + { + "cell_type": "markdown", + "id": "f8151d82", + "metadata": {}, + "source": [ + "We'll also need a function to get an evenly spaced set of places between two endpoints:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e899e0b3", + "metadata": {}, + "outputs": [], + "source": [ + "def location_sequence(start, end, steps):\n", + " \"\"\"Generate a sequence of evenly spaced locations between two sets of coordinates.\"\"\"\n", + "\n", + " start_latitude, start_longitude = start\n", + " end_latitude, end_longitude = end\n", + " \n", + " latitudes = np.linspace(start_latitude, end_latitude, steps)\n", + " longitudes = np.linspace(start_longitude, end_longitude, steps)\n", + "\n", + " path = np.vstack([latitudes, longitudes]).transpose()\n", + " \n", + " return path\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ce53687", + "metadata": {}, + "outputs": [], + "source": [ + "london_to_cambridge = location_sequence(\n", + " start=geolocate(\"London\"),\n", + " end=geolocate(\"Cambridge\"),\n", + " steps=5,\n", + ")\n", + "print(london_to_cambridge)" + ] + }, + { + "cell_type": "markdown", + "id": "0456a156", + "metadata": {}, + "source": [ + "### Creating Images" + ] + }, + { + "cell_type": "markdown", + "id": "60981f34", + "metadata": {}, + "source": [ + "We should display the green content to check our work:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "246e5a14", + "metadata": {}, + "outputs": [], + "source": [ + "def show_green_in_png(data):\n", + " \"\"\"Convert all non-green pixels in an RGB image to black.\n", + "\n", + " Red and blue channel are set to 0 for all pixels.\n", + " Pixels that are green will have the green channel set to its max value.\n", + " Pixels that are non-green will have the green channel set to 0.\n", + " \"\"\"\n", + "\n", + " f = BytesIO(data)\n", + " pixels = iio.imread(f) # Get our PNG image as a numpy array\n", + " green_pixels = is_green(pixels)\n", + "\n", + " green_channel = 1\n", + " binary_pixels = np.zeros_like(pixels, dtype=np.uint8)\n", + " max_possible_value = np.iinfo(binary_pixels.dtype).max\n", + " binary_pixels[green_pixels, green_channel] = max_possible_value\n", + "\n", + " buffer = BytesIO()\n", + " binary_image = iio.imwrite(buffer, binary_pixels, extension='.png')\n", + "\n", + " return buffer.getvalue()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d80e69af", + "metadata": {}, + "outputs": [], + "source": [ + "london_location" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2a63eff", + "metadata": {}, + "outputs": [], + "source": [ + "IPython.display.Image(\n", + " map_content_at(london_latitude, london_longitude, satellite=True)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8b1c713", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "IPython.display.Image(\n", + " show_green_in_png(\n", + " map_content_at(\n", + " london_latitude,\n", + " london_longitude,\n", + " satellite=True,\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6ccce4ed", + "metadata": {}, + "source": [ + "### Looping" + ] + }, + { + "cell_type": "markdown", + "id": "03368ed0", + "metadata": {}, + "source": [ + "We can loop over each element in out list of coordinates and get a map for that place:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93c2feab", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "london_to_birmingham = location_sequence(\n", + " start=geolocate(\"London\"),\n", + " end=geolocate(\"Birmingham\"),\n", + " steps=10,\n", + ")\n", + "\n", + "london_to_birmingham_maps = []\n", + "\n", + "for latitude, longitude in london_to_birmingham:\n", + "\n", + " current_map = map_content_at(latitude, longitude)\n", + " london_to_birmingham_maps.append(current_map)\n", + " \n", + " IPython.display.display(\n", + " IPython.display.Image(\n", + " current_map,\n", + " )\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ebbf6ace", + "metadata": {}, + "source": [ + "So now we can count the green from London to Birmingham!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "580f0a6e", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "green_at_each_location = [count_green_in_png(current_map) for current_map in london_to_birmingham_maps]\n", + "print(green_at_each_location)" + ] + }, + { + "cell_type": "markdown", + "id": "6e279106", + "metadata": {}, + "source": [ + "### Plotting graphs" + ] + }, + { + "cell_type": "markdown", + "id": "c284db1e", + "metadata": {}, + "source": [ + "Let's plot a graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b66c7be", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73ee999f", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(green_at_each_location)\n", + "\n", + "plt.xticks(range(10))\n", + "plt.xlabel(\"Sequence step\")\n", + "plt.ylabel(r\"$N_{green}$\")" + ] + }, + { + "cell_type": "markdown", + "id": "27582c53", + "metadata": {}, + "source": [ + "From a research perspective, of course, this code needs a lot of work. But I hope the power of using programming is clear.\n" + ] + }, + { + "cell_type": "markdown", + "id": "4f95e08d", + "metadata": {}, + "source": [ + "### Composing Program Elements" + ] + }, + { + "cell_type": "markdown", + "id": "f043f3c8", + "metadata": {}, + "source": [ + "We built little pieces of useful code, to:\n", + "\n", + "* Find latitude and longitude of a place\n", + "* Get a map at a given latitude and longitude\n", + "* Decide whether a (red,green,blue) triple is mainly green\n", + "* Decide whether each pixel is mainly green\n", + "* Plot a new image showing the green places\n", + "* Find evenly spaced points between two places" + ] + }, + { + "cell_type": "markdown", + "id": "25480c45", + "metadata": {}, + "source": [ + "By putting these together, we can make a function which can plot this graph automatically for any two places:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d3bccf9", + "metadata": {}, + "outputs": [], + "source": [ + "def green_between(start, end, steps):\n", + " \"\"\"Count the amount of green space along a linear path between two locations.\"\"\"\n", + "\n", + " sequence = location_sequence(\n", + " start=geolocate(start),\n", + " end=geolocate(end),\n", + " steps=steps,\n", + " )\n", + " maps = [map_content_at(latitude, longitude) for latitude, longitude in sequence]\n", + " green_at_each_location = [count_green_in_png(current_map) for current_map in maps]\n", + " \n", + " return green_at_each_location\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5296d250", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(green_between('New York', 'Chicago', 20))" + ] + }, + { + "cell_type": "markdown", + "id": "9d833199", + "metadata": {}, + "source": [ + "We can also put the plotting command into a function, to make it more general:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c69559c", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_green_between(start, end, steps):\n", + " \"\"\"ount the amount of green space along a linear path between two locations\"\"\"\n", + " green_between_locations = green_between(start, end, steps)\n", + " plt.plot(green_between_locations)\n", + " xticks_steps = 5 if steps > 10 else 1\n", + " plt.xticks(range(0, steps, xticks_steps))\n", + " plt.xlabel(\"Sequence step\")\n", + " plt.ylabel(r\"$N_{green}$\")\n", + " plt.title(f\"{start} -- {end}\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "915dac71", + "metadata": {}, + "outputs": [], + "source": [ + "plot_green_between('New York', 'Chicago', 20)" + ] + }, + { + "cell_type": "markdown", + "id": "f96a1b81", + "metadata": {}, + "source": [ + "And that's it! We've covered - very very quickly - a lot of the Python language, and have introduced some of the most important concepts in modern software engineering." + ] + }, + { + "cell_type": "markdown", + "id": "fc7e54c1", + "metadata": {}, + "source": [ + "Now we'll go back, carefully, through all the concepts we touched on, and learn how to use them properly ourselves." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "An example program" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/010exemplar.ipynb.py b/ch01python/010exemplar.ipynb.py new file mode 100644 index 000000000..688b6ada4 --- /dev/null +++ b/ch01python/010exemplar.ipynb.py @@ -0,0 +1,495 @@ +# --- +# jupyter: +# jekyll: +# display_name: An example program +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## An example Python data analysis notebook + +# %% [markdown] +# This page illustrates how to use Python to perform a simple but complete analysis: retrieve data, do some computations based on it, and visualise the results. +# +# **Don't worry if you don't understand everything on this page!** Its purpose is to give you an example of things you can do and how to go about doing them - you are not expected to be able to reproduce an analysis like this in Python at this stage! We will be looking at the concepts and practices introduced on this page as we go along the course. +# +# As we show the code for different parts of the work, we will be touching on various aspects you may want to keep in mind, either related to Python specifically, or to research programming more generally. + +# %% [markdown] +# ### Why write software to manage your data and plots? + +# %% [markdown] +# We can use programs for our entire research pipeline. Not just big scientific simulation codes, but also the small scripts which we use to tidy up data and produce plots. This should be code, so that the whole research pipeline +# is recorded for reproducibility. Data manipulation in spreadsheets is much harder to share or +# check. + +# %% [markdown] +# You can see another similar demonstration on the [software carpentry site](https://swcarpentry.github.io/python-novice-inflammation/02-numpy.html). +# We'll try to give links to other sources of Python training along the way. +# Part of our approach is that we assume you know how to use the internet! If you +# find something confusing out there, please bring it along to the next session. In this course, we'll always try to draw your attention to other sources of information about what we're learning. Paying attention to as many of these as you need to, is just as important as these core notes. + +# %% [markdown] +# ### Importing Libraries + +# %% [markdown] +# Research programming is all about using libraries: tools other people have provided programs that do many cool things. +# By combining them we can feel really powerful but doing minimum work ourselves. The python syntax to import someone else's library is "import". + +# %% +import geopy # A python library for investigating geographic information. +# https://pypi.org/project/geopy/ + +# %% [markdown] +# Now, if you try to follow along on this example in an Jupyter notebook, you'll probably find that +# you just got an error message. +# +# You'll need to wait until we've covered installation of additional python libraries later in the course, then come +# back to this and try again. For now, just follow along and try get the feel for how programming for data-focused +# research works. + +# %% +# Select geocoding service provided by OpenStreetMap's Nominatim - https://wiki.openstreetmap.org/wiki/Nominatim +geocoder = geopy.geocoders.Nominatim(user_agent="comp0023") +geocoder.geocode('Cambridge', exactly_one=False) + +# %% [markdown] +# The results come out as a **list** inside a list: `[Name, [Latitude, Longitude]]`. +# Programs represent data in a variety of different **containers** like this. + +# %% [markdown] +# ### Comments + +# %% [markdown] +# Code after a `#` symbol doesn't get run. + +# %% +print("This runs") # print("This doesn't") +# print("This doesn't either") + +# %% [markdown] +# ### Functions + +# %% [markdown] +# We can wrap code up in a **function**, so that we can repeatedly get just the information we want. +# + +# %% +def geolocate(city): + """Get the latitude and longitude of a specific location.""" + + full_name, coordinates = geocoder.geocode(city) + return coordinates + + +# %% [markdown] +# Defining **functions** which put together code to make a more complex task seem simple from the outside is the most important thing in programming. The output of the function is specified using the `return` keyword. The input to the function is put inside brackets after the function name: +# + +# %% +geolocate(city='Cambridge') + +# %% [markdown] +# ### Variables + +# %% [markdown] +# We can store a result in a variable: + +# %% +london_location = geolocate("London") +print(london_location) + +# %% [markdown] +# ### More complex functions + +# %% [markdown] +# We'll fetch a map of a place from the Google Maps server, given a longitude and latitude. +# The URLs look like: `https://mt0.google.com/vt?x=658&y=340&z=10&lyrs=s`. Since we'll frequently be generating these URLs, we will create two helper functions to make our life easier. +# +# The first is a function to [convert our latitude and longitude into the coordinate tiles system used by Google Maps](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#X_and_Y). +# We will then create a second function to build up a web request from the URL given our parameters. +# + +# %% +import os +import math +import requests + +def deg2num(lat_deg, lon_deg, zoom): + """Convert latitude and longitude to XY tiles coordinates.""" + + lat_rad = math.radians(lat_deg) + n = 2.0 ** zoom + x_tiles_coord = int((lon_deg + 180.0) / 360.0 * n) + y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + + return (x_tiles_coord, y_tiles_coord) + +def request_map_at(latitude, longitude, zoom=10, satellite=True): + """Retrieve a map from Google at a given location.""" + + base_url = "https://mt0.google.com/vt?" + x_coord, y_coord = deg2num(latitude, longitude, zoom) + + params = dict( + x=x_coord, + y=y_coord, + z=zoom, + ) + if satellite: + params['lyrs'] = 's' + + return requests.get(base_url, params=params) + + + +# %% +london_latitude, london_longitude = london_location +map_response = request_map_at(london_latitude, london_longitude) + + +# %% [markdown] +# ### Checking our work + +# %% [markdown] +# Let's see what URL we ended up with. +# +# Firsty we will define two constants so that we can split the returned URL into the base URL and the part of the URL that corresponds to the location we requested: +# + +# %% +url = map_response.url + +first_25s = slice(0, 25) +from_25th = slice(25, None) + +print(url) +print(url[first_25s]) +print(url[from_25th]) + + +# %% [markdown] +# `url` is a string and we can select parts of this string using the `slice`s we defined above. `first_25s` will select characters 0 to 24 of the string and `from_25th` will select all characters from the 25th onwards. +# + +# %% [markdown] +# We can write **tests** so that if we change our code later we can check the results are still valid. We will do this here using `assert` statements. If any of those `assert` statements are `False` we will get an error. If we receive an error from our tests we know we need to fix something in our code. + +# %% +assert "https://mt0.google.com/vt?" in url +assert "z=10" in url +assert "lyrs=s" in url + + +# %% [markdown] +# Our previous function comes back with an Object representing the web request. In Python, we can use the `. +# operator` to get access to a particular **attribute** of the object. In this case, the image at the requested URL is stored in the `content` attribute. It's a big file, so let's just get look at first few bytes: +# + +# %% +map_response.content[0:20] + + +# %% [markdown] +# ### Displaying results + +# %% [markdown] +# We'll need to do this a lot, so we can wrap up our previous function in another function to save on typing. + +# %% +def map_content_at(latitude, longitude, zoom=10, satellite=True): + """Retrieve a map image from Google at a given location.""" + + return request_map_at(latitude, longitude, zoom=10, satellite=True).content + + + +# %% [markdown] +# We can use a library that comes with Jupyter notebook to display the image. This is one of the most powerful things about modern programming languages like Python - being able to work with images, documents, or any other kind of data just as easily as we can with numbers or strings. +# + +# %% +import IPython + +map_png = map_content_at(london_latitude, london_longitude) + + +# %% +print("The type of our map result is actually a: ", type(map_png)) + + +# %% +IPython.display.Image(map_png) + +# %% +IPython.display.Image(map_content_at(*geolocate("New Delhi"))) + +# %% [markdown] +# ### Manipulating Numbers + +# %% [markdown] +# Now we get to our research project: we want to use satellite imagery to find out how urbanised the world is along a line between two cites. We expect the satellite image to be greener in the countryside. +# + +# %% [markdown] +# We'll need to import a few more libraries to count how much green there is in an image. +# + +# %% +from io import BytesIO # A library to convert between files and strings +import numpy as np # A library to deal with matrices +import imageio.v3 as iio # A library to deal with images + + +# %% [markdown] +# Let's define what we count as green: + +# %% +def is_green(pixels): + """Determine if each pixel in an image array is green.""" + + # RGB indices + red, green, blue = range(3) + + threshold = 1.1 + greener_than_red = pixels[:, :, green] > threshold * pixels[:, :, red] + greener_than_blue = pixels[:, :, green] > threshold * pixels[:, :, blue] + green = np.logical_and(greener_than_red, greener_than_blue) + + return green + + + +# %% [markdown] +# This code has assumed we have our pixel data for the image as a $256 \times 256 \times 3$ 3-d matrix, +# with each of the three layers being red, green, and blue pixels. +# +# We find out which pixels are green by comparing, element-by-element, the middle (green, number 1) layer to the top (red, zero) and bottom (blue, 2) + +# %% [markdown] +# Now we just need to parse in our data, which is a PNG image, and turn it into our matrix format: + +# %% +def count_green_in_png(data): + """Determine the total number of green pixels in an image.""" + + f = BytesIO(data) + pixels = iio.imread(f) # Get our PNG image as a numpy array + + return np.sum(is_green(pixels)) + + + +# %% +london_map = map_content_at(london_latitude, london_longitude) +green_count_london = count_green_in_png(london_map) +print(green_count_london) + + +# %% +iio.imread(BytesIO(london_map)).shape + + +# %% [markdown] +# We'll also need a function to get an evenly spaced set of places between two endpoints: + +# %% +def location_sequence(start, end, steps): + """Generate a sequence of evenly spaced locations between two sets of coordinates.""" + + start_latitude, start_longitude = start + end_latitude, end_longitude = end + + latitudes = np.linspace(start_latitude, end_latitude, steps) + longitudes = np.linspace(start_longitude, end_longitude, steps) + + path = np.vstack([latitudes, longitudes]).transpose() + + return path + + + +# %% +london_to_cambridge = location_sequence( + start=geolocate("London"), + end=geolocate("Cambridge"), + steps=5, +) +print(london_to_cambridge) + + +# %% [markdown] +# ### Creating Images + +# %% [markdown] +# We should display the green content to check our work: + +# %% +def show_green_in_png(data): + """Convert all non-green pixels in an RGB image to black. + + Red and blue channel are set to 0 for all pixels. + Pixels that are green will have the green channel set to its max value. + Pixels that are non-green will have the green channel set to 0. + """ + + f = BytesIO(data) + pixels = iio.imread(f) # Get our PNG image as a numpy array + green_pixels = is_green(pixels) + + green_channel = 1 + binary_pixels = np.zeros_like(pixels, dtype=np.uint8) + max_possible_value = np.iinfo(binary_pixels.dtype).max + binary_pixels[green_pixels, green_channel] = max_possible_value + + buffer = BytesIO() + binary_image = iio.imwrite(buffer, binary_pixels, extension='.png') + + return buffer.getvalue() + + + +# %% +london_location + +# %% +IPython.display.Image( + map_content_at(london_latitude, london_longitude, satellite=True) +) + +# %% +IPython.display.Image( + show_green_in_png( + map_content_at( + london_latitude, + london_longitude, + satellite=True, + ) + ) +) + + +# %% [markdown] +# ### Looping + +# %% [markdown] +# We can loop over each element in out list of coordinates and get a map for that place: + +# %% +london_to_birmingham = location_sequence( + start=geolocate("London"), + end=geolocate("Birmingham"), + steps=10, +) + +london_to_birmingham_maps = [] + +for latitude, longitude in london_to_birmingham: + + current_map = map_content_at(latitude, longitude) + london_to_birmingham_maps.append(current_map) + + IPython.display.display( + IPython.display.Image( + current_map, + ) + ) + + +# %% [markdown] +# So now we can count the green from London to Birmingham! + +# %% +green_at_each_location = [count_green_in_png(current_map) for current_map in london_to_birmingham_maps] +print(green_at_each_location) + + +# %% [markdown] +# ### Plotting graphs + +# %% [markdown] +# Let's plot a graph. + +# %% +import matplotlib.pyplot as plt +# %matplotlib inline + +# %% +plt.plot(green_at_each_location) + +plt.xticks(range(10)) +plt.xlabel("Sequence step") +plt.ylabel(r"$N_{green}$") + + +# %% [markdown] +# From a research perspective, of course, this code needs a lot of work. But I hope the power of using programming is clear. +# + +# %% [markdown] +# ### Composing Program Elements + +# %% [markdown] +# We built little pieces of useful code, to: +# +# * Find latitude and longitude of a place +# * Get a map at a given latitude and longitude +# * Decide whether a (red,green,blue) triple is mainly green +# * Decide whether each pixel is mainly green +# * Plot a new image showing the green places +# * Find evenly spaced points between two places + +# %% [markdown] +# By putting these together, we can make a function which can plot this graph automatically for any two places: + +# %% +def green_between(start, end, steps): + """Count the amount of green space along a linear path between two locations.""" + + sequence = location_sequence( + start=geolocate(start), + end=geolocate(end), + steps=steps, + ) + maps = [map_content_at(latitude, longitude) for latitude, longitude in sequence] + green_at_each_location = [count_green_in_png(current_map) for current_map in maps] + + return green_at_each_location + + + +# %% +plt.plot(green_between('New York', 'Chicago', 20)) + + +# %% [markdown] +# We can also put the plotting command into a function, to make it more general: +# + +# %% +def plot_green_between(start, end, steps): + """ount the amount of green space along a linear path between two locations""" + green_between_locations = green_between(start, end, steps) + plt.plot(green_between_locations) + xticks_steps = 5 if steps > 10 else 1 + plt.xticks(range(0, steps, xticks_steps)) + plt.xlabel("Sequence step") + plt.ylabel(r"$N_{green}$") + plt.title(f"{start} -- {end}") + + + +# %% +plot_green_between('New York', 'Chicago', 20) + +# %% [markdown] +# And that's it! We've covered - very very quickly - a lot of the Python language, and have introduced some of the most important concepts in modern software engineering. + +# %% [markdown] +# Now we'll go back, carefully, through all the concepts we touched on, and learn how to use them properly ourselves. diff --git a/ch01python/015variables.html b/ch01python/015variables.html new file mode 100644 index 000000000..3cc5e3b1f --- /dev/null +++ b/ch01python/015variables.html @@ -0,0 +1,1279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Variables + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Variables

+
+
+
+
+
+
+

Variable Assignment

+
+
+
+
+
+
+

When we generate a result, the answer is displayed, but not kept anywhere.

+
+
+
+
+
+
In [1]:
+
+
+
2 * 3
+
+
+
+
+
+
+
+
Out[1]:
+
+
6
+
+
+
+
+
+
+
+
+

If we want to get back to that result, we have to store it. We put it in a box, with a name on the box. This is a variable.

+
+
+
+
+
+
In [2]:
+
+
+
six = 2 * 3
+
+
+
+
+
+
+
+
In [3]:
+
+
+
print(six)
+
+
+
+
+
+
+
+
+
+
6
+
+
+
+
+
+
+
+
+
+

If we look for a variable that hasn't ever been defined, we get an error.

+
+
+
+
+
+
In [4]:
+
+
+
print(seven)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+NameError                                 Traceback (most recent call last)
+Cell In[4], line 1
+----> 1 print(seven)
+
+NameError: name 'seven' is not defined
+
+
+
+
+
+
+
+
+

That's not the same as an empty box, well labeled:

+
+
+
+
+
+
In [5]:
+
+
+
nothing = None
+
+
+
+
+
+
+
+
In [6]:
+
+
+
print(nothing)
+
+
+
+
+
+
+
+
+
+
None
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
type(None)
+
+
+
+
+
+
+
+
Out[7]:
+
+
NoneType
+
+
+
+
+
+
+
+
+

(None is the special python value for a no-value variable.)

+
+
+
+
+
+
+

Supplementary Materials: There's more on variables at Software Carpentry's Python lesson.

+
+
+
+
+
+
+

Anywhere we could put a raw number, we can put a variable label, and that works fine:

+
+
+
+
+
+
In [8]:
+
+
+
print(5 * six)
+
+
+
+
+
+
+
+
+
+
30
+
+
+
+
+
+
+
+
+
In [9]:
+
+
+
scary = six * six * six
+
+
+
+
+
+
+
+
In [10]:
+
+
+
print(scary)
+
+
+
+
+
+
+
+
+
+
216
+
+
+
+
+
+
+
+
+
+

Reassignment and multiple labels

+
+
+
+
+
+
+

But here's the real scary thing: it seems like we can put something else in that box:

+
+
+
+
+
+
In [11]:
+
+
+
scary = 25
+
+
+
+
+
+
+
+
In [12]:
+
+
+
print(scary)
+
+
+
+
+
+
+
+
+
+
25
+
+
+
+
+
+
+
+
+
+

Note that the data that was there before has been lost.

+
+
+
+
+
+
+

No labels refer to it any more - so it has been "Garbage Collected"! We might imagine something pulled out of the box, and thrown on the floor, to make way for the next occupant.

+
+
+
+
+
+
+

In fact, though, it is the label that has moved. We can see this because we have more than one label refering to the same box:

+
+
+
+
+
+
In [13]:
+
+
+
name = "Eric"
+
+
+
+
+
+
+
+
In [14]:
+
+
+
nom = name
+
+
+
+
+
+
+
+
In [15]:
+
+
+
print(nom)
+
+
+
+
+
+
+
+
+
+
Eric
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
print(name)
+
+
+
+
+
+
+
+
+
+
Eric
+
+
+
+
+
+
+
+
+
+

And we can move just one of those labels:

+
+
+
+
+
+
In [17]:
+
+
+
nom = "Idle"
+
+
+
+
+
+
+
+
In [18]:
+
+
+
print(name)
+
+
+
+
+
+
+
+
+
+
Eric
+
+
+
+
+
+
+
+
+
In [19]:
+
+
+
print(nom)
+
+
+
+
+
+
+
+
+
+
Idle
+
+
+
+
+
+
+
+
+
+

So we can now develop a better understanding of our labels and boxes: each box is a piece of space (an address) in computer memory. +Each label (variable) is a reference to such a place.

+
+
+
+
+
+
+

When the number of labels on a box ("variables referencing an address") gets down to zero, then the data in the box cannot be found any more.

+
+
+
+
+
+
+

After a while, the language's "Garbage collector" will wander by, notice a box with no labels, and throw the data away, making that box +available for more data.

+
+
+
+
+
+
+

Old fashioned languages like C and Fortran don't have Garbage collectors. So a memory address with no references to it +still takes up memory, and the computer can more easily run out.

+
+
+
+
+
+
+

So when I write:

+
+
+
+
+
+
In [20]:
+
+
+
name = "Michael"
+
+
+
+
+
+
+
+
+

The following things happen:

+
+
+
+
+
+
+
    +
  1. A new text object is created, and an address in memory is found for it.
  2. +
  3. The variable "name" is moved to refer to that address.
  4. +
  5. The old address, containing "James", now has no labels.
  6. +
  7. The garbage collector frees the memory at the old address.
  8. +
+
+
+
+
+
+
+

Supplementary materials: There's an online python tutor which is great for visualising memory and references. Try the scenario we just looked at.

+

Labels are contained in groups called "frames": our frame contains two labels, 'nom' and 'name'.

+
+
+
+
+
+
+

Objects and types

+
+
+
+
+
+
+

An object, like name, has a type. In the online python tutor example, we see that the objects have type "str". +str means a text object: Programmers call these 'strings'.

+
+
+
+
+
+
In [21]:
+
+
+
type(name)
+
+
+
+
+
+
+
+
Out[21]:
+
+
str
+
+
+
+
+
+
+
+
+

Depending on its type, an object can have different properties: data fields Inside the object.

+
+
+
+
+
+
+

Consider a Python complex number for example:

+
+
+
+
+
+
In [22]:
+
+
+
z = 3 + 1j
+
+
+
+
+
+
+
+
+

We can see what properties and methods an object has available using the dir function:

+
+
+
+
+
+
In [23]:
+
+
+
dir(z)
+
+
+
+
+
+
+
+
Out[23]:
+
+
['__abs__',
+ '__add__',
+ '__bool__',
+ '__class__',
+ '__delattr__',
+ '__dir__',
+ '__divmod__',
+ '__doc__',
+ '__eq__',
+ '__float__',
+ '__floordiv__',
+ '__format__',
+ '__ge__',
+ '__getattribute__',
+ '__getnewargs__',
+ '__gt__',
+ '__hash__',
+ '__init__',
+ '__init_subclass__',
+ '__int__',
+ '__le__',
+ '__lt__',
+ '__mod__',
+ '__mul__',
+ '__ne__',
+ '__neg__',
+ '__new__',
+ '__pos__',
+ '__pow__',
+ '__radd__',
+ '__rdivmod__',
+ '__reduce__',
+ '__reduce_ex__',
+ '__repr__',
+ '__rfloordiv__',
+ '__rmod__',
+ '__rmul__',
+ '__rpow__',
+ '__rsub__',
+ '__rtruediv__',
+ '__setattr__',
+ '__sizeof__',
+ '__str__',
+ '__sub__',
+ '__subclasshook__',
+ '__truediv__',
+ 'conjugate',
+ 'imag',
+ 'real']
+
+
+
+
+
+
+
+
+

You can see that there are several methods whose name starts and ends with __ (e.g. __init__): these are special methods that Python uses internally, and we will discuss some of them later on in this course. The others (in this case, conjugate, img and real) are the methods and fields through which we can interact with this object.

+
+
+
+
+
+
In [24]:
+
+
+
type(z)
+
+
+
+
+
+
+
+
Out[24]:
+
+
complex
+
+
+
+
+
+
+
+
In [25]:
+
+
+
z.real
+
+
+
+
+
+
+
+
Out[25]:
+
+
3.0
+
+
+
+
+
+
+
+
In [26]:
+
+
+
z.imag
+
+
+
+
+
+
+
+
Out[26]:
+
+
1.0
+
+
+
+
+
+
+
+
+

A property of an object is accessed with a dot.

+
+
+
+
+
+
+

The jargon is that the "dot operator" is used to obtain a property of an object.

+
+
+
+
+
+
+

When we try to access a property that doesn't exist, we get an error:

+
+
+
+
+
+
In [27]:
+
+
+
z.wrong
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[27], line 1
+----> 1 z.wrong
+
+AttributeError: 'complex' object has no attribute 'wrong'
+
+
+
+
+
+
+
+
+

Reading error messages.

+
+
+
+
+
+
+

It's important, when learning to program, to develop an ability to read an error message and find, from in amongst +all the confusing noise, the bit of the error message which tells you what to change!

+
+
+
+
+
+
+

We don't yet know what is meant by AttributeError, or "Traceback".

+
+
+
+
+
+
In [28]:
+
+
+
z2 = 5 - 6j
+print("Gets to here")
+print(z.wrong)
+print("Didn't get to here")
+
+
+
+
+
+
+
+
+
+
Gets to here
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[28], line 3
+      1 z2 = 5 - 6j
+      2 print("Gets to here")
+----> 3 print(z.wrong)
+      4 print("Didn't get to here")
+
+AttributeError: 'complex' object has no attribute 'wrong'
+
+
+
+
+
+
+
+
+

But in the above, we can see that the error happens on the third line of our code cell.

+
+
+
+
+
+
+

We can also see that the error message:

+
+

'complex' object has no attribute 'wrong'

+
+

...tells us something important. Even if we don't understand the rest, this is useful for debugging!

+
+
+
+
+
+
+

Variables and the notebook kernel

+
+
+
+
+
+
+

When I type code in the notebook, the objects live in memory between cells.

+
+
+
+
+
+
In [29]:
+
+
+
number = 0
+
+
+
+
+
+
+
+
In [30]:
+
+
+
print(number)
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
+

If I change a variable:

+
+
+
+
+
+
In [31]:
+
+
+
number = number + 1
+
+
+
+
+
+
+
+
In [32]:
+
+
+
print(number)
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
+
+
+
+

It keeps its new value for the next cell.

+
+
+
+
+
+
+

But cells are not always evaluated in order.

+
+
+
+
+
+
+

If I now go back to Input 31, reading number = number + 1, I can run it again, with Shift-Enter. The value of number will change from 2 to 3, then from 3 to 4 - but the output of the next cell (containing the print statement) will not change unless I rerun that too. Try it!

+
+
+
+
+
+
+

So it's important to remember that if you move your cursor around in the notebook, it doesn't always run top to bottom.

+
+
+
+
+
+
+

Supplementary material: (1) Jupyter notebook documentation.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/015variables.ipynb b/ch01python/015variables.ipynb new file mode 100644 index 000000000..8dca02106 --- /dev/null +++ b/ch01python/015variables.ipynb @@ -0,0 +1,711 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cf7d75be", + "metadata": {}, + "source": [ + "## Variables" + ] + }, + { + "cell_type": "markdown", + "id": "bbc6b8cc", + "metadata": {}, + "source": [ + "### Variable Assignment" + ] + }, + { + "cell_type": "markdown", + "id": "871803d9", + "metadata": {}, + "source": [ + "When we generate a result, the answer is displayed, but not kept anywhere." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5281a1ae", + "metadata": {}, + "outputs": [], + "source": [ + "2 * 3" + ] + }, + { + "cell_type": "markdown", + "id": "d49aebb0", + "metadata": {}, + "source": [ + "If we want to get back to that result, we have to store it. We put it in a box, with a name on the box. This is a **variable**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a19c31b", + "metadata": {}, + "outputs": [], + "source": [ + "six = 2 * 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c9b330b", + "metadata": {}, + "outputs": [], + "source": [ + "print(six)" + ] + }, + { + "cell_type": "markdown", + "id": "be1e6529", + "metadata": {}, + "source": [ + "If we look for a variable that hasn't ever been defined, we get an error. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19ede309", + "metadata": {}, + "outputs": [], + "source": [ + "print(seven)" + ] + }, + { + "cell_type": "markdown", + "id": "e219ca67", + "metadata": {}, + "source": [ + "That's **not** the same as an empty box, well labeled:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89ebc667", + "metadata": {}, + "outputs": [], + "source": [ + "nothing = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4673d164", + "metadata": {}, + "outputs": [], + "source": [ + "print(nothing)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "140098d7", + "metadata": {}, + "outputs": [], + "source": [ + "type(None)" + ] + }, + { + "cell_type": "markdown", + "id": "1902488a", + "metadata": {}, + "source": [ + "(None is the special python value for a no-value variable.)" + ] + }, + { + "cell_type": "markdown", + "id": "e9d75d58", + "metadata": {}, + "source": [ + "*Supplementary Materials*: There's more on variables at [Software Carpentry's Python lesson](https://swcarpentry.github.io/python-novice-inflammation/01-intro.html)." + ] + }, + { + "cell_type": "markdown", + "id": "94f8ab4c", + "metadata": {}, + "source": [ + "Anywhere we could put a raw number, we can put a variable label, and that works fine:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dc1ab24", + "metadata": {}, + "outputs": [], + "source": [ + "print(5 * six)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d198da29", + "metadata": {}, + "outputs": [], + "source": [ + "scary = six * six * six" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60f27040", + "metadata": {}, + "outputs": [], + "source": [ + "print(scary)" + ] + }, + { + "cell_type": "markdown", + "id": "25f6550d", + "metadata": {}, + "source": [ + "### Reassignment and multiple labels" + ] + }, + { + "cell_type": "markdown", + "id": "81c2e28e", + "metadata": {}, + "source": [ + "But here's the real scary thing: it seems like we can put something else in that box:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7898b4dc", + "metadata": {}, + "outputs": [], + "source": [ + "scary = 25" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9915e5c7", + "metadata": {}, + "outputs": [], + "source": [ + "print(scary)" + ] + }, + { + "cell_type": "markdown", + "id": "a098a731", + "metadata": {}, + "source": [ + "Note that **the data that was there before has been lost**. " + ] + }, + { + "cell_type": "markdown", + "id": "e9b1c3f5", + "metadata": {}, + "source": [ + "No labels refer to it any more - so it has been \"Garbage Collected\"! We might imagine something pulled out of the box, and thrown on the floor, to make way for the next occupant." + ] + }, + { + "cell_type": "markdown", + "id": "677d173c", + "metadata": {}, + "source": [ + "In fact, though, it is the **label** that has moved. We can see this because we have more than one label refering to the same box:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c2d9e56", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Eric\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "570e649d", + "metadata": {}, + "outputs": [], + "source": [ + "nom = name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc9d8d33", + "metadata": {}, + "outputs": [], + "source": [ + "print(nom)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce3172cb", + "metadata": {}, + "outputs": [], + "source": [ + "print(name)" + ] + }, + { + "cell_type": "markdown", + "id": "6fa365c1", + "metadata": {}, + "source": [ + "And we can move just one of those labels:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03183869", + "metadata": {}, + "outputs": [], + "source": [ + "nom = \"Idle\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c66b24d", + "metadata": {}, + "outputs": [], + "source": [ + "print(name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff02c431", + "metadata": {}, + "outputs": [], + "source": [ + "print(nom)" + ] + }, + { + "cell_type": "markdown", + "id": "0a94c22c", + "metadata": {}, + "source": [ + "So we can now develop a better understanding of our labels and boxes: each box is a piece of space (an *address*) in computer memory.\n", + "Each label (variable) is a reference to such a place." + ] + }, + { + "cell_type": "markdown", + "id": "b4a32f0a", + "metadata": {}, + "source": [ + "When the number of labels on a box (\"variables referencing an address\") gets down to zero, then the data in the box cannot be found any more." + ] + }, + { + "cell_type": "markdown", + "id": "65aa2800", + "metadata": {}, + "source": [ + "After a while, the language's \"Garbage collector\" will wander by, notice a box with no labels, and throw the data away, **making that box\n", + "available for more data**." + ] + }, + { + "cell_type": "markdown", + "id": "41eebd46", + "metadata": {}, + "source": [ + "Old fashioned languages like C and Fortran don't have Garbage collectors. So a memory address with no references to it\n", + "still takes up memory, and the computer can more easily run out." + ] + }, + { + "cell_type": "markdown", + "id": "3c7f1b64", + "metadata": {}, + "source": [ + "So when I write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bb9a03f", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Michael\"" + ] + }, + { + "cell_type": "markdown", + "id": "bebdd32c", + "metadata": {}, + "source": [ + "The following things happen:" + ] + }, + { + "cell_type": "markdown", + "id": "883affd0", + "metadata": {}, + "source": [ + "1. A new text **object** is created, and an address in memory is found for it.\n", + "1. The variable \"name\" is moved to refer to that address.\n", + "1. The old address, containing \"James\", now has no labels.\n", + "1. The garbage collector frees the memory at the old address." + ] + }, + { + "cell_type": "markdown", + "id": "b0be2aa4", + "metadata": {}, + "source": [ + "**Supplementary materials**: There's an online python tutor which is great for visualising memory and references. Try the [scenario we just looked at](http://www.pythontutor.com/visualize.html#code=name%20%3D%20%22Eric%22%0Anom%20%3D%20name%0Aprint%28nom%29%0Aprint%28name%29%0Anom%20%3D%20%22Idle%22%0Aprint%28name%29%0Aprint%28nom%29%0Aname%20%3D%20%22Michael%22%0Aprint%28name%29%0Aprint%28nom%29%0A&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false).\n", + "\n", + "Labels are contained in groups called \"frames\": our frame contains two labels, 'nom' and 'name'." + ] + }, + { + "cell_type": "markdown", + "id": "ed633c13", + "metadata": {}, + "source": [ + "### Objects and types" + ] + }, + { + "cell_type": "markdown", + "id": "680c1686", + "metadata": {}, + "source": [ + "An object, like `name`, has a type. In the online python tutor example, we see that the objects have type \"str\".\n", + "`str` means a text object: Programmers call these 'strings'. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87f6c824", + "metadata": {}, + "outputs": [], + "source": [ + "type(name)" + ] + }, + { + "cell_type": "markdown", + "id": "d8c40492", + "metadata": {}, + "source": [ + "Depending on its type, an object can have different *properties*: data fields Inside the object." + ] + }, + { + "cell_type": "markdown", + "id": "f26d4da0", + "metadata": {}, + "source": [ + "Consider a Python complex number for example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "622d0914", + "metadata": {}, + "outputs": [], + "source": [ + "z = 3 + 1j" + ] + }, + { + "cell_type": "markdown", + "id": "36ceb396", + "metadata": {}, + "source": [ + "We can see what properties and methods an object has available using the `dir` function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f199999", + "metadata": {}, + "outputs": [], + "source": [ + "dir(z)" + ] + }, + { + "cell_type": "markdown", + "id": "c8232676", + "metadata": {}, + "source": [ + "You can see that there are several methods whose name starts and ends with `__` (e.g. `__init__`): these are special methods that Python uses internally, and we will discuss some of them later on in this course. The others (in this case, `conjugate`, `img` and `real`) are the methods and fields through which we can interact with this object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3101f439", + "metadata": {}, + "outputs": [], + "source": [ + "type(z)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d26d4be", + "metadata": {}, + "outputs": [], + "source": [ + "z.real" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2e94b6f", + "metadata": {}, + "outputs": [], + "source": [ + "z.imag" + ] + }, + { + "cell_type": "markdown", + "id": "bd6d84ff", + "metadata": {}, + "source": [ + "A property of an object is accessed with a dot." + ] + }, + { + "cell_type": "markdown", + "id": "0e41b886", + "metadata": {}, + "source": [ + "The jargon is that the \"dot operator\" is used to obtain a property of an object." + ] + }, + { + "cell_type": "markdown", + "id": "ead99d77", + "metadata": {}, + "source": [ + "When we try to access a property that doesn't exist, we get an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48a06f07", + "metadata": {}, + "outputs": [], + "source": [ + "z.wrong" + ] + }, + { + "cell_type": "markdown", + "id": "48f0d127", + "metadata": {}, + "source": [ + "### Reading error messages." + ] + }, + { + "cell_type": "markdown", + "id": "a92fd097", + "metadata": {}, + "source": [ + "It's important, when learning to program, to develop an ability to read an error message and find, from in amongst\n", + "all the confusing noise, the bit of the error message which tells you what to change!" + ] + }, + { + "cell_type": "markdown", + "id": "1094c3ac", + "metadata": {}, + "source": [ + "We don't yet know what is meant by `AttributeError`, or \"Traceback\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c305163d", + "metadata": {}, + "outputs": [], + "source": [ + "z2 = 5 - 6j\n", + "print(\"Gets to here\")\n", + "print(z.wrong)\n", + "print(\"Didn't get to here\")" + ] + }, + { + "cell_type": "markdown", + "id": "09a976ae", + "metadata": {}, + "source": [ + "But in the above, we can see that the error happens on the **third** line of our code cell." + ] + }, + { + "cell_type": "markdown", + "id": "0ac21d40", + "metadata": {}, + "source": [ + "We can also see that the error message: \n", + "> 'complex' object has no attribute 'wrong' \n", + "\n", + "...tells us something important. Even if we don't understand the rest, this is useful for debugging!" + ] + }, + { + "cell_type": "markdown", + "id": "01175fd9", + "metadata": {}, + "source": [ + "### Variables and the notebook kernel" + ] + }, + { + "cell_type": "markdown", + "id": "2bf7b50d", + "metadata": {}, + "source": [ + "When I type code in the notebook, the objects live in memory between cells." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64f799a8", + "metadata": {}, + "outputs": [], + "source": [ + "number = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3cc1b2bd", + "metadata": {}, + "outputs": [], + "source": [ + "print(number)" + ] + }, + { + "cell_type": "markdown", + "id": "23f8616e", + "metadata": {}, + "source": [ + "If I change a variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dcc83f4", + "metadata": {}, + "outputs": [], + "source": [ + "number = number + 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6124ed65", + "metadata": {}, + "outputs": [], + "source": [ + "print(number)" + ] + }, + { + "cell_type": "markdown", + "id": "b283c1f4", + "metadata": {}, + "source": [ + "It keeps its new value for the next cell." + ] + }, + { + "cell_type": "markdown", + "id": "43640ab0", + "metadata": {}, + "source": [ + "But cells are **not** always evaluated in order." + ] + }, + { + "cell_type": "markdown", + "id": "9e5f03ce", + "metadata": {}, + "source": [ + "If I now go back to Input 31, reading `number = number + 1`, I can run it again, with Shift-Enter. The value of `number` will change from 2 to 3, then from 3 to 4 - but the output of the next cell (containing the `print` statement) will not change unless I rerun that too. Try it!" + ] + }, + { + "cell_type": "markdown", + "id": "b53b141e", + "metadata": {}, + "source": [ + "So it's important to remember that if you move your cursor around in the notebook, it doesn't always run top to bottom." + ] + }, + { + "cell_type": "markdown", + "id": "44cb6371", + "metadata": {}, + "source": [ + "**Supplementary material**: (1) [Jupyter notebook documentation](https://jupyter-notebook.readthedocs.io/en/latest/)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Variables" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/015variables.ipynb.py b/ch01python/015variables.ipynb.py new file mode 100644 index 000000000..1a2513c0e --- /dev/null +++ b/ch01python/015variables.ipynb.py @@ -0,0 +1,259 @@ +# --- +# jupyter: +# jekyll: +# display_name: Variables +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Variables + +# %% [markdown] +# ### Variable Assignment + +# %% [markdown] +# When we generate a result, the answer is displayed, but not kept anywhere. + +# %% +2 * 3 + +# %% [markdown] +# If we want to get back to that result, we have to store it. We put it in a box, with a name on the box. This is a **variable**. + +# %% +six = 2 * 3 + +# %% +print(six) + +# %% [markdown] +# If we look for a variable that hasn't ever been defined, we get an error. + +# %% +print(seven) + +# %% [markdown] +# That's **not** the same as an empty box, well labeled: + +# %% +nothing = None + +# %% +print(nothing) + +# %% +type(None) + +# %% [markdown] +# (None is the special python value for a no-value variable.) + +# %% [markdown] +# *Supplementary Materials*: There's more on variables at [Software Carpentry's Python lesson](https://swcarpentry.github.io/python-novice-inflammation/01-intro.html). + +# %% [markdown] +# Anywhere we could put a raw number, we can put a variable label, and that works fine: + +# %% +print(5 * six) + +# %% +scary = six * six * six + +# %% +print(scary) + +# %% [markdown] +# ### Reassignment and multiple labels + +# %% [markdown] +# But here's the real scary thing: it seems like we can put something else in that box: + +# %% +scary = 25 + +# %% +print(scary) + +# %% [markdown] +# Note that **the data that was there before has been lost**. + +# %% [markdown] +# No labels refer to it any more - so it has been "Garbage Collected"! We might imagine something pulled out of the box, and thrown on the floor, to make way for the next occupant. + +# %% [markdown] +# In fact, though, it is the **label** that has moved. We can see this because we have more than one label refering to the same box: + +# %% +name = "Eric" + +# %% +nom = name + +# %% +print(nom) + +# %% +print(name) + +# %% [markdown] +# And we can move just one of those labels: + +# %% +nom = "Idle" + +# %% +print(name) + +# %% +print(nom) + +# %% [markdown] +# So we can now develop a better understanding of our labels and boxes: each box is a piece of space (an *address*) in computer memory. +# Each label (variable) is a reference to such a place. + +# %% [markdown] +# When the number of labels on a box ("variables referencing an address") gets down to zero, then the data in the box cannot be found any more. + +# %% [markdown] +# After a while, the language's "Garbage collector" will wander by, notice a box with no labels, and throw the data away, **making that box +# available for more data**. + +# %% [markdown] +# Old fashioned languages like C and Fortran don't have Garbage collectors. So a memory address with no references to it +# still takes up memory, and the computer can more easily run out. + +# %% [markdown] +# So when I write: + +# %% +name = "Michael" + +# %% [markdown] +# The following things happen: + +# %% [markdown] +# 1. A new text **object** is created, and an address in memory is found for it. +# 1. The variable "name" is moved to refer to that address. +# 1. The old address, containing "James", now has no labels. +# 1. The garbage collector frees the memory at the old address. + +# %% [markdown] +# **Supplementary materials**: There's an online python tutor which is great for visualising memory and references. Try the [scenario we just looked at](http://www.pythontutor.com/visualize.html#code=name%20%3D%20%22Eric%22%0Anom%20%3D%20name%0Aprint%28nom%29%0Aprint%28name%29%0Anom%20%3D%20%22Idle%22%0Aprint%28name%29%0Aprint%28nom%29%0Aname%20%3D%20%22Michael%22%0Aprint%28name%29%0Aprint%28nom%29%0A&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). +# +# Labels are contained in groups called "frames": our frame contains two labels, 'nom' and 'name'. + +# %% [markdown] +# ### Objects and types + +# %% [markdown] +# An object, like `name`, has a type. In the online python tutor example, we see that the objects have type "str". +# `str` means a text object: Programmers call these 'strings'. + +# %% +type(name) + +# %% [markdown] +# Depending on its type, an object can have different *properties*: data fields Inside the object. + +# %% [markdown] +# Consider a Python complex number for example: + +# %% +z = 3 + 1j + +# %% [markdown] +# We can see what properties and methods an object has available using the `dir` function: + +# %% +dir(z) + +# %% [markdown] +# You can see that there are several methods whose name starts and ends with `__` (e.g. `__init__`): these are special methods that Python uses internally, and we will discuss some of them later on in this course. The others (in this case, `conjugate`, `img` and `real`) are the methods and fields through which we can interact with this object. + +# %% +type(z) + +# %% +z.real + +# %% +z.imag + +# %% [markdown] +# A property of an object is accessed with a dot. + +# %% [markdown] +# The jargon is that the "dot operator" is used to obtain a property of an object. + +# %% [markdown] +# When we try to access a property that doesn't exist, we get an error: + +# %% +z.wrong + +# %% [markdown] +# ### Reading error messages. + +# %% [markdown] +# It's important, when learning to program, to develop an ability to read an error message and find, from in amongst +# all the confusing noise, the bit of the error message which tells you what to change! + +# %% [markdown] +# We don't yet know what is meant by `AttributeError`, or "Traceback". + +# %% +z2 = 5 - 6j +print("Gets to here") +print(z.wrong) +print("Didn't get to here") + +# %% [markdown] +# But in the above, we can see that the error happens on the **third** line of our code cell. + +# %% [markdown] +# We can also see that the error message: +# > 'complex' object has no attribute 'wrong' +# +# ...tells us something important. Even if we don't understand the rest, this is useful for debugging! + +# %% [markdown] +# ### Variables and the notebook kernel + +# %% [markdown] +# When I type code in the notebook, the objects live in memory between cells. + +# %% +number = 0 + +# %% +print(number) + +# %% [markdown] +# If I change a variable: + +# %% +number = number + 1 + +# %% +print(number) + +# %% [markdown] +# It keeps its new value for the next cell. + +# %% [markdown] +# But cells are **not** always evaluated in order. + +# %% [markdown] +# If I now go back to Input 31, reading `number = number + 1`, I can run it again, with Shift-Enter. The value of `number` will change from 2 to 3, then from 3 to 4 - but the output of the next cell (containing the `print` statement) will not change unless I rerun that too. Try it! + +# %% [markdown] +# So it's important to remember that if you move your cursor around in the notebook, it doesn't always run top to bottom. + +# %% [markdown] +# **Supplementary material**: (1) [Jupyter notebook documentation](https://jupyter-notebook.readthedocs.io/en/latest/). diff --git a/ch01python/016using_functions.html b/ch01python/016using_functions.html new file mode 100644 index 000000000..7e42fc2f8 --- /dev/null +++ b/ch01python/016using_functions.html @@ -0,0 +1,1398 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Using Functions + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Using Functions

+
+
+
+
+
+
+

Calling functions

+
+
+
+
+
+
+

We often want to do things to our objects that are more complicated than just assigning them to variables.

+
+
+
+
+
+
In [1]:
+
+
+
len("pneumonoultramicroscopicsilicovolcanoconiosis")
+
+
+
+
+
+
+
+
Out[1]:
+
+
45
+
+
+
+
+
+
+
+
+

Here we have "called a function".

+
+
+
+
+
+
+

The function len takes one input, and has one output. The output is the length of whatever the input was.

+
+
+
+
+
+
+

Programmers also call function inputs "parameters" or, confusingly, "arguments".

+
+
+
+
+
+
+

Here's another example:

+
+
+
+
+
+
In [2]:
+
+
+
sorted("Python")
+
+
+
+
+
+
+
+
Out[2]:
+
+
['P', 'h', 'n', 'o', 't', 'y']
+
+
+
+
+
+
+
+
+

Which gives us back a list of the letters in Python, sorted alphabetically (more specifically, according to their Unicode order).

+
+
+
+
+
+
+

The input goes in brackets after the function name, and the output emerges wherever the function is used.

+
+
+
+
+
+
+

So we can put a function call anywhere we could put a "literal" object or a variable.

+
+
+
+
+
+
In [3]:
+
+
+
len('Jim') * 8
+
+
+
+
+
+
+
+
Out[3]:
+
+
24
+
+
+
+
+
+
+
+
In [4]:
+
+
+
x = len('Mike')
+y = len('Bob')
+z = x + y
+
+
+
+
+
+
+
+
In [5]:
+
+
+
print(z)
+
+
+
+
+
+
+
+
+
+
7
+
+
+
+
+
+
+
+
+
+

Using methods

+
+
+
+
+
+
+

Objects come associated with a bunch of functions designed for working on objects of that type. We access these with a dot, just as we do for data attributes:

+
+
+
+
+
+
In [6]:
+
+
+
"shout".upper()
+
+
+
+
+
+
+
+
Out[6]:
+
+
'SHOUT'
+
+
+
+
+
+
+
+
+

These are called methods. If you try to use a method defined for a different type, you get an error:

+
+
+
+
+
+
In [7]:
+
+
+
x = 5
+
+
+
+
+
+
+
+
In [8]:
+
+
+
type(x)
+
+
+
+
+
+
+
+
Out[8]:
+
+
int
+
+
+
+
+
+
+
+
In [9]:
+
+
+
x.upper()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[9], line 1
+----> 1 x.upper()
+
+AttributeError: 'int' object has no attribute 'upper'
+
+
+
+
+
+
+
+
+

If you try to use a method that doesn't exist, you get an error:

+
+
+
+
+
+
In [10]:
+
+
+
x.wrong
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[10], line 1
+----> 1 x.wrong
+
+AttributeError: 'int' object has no attribute 'wrong'
+
+
+
+
+
+
+
+
+

Methods and properties are both kinds of attribute, so both are accessed with the dot operator.

+
+
+
+
+
+
+

Objects can have both properties and methods:

+
+
+
+
+
+
In [11]:
+
+
+
z = 1 + 5j
+
+
+
+
+
+
+
+
In [12]:
+
+
+
z.real
+
+
+
+
+
+
+
+
Out[12]:
+
+
1.0
+
+
+
+
+
+
+
+
In [13]:
+
+
+
z.conjugate()
+
+
+
+
+
+
+
+
Out[13]:
+
+
(1-5j)
+
+
+
+
+
+
+
+
In [14]:
+
+
+
z.conjugate
+
+
+
+
+
+
+
+
Out[14]:
+
+
<function complex.conjugate>
+
+
+
+
+
+
+
+
+

Functions are just a type of object!

+
+
+
+
+
+
+

Now for something that will take a while to understand: don't worry if you don't get this yet, we'll +look again at this in much more depth later in the course.

+

If we forget the (), we realise that a method is just a property which is a function!

+
+
+
+
+
+
In [15]:
+
+
+
z.conjugate
+
+
+
+
+
+
+
+
Out[15]:
+
+
<function complex.conjugate>
+
+
+
+
+
+
+
+
In [16]:
+
+
+
type(z.conjugate)
+
+
+
+
+
+
+
+
Out[16]:
+
+
builtin_function_or_method
+
+
+
+
+
+
+
+
In [17]:
+
+
+
somefunc = z.conjugate
+
+
+
+
+
+
+
+
In [18]:
+
+
+
somefunc()
+
+
+
+
+
+
+
+
Out[18]:
+
+
(1-5j)
+
+
+
+
+
+
+
+
+

Functions are just a kind of variable, and we can assign new labels to them:

+
+
+
+
+
+
In [19]:
+
+
+
sorted([1, 5, 3, 4])
+
+
+
+
+
+
+
+
Out[19]:
+
+
[1, 3, 4, 5]
+
+
+
+
+
+
+
+
In [20]:
+
+
+
magic = sorted
+
+
+
+
+
+
+
+
In [21]:
+
+
+
type(magic)
+
+
+
+
+
+
+
+
Out[21]:
+
+
builtin_function_or_method
+
+
+
+
+
+
+
+
In [22]:
+
+
+
magic(["Technology", "Advanced"])
+
+
+
+
+
+
+
+
Out[22]:
+
+
['Advanced', 'Technology']
+
+
+
+
+
+
+
+
+

Getting help on functions and methods

+
+
+
+
+
+
+

The 'help' function, when applied to a function, gives help on it!

+
+
+
+
+
+
In [23]:
+
+
+
help(sorted)
+
+
+
+
+
+
+
+
+
+
Help on built-in function sorted in module builtins:
+
+sorted(iterable, /, *, key=None, reverse=False)
+    Return a new list containing all items from the iterable in ascending order.
+    
+    A custom key function can be supplied to customize the sort order, and the
+    reverse flag can be set to request the result in descending order.
+
+
+
+
+
+
+
+
+
+
+

The 'dir' function, when applied to an object, lists all its attributes (properties and methods):

+
+
+
+
+
+
In [24]:
+
+
+
dir("Hexxo")
+
+
+
+
+
+
+
+
Out[24]:
+
+
['__add__',
+ '__class__',
+ '__contains__',
+ '__delattr__',
+ '__dir__',
+ '__doc__',
+ '__eq__',
+ '__format__',
+ '__ge__',
+ '__getattribute__',
+ '__getitem__',
+ '__getnewargs__',
+ '__gt__',
+ '__hash__',
+ '__init__',
+ '__init_subclass__',
+ '__iter__',
+ '__le__',
+ '__len__',
+ '__lt__',
+ '__mod__',
+ '__mul__',
+ '__ne__',
+ '__new__',
+ '__reduce__',
+ '__reduce_ex__',
+ '__repr__',
+ '__rmod__',
+ '__rmul__',
+ '__setattr__',
+ '__sizeof__',
+ '__str__',
+ '__subclasshook__',
+ 'capitalize',
+ 'casefold',
+ 'center',
+ 'count',
+ 'encode',
+ 'endswith',
+ 'expandtabs',
+ 'find',
+ 'format',
+ 'format_map',
+ 'index',
+ 'isalnum',
+ 'isalpha',
+ 'isascii',
+ 'isdecimal',
+ 'isdigit',
+ 'isidentifier',
+ 'islower',
+ 'isnumeric',
+ 'isprintable',
+ 'isspace',
+ 'istitle',
+ 'isupper',
+ 'join',
+ 'ljust',
+ 'lower',
+ 'lstrip',
+ 'maketrans',
+ 'partition',
+ 'replace',
+ 'rfind',
+ 'rindex',
+ 'rjust',
+ 'rpartition',
+ 'rsplit',
+ 'rstrip',
+ 'split',
+ 'splitlines',
+ 'startswith',
+ 'strip',
+ 'swapcase',
+ 'title',
+ 'translate',
+ 'upper',
+ 'zfill']
+
+
+
+
+
+
+
+
+

Most of these are confusing methods beginning and ending with __, part of the internals of python.

+
+
+
+
+
+
+

Again, just as with error messages, we have to learn to read past the bits that are confusing, to the bit we want:

+
+
+
+
+
+
In [25]:
+
+
+
"Hexxo".replace("x", "l")
+
+
+
+
+
+
+
+
Out[25]:
+
+
'Hello'
+
+
+
+
+
+
+
+
In [26]:
+
+
+
help("FIsh".replace)
+
+
+
+
+
+
+
+
+
+
Help on built-in function replace:
+
+replace(old, new, count=-1, /) method of builtins.str instance
+    Return a copy with all occurrences of substring old replaced by new.
+    
+      count
+        Maximum number of occurrences to replace.
+        -1 (the default value) means replace all occurrences.
+    
+    If the optional argument count is given, only the first count occurrences are
+    replaced.
+
+
+
+
+
+
+
+
+
+
+

Operators

+
+
+
+
+
+
+

Now that we know that functions are a way of taking a number of inputs and producing an output, we should look again at +what happens when we write:

+
+
+
+
+
+
In [27]:
+
+
+
x = 2 + 3
+
+
+
+
+
+
+
+
In [28]:
+
+
+
print(x)
+
+
+
+
+
+
+
+
+
+
5
+
+
+
+
+
+
+
+
+
+

This is just a pretty way of calling an "add" function. Things would be more symmetrical if add were actually written

+
x = +(2, 3)
+

Where '+' is just the name of the name of the adding function.

+
+
+
+
+
+
+

In python, these functions do exist, but they're actually methods of the first input: they're the mysterious __ functions we saw earlier (Two underscores.)

+
+
+
+
+
+
In [29]:
+
+
+
x.__add__(7)
+
+
+
+
+
+
+
+
Out[29]:
+
+
12
+
+
+
+
+
+
+
+
+

We call these symbols, +, - etc, "operators".

+
+
+
+
+
+
+

The meaning of an operator varies for different types:

+
+
+
+
+
+
In [30]:
+
+
+
"Hello" + "Goodbye"
+
+
+
+
+
+
+
+
Out[30]:
+
+
'HelloGoodbye'
+
+
+
+
+
+
+
+
In [31]:
+
+
+
[2, 3, 4] + [5, 6]
+
+
+
+
+
+
+
+
Out[31]:
+
+
[2, 3, 4, 5, 6]
+
+
+
+
+
+
+
+
+

Sometimes we get an error when a type doesn't have an operator:

+
+
+
+
+
+
In [32]:
+
+
+
7 - 2
+
+
+
+
+
+
+
+
Out[32]:
+
+
5
+
+
+
+
+
+
+
+
In [33]:
+
+
+
[2, 3, 4] - [5, 6]
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[33], line 1
+----> 1 [2, 3, 4] - [5, 6]
+
+TypeError: unsupported operand type(s) for -: 'list' and 'list'
+
+
+
+
+
+
+
+
+

The word "operand" means "thing that an operator operates on"!

+
+
+
+
+
+
+

Or when two types can't work together with an operator:

+
+
+
+
+
+
In [34]:
+
+
+
[2, 3, 4] + 5
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[34], line 1
+----> 1 [2, 3, 4] + 5
+
+TypeError: can only concatenate list (not "int") to list
+
+
+
+
+
+
+
+
+

To do this, put:

+
+
+
+
+
+
In [35]:
+
+
+
[2, 3, 4] + [5]
+
+
+
+
+
+
+
+
Out[35]:
+
+
[2, 3, 4, 5]
+
+
+
+
+
+
+
+
+

Just as in Mathematics, operators have a built-in precedence, with brackets used to force an order of operations:

+
+
+
+
+
+
In [36]:
+
+
+
print(2 + 3 * 4)
+
+
+
+
+
+
+
+
+
+
14
+
+
+
+
+
+
+
+
+
In [37]:
+
+
+
print((2 + 3) * 4)
+
+
+
+
+
+
+
+
+
+
20
+
+
+
+
+
+
+
+
+
+

Supplementary material: Python operator precedence.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/016using_functions.ipynb b/ch01python/016using_functions.ipynb new file mode 100644 index 000000000..aedb14204 --- /dev/null +++ b/ch01python/016using_functions.ipynb @@ -0,0 +1,683 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6da901af", + "metadata": {}, + "source": [ + "## Using Functions" + ] + }, + { + "cell_type": "markdown", + "id": "61b4c0ac", + "metadata": {}, + "source": [ + "### Calling functions" + ] + }, + { + "cell_type": "markdown", + "id": "a5b41b75", + "metadata": {}, + "source": [ + "We often want to do things to our objects that are more complicated than just assigning them to variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14b32faf", + "metadata": {}, + "outputs": [], + "source": [ + "len(\"pneumonoultramicroscopicsilicovolcanoconiosis\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b7728d3", + "metadata": {}, + "source": [ + "Here we have \"called a function\"." + ] + }, + { + "cell_type": "markdown", + "id": "af91f0a3", + "metadata": {}, + "source": [ + "The function `len` takes one input, and has one output. The output is the length of whatever the input was." + ] + }, + { + "cell_type": "markdown", + "id": "51d19d2d", + "metadata": {}, + "source": [ + "Programmers also call function inputs \"parameters\" or, confusingly, \"arguments\"." + ] + }, + { + "cell_type": "markdown", + "id": "3b14b161", + "metadata": {}, + "source": [ + "Here's another example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "942fdd6d", + "metadata": {}, + "outputs": [], + "source": [ + "sorted(\"Python\")" + ] + }, + { + "cell_type": "markdown", + "id": "d3eb8142", + "metadata": {}, + "source": [ + "Which gives us back a *list* of the letters in Python, sorted alphabetically (more specifically, according to their [Unicode order](https://www.ssec.wisc.edu/~tomw/java/unicode.html#x0000))." + ] + }, + { + "cell_type": "markdown", + "id": "9c570eff", + "metadata": {}, + "source": [ + "The input goes in brackets after the function name, and the output emerges wherever the function is used." + ] + }, + { + "cell_type": "markdown", + "id": "c96d551d", + "metadata": {}, + "source": [ + "So we can put a function call anywhere we could put a \"literal\" object or a variable. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c915771", + "metadata": {}, + "outputs": [], + "source": [ + "len('Jim') * 8" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9137cf7", + "metadata": {}, + "outputs": [], + "source": [ + "x = len('Mike')\n", + "y = len('Bob')\n", + "z = x + y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "778c3507", + "metadata": {}, + "outputs": [], + "source": [ + "print(z)" + ] + }, + { + "cell_type": "markdown", + "id": "dffd727b", + "metadata": {}, + "source": [ + "### Using methods" + ] + }, + { + "cell_type": "markdown", + "id": "4e3c3474", + "metadata": {}, + "source": [ + "Objects come associated with a bunch of functions designed for working on objects of that type. We access these with a dot, just as we do for data attributes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08c9eeb3", + "metadata": {}, + "outputs": [], + "source": [ + "\"shout\".upper()" + ] + }, + { + "cell_type": "markdown", + "id": "3dec7dae", + "metadata": {}, + "source": [ + "These are called methods. If you try to use a method defined for a different type, you get an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04488340", + "metadata": {}, + "outputs": [], + "source": [ + "x = 5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f06bbf0f", + "metadata": {}, + "outputs": [], + "source": [ + "type(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35c2c47e", + "metadata": {}, + "outputs": [], + "source": [ + "x.upper()" + ] + }, + { + "cell_type": "markdown", + "id": "6a84ab65", + "metadata": {}, + "source": [ + "If you try to use a method that doesn't exist, you get an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a680c57e", + "metadata": {}, + "outputs": [], + "source": [ + "x.wrong" + ] + }, + { + "cell_type": "markdown", + "id": "389f43f9", + "metadata": {}, + "source": [ + "Methods and properties are both kinds of **attribute**, so both are accessed with the dot operator." + ] + }, + { + "cell_type": "markdown", + "id": "17adb725", + "metadata": {}, + "source": [ + "Objects can have both properties and methods:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55be7288", + "metadata": {}, + "outputs": [], + "source": [ + "z = 1 + 5j" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b69bbf21", + "metadata": {}, + "outputs": [], + "source": [ + "z.real" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5260712", + "metadata": {}, + "outputs": [], + "source": [ + "z.conjugate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f3a683f", + "metadata": {}, + "outputs": [], + "source": [ + "z.conjugate" + ] + }, + { + "cell_type": "markdown", + "id": "6f47287c", + "metadata": {}, + "source": [ + "### Functions are just a type of object!" + ] + }, + { + "cell_type": "markdown", + "id": "e7e74732", + "metadata": {}, + "source": [ + "Now for something that will take a while to understand: don't worry if you don't get this yet, we'll\n", + "look again at this in much more depth later in the course.\n", + "\n", + "If we forget the (), we realise that a *method is just a property which is a function*!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "caf18774", + "metadata": {}, + "outputs": [], + "source": [ + "z.conjugate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca5c7d6b", + "metadata": {}, + "outputs": [], + "source": [ + "type(z.conjugate)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8461a9c2", + "metadata": {}, + "outputs": [], + "source": [ + "somefunc = z.conjugate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6b7c333", + "metadata": {}, + "outputs": [], + "source": [ + "somefunc()" + ] + }, + { + "cell_type": "markdown", + "id": "28c80875", + "metadata": {}, + "source": [ + "Functions are just a kind of variable, and we can assign new labels to them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0652ce5", + "metadata": {}, + "outputs": [], + "source": [ + "sorted([1, 5, 3, 4])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d59c2454", + "metadata": {}, + "outputs": [], + "source": [ + "magic = sorted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "685a46d4", + "metadata": {}, + "outputs": [], + "source": [ + "type(magic)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf6072b6", + "metadata": {}, + "outputs": [], + "source": [ + "magic([\"Technology\", \"Advanced\"])" + ] + }, + { + "cell_type": "markdown", + "id": "8a9b27f0", + "metadata": {}, + "source": [ + "### Getting help on functions and methods" + ] + }, + { + "cell_type": "markdown", + "id": "23df4fce", + "metadata": {}, + "source": [ + "The 'help' function, when applied to a function, gives help on it!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f16bb8a6", + "metadata": {}, + "outputs": [], + "source": [ + "help(sorted)" + ] + }, + { + "cell_type": "markdown", + "id": "37da7e5a", + "metadata": {}, + "source": [ + "The 'dir' function, when applied to an object, lists all its attributes (properties and methods):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9161a10f", + "metadata": {}, + "outputs": [], + "source": [ + "dir(\"Hexxo\")" + ] + }, + { + "cell_type": "markdown", + "id": "5eeabb83", + "metadata": {}, + "source": [ + "Most of these are confusing methods beginning and ending with __, part of the internals of python." + ] + }, + { + "cell_type": "markdown", + "id": "35c8c250", + "metadata": {}, + "source": [ + "Again, just as with error messages, we have to learn to read past the bits that are confusing, to the bit we want:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfd184a8", + "metadata": {}, + "outputs": [], + "source": [ + "\"Hexxo\".replace(\"x\", \"l\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8705221", + "metadata": {}, + "outputs": [], + "source": [ + "help(\"FIsh\".replace)" + ] + }, + { + "cell_type": "markdown", + "id": "d6da2ec7", + "metadata": {}, + "source": [ + "### Operators" + ] + }, + { + "cell_type": "markdown", + "id": "edd50077", + "metadata": {}, + "source": [ + "Now that we know that functions are a way of taking a number of inputs and producing an output, we should look again at\n", + "what happens when we write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8355a501", + "metadata": {}, + "outputs": [], + "source": [ + "x = 2 + 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f362896", + "metadata": {}, + "outputs": [], + "source": [ + "print(x)" + ] + }, + { + "cell_type": "markdown", + "id": "831a8d4c", + "metadata": {}, + "source": [ + "This is just a pretty way of calling an \"add\" function. Things would be more symmetrical if add were actually written\n", + "\n", + " x = +(2, 3)\n", + " \n", + "Where '+' is just the name of the name of the adding function." + ] + }, + { + "cell_type": "markdown", + "id": "c6ff8713", + "metadata": {}, + "source": [ + "In python, these functions **do** exist, but they're actually **methods** of the first input: they're the mysterious `__` functions we saw earlier (Two underscores.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d08880d", + "metadata": {}, + "outputs": [], + "source": [ + "x.__add__(7)" + ] + }, + { + "cell_type": "markdown", + "id": "c0917381", + "metadata": {}, + "source": [ + "We call these symbols, `+`, `-` etc, \"operators\"." + ] + }, + { + "cell_type": "markdown", + "id": "159b0df0", + "metadata": {}, + "source": [ + "The meaning of an operator varies for different types:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "643e2bfa", + "metadata": {}, + "outputs": [], + "source": [ + "\"Hello\" + \"Goodbye\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "162eedc3", + "metadata": {}, + "outputs": [], + "source": [ + "[2, 3, 4] + [5, 6]" + ] + }, + { + "cell_type": "markdown", + "id": "3fec4671", + "metadata": {}, + "source": [ + "Sometimes we get an error when a type doesn't have an operator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "defc69b2", + "metadata": {}, + "outputs": [], + "source": [ + "7 - 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd6d04c7", + "metadata": {}, + "outputs": [], + "source": [ + "[2, 3, 4] - [5, 6]" + ] + }, + { + "cell_type": "markdown", + "id": "2a016076", + "metadata": {}, + "source": [ + "The word \"operand\" means \"thing that an operator operates on\"!" + ] + }, + { + "cell_type": "markdown", + "id": "0d40333d", + "metadata": {}, + "source": [ + "Or when two types can't work together with an operator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7eef491e", + "metadata": {}, + "outputs": [], + "source": [ + "[2, 3, 4] + 5" + ] + }, + { + "cell_type": "markdown", + "id": "a9f6d5a9", + "metadata": {}, + "source": [ + "To do this, put:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ef179af", + "metadata": {}, + "outputs": [], + "source": [ + "[2, 3, 4] + [5]" + ] + }, + { + "cell_type": "markdown", + "id": "1c1815e8", + "metadata": {}, + "source": [ + "Just as in Mathematics, operators have a built-in precedence, with brackets used to force an order of operations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb79bae", + "metadata": {}, + "outputs": [], + "source": [ + "print(2 + 3 * 4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02f99937", + "metadata": {}, + "outputs": [], + "source": [ + "print((2 + 3) * 4)" + ] + }, + { + "cell_type": "markdown", + "id": "44c0da10", + "metadata": {}, + "source": [ + "*Supplementary material*: [Python operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Using Functions" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/016using_functions.ipynb.py b/ch01python/016using_functions.ipynb.py new file mode 100644 index 000000000..dd176f96e --- /dev/null +++ b/ch01python/016using_functions.ipynb.py @@ -0,0 +1,241 @@ +# --- +# jupyter: +# jekyll: +# display_name: Using Functions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Using Functions + +# %% [markdown] +# ### Calling functions + +# %% [markdown] +# We often want to do things to our objects that are more complicated than just assigning them to variables. + +# %% +len("pneumonoultramicroscopicsilicovolcanoconiosis") + +# %% [markdown] +# Here we have "called a function". + +# %% [markdown] +# The function `len` takes one input, and has one output. The output is the length of whatever the input was. + +# %% [markdown] +# Programmers also call function inputs "parameters" or, confusingly, "arguments". + +# %% [markdown] +# Here's another example: + +# %% +sorted("Python") + +# %% [markdown] +# Which gives us back a *list* of the letters in Python, sorted alphabetically (more specifically, according to their [Unicode order](https://www.ssec.wisc.edu/~tomw/java/unicode.html#x0000)). + +# %% [markdown] +# The input goes in brackets after the function name, and the output emerges wherever the function is used. + +# %% [markdown] +# So we can put a function call anywhere we could put a "literal" object or a variable. + +# %% +len('Jim') * 8 + +# %% +x = len('Mike') +y = len('Bob') +z = x + y + +# %% +print(z) + +# %% [markdown] +# ### Using methods + +# %% [markdown] +# Objects come associated with a bunch of functions designed for working on objects of that type. We access these with a dot, just as we do for data attributes: + +# %% +"shout".upper() + +# %% [markdown] +# These are called methods. If you try to use a method defined for a different type, you get an error: + +# %% +x = 5 + +# %% +type(x) + +# %% +x.upper() + +# %% [markdown] +# If you try to use a method that doesn't exist, you get an error: + +# %% +x.wrong + +# %% [markdown] +# Methods and properties are both kinds of **attribute**, so both are accessed with the dot operator. + +# %% [markdown] +# Objects can have both properties and methods: + +# %% +z = 1 + 5j + +# %% +z.real + +# %% +z.conjugate() + +# %% +z.conjugate + +# %% [markdown] +# ### Functions are just a type of object! + +# %% [markdown] +# Now for something that will take a while to understand: don't worry if you don't get this yet, we'll +# look again at this in much more depth later in the course. +# +# If we forget the (), we realise that a *method is just a property which is a function*! + +# %% +z.conjugate + +# %% +type(z.conjugate) + +# %% +somefunc = z.conjugate + +# %% +somefunc() + +# %% [markdown] +# Functions are just a kind of variable, and we can assign new labels to them: + +# %% +sorted([1, 5, 3, 4]) + +# %% +magic = sorted + +# %% +type(magic) + +# %% +magic(["Technology", "Advanced"]) + +# %% [markdown] +# ### Getting help on functions and methods + +# %% [markdown] +# The 'help' function, when applied to a function, gives help on it! + +# %% +help(sorted) + +# %% [markdown] +# The 'dir' function, when applied to an object, lists all its attributes (properties and methods): + +# %% +dir("Hexxo") + +# %% [markdown] +# Most of these are confusing methods beginning and ending with __, part of the internals of python. + +# %% [markdown] +# Again, just as with error messages, we have to learn to read past the bits that are confusing, to the bit we want: + +# %% +"Hexxo".replace("x", "l") + +# %% +help("FIsh".replace) + +# %% [markdown] +# ### Operators + +# %% [markdown] +# Now that we know that functions are a way of taking a number of inputs and producing an output, we should look again at +# what happens when we write: + +# %% +x = 2 + 3 + +# %% +print(x) + +# %% [markdown] +# This is just a pretty way of calling an "add" function. Things would be more symmetrical if add were actually written +# +# x = +(2, 3) +# +# Where '+' is just the name of the name of the adding function. + +# %% [markdown] +# In python, these functions **do** exist, but they're actually **methods** of the first input: they're the mysterious `__` functions we saw earlier (Two underscores.) + +# %% +x.__add__(7) + +# %% [markdown] +# We call these symbols, `+`, `-` etc, "operators". + +# %% [markdown] +# The meaning of an operator varies for different types: + +# %% +"Hello" + "Goodbye" + +# %% +[2, 3, 4] + [5, 6] + +# %% [markdown] +# Sometimes we get an error when a type doesn't have an operator: + +# %% +7 - 2 + +# %% +[2, 3, 4] - [5, 6] + +# %% [markdown] +# The word "operand" means "thing that an operator operates on"! + +# %% [markdown] +# Or when two types can't work together with an operator: + +# %% +[2, 3, 4] + 5 + +# %% [markdown] +# To do this, put: + +# %% +[2, 3, 4] + [5] + +# %% [markdown] +# Just as in Mathematics, operators have a built-in precedence, with brackets used to force an order of operations: + +# %% +print(2 + 3 * 4) + +# %% +print((2 + 3) * 4) + +# %% [markdown] +# *Supplementary material*: [Python operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence). diff --git a/ch01python/023types.html b/ch01python/023types.html new file mode 100644 index 000000000..9a75ad234 --- /dev/null +++ b/ch01python/023types.html @@ -0,0 +1,1761 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Types + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Types

+
+
+
+
+
+
+

We have seen that Python objects have a 'type':

+
+
+
+
+
+
In [1]:
+
+
+
type(5)
+
+
+
+
+
+
+
+
Out[1]:
+
+
int
+
+
+
+
+
+
+
+
+

Floats and integers

+
+
+
+
+
+
+

Python has two core numeric types, int for integer, and float for real number.

+
+
+
+
+
+
In [2]:
+
+
+
one = 1
+ten = 10
+one_float = 1.0
+ten_float = 10.
+
+
+
+
+
+
+
+
+

The zero after a decimal point is optional - it is the Dot makes it a float. However, it is better to always include the zero to improve readability.

+
+
+
+
+
+
In [3]:
+
+
+
tenth= one_float / ten_float
+
+
+
+
+
+
+
+
In [4]:
+
+
+
tenth
+
+
+
+
+
+
+
+
Out[4]:
+
+
0.1
+
+
+
+
+
+
+
+
In [5]:
+
+
+
type(one)
+
+
+
+
+
+
+
+
Out[5]:
+
+
int
+
+
+
+
+
+
+
+
In [6]:
+
+
+
type(one_float)
+
+
+
+
+
+
+
+
Out[6]:
+
+
float
+
+
+
+
+
+
+
+
+

The meaning of an operator varies depending on the type it is applied to!

+
+
+
+
+
+
In [7]:
+
+
+
print(1 + 2)  # returns an integer
+
+
+
+
+
+
+
+
+
+
3
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
print(1.0 + 2.0)  # returns a float
+
+
+
+
+
+
+
+
+
+
3.0
+
+
+
+
+
+
+
+
+
+

The division by operator always returns a float, whether it's applied to floats or ints.

+
+
+
+
+
+
In [9]:
+
+
+
10 / 3
+
+
+
+
+
+
+
+
Out[9]:
+
+
3.3333333333333335
+
+
+
+
+
+
+
+
In [10]:
+
+
+
10.0 / 3
+
+
+
+
+
+
+
+
Out[10]:
+
+
3.3333333333333335
+
+
+
+
+
+
+
+
In [11]:
+
+
+
10 / 3.0
+
+
+
+
+
+
+
+
Out[11]:
+
+
3.3333333333333335
+
+
+
+
+
+
+
+
+

To perform integer division we need to use the divmod function, which returns the quotiant and remainder of the division.

+
+
+
+
+
+
In [12]:
+
+
+
quotiant, remainder = divmod(10, 3)
+print(f"{quotiant=}, {remainder=}")
+
+
+
+
+
+
+
+
+
+
quotiant=3, remainder=1
+
+
+
+
+
+
+
+
+
+

Note that if either of the input type are float, the returned values will also be floats.

+
+
+
+
+
+
In [13]:
+
+
+
divmod(10, 3.0)
+
+
+
+
+
+
+
+
Out[13]:
+
+
(3.0, 1.0)
+
+
+
+
+
+
+
+
+

There is a function for every built-in type, which is used to convert the input to an output of the desired type.

+
+
+
+
+
+
In [14]:
+
+
+
x = float(5)
+type(x)
+
+
+
+
+
+
+
+
Out[14]:
+
+
float
+
+
+
+
+
+
+
+
In [15]:
+
+
+
divmod(10, float(3))
+
+
+
+
+
+
+
+
Out[15]:
+
+
(3.0, 1.0)
+
+
+
+
+
+
+
+
+

I lied when I said that the float type was a real number. It's actually a computer representation of a real number +called a "floating point number". Representing $\sqrt 2$ or $\frac{1}{3}$ perfectly would be impossible in a computer, so we use a finite amount of memory to do it.

+
+
+
+
+
+
In [16]:
+
+
+
N = 10000.0
+sum([1 / N] * int(N))
+
+
+
+
+
+
+
+
Out[16]:
+
+
0.9999999999999062
+
+
+
+
+
+ +
+
+
+

Strings

+
+
+
+
+
+
+

Python has a built in string type, supporting many +useful methods.

+
+
+
+
+
+
In [17]:
+
+
+
given = "Terry"
+family = "Jones"
+full = given + " " + family
+
+
+
+
+
+
+
+
+

So + for strings means "join them together" - concatenate.

+
+
+
+
+
+
In [18]:
+
+
+
print(full.upper())
+
+
+
+
+
+
+
+
+
+
TERRY JONES
+
+
+
+
+
+
+
+
+
+

As for float and int, the name of a type can be used as a function to convert between types:

+
+
+
+
+
+
In [19]:
+
+
+
ten, one
+
+
+
+
+
+
+
+
Out[19]:
+
+
(10, 1)
+
+
+
+
+
+
+
+
In [20]:
+
+
+
print(ten + one)
+
+
+
+
+
+
+
+
+
+
11
+
+
+
+
+
+
+
+
+
In [21]:
+
+
+
print(float(str(ten) + str(one)))
+
+
+
+
+
+
+
+
+
+
101.0
+
+
+
+
+
+
+
+
+
+

We can remove extraneous material from the start and end of a string:

+
+
+
+
+
+
In [22]:
+
+
+
"    Hello  ".strip()
+
+
+
+
+
+
+
+
Out[22]:
+
+
'Hello'
+
+
+
+
+
+
+
+
+

Note that you can write strings in Python using either single (' ... ') or double (" ... ") quote marks. The two ways are equivalent. However, if your string includes a single quote (e.g. an apostrophe), you should use double quotes to surround it:

+
+
+
+
+
+
In [23]:
+
+
+
"Terry's animation"
+
+
+
+
+
+
+
+
Out[23]:
+
+
"Terry's animation"
+
+
+
+
+
+
+
+
+

And vice versa: if your string has a double quote inside it, you should wrap the whole string in single quotes.

+
+
+
+
+
+
In [24]:
+
+
+
'"Wow!", said John.'
+
+
+
+
+
+
+
+
Out[24]:
+
+
'"Wow!", said John.'
+
+
+
+
+
+
+
+
+

Lists

+
+
+
+
+
+
+

Python's basic container type is the list.

+
+
+
+
+
+
+

We can define our own list with square brackets:

+
+
+
+
+
+
In [25]:
+
+
+
[1, 3, 7]
+
+
+
+
+
+
+
+
Out[25]:
+
+
[1, 3, 7]
+
+
+
+
+
+
+
+
In [26]:
+
+
+
type([1, 3, 7])
+
+
+
+
+
+
+
+
Out[26]:
+
+
list
+
+
+
+
+
+
+
+
+

Lists do not have to contain just one type:

+
+
+
+
+
+
In [27]:
+
+
+
various_things = [1, 2, "banana", 3.4, [1,2] ]
+
+
+
+
+
+
+
+
+

We access an element of a list with an int in square brackets:

+
+
+
+
+
+
In [28]:
+
+
+
various_things[2]
+
+
+
+
+
+
+
+
Out[28]:
+
+
'banana'
+
+
+
+
+
+
+
+
In [29]:
+
+
+
index = 0
+various_things[index]
+
+
+
+
+
+
+
+
Out[29]:
+
+
1
+
+
+
+
+
+
+
+
+

Note that list indices start from zero.

+
+
+
+
+
+
+

We can use a string to join together a list of strings:

+
+
+
+
+
+
In [30]:
+
+
+
name = ["Sir", "Michael", "Edward", "Palin"]
+print("==".join(name))
+
+
+
+
+
+
+
+
+
+
Sir==Michael==Edward==Palin
+
+
+
+
+
+
+
+
+
+

And we can split up a string into a list:

+
+
+
+
+
+
In [31]:
+
+
+
"Ernst Stavro Blofeld".split(" ")
+
+
+
+
+
+
+
+
Out[31]:
+
+
['Ernst', 'Stavro', 'Blofeld']
+
+
+
+
+
+
+
+
In [32]:
+
+
+
"Ernst Stavro Blofeld".split("o")
+
+
+
+
+
+
+
+
Out[32]:
+
+
['Ernst Stavr', ' Bl', 'feld']
+
+
+
+
+
+
+
+
+

And combine these:

+
+
+
+
+
+
In [33]:
+
+
+
"->".join("John Ronald Reuel Tolkein".split(" "))
+
+
+
+
+
+
+
+
Out[33]:
+
+
'John->Ronald->Reuel->Tolkein'
+
+
+
+
+
+
+
+
+

A matrix can be represented by nesting lists -- putting lists inside other lists.

+
+
+
+
+
+
In [34]:
+
+
+
identity = [[1, 0], [0, 1]]
+
+
+
+
+
+
+
+
In [35]:
+
+
+
identity[0][0]
+
+
+
+
+
+
+
+
Out[35]:
+
+
1
+
+
+
+
+
+
+
+
+

... but later we will learn about a better way of representing matrices.

+
+
+
+
+
+
+

Ranges

+
+
+
+
+
+
+

Another useful type is range, which gives you a sequence of consecutive numbers. In contrast to a list, ranges generate the numbers as you need them, rather than all at once.

+

If you try to print a range, you'll see something that looks a little strange:

+
+
+
+
+
+
In [36]:
+
+
+
range(5)
+
+
+
+
+
+
+
+
Out[36]:
+
+
range(0, 5)
+
+
+
+
+
+
+
+
+

We don't see the contents, because they haven't been generatead yet. Instead, Python gives us a description of the object - in this case, its type (range) and its lower and upper limits.

+
+
+
+
+
+
+

We can quickly make a list with numbers counted up by converting this range:

+
+
+
+
+
+
In [37]:
+
+
+
count_to_five = range(5)
+print(list(count_to_five))
+
+
+
+
+
+
+
+
+
+
[0, 1, 2, 3, 4]
+
+
+
+
+
+
+
+
+
+

Ranges in Python can be customised in other ways, such as by specifying the lower limit or the step (that is, the difference between successive elements). You can find more information about them in the official Python documentation.

+
+
+
+
+
+
+

Sequences

+
+
+
+
+
+
+

Many other things can be treated like lists. Python calls things that can be treated like lists sequences.

+
+
+
+
+
+
+

A string is one such sequence type.

+
+
+
+
+
+
+

Sequences support various useful operations, including:

+
    +
  • Accessing a single element at a particular index: sequence[index]
  • +
  • Accessing multiple elements (a slice): sequence[start:end_plus_one]
  • +
  • Getting the length of a sequence: len(sequence)
  • +
  • Checking whether the sequence contains an element: element in sequence
  • +
+

The following examples illustrate these operations with lists, strings and ranges.

+
+
+
+
+
+
In [38]:
+
+
+
print(count_to_five[1])
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
+
+
+
In [39]:
+
+
+
print("Palin"[2])
+
+
+
+
+
+
+
+
+
+
l
+
+
+
+
+
+
+
+
+
In [40]:
+
+
+
count_to_five = range(5)
+
+
+
+
+
+
+
+
In [41]:
+
+
+
count_to_five[1:3]
+
+
+
+
+
+
+
+
Out[41]:
+
+
range(1, 3)
+
+
+
+
+
+
+
+
In [42]:
+
+
+
"Hello World"[4:8]
+
+
+
+
+
+
+
+
Out[42]:
+
+
'o Wo'
+
+
+
+
+
+
+
+
In [43]:
+
+
+
len(various_things)
+
+
+
+
+
+
+
+
Out[43]:
+
+
5
+
+
+
+
+
+
+
+
In [44]:
+
+
+
len("Python")
+
+
+
+
+
+
+
+
Out[44]:
+
+
6
+
+
+
+
+
+
+
+
In [45]:
+
+
+
name
+
+
+
+
+
+
+
+
Out[45]:
+
+
['Sir', 'Michael', 'Edward', 'Palin']
+
+
+
+
+
+
+
+
In [46]:
+
+
+
"Edward" in name
+
+
+
+
+
+
+
+
Out[46]:
+
+
True
+
+
+
+
+
+
+
+
In [47]:
+
+
+
3 in count_to_five
+
+
+
+
+
+
+
+
Out[47]:
+
+
True
+
+
+
+
+
+
+
+
+

Unpacking

+
+
+
+
+
+
+

Multiple values can be unpacked when assigning from sequences, like dealing out decks of cards.

+
+
+
+
+
+
In [48]:
+
+
+
mylist = ['Hello', 'World']
+a, b = mylist
+print(b)
+
+
+
+
+
+
+
+
+
+
World
+
+
+
+
+
+
+
+
+
In [49]:
+
+
+
range(4)
+
+
+
+
+
+
+
+
Out[49]:
+
+
range(0, 4)
+
+
+
+
+
+
+
+
In [50]:
+
+
+
zero, one, two, three = range(4)
+
+
+
+
+
+
+
+
In [51]:
+
+
+
two
+
+
+
+
+
+
+
+
Out[51]:
+
+
2
+
+
+
+
+
+
+
+
+

If there is too much or too little data, an error results:

+
+
+
+
+
+
In [52]:
+
+
+
zero, one, two, three = range(7)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[52], line 1
+----> 1 zero, one, two, three = range(7)
+
+ValueError: too many values to unpack (expected 4)
+
+
+
+
+
+
+
+
In [53]:
+
+
+
zero, one, two, three = range(2)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[53], line 1
+----> 1 zero, one, two, three = range(2)
+
+ValueError: not enough values to unpack (expected 4, got 2)
+
+
+
+
+
+
+
+
+

Python provides some handy syntax to split a sequence into its first element ("head") and the remaining ones (its "tail"):

+
+
+
+
+
+
In [54]:
+
+
+
head, *tail = range(4)
+print("head is", head)
+print("tail is", tail)
+
+
+
+
+
+
+
+
+
+
head is 0
+tail is [1, 2, 3]
+
+
+
+
+
+
+
+
+
+

Note the syntax with the *. The same pattern can be used, for example, to extract the middle segment of a sequence whose length we might not know:

+
+
+
+
+
+
In [55]:
+
+
+
one, *two, three = range(10)
+
+
+
+
+
+
+
+
In [56]:
+
+
+
print("one is", one)
+print("two is", two)
+print("three is", three)
+
+
+
+
+
+
+
+
+
+
one is 0
+two is [1, 2, 3, 4, 5, 6, 7, 8]
+three is 9
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/023types.ipynb b/ch01python/023types.ipynb new file mode 100644 index 000000000..4a58dad3e --- /dev/null +++ b/ch01python/023types.ipynb @@ -0,0 +1,958 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d51d499a", + "metadata": {}, + "source": [ + "## Types" + ] + }, + { + "cell_type": "markdown", + "id": "442200f4", + "metadata": {}, + "source": [ + "We have seen that Python objects have a 'type':" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3bc5b75", + "metadata": {}, + "outputs": [], + "source": [ + "type(5)" + ] + }, + { + "cell_type": "markdown", + "id": "75ea5f6c", + "metadata": {}, + "source": [ + "### Floats and integers" + ] + }, + { + "cell_type": "markdown", + "id": "ab6d4533", + "metadata": {}, + "source": [ + "Python has two core numeric types, `int` for integer, and `float` for real number." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2ffb123", + "metadata": {}, + "outputs": [], + "source": [ + "one = 1\n", + "ten = 10\n", + "one_float = 1.0\n", + "ten_float = 10." + ] + }, + { + "cell_type": "markdown", + "id": "2fa65f0f", + "metadata": {}, + "source": [ + "The zero after a decimal point is optional - it is the **Dot** makes it a float. However, it is better to always include the zero to improve readability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da5f1c88", + "metadata": {}, + "outputs": [], + "source": [ + "tenth= one_float / ten_float" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ae36666", + "metadata": {}, + "outputs": [], + "source": [ + "tenth" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46cdc034", + "metadata": {}, + "outputs": [], + "source": [ + "type(one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79c8f4e1", + "metadata": {}, + "outputs": [], + "source": [ + "type(one_float)" + ] + }, + { + "cell_type": "markdown", + "id": "6145c6e9", + "metadata": {}, + "source": [ + "The meaning of an operator varies depending on the type it is applied to!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdbbff2a", + "metadata": {}, + "outputs": [], + "source": [ + "print(1 + 2) # returns an integer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73754771", + "metadata": {}, + "outputs": [], + "source": [ + "print(1.0 + 2.0) # returns a float" + ] + }, + { + "cell_type": "markdown", + "id": "cc189bbe", + "metadata": {}, + "source": [ + "The division by operator always returns a `float`, whether it's applied to `float`s or `int`s." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d9659f5", + "metadata": {}, + "outputs": [], + "source": [ + "10 / 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b90fb7a", + "metadata": {}, + "outputs": [], + "source": [ + "10.0 / 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33c4f4db", + "metadata": {}, + "outputs": [], + "source": [ + "10 / 3.0" + ] + }, + { + "cell_type": "markdown", + "id": "ce4b866e", + "metadata": {}, + "source": [ + "To perform integer division we need to use the `divmod` function, which returns the quotiant and remainder of the division." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4638a898", + "metadata": {}, + "outputs": [], + "source": [ + "quotiant, remainder = divmod(10, 3)\n", + "print(f\"{quotiant=}, {remainder=}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b5c8bbc0", + "metadata": {}, + "source": [ + "Note that if either of the input type are `float`, the returned values will also be `float`s." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7439bf0b", + "metadata": {}, + "outputs": [], + "source": [ + "divmod(10, 3.0)" + ] + }, + { + "cell_type": "markdown", + "id": "346f9722", + "metadata": {}, + "source": [ + "There is a function for every built-in type, which is used to convert the input to an output of the desired type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f07b813", + "metadata": {}, + "outputs": [], + "source": [ + "x = float(5)\n", + "type(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "903c64ba", + "metadata": {}, + "outputs": [], + "source": [ + "divmod(10, float(3))" + ] + }, + { + "cell_type": "markdown", + "id": "54dfc22a", + "metadata": {}, + "source": [ + "I lied when I said that the `float` type was a real number. It's actually a computer representation of a real number\n", + "called a \"floating point number\". Representing $\\sqrt 2$ or $\\frac{1}{3}$ perfectly would be impossible in a computer, so we use a finite amount of memory to do it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44dba4fe", + "metadata": {}, + "outputs": [], + "source": [ + "N = 10000.0\n", + "sum([1 / N] * int(N))" + ] + }, + { + "cell_type": "markdown", + "id": "c8b8bfe0", + "metadata": {}, + "source": [ + "*Supplementary material*:\n", + "\n", + "* [Python's documentation about floating point arithmetic](https://docs.python.org/tutorial/floatingpoint.html);\n", + "* [How floating point numbers work](http://floating-point-gui.de/formats/fp/);\n", + "* Advanced: [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)." + ] + }, + { + "cell_type": "markdown", + "id": "2ba26ec3", + "metadata": {}, + "source": [ + "### Strings" + ] + }, + { + "cell_type": "markdown", + "id": "f10c50b1", + "metadata": {}, + "source": [ + "Python has a built in `string` type, supporting many\n", + "useful methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a077e54a", + "metadata": {}, + "outputs": [], + "source": [ + "given = \"Terry\"\n", + "family = \"Jones\"\n", + "full = given + \" \" + family" + ] + }, + { + "cell_type": "markdown", + "id": "72ddb441", + "metadata": {}, + "source": [ + "So `+` for strings means \"join them together\" - *concatenate*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd9d9114", + "metadata": {}, + "outputs": [], + "source": [ + "print(full.upper())" + ] + }, + { + "cell_type": "markdown", + "id": "f67001c6", + "metadata": {}, + "source": [ + "As for `float` and `int`, the name of a type can be used as a function to convert between types:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f78a717", + "metadata": {}, + "outputs": [], + "source": [ + "ten, one" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab23cecb", + "metadata": {}, + "outputs": [], + "source": [ + "print(ten + one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "201035fe", + "metadata": {}, + "outputs": [], + "source": [ + "print(float(str(ten) + str(one)))" + ] + }, + { + "cell_type": "markdown", + "id": "509bed9d", + "metadata": {}, + "source": [ + "We can remove extraneous material from the start and end of a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5906bd1", + "metadata": {}, + "outputs": [], + "source": [ + "\" Hello \".strip()" + ] + }, + { + "cell_type": "markdown", + "id": "3c1c24a6", + "metadata": {}, + "source": [ + "Note that you can write strings in Python using either single (`' ... '`) or double (`\" ... \"`) quote marks. The two ways are equivalent. However, if your string includes a single quote (e.g. an apostrophe), you should use double quotes to surround it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7014eed", + "metadata": {}, + "outputs": [], + "source": [ + "\"Terry's animation\"" + ] + }, + { + "cell_type": "markdown", + "id": "8d4e1e1b", + "metadata": {}, + "source": [ + "And vice versa: if your string has a double quote inside it, you should wrap the whole string in single quotes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0190966e", + "metadata": {}, + "outputs": [], + "source": [ + "'\"Wow!\", said John.'" + ] + }, + { + "cell_type": "markdown", + "id": "777cd6a9", + "metadata": {}, + "source": [ + "### Lists" + ] + }, + { + "cell_type": "markdown", + "id": "c89718b0", + "metadata": {}, + "source": [ + "Python's basic **container** type is the `list`." + ] + }, + { + "cell_type": "markdown", + "id": "c35e1efc", + "metadata": {}, + "source": [ + "We can define our own list with square brackets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cd7c343", + "metadata": {}, + "outputs": [], + "source": [ + "[1, 3, 7]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab7cedc9", + "metadata": {}, + "outputs": [], + "source": [ + "type([1, 3, 7])" + ] + }, + { + "cell_type": "markdown", + "id": "6492a71d", + "metadata": {}, + "source": [ + "Lists *do not* have to contain just one type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5480de1c", + "metadata": {}, + "outputs": [], + "source": [ + "various_things = [1, 2, \"banana\", 3.4, [1,2] ]" + ] + }, + { + "cell_type": "markdown", + "id": "6c850745", + "metadata": {}, + "source": [ + "We access an **element** of a list with an `int` in square brackets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4d8a195", + "metadata": {}, + "outputs": [], + "source": [ + "various_things[2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c93c1d3c", + "metadata": {}, + "outputs": [], + "source": [ + "index = 0\n", + "various_things[index]" + ] + }, + { + "cell_type": "markdown", + "id": "a2375b74", + "metadata": {}, + "source": [ + "Note that list indices start from zero." + ] + }, + { + "cell_type": "markdown", + "id": "4d1ccf3b", + "metadata": {}, + "source": [ + "We can use a string to join together a list of strings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28f83419", + "metadata": {}, + "outputs": [], + "source": [ + "name = [\"Sir\", \"Michael\", \"Edward\", \"Palin\"]\n", + "print(\"==\".join(name))" + ] + }, + { + "cell_type": "markdown", + "id": "4f05983d", + "metadata": {}, + "source": [ + "And we can split up a string into a list:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db3aabbf", + "metadata": {}, + "outputs": [], + "source": [ + "\"Ernst Stavro Blofeld\".split(\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed540d23", + "metadata": {}, + "outputs": [], + "source": [ + "\"Ernst Stavro Blofeld\".split(\"o\")" + ] + }, + { + "cell_type": "markdown", + "id": "0631c48b", + "metadata": {}, + "source": [ + "And combine these:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5300880e", + "metadata": {}, + "outputs": [], + "source": [ + "\"->\".join(\"John Ronald Reuel Tolkein\".split(\" \"))" + ] + }, + { + "cell_type": "markdown", + "id": "38fb9220", + "metadata": {}, + "source": [ + "A matrix can be represented by **nesting** lists -- putting lists inside other lists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09447677", + "metadata": {}, + "outputs": [], + "source": [ + "identity = [[1, 0], [0, 1]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f06a4c5", + "metadata": {}, + "outputs": [], + "source": [ + "identity[0][0]" + ] + }, + { + "cell_type": "markdown", + "id": "92a44555", + "metadata": {}, + "source": [ + "... but later we will learn about a better way of representing matrices." + ] + }, + { + "cell_type": "markdown", + "id": "aab769da", + "metadata": {}, + "source": [ + "### Ranges" + ] + }, + { + "cell_type": "markdown", + "id": "6ddfc7df", + "metadata": {}, + "source": [ + "Another useful type is range, which gives you a sequence of consecutive numbers. In contrast to a list, ranges generate the numbers as you need them, rather than all at once.\n", + "\n", + "If you try to print a range, you'll see something that looks a little strange: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6c9ef25", + "metadata": {}, + "outputs": [], + "source": [ + "range(5)" + ] + }, + { + "cell_type": "markdown", + "id": "c7f98769", + "metadata": {}, + "source": [ + "We don't see the contents, because *they haven't been generatead yet*. Instead, Python gives us a description of the object - in this case, its type (range) and its lower and upper limits." + ] + }, + { + "cell_type": "markdown", + "id": "3b10bcd7", + "metadata": {}, + "source": [ + "We can quickly make a list with numbers counted up by converting this range:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dab4fff5", + "metadata": {}, + "outputs": [], + "source": [ + "count_to_five = range(5)\n", + "print(list(count_to_five))" + ] + }, + { + "cell_type": "markdown", + "id": "593c9806", + "metadata": {}, + "source": [ + "Ranges in Python can be customised in other ways, such as by specifying the lower limit or the step (that is, the difference between successive elements). You can find more information about them in the [official Python documentation](https://docs.python.org/3/library/stdtypes.html#ranges)." + ] + }, + { + "cell_type": "markdown", + "id": "9e33cef6", + "metadata": {}, + "source": [ + "### Sequences" + ] + }, + { + "cell_type": "markdown", + "id": "e84a2907", + "metadata": {}, + "source": [ + "Many other things can be treated like `lists`. Python calls things that can be treated like lists `sequences`." + ] + }, + { + "cell_type": "markdown", + "id": "ade9a8e8", + "metadata": {}, + "source": [ + "A string is one such *sequence type*." + ] + }, + { + "cell_type": "markdown", + "id": "6e8f8239", + "metadata": {}, + "source": [ + "Sequences support various useful operations, including:\n", + "- Accessing a single element at a particular index: `sequence[index]`\n", + "- Accessing multiple elements (a *slice*): `sequence[start:end_plus_one]`\n", + "- Getting the length of a sequence: `len(sequence)`\n", + "- Checking whether the sequence contains an element: `element in sequence`\n", + "\n", + "The following examples illustrate these operations with lists, strings and ranges." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94db6c2c", + "metadata": {}, + "outputs": [], + "source": [ + "print(count_to_five[1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4db9fcf", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Palin\"[2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "242a737f", + "metadata": {}, + "outputs": [], + "source": [ + "count_to_five = range(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec29d8aa", + "metadata": {}, + "outputs": [], + "source": [ + "count_to_five[1:3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fb2a46f", + "metadata": {}, + "outputs": [], + "source": [ + "\"Hello World\"[4:8]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ecf4b03", + "metadata": {}, + "outputs": [], + "source": [ + "len(various_things)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c1f039e", + "metadata": {}, + "outputs": [], + "source": [ + "len(\"Python\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30e65083", + "metadata": {}, + "outputs": [], + "source": [ + "name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95092906", + "metadata": {}, + "outputs": [], + "source": [ + "\"Edward\" in name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e47932d", + "metadata": {}, + "outputs": [], + "source": [ + "3 in count_to_five" + ] + }, + { + "cell_type": "markdown", + "id": "b00fa690", + "metadata": {}, + "source": [ + "### Unpacking" + ] + }, + { + "cell_type": "markdown", + "id": "0bd80f9b", + "metadata": {}, + "source": [ + "Multiple values can be **unpacked** when assigning from sequences, like dealing out decks of cards." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30960860", + "metadata": {}, + "outputs": [], + "source": [ + "mylist = ['Hello', 'World']\n", + "a, b = mylist\n", + "print(b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82c9b610", + "metadata": {}, + "outputs": [], + "source": [ + "range(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60df67a6", + "metadata": {}, + "outputs": [], + "source": [ + "zero, one, two, three = range(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5ade1d5", + "metadata": {}, + "outputs": [], + "source": [ + "two" + ] + }, + { + "cell_type": "markdown", + "id": "a34d60d4", + "metadata": {}, + "source": [ + "If there is too much or too little data, an error results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4cbeaf3", + "metadata": {}, + "outputs": [], + "source": [ + "zero, one, two, three = range(7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36797fd4", + "metadata": {}, + "outputs": [], + "source": [ + "zero, one, two, three = range(2)" + ] + }, + { + "cell_type": "markdown", + "id": "4f54118f", + "metadata": {}, + "source": [ + "Python provides some handy syntax to split a sequence into its first element (\"head\") and the remaining ones (its \"tail\"):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d5e49e7", + "metadata": {}, + "outputs": [], + "source": [ + "head, *tail = range(4)\n", + "print(\"head is\", head)\n", + "print(\"tail is\", tail)" + ] + }, + { + "cell_type": "markdown", + "id": "f1566934", + "metadata": {}, + "source": [ + "Note the syntax with the \\*. The same pattern can be used, for example, to extract the middle segment of a sequence whose length we might not know:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21ce3f68", + "metadata": {}, + "outputs": [], + "source": [ + "one, *two, three = range(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1616b787", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"one is\", one)\n", + "print(\"two is\", two)\n", + "print(\"three is\", three)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Types" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/023types.ipynb.py b/ch01python/023types.ipynb.py new file mode 100644 index 000000000..0efc6f335 --- /dev/null +++ b/ch01python/023types.ipynb.py @@ -0,0 +1,343 @@ +# --- +# jupyter: +# jekyll: +# display_name: Types +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Types + +# %% [markdown] +# We have seen that Python objects have a 'type': + +# %% +type(5) + +# %% [markdown] +# ### Floats and integers + +# %% [markdown] +# Python has two core numeric types, `int` for integer, and `float` for real number. + +# %% +one = 1 +ten = 10 +one_float = 1.0 +ten_float = 10. + +# %% [markdown] +# The zero after a decimal point is optional - it is the **Dot** makes it a float. However, it is better to always include the zero to improve readability. + +# %% +tenth= one_float / ten_float + +# %% +tenth + +# %% +type(one) + +# %% +type(one_float) + +# %% [markdown] +# The meaning of an operator varies depending on the type it is applied to! + +# %% +print(1 + 2) # returns an integer + +# %% +print(1.0 + 2.0) # returns a float + +# %% [markdown] +# The division by operator always returns a `float`, whether it's applied to `float`s or `int`s. + +# %% +10 / 3 + +# %% +10.0 / 3 + +# %% +10 / 3.0 + +# %% [markdown] +# To perform integer division we need to use the `divmod` function, which returns the quotiant and remainder of the division. + +# %% +quotiant, remainder = divmod(10, 3) +print(f"{quotiant=}, {remainder=}") + +# %% [markdown] +# Note that if either of the input type are `float`, the returned values will also be `float`s. + +# %% +divmod(10, 3.0) + +# %% [markdown] +# There is a function for every built-in type, which is used to convert the input to an output of the desired type. + +# %% +x = float(5) +type(x) + +# %% +divmod(10, float(3)) + +# %% [markdown] +# I lied when I said that the `float` type was a real number. It's actually a computer representation of a real number +# called a "floating point number". Representing $\sqrt 2$ or $\frac{1}{3}$ perfectly would be impossible in a computer, so we use a finite amount of memory to do it. + +# %% +N = 10000.0 +sum([1 / N] * int(N)) + +# %% [markdown] +# *Supplementary material*: +# +# * [Python's documentation about floating point arithmetic](https://docs.python.org/tutorial/floatingpoint.html); +# * [How floating point numbers work](http://floating-point-gui.de/formats/fp/); +# * Advanced: [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html). + +# %% [markdown] +# ### Strings + +# %% [markdown] +# Python has a built in `string` type, supporting many +# useful methods. + +# %% +given = "Terry" +family = "Jones" +full = given + " " + family + +# %% [markdown] +# So `+` for strings means "join them together" - *concatenate*. + +# %% +print(full.upper()) + +# %% [markdown] +# As for `float` and `int`, the name of a type can be used as a function to convert between types: + +# %% +ten, one + +# %% +print(ten + one) + +# %% +print(float(str(ten) + str(one))) + +# %% [markdown] +# We can remove extraneous material from the start and end of a string: + +# %% +" Hello ".strip() + +# %% [markdown] +# Note that you can write strings in Python using either single (`' ... '`) or double (`" ... "`) quote marks. The two ways are equivalent. However, if your string includes a single quote (e.g. an apostrophe), you should use double quotes to surround it: + +# %% +"Terry's animation" + +# %% [markdown] +# And vice versa: if your string has a double quote inside it, you should wrap the whole string in single quotes. + +# %% +'"Wow!", said John.' + +# %% [markdown] +# ### Lists + +# %% [markdown] +# Python's basic **container** type is the `list`. + +# %% [markdown] +# We can define our own list with square brackets: + +# %% +[1, 3, 7] + +# %% +type([1, 3, 7]) + +# %% [markdown] +# Lists *do not* have to contain just one type: + +# %% +various_things = [1, 2, "banana", 3.4, [1,2] ] + +# %% [markdown] +# We access an **element** of a list with an `int` in square brackets: + +# %% +various_things[2] + +# %% +index = 0 +various_things[index] + +# %% [markdown] +# Note that list indices start from zero. + +# %% [markdown] +# We can use a string to join together a list of strings: + +# %% +name = ["Sir", "Michael", "Edward", "Palin"] +print("==".join(name)) + +# %% [markdown] +# And we can split up a string into a list: + +# %% +"Ernst Stavro Blofeld".split(" ") + +# %% +"Ernst Stavro Blofeld".split("o") + +# %% [markdown] +# And combine these: + +# %% +"->".join("John Ronald Reuel Tolkein".split(" ")) + +# %% [markdown] +# A matrix can be represented by **nesting** lists -- putting lists inside other lists. + +# %% +identity = [[1, 0], [0, 1]] + +# %% +identity[0][0] + +# %% [markdown] +# ... but later we will learn about a better way of representing matrices. + +# %% [markdown] +# ### Ranges + +# %% [markdown] +# Another useful type is range, which gives you a sequence of consecutive numbers. In contrast to a list, ranges generate the numbers as you need them, rather than all at once. +# +# If you try to print a range, you'll see something that looks a little strange: + +# %% +range(5) + +# %% [markdown] +# We don't see the contents, because *they haven't been generatead yet*. Instead, Python gives us a description of the object - in this case, its type (range) and its lower and upper limits. + +# %% [markdown] +# We can quickly make a list with numbers counted up by converting this range: + +# %% +count_to_five = range(5) +print(list(count_to_five)) + +# %% [markdown] +# Ranges in Python can be customised in other ways, such as by specifying the lower limit or the step (that is, the difference between successive elements). You can find more information about them in the [official Python documentation](https://docs.python.org/3/library/stdtypes.html#ranges). + +# %% [markdown] +# ### Sequences + +# %% [markdown] +# Many other things can be treated like `lists`. Python calls things that can be treated like lists `sequences`. + +# %% [markdown] +# A string is one such *sequence type*. + +# %% [markdown] +# Sequences support various useful operations, including: +# - Accessing a single element at a particular index: `sequence[index]` +# - Accessing multiple elements (a *slice*): `sequence[start:end_plus_one]` +# - Getting the length of a sequence: `len(sequence)` +# - Checking whether the sequence contains an element: `element in sequence` +# +# The following examples illustrate these operations with lists, strings and ranges. + +# %% +print(count_to_five[1]) + +# %% +print("Palin"[2]) + +# %% +count_to_five = range(5) + +# %% +count_to_five[1:3] + +# %% +"Hello World"[4:8] + +# %% +len(various_things) + +# %% +len("Python") + +# %% +name + +# %% +"Edward" in name + +# %% +3 in count_to_five + +# %% [markdown] +# ### Unpacking + +# %% [markdown] +# Multiple values can be **unpacked** when assigning from sequences, like dealing out decks of cards. + +# %% +mylist = ['Hello', 'World'] +a, b = mylist +print(b) + +# %% +range(4) + +# %% +zero, one, two, three = range(4) + +# %% +two + +# %% [markdown] +# If there is too much or too little data, an error results: + +# %% +zero, one, two, three = range(7) + +# %% +zero, one, two, three = range(2) + +# %% [markdown] +# Python provides some handy syntax to split a sequence into its first element ("head") and the remaining ones (its "tail"): + +# %% +head, *tail = range(4) +print("head is", head) +print("tail is", tail) + +# %% [markdown] +# Note the syntax with the \*. The same pattern can be used, for example, to extract the middle segment of a sequence whose length we might not know: + +# %% +one, *two, three = range(10) + +# %% +print("one is", one) +print("two is", two) +print("three is", three) diff --git a/ch01python/025containers.html b/ch01python/025containers.html new file mode 100644 index 000000000..2c1c57d3b --- /dev/null +++ b/ch01python/025containers.html @@ -0,0 +1,1159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Containers + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Containers

+
+
+
+
+
+
+

Checking for containment.

+
+
+
+
+
+
+

The list we saw is a container type: its purpose is to hold other objects. We can ask python whether or not a +container contains a particular item:

+
+
+
+
+
+
In [1]:
+
+
+
'Dog' in ['Cat', 'Dog', 'Horse']
+
+
+
+
+
+
+
+
Out[1]:
+
+
True
+
+
+
+
+
+
+
+
In [2]:
+
+
+
'Bird' in ['Cat', 'Dog', 'Horse']
+
+
+
+
+
+
+
+
Out[2]:
+
+
False
+
+
+
+
+
+
+
+
In [3]:
+
+
+
2 in range(5)
+
+
+
+
+
+
+
+
Out[3]:
+
+
True
+
+
+
+
+
+
+
+
In [4]:
+
+
+
99 in range(5)
+
+
+
+
+
+
+
+
Out[4]:
+
+
False
+
+
+
+
+
+
+
+
+

Mutability

+
+
+
+
+
+
+

A list can be modified:

+
+
+
+
+
+
In [5]:
+
+
+
name = "Sir Michael Edward Palin".split(" ")
+print(name)
+
+
+
+
+
+
+
+
+
+
['Sir', 'Michael', 'Edward', 'Palin']
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
name[0] = "Knight"
+name[1:3] = ["Mike-"]
+name.append("FRGS")
+
+print(" ".join(name))
+
+
+
+
+
+
+
+
+
+
Knight Mike- Palin FRGS
+
+
+
+
+
+
+
+
+
+

Tuples

+
+
+
+
+
+
+

A tuple is an immutable sequence. It is like a list, execpt it cannot be changed. It is defined with round brackets.

+
+
+
+
+
+
In [7]:
+
+
+
x = 0,
+type(x)
+
+
+
+
+
+
+
+
Out[7]:
+
+
tuple
+
+
+
+
+
+
+
+
In [8]:
+
+
+
my_tuple = ("Hello", "World")
+my_tuple[0] = "Goodbye"
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[8], line 2
+      1 my_tuple = ("Hello", "World")
+----> 2 my_tuple[0] = "Goodbye"
+
+TypeError: 'tuple' object does not support item assignment
+
+
+
+
+
+
+
+
In [9]:
+
+
+
type(my_tuple)
+
+
+
+
+
+
+
+
Out[9]:
+
+
tuple
+
+
+
+
+
+
+
+
+

str is immutable too:

+
+
+
+
+
+
In [10]:
+
+
+
fish = "Hake"
+fish[0] = 'R'
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[10], line 2
+      1 fish = "Hake"
+----> 2 fish[0] = 'R'
+
+TypeError: 'str' object does not support item assignment
+
+
+
+
+
+
+
+
+

But note that container reassignment is moving a label, not changing an element:

+
+
+
+
+
+
In [11]:
+
+
+
fish = "Rake" ## OK!
+
+
+
+
+
+
+
+
+

Supplementary material: Try the online memory visualiser for this one.

+
+
+
+
+
+
+

Memory and containers

+
+
+
+
+
+
+

The way memory works with containers can be important:

+
+
+
+
+
+
In [12]:
+
+
+
x = list(range(3))
+x
+
+
+
+
+
+
+
+
Out[12]:
+
+
[0, 1, 2]
+
+
+
+
+
+
+
+
In [13]:
+
+
+
y = x
+y
+
+
+
+
+
+
+
+
Out[13]:
+
+
[0, 1, 2]
+
+
+
+
+
+
+
+
In [14]:
+
+
+
z = x[0:3]
+y[1] = "Gotcha!"
+
+
+
+
+
+
+
+
In [15]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[15]:
+
+
[0, 'Gotcha!', 2]
+
+
+
+
+
+
+
+
In [16]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[16]:
+
+
[0, 'Gotcha!', 2]
+
+
+
+
+
+
+
+
In [17]:
+
+
+
z
+
+
+
+
+
+
+
+
Out[17]:
+
+
[0, 1, 2]
+
+
+
+
+
+
+
+
In [18]:
+
+
+
z[2] = "Really?"
+
+
+
+
+
+
+
+
In [19]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[19]:
+
+
[0, 'Gotcha!', 2]
+
+
+
+
+
+
+
+
In [20]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[20]:
+
+
[0, 'Gotcha!', 2]
+
+
+
+
+
+
+
+
In [21]:
+
+
+
z
+
+
+
+
+
+
+
+
Out[21]:
+
+
[0, 1, 'Really?']
+
+
+
+
+
+
+
+
+

Supplementary material: This one works well at the memory visualiser.

+
+
+
+
+
+
+

The explanation: While y is a second label on the same object, z is a separate object with the same data. Writing x[:] creates a new list containing all the elements of x (remember: [:] is equivalent to [0:<last>]). This is the case whenever we take a slice from a list, not just when taking all the elements with [:].

+

The difference between y=x and z=x[:] is important!

+
+
+
+
+
+
+

Nested objects make it even more complicated:

+
+
+
+
+
+
In [22]:
+
+
+
x = [['a', 'b'] , 'c']
+y = x
+z = x[0:2]
+
+
+
+
+
+
+
+
In [23]:
+
+
+
x[0][1] = 'd'
+z[1] = 'e'
+
+
+
+
+
+
+
+
In [24]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[24]:
+
+
[['a', 'd'], 'c']
+
+
+
+
+
+
+
+
In [25]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[25]:
+
+
[['a', 'd'], 'c']
+
+
+
+
+
+
+
+
In [26]:
+
+
+
z
+
+
+
+
+
+
+
+
Out[26]:
+
+
[['a', 'd'], 'e']
+
+
+
+
+
+
+
+
+

Try the visualiser again.

+

Supplementary material: The copies that we make through slicing are called shallow copies: we don't copy all the objects they contain, only the references to them. This is why the nested list in x[0] is not copied, so z[0] still refers to it. It is possible to actually create copies of all the contents, however deeply nested they are - this is called a deep copy. Python provides methods for that in its standard library, in the copy module. You can read more about that, as well as about shallow and deep copies, in the library reference.

+
+
+
+
+
+
+

Identity vs Equality

Having the same data is different from being the same actual object +in memory:

+
+
+
+
+
+
In [27]:
+
+
+
[1, 2] == [1, 2]
+
+
+
+
+
+
+
+
Out[27]:
+
+
True
+
+
+
+
+
+
+
+
In [28]:
+
+
+
[1, 2] is [1, 2]
+
+
+
+
+
+
+
+
Out[28]:
+
+
False
+
+
+
+
+
+
+
+
+

The == operator checks, element by element, that two containers have the same data. +The is operator checks that they are actually the same object.

+
+
+
+
+
+
+

But, and this point is really subtle, for immutables, the python language might save memory by reusing a single instantiated copy. This will always be safe.

+
+
+
+
+
+
In [29]:
+
+
+
"Hello" == "Hello"
+
+
+
+
+
+
+
+
Out[29]:
+
+
True
+
+
+
+
+
+
+
+
In [30]:
+
+
+
"Hello" is "Hello"
+
+
+
+
+
+
+
+
+
+
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+/tmp/ipykernel_11292/3904443404.py:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+  "Hello" is "Hello"
+
+
+
+
+
Out[30]:
+
+
True
+
+
+
+
+
+
+
+
+

This can be useful in understanding problems like the one above:

+
+
+
+
+
+
In [31]:
+
+
+
x = range(3)
+y = x
+z = x[:]
+
+
+
+
+
+
+
+
In [32]:
+
+
+
x == y
+
+
+
+
+
+
+
+
Out[32]:
+
+
True
+
+
+
+
+
+
+
+
In [33]:
+
+
+
x is y
+
+
+
+
+
+
+
+
Out[33]:
+
+
True
+
+
+
+
+
+
+
+
In [34]:
+
+
+
x == z
+
+
+
+
+
+
+
+
Out[34]:
+
+
True
+
+
+
+
+
+
+
+
In [35]:
+
+
+
x is z
+
+
+
+
+
+
+
+
Out[35]:
+
+
False
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/025containers.ipynb b/ch01python/025containers.ipynb new file mode 100644 index 000000000..1bd6c1f5c --- /dev/null +++ b/ch01python/025containers.ipynb @@ -0,0 +1,554 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7c0f9e5d", + "metadata": {}, + "source": [ + "## Containers" + ] + }, + { + "cell_type": "markdown", + "id": "ae11252c", + "metadata": {}, + "source": [ + "### Checking for containment." + ] + }, + { + "cell_type": "markdown", + "id": "562ed54d", + "metadata": {}, + "source": [ + "The `list` we saw is a container type: its purpose is to hold other objects. We can ask python whether or not a\n", + "container contains a particular item:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e06bb840", + "metadata": {}, + "outputs": [], + "source": [ + "'Dog' in ['Cat', 'Dog', 'Horse']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fcb9e9f", + "metadata": {}, + "outputs": [], + "source": [ + "'Bird' in ['Cat', 'Dog', 'Horse']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1a9a7a1", + "metadata": {}, + "outputs": [], + "source": [ + "2 in range(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51f76cec", + "metadata": {}, + "outputs": [], + "source": [ + "99 in range(5)" + ] + }, + { + "cell_type": "markdown", + "id": "a9a9ae55", + "metadata": {}, + "source": [ + "### Mutability" + ] + }, + { + "cell_type": "markdown", + "id": "f1b7cf53", + "metadata": {}, + "source": [ + "A list can be modified:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61112266", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Sir Michael Edward Palin\".split(\" \")\n", + "print(name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba4d8cbf", + "metadata": {}, + "outputs": [], + "source": [ + "name[0] = \"Knight\"\n", + "name[1:3] = [\"Mike-\"]\n", + "name.append(\"FRGS\")\n", + "\n", + "print(\" \".join(name))" + ] + }, + { + "cell_type": "markdown", + "id": "99fdb6b3", + "metadata": {}, + "source": [ + "### Tuples" + ] + }, + { + "cell_type": "markdown", + "id": "39c8ebd0", + "metadata": {}, + "source": [ + "A `tuple` is an immutable sequence. It is like a list, execpt it cannot be changed. It is defined with round brackets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "511c0254", + "metadata": {}, + "outputs": [], + "source": [ + "x = 0,\n", + "type(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b2ea24a", + "metadata": {}, + "outputs": [], + "source": [ + "my_tuple = (\"Hello\", \"World\")\n", + "my_tuple[0] = \"Goodbye\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab34e989", + "metadata": {}, + "outputs": [], + "source": [ + "type(my_tuple)" + ] + }, + { + "cell_type": "markdown", + "id": "fd7843c6", + "metadata": {}, + "source": [ + "`str` is immutable too:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9eb55829", + "metadata": {}, + "outputs": [], + "source": [ + "fish = \"Hake\"\n", + "fish[0] = 'R'" + ] + }, + { + "cell_type": "markdown", + "id": "d73725be", + "metadata": {}, + "source": [ + "But note that container reassignment is moving a label, **not** changing an element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e189bd83", + "metadata": {}, + "outputs": [], + "source": [ + "fish = \"Rake\" ## OK!" + ] + }, + { + "cell_type": "markdown", + "id": "5e3dae64", + "metadata": {}, + "source": [ + "*Supplementary material*: Try the [online memory visualiser](http://www.pythontutor.com/visualize.html#code=name+%3D++%22Sir+Michael+Edward+Palin%22.split%28%22+%22%29%0A%0Aname%5B0%5D+%3D+%22Knight%22%0Aname%5B1%3A3%5D+%3D+%5B%22Mike-%22%5D%0Aname.append%28%22FRGS%22%29%0A&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) for this one." + ] + }, + { + "cell_type": "markdown", + "id": "cad43572", + "metadata": {}, + "source": [ + "### Memory and containers" + ] + }, + { + "cell_type": "markdown", + "id": "6abb9dba", + "metadata": {}, + "source": [ + "\n", + "The way memory works with containers can be important:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8309ea8", + "metadata": {}, + "outputs": [], + "source": [ + "x = list(range(3))\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e3446b7", + "metadata": {}, + "outputs": [], + "source": [ + "y = x\n", + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7c30487", + "metadata": {}, + "outputs": [], + "source": [ + "z = x[0:3]\n", + "y[1] = \"Gotcha!\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67c19607", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "878d2e54", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baeeeb37", + "metadata": {}, + "outputs": [], + "source": [ + "z" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c056318", + "metadata": {}, + "outputs": [], + "source": [ + "z[2] = \"Really?\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97c5db16", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca32bbcf", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9024d12a", + "metadata": {}, + "outputs": [], + "source": [ + "z" + ] + }, + { + "cell_type": "markdown", + "id": "35eef68f", + "metadata": {}, + "source": [ + "*Supplementary material*: This one works well at the [memory visualiser](http://www.pythontutor.com/visualize.html#code=x+%3D+%5B%22What's%22,+%22Going%22,+%22On%3F%22%5D%0Ay+%3D+x%0Az+%3D+x%5B0%3A3%5D%0A%0Ay%5B1%5D+%3D+%22Gotcha!%22%0Az%5B2%5D+%3D+%22Really%3F%22&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0)." + ] + }, + { + "cell_type": "markdown", + "id": "01464a71", + "metadata": {}, + "source": [ + "The explanation: While `y` is a second label on the *same object*, `z` is a separate object with the same data. Writing `x[:]` creates a new list containing all the elements of `x` (remember: `[:]` is equivalent to `[0:]`). This is the case whenever we take a slice from a list, not just when taking all the elements with `[:]`.\n", + "\n", + "The difference between `y=x` and `z=x[:]` is important!" + ] + }, + { + "cell_type": "markdown", + "id": "5987e2e5", + "metadata": {}, + "source": [ + "Nested objects make it even more complicated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3482026", + "metadata": {}, + "outputs": [], + "source": [ + "x = [['a', 'b'] , 'c']\n", + "y = x\n", + "z = x[0:2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db7b5af8", + "metadata": {}, + "outputs": [], + "source": [ + "x[0][1] = 'd'\n", + "z[1] = 'e'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39c74606", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77bc586d", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0be2720", + "metadata": {}, + "outputs": [], + "source": [ + "z" + ] + }, + { + "cell_type": "markdown", + "id": "80c24c26", + "metadata": {}, + "source": [ + "Try the [visualiser](http://www.pythontutor.com/visualize.html#code=x%20%3D%20%5B%5B'a',%20'b'%5D,%20'c'%5D%0Ay%20%3D%20x%0Az%20%3D%20x%5B0%3A2%5D%0A%0Ax%5B0%5D%5B1%5D%20%3D%20'd'%0Az%5B1%5D%20%3D%20'e'&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) again.\n", + "\n", + "*Supplementary material*: The copies that we make through slicing are called *shallow copies*: we don't copy all the objects they contain, only the references to them. This is why the nested list in `x[0]` is not copied, so `z[0]` still refers to it. It is possible to actually create copies of all the contents, however deeply nested they are - this is called a *deep copy*. Python provides methods for that in its standard library, in the `copy` module. You can read more about that, as well as about shallow and deep copies, in the [library reference](https://docs.python.org/3/library/copy.html)." + ] + }, + { + "cell_type": "markdown", + "id": "bb883feb", + "metadata": {}, + "source": [ + "### Identity vs Equality\n", + "\n", + "\n", + "Having the same data is different from being the same actual object\n", + "in memory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b75b57c5", + "metadata": {}, + "outputs": [], + "source": [ + "[1, 2] == [1, 2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9165f6f", + "metadata": {}, + "outputs": [], + "source": [ + "[1, 2] is [1, 2]" + ] + }, + { + "cell_type": "markdown", + "id": "4dcaee63", + "metadata": {}, + "source": [ + "The == operator checks, element by element, that two containers have the same data. \n", + "The `is` operator checks that they are actually the same object." + ] + }, + { + "cell_type": "markdown", + "id": "ba1166a9", + "metadata": {}, + "source": [ + "But, and this point is really subtle, for immutables, the python language might save memory by reusing a single instantiated copy. This will always be safe." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0adbb03f", + "metadata": {}, + "outputs": [], + "source": [ + "\"Hello\" == \"Hello\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d91f7595", + "metadata": {}, + "outputs": [], + "source": [ + "\"Hello\" is \"Hello\"" + ] + }, + { + "cell_type": "markdown", + "id": "71fa80d4", + "metadata": {}, + "source": [ + "This can be useful in understanding problems like the one above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94c1db1e", + "metadata": {}, + "outputs": [], + "source": [ + "x = range(3)\n", + "y = x\n", + "z = x[:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4655770", + "metadata": {}, + "outputs": [], + "source": [ + "x == y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b16c387", + "metadata": {}, + "outputs": [], + "source": [ + "x is y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f94e05d9", + "metadata": {}, + "outputs": [], + "source": [ + "x == z" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbd59679", + "metadata": {}, + "outputs": [], + "source": [ + "x is z" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Containers" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/025containers.ipynb.py b/ch01python/025containers.ipynb.py new file mode 100644 index 000000000..56943c6df --- /dev/null +++ b/ch01python/025containers.ipynb.py @@ -0,0 +1,207 @@ +# --- +# jupyter: +# jekyll: +# display_name: Containers +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Containers + +# %% [markdown] +# ### Checking for containment. + +# %% [markdown] +# The `list` we saw is a container type: its purpose is to hold other objects. We can ask python whether or not a +# container contains a particular item: + +# %% +'Dog' in ['Cat', 'Dog', 'Horse'] + +# %% +'Bird' in ['Cat', 'Dog', 'Horse'] + +# %% +2 in range(5) + +# %% +99 in range(5) + +# %% [markdown] +# ### Mutability + +# %% [markdown] +# A list can be modified: + +# %% +name = "Sir Michael Edward Palin".split(" ") +print(name) + +# %% +name[0] = "Knight" +name[1:3] = ["Mike-"] +name.append("FRGS") + +print(" ".join(name)) + +# %% [markdown] +# ### Tuples + +# %% [markdown] +# A `tuple` is an immutable sequence. It is like a list, execpt it cannot be changed. It is defined with round brackets. + +# %% +x = 0, +type(x) + +# %% +my_tuple = ("Hello", "World") +my_tuple[0] = "Goodbye" + +# %% +type(my_tuple) + +# %% [markdown] +# `str` is immutable too: + +# %% +fish = "Hake" +fish[0] = 'R' + +# %% [markdown] +# But note that container reassignment is moving a label, **not** changing an element: + +# %% +fish = "Rake" ## OK! + +# %% [markdown] +# *Supplementary material*: Try the [online memory visualiser](http://www.pythontutor.com/visualize.html#code=name+%3D++%22Sir+Michael+Edward+Palin%22.split%28%22+%22%29%0A%0Aname%5B0%5D+%3D+%22Knight%22%0Aname%5B1%3A3%5D+%3D+%5B%22Mike-%22%5D%0Aname.append%28%22FRGS%22%29%0A&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) for this one. + +# %% [markdown] +# ### Memory and containers + +# %% [markdown] +# +# The way memory works with containers can be important: +# +# +# + +# %% +x = list(range(3)) +x + +# %% +y = x +y + +# %% +z = x[0:3] +y[1] = "Gotcha!" + +# %% +x + +# %% +y + +# %% +z + +# %% +z[2] = "Really?" + +# %% +x + +# %% +y + +# %% +z + +# %% [markdown] +# *Supplementary material*: This one works well at the [memory visualiser](http://www.pythontutor.com/visualize.html#code=x+%3D+%5B%22What's%22,+%22Going%22,+%22On%3F%22%5D%0Ay+%3D+x%0Az+%3D+x%5B0%3A3%5D%0A%0Ay%5B1%5D+%3D+%22Gotcha!%22%0Az%5B2%5D+%3D+%22Really%3F%22&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0). + +# %% [markdown] +# The explanation: While `y` is a second label on the *same object*, `z` is a separate object with the same data. Writing `x[:]` creates a new list containing all the elements of `x` (remember: `[:]` is equivalent to `[0:]`). This is the case whenever we take a slice from a list, not just when taking all the elements with `[:]`. +# +# The difference between `y=x` and `z=x[:]` is important! + +# %% [markdown] +# Nested objects make it even more complicated: + +# %% +x = [['a', 'b'] , 'c'] +y = x +z = x[0:2] + +# %% +x[0][1] = 'd' +z[1] = 'e' + +# %% +x + +# %% +y + +# %% +z + +# %% [markdown] +# Try the [visualiser](http://www.pythontutor.com/visualize.html#code=x%20%3D%20%5B%5B'a',%20'b'%5D,%20'c'%5D%0Ay%20%3D%20x%0Az%20%3D%20x%5B0%3A2%5D%0A%0Ax%5B0%5D%5B1%5D%20%3D%20'd'%0Az%5B1%5D%20%3D%20'e'&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) again. +# +# *Supplementary material*: The copies that we make through slicing are called *shallow copies*: we don't copy all the objects they contain, only the references to them. This is why the nested list in `x[0]` is not copied, so `z[0]` still refers to it. It is possible to actually create copies of all the contents, however deeply nested they are - this is called a *deep copy*. Python provides methods for that in its standard library, in the `copy` module. You can read more about that, as well as about shallow and deep copies, in the [library reference](https://docs.python.org/3/library/copy.html). + +# %% [markdown] +# ### Identity vs Equality +# +# +# Having the same data is different from being the same actual object +# in memory: + +# %% +[1, 2] == [1, 2] + +# %% +[1, 2] is [1, 2] + +# %% [markdown] +# The == operator checks, element by element, that two containers have the same data. +# The `is` operator checks that they are actually the same object. + +# %% [markdown] +# But, and this point is really subtle, for immutables, the python language might save memory by reusing a single instantiated copy. This will always be safe. + +# %% +"Hello" == "Hello" + +# %% +"Hello" is "Hello" + +# %% [markdown] +# This can be useful in understanding problems like the one above: + +# %% +x = range(3) +y = x +z = x[:] + +# %% +x == y + +# %% +x is y + +# %% +x == z + +# %% +x is z diff --git a/ch01python/028dictionaries.html b/ch01python/028dictionaries.html new file mode 100644 index 000000000..cdddb3a62 --- /dev/null +++ b/ch01python/028dictionaries.html @@ -0,0 +1,972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dictionaries + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Dictionaries

+
+
+
+
+
+
+

The Python Dictionary

+
+
+
+
+
+
+

Python supports a container type called a dictionary.

+
+
+
+
+
+
+

This is also known as an "associative array", "map" or "hash" in other languages.

+
+
+
+
+
+
+

In a list, we use a number to look up an element:

+
+
+
+
+
+
In [1]:
+
+
+
names = "Martin Luther King".split(" ")
+
+
+
+
+
+
+
+
In [2]:
+
+
+
names[1]
+
+
+
+
+
+
+
+
Out[2]:
+
+
'Luther'
+
+
+
+
+
+
+
+
+

In a dictionary, we look up an element using another object of our choice:

+
+
+
+
+
+
In [3]:
+
+
+
chapman = {"name": "Graham", "age": 48, 
+           "Jobs": ["Comedian", "Writer"] }
+
+
+
+
+
+
+
+
In [4]:
+
+
+
chapman
+
+
+
+
+
+
+
+
Out[4]:
+
+
{'name': 'Graham', 'age': 48, 'Jobs': ['Comedian', 'Writer']}
+
+
+
+
+
+
+
+
In [5]:
+
+
+
chapman['Jobs']
+
+
+
+
+
+
+
+
Out[5]:
+
+
['Comedian', 'Writer']
+
+
+
+
+
+
+
+
In [6]:
+
+
+
chapman['age']
+
+
+
+
+
+
+
+
Out[6]:
+
+
48
+
+
+
+
+
+
+
+
In [7]:
+
+
+
type(chapman)
+
+
+
+
+
+
+
+
Out[7]:
+
+
dict
+
+
+
+
+
+
+
+
+

Keys and Values

+
+
+
+
+
+
+

The things we can use to look up with are called keys:

+
+
+
+
+
+
In [8]:
+
+
+
chapman.keys()
+
+
+
+
+
+
+
+
Out[8]:
+
+
dict_keys(['name', 'age', 'Jobs'])
+
+
+
+
+
+
+
+
+

The things we can look up are called values:

+
+
+
+
+
+
In [9]:
+
+
+
chapman.values()
+
+
+
+
+
+
+
+
Out[9]:
+
+
dict_values(['Graham', 48, ['Comedian', 'Writer']])
+
+
+
+
+
+
+
+
+

When we test for containment on a dict we test on the keys:

+
+
+
+
+
+
In [10]:
+
+
+
'Jobs' in chapman
+
+
+
+
+
+
+
+
Out[10]:
+
+
True
+
+
+
+
+
+
+
+
In [11]:
+
+
+
'Graham' in chapman
+
+
+
+
+
+
+
+
Out[11]:
+
+
False
+
+
+
+
+
+
+
+
In [12]:
+
+
+
'Graham' in chapman.values()
+
+
+
+
+
+
+
+
Out[12]:
+
+
True
+
+
+
+
+
+
+
+
+

Immutable Keys Only

+
+
+
+
+
+
+

The way in which dictionaries work is one of the coolest things in computer science: +the "hash table". The details of this are beyond the scope of this course, but we will consider some aspects in the section on performance programming.

+

One consequence of this implementation is that you can only use immutable things as keys.

+
+
+
+
+
+
In [13]:
+
+
+
good_match = {
+    ("Lamb", "Mint"): True, 
+    ("Bacon", "Chocolate"): False
+   }
+
+
+
+
+
+
+
+
+

but:

+
+
+
+
+
+
In [14]:
+
+
+
illegal = {
+    ["Lamb", "Mint"]: True, 
+    ["Bacon", "Chocolate"]: False
+   }
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[14], line 1
+----> 1 illegal = {
+      2     ["Lamb", "Mint"]: True, 
+      3     ["Bacon", "Chocolate"]: False
+      4    }
+
+TypeError: unhashable type: 'list'
+
+
+
+
+
+
+
+
+

Remember -- square brackets denote lists, round brackets denote tuples.

+
+
+
+
+
+
+

No guarantee of order (before Python 3.7)

+
+
+
+
+
+
+

Another consequence of the way dictionaries used to work is that there was no guaranteed order among the +elements. However, since Python 3.7, it's guaranteed that dictionaries return elements in the order in which they were inserted. Read more about why that changed and how it is still fast.

+
+
+
+
+
+
In [15]:
+
+
+
my_dict = {'0': 0, '1':1, '2': 2, '3': 3, '4': 4}
+print(my_dict)
+print(my_dict.values())
+
+
+
+
+
+
+
+
+
+
{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
+dict_values([0, 1, 2, 3, 4])
+
+
+
+
+
+
+
+
+
+

Sets

+
+
+
+
+
+
+

A set is a list which cannot contain the same element twice. +We make one by calling set() on any sequence, e.g. a list or string.

+
+
+
+
+
+
In [16]:
+
+
+
name = "Graham Chapman"
+unique_letters = set(name)
+
+
+
+
+
+
+
+
In [17]:
+
+
+
unique_letters
+
+
+
+
+
+
+
+
Out[17]:
+
+
{' ', 'C', 'G', 'a', 'h', 'm', 'n', 'p', 'r'}
+
+
+
+
+
+
+
+
+

Or by defining a literal like a dictionary, but without the colons:

+
+
+
+
+
+
In [18]:
+
+
+
primes_below_ten = { 2, 3, 5, 7}
+
+
+
+
+
+
+
+
In [19]:
+
+
+
type(unique_letters)
+
+
+
+
+
+
+
+
Out[19]:
+
+
set
+
+
+
+
+
+
+
+
In [20]:
+
+
+
type(primes_below_ten)
+
+
+
+
+
+
+
+
Out[20]:
+
+
set
+
+
+
+
+
+
+
+
In [21]:
+
+
+
unique_letters
+
+
+
+
+
+
+
+
Out[21]:
+
+
{' ', 'C', 'G', 'a', 'h', 'm', 'n', 'p', 'r'}
+
+
+
+
+
+
+
+
+

This will be easier to read if we turn the set of letters back into a string, with join:

+
+
+
+
+
+
In [22]:
+
+
+
"".join(unique_letters)
+
+
+
+
+
+
+
+
Out[22]:
+
+
'maGpChrn '
+
+
+
+
+
+
+
+
+

A set has no particular order, but is really useful for checking or storing unique values.

+
+
+
+
+
+
+

Set operations work as in mathematics:

+
+
+
+
+
+
In [23]:
+
+
+
x = set("Hello")
+y = set("Goodbye")
+
+
+
+
+
+
+
+
In [24]:
+
+
+
x & y # Intersection
+
+
+
+
+
+
+
+
Out[24]:
+
+
{'e', 'o'}
+
+
+
+
+
+
+
+
In [25]:
+
+
+
x | y # Union
+
+
+
+
+
+
+
+
Out[25]:
+
+
{'G', 'H', 'b', 'd', 'e', 'l', 'o', 'y'}
+
+
+
+
+
+
+
+
In [26]:
+
+
+
y - x # y intersection with complement of x: letters in Goodbye but not in Hello
+
+
+
+
+
+
+
+
Out[26]:
+
+
{'G', 'b', 'd', 'y'}
+
+
+
+
+
+
+
+
+

Your programs will be faster and more readable if you use the appropriate container type for your data's meaning. +Always use a set for lists which can't in principle contain the same data twice, always use a dictionary for anything +which feels like a mapping from keys to values.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/028dictionaries.ipynb b/ch01python/028dictionaries.ipynb new file mode 100644 index 000000000..07a630ddd --- /dev/null +++ b/ch01python/028dictionaries.ipynb @@ -0,0 +1,480 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "573a6edf", + "metadata": {}, + "source": [ + "## Dictionaries" + ] + }, + { + "cell_type": "markdown", + "id": "42919c51", + "metadata": {}, + "source": [ + "### The Python Dictionary" + ] + }, + { + "cell_type": "markdown", + "id": "5ba26612", + "metadata": {}, + "source": [ + "Python supports a container type called a dictionary." + ] + }, + { + "cell_type": "markdown", + "id": "848b415f", + "metadata": {}, + "source": [ + "This is also known as an \"associative array\", \"map\" or \"hash\" in other languages." + ] + }, + { + "cell_type": "markdown", + "id": "eb8dec9f", + "metadata": {}, + "source": [ + "In a list, we use a number to look up an element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2adf4ed", + "metadata": {}, + "outputs": [], + "source": [ + "names = \"Martin Luther King\".split(\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a14fd01b", + "metadata": {}, + "outputs": [], + "source": [ + "names[1]" + ] + }, + { + "cell_type": "markdown", + "id": "335013db", + "metadata": {}, + "source": [ + "In a dictionary, we look up an element using **another object of our choice**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be6ac507", + "metadata": {}, + "outputs": [], + "source": [ + "chapman = {\"name\": \"Graham\", \"age\": 48, \n", + " \"Jobs\": [\"Comedian\", \"Writer\"] }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6e1aec5", + "metadata": {}, + "outputs": [], + "source": [ + "chapman" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fc53880", + "metadata": {}, + "outputs": [], + "source": [ + "chapman['Jobs']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2014c0d5", + "metadata": {}, + "outputs": [], + "source": [ + "chapman['age']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12f798a5", + "metadata": {}, + "outputs": [], + "source": [ + "type(chapman)" + ] + }, + { + "cell_type": "markdown", + "id": "b4686d77", + "metadata": {}, + "source": [ + "### Keys and Values" + ] + }, + { + "cell_type": "markdown", + "id": "3dcc9a13", + "metadata": {}, + "source": [ + "The things we can use to look up with are called **keys**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a43474b", + "metadata": {}, + "outputs": [], + "source": [ + "chapman.keys()" + ] + }, + { + "cell_type": "markdown", + "id": "ccd2911d", + "metadata": {}, + "source": [ + "The things we can look up are called **values**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab9911d9", + "metadata": {}, + "outputs": [], + "source": [ + "chapman.values()" + ] + }, + { + "cell_type": "markdown", + "id": "01703133", + "metadata": {}, + "source": [ + "When we test for containment on a `dict` we test on the **keys**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32909e72", + "metadata": {}, + "outputs": [], + "source": [ + "'Jobs' in chapman" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16f3d213", + "metadata": {}, + "outputs": [], + "source": [ + "'Graham' in chapman" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e147d0d9", + "metadata": {}, + "outputs": [], + "source": [ + "'Graham' in chapman.values()" + ] + }, + { + "cell_type": "markdown", + "id": "49457676", + "metadata": {}, + "source": [ + "### Immutable Keys Only" + ] + }, + { + "cell_type": "markdown", + "id": "1724fe07", + "metadata": {}, + "source": [ + "The way in which dictionaries work is one of the coolest things in computer science:\n", + "the \"hash table\". The details of this are beyond the scope of this course, but we will consider some aspects in the section on performance programming. \n", + "\n", + "One consequence of this implementation is that you can only use **immutable** things as keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eff40c34", + "metadata": {}, + "outputs": [], + "source": [ + "good_match = {\n", + " (\"Lamb\", \"Mint\"): True, \n", + " (\"Bacon\", \"Chocolate\"): False\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "cfa9ef1b", + "metadata": {}, + "source": [ + "but:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc5b610b", + "metadata": {}, + "outputs": [], + "source": [ + "illegal = {\n", + " [\"Lamb\", \"Mint\"]: True, \n", + " [\"Bacon\", \"Chocolate\"]: False\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "dac5c37d", + "metadata": {}, + "source": [ + "Remember -- square brackets denote lists, round brackets denote `tuple`s." + ] + }, + { + "cell_type": "markdown", + "id": "c2767f2c", + "metadata": {}, + "source": [ + "### No guarantee of order (before Python 3.7)" + ] + }, + { + "cell_type": "markdown", + "id": "7008d0cd", + "metadata": {}, + "source": [ + "\n", + "Another consequence of the way dictionaries used to work is that there was no guaranteed order among the\n", + "elements. However, since Python 3.7, it's guaranteed that dictionaries return elements in the order in which they were inserted. Read more about [why that changed and how it is still fast](https://stackoverflow.com/a/39980744/1087595).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ba5b69d", + "metadata": {}, + "outputs": [], + "source": [ + "my_dict = {'0': 0, '1':1, '2': 2, '3': 3, '4': 4}\n", + "print(my_dict)\n", + "print(my_dict.values())" + ] + }, + { + "cell_type": "markdown", + "id": "9673c155", + "metadata": {}, + "source": [ + "### Sets" + ] + }, + { + "cell_type": "markdown", + "id": "0eee3fe6", + "metadata": {}, + "source": [ + "A set is a `list` which cannot contain the same element twice.\n", + "We make one by calling `set()` on any sequence, e.g. a list or string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2405b27", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Graham Chapman\"\n", + "unique_letters = set(name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24534a96", + "metadata": {}, + "outputs": [], + "source": [ + "unique_letters" + ] + }, + { + "cell_type": "markdown", + "id": "d04b8416", + "metadata": {}, + "source": [ + "Or by defining a literal like a dictionary, but without the colons:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e3bd152", + "metadata": {}, + "outputs": [], + "source": [ + "primes_below_ten = { 2, 3, 5, 7}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f14ffdc2", + "metadata": {}, + "outputs": [], + "source": [ + "type(unique_letters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "207bf662", + "metadata": {}, + "outputs": [], + "source": [ + "type(primes_below_ten)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5cd8f1f", + "metadata": {}, + "outputs": [], + "source": [ + "unique_letters" + ] + }, + { + "cell_type": "markdown", + "id": "d5c477ea", + "metadata": {}, + "source": [ + "This will be easier to read if we turn the set of letters back into a string, with `join`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3e60906", + "metadata": {}, + "outputs": [], + "source": [ + "\"\".join(unique_letters)" + ] + }, + { + "cell_type": "markdown", + "id": "c8e756f3", + "metadata": {}, + "source": [ + "A set has no particular order, but is really useful for checking or storing **unique** values." + ] + }, + { + "cell_type": "markdown", + "id": "da05dd86", + "metadata": {}, + "source": [ + "Set operations work as in mathematics:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0cac04e", + "metadata": {}, + "outputs": [], + "source": [ + "x = set(\"Hello\")\n", + "y = set(\"Goodbye\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "122fb4f4", + "metadata": {}, + "outputs": [], + "source": [ + "x & y # Intersection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7617abb5", + "metadata": {}, + "outputs": [], + "source": [ + "x | y # Union" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77e4f889", + "metadata": {}, + "outputs": [], + "source": [ + "y - x # y intersection with complement of x: letters in Goodbye but not in Hello" + ] + }, + { + "cell_type": "markdown", + "id": "93fe057e", + "metadata": {}, + "source": [ + "Your programs will be faster and more readable if you use the appropriate container type for your data's meaning.\n", + "Always use a set for lists which can't in principle contain the same data twice, always use a dictionary for anything\n", + "which feels like a mapping from keys to values." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Dictionaries" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/028dictionaries.ipynb.py b/ch01python/028dictionaries.ipynb.py new file mode 100644 index 000000000..2bf867197 --- /dev/null +++ b/ch01python/028dictionaries.ipynb.py @@ -0,0 +1,181 @@ +# --- +# jupyter: +# jekyll: +# display_name: Dictionaries +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Dictionaries + +# %% [markdown] +# ### The Python Dictionary + +# %% [markdown] +# Python supports a container type called a dictionary. + +# %% [markdown] +# This is also known as an "associative array", "map" or "hash" in other languages. + +# %% [markdown] +# In a list, we use a number to look up an element: + +# %% +names = "Martin Luther King".split(" ") + +# %% +names[1] + +# %% [markdown] +# In a dictionary, we look up an element using **another object of our choice**: + +# %% +chapman = {"name": "Graham", "age": 48, + "Jobs": ["Comedian", "Writer"] } + +# %% +chapman + +# %% +chapman['Jobs'] + +# %% +chapman['age'] + +# %% +type(chapman) + +# %% [markdown] +# ### Keys and Values + +# %% [markdown] +# The things we can use to look up with are called **keys**: + +# %% +chapman.keys() + +# %% [markdown] +# The things we can look up are called **values**: + +# %% +chapman.values() + +# %% [markdown] +# When we test for containment on a `dict` we test on the **keys**: + +# %% +'Jobs' in chapman + +# %% +'Graham' in chapman + +# %% +'Graham' in chapman.values() + +# %% [markdown] +# ### Immutable Keys Only + +# %% [markdown] +# The way in which dictionaries work is one of the coolest things in computer science: +# the "hash table". The details of this are beyond the scope of this course, but we will consider some aspects in the section on performance programming. +# +# One consequence of this implementation is that you can only use **immutable** things as keys. + +# %% +good_match = { + ("Lamb", "Mint"): True, + ("Bacon", "Chocolate"): False + } + +# %% [markdown] +# but: + +# %% +illegal = { + ["Lamb", "Mint"]: True, + ["Bacon", "Chocolate"]: False + } + +# %% [markdown] +# Remember -- square brackets denote lists, round brackets denote `tuple`s. + +# %% [markdown] +# ### No guarantee of order (before Python 3.7) + +# %% [markdown] +# +# Another consequence of the way dictionaries used to work is that there was no guaranteed order among the +# elements. However, since Python 3.7, it's guaranteed that dictionaries return elements in the order in which they were inserted. Read more about [why that changed and how it is still fast](https://stackoverflow.com/a/39980744/1087595). +# +# +# + +# %% +my_dict = {'0': 0, '1':1, '2': 2, '3': 3, '4': 4} +print(my_dict) +print(my_dict.values()) + +# %% [markdown] +# ### Sets + +# %% [markdown] +# A set is a `list` which cannot contain the same element twice. +# We make one by calling `set()` on any sequence, e.g. a list or string. + +# %% +name = "Graham Chapman" +unique_letters = set(name) + +# %% +unique_letters + +# %% [markdown] +# Or by defining a literal like a dictionary, but without the colons: + +# %% +primes_below_ten = { 2, 3, 5, 7} + +# %% +type(unique_letters) + +# %% +type(primes_below_ten) + +# %% +unique_letters + +# %% [markdown] +# This will be easier to read if we turn the set of letters back into a string, with `join`: + +# %% +"".join(unique_letters) + +# %% [markdown] +# A set has no particular order, but is really useful for checking or storing **unique** values. + +# %% [markdown] +# Set operations work as in mathematics: + +# %% +x = set("Hello") +y = set("Goodbye") + +# %% +x & y # Intersection + +# %% +x | y # Union + +# %% +y - x # y intersection with complement of x: letters in Goodbye but not in Hello + +# %% [markdown] +# Your programs will be faster and more readable if you use the appropriate container type for your data's meaning. +# Always use a set for lists which can't in principle contain the same data twice, always use a dictionary for anything +# which feels like a mapping from keys to values. diff --git a/ch01python/029structures.html b/ch01python/029structures.html new file mode 100644 index 000000000..780713c4c --- /dev/null +++ b/ch01python/029structures.html @@ -0,0 +1,572 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Structures + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Data structures

+
+
+
+
+
+
+

Nested Lists and Dictionaries

+
+
+
+
+
+
+

In research programming, one of our most common tasks is building an appropriate structure to model our complicated +data. Later in the course, we'll see how we can define our own types, with their own attributes, properties, and methods. But probably the most common approach is to use nested structures of lists, dictionaries, and sets to model our data. For example, an address might be modelled as a dictionary with appropriately named fields:

+
+
+
+
+
+
In [1]:
+
+
+
UCL = {
+    'City': 'London',
+    'Street': 'Gower Street',
+    'Postcode': 'WC1E 6BT'
+}
+
+
+
+
+
+
+
+
In [2]:
+
+
+
Chapman = {
+    'City': 'London',
+    'Street': 'Southwood ln',
+    'Postcode': 'N6 5TB'
+}
+
+
+
+
+
+
+
+
+

A collection of people's addresses is then a list of dictionaries:

+
+
+
+
+
+
In [3]:
+
+
+
addresses = [UCL, Chapman]
+
+
+
+
+
+
+
+
In [4]:
+
+
+
addresses
+
+
+
+
+
+
+
+
Out[4]:
+
+
[{'City': 'London', 'Street': 'Gower Street', 'Postcode': 'WC1E 6BT'},
+ {'City': 'London', 'Street': 'Southwood ln', 'Postcode': 'N6 5TB'}]
+
+
+
+
+
+
+
+
+

A more complicated data structure, for example for a census database, might have a list of residents or employees at each address:

+
+
+
+
+
+
In [5]:
+
+
+
UCL['people'] = ['Jeremy','Leonard', 'James', 'Henry']
+
+
+
+
+
+
+
+
In [6]:
+
+
+
Chapman['people'] = ['Graham', 'David']
+
+
+
+
+
+
+
+
In [7]:
+
+
+
addresses
+
+
+
+
+
+
+
+
Out[7]:
+
+
[{'City': 'London',
+  'Street': 'Gower Street',
+  'Postcode': 'WC1E 6BT',
+  'people': ['Jeremy', 'Leonard', 'James', 'Henry']},
+ {'City': 'London',
+  'Street': 'Southwood ln',
+  'Postcode': 'N6 5TB',
+  'people': ['Graham', 'David']}]
+
+
+
+
+
+
+
+
+

Which is then a list of dictionaries, with keys which are strings or lists.

+
+
+
+
+
+
+

We can go further, e.g.:

+
+
+
+
+
+
In [8]:
+
+
+
UCL['Residential'] = False
+
+
+
+
+
+
+
+
+

And we can write code against our structures:

+
+
+
+
+
+
In [9]:
+
+
+
leaders = [place['people'][0] for place in addresses]
+leaders
+
+
+
+
+
+
+
+
Out[9]:
+
+
['Jeremy', 'Graham']
+
+
+
+
+
+
+
+
+

This was an example of a 'list comprehension', which have used to get data of this structure, and which we'll see more of in a moment...

+
+
+
+
+
+
+

Exercise: a Maze Model.

+
+
+
+
+
+
+

Work with a partner to design a data structure to represent a maze using dictionaries and lists.

+
+
+
+
+
+
+
    +
  • Each place in the maze has a name, which is a string.
  • +
  • Each place in the maze has one or more people currently standing at it, by name.
  • +
  • Each place in the maze has a maximum capacity of people that can fit in it.
  • +
  • From each place in the maze, you can go from that place to a few other places, using a direction like 'up', 'north', +or 'sideways'
  • +
+
+
+
+
+
+
+

Create an example instance, in a notebook, of a simple structure for your maze:

+
+
+
+
+
+
+
    +
  • The front room can hold 2 people. Graham is currently there. You can go outside to the garden, or upstairs to the bedroom, or north to the kitchen.
  • +
  • From the kitchen, you can go south to the front room. It fits 1 person.
  • +
  • From the garden you can go inside to front room. It fits 3 people. David is currently there.
  • +
  • From the bedroom, you can go downstairs to the front room. You can also jump out of the window to the garden. It fits 2 people.
  • +
+
+
+
+
+
+
+

Make sure that your model:

+
    +
  • Allows empty rooms
  • +
  • Allows you to jump out of the upstairs window, but not to fly back up.
  • +
  • Allows rooms which people can't fit in.
  • +
+
+
+
+
+
+
+

myhouse = [ "Your answer here" ]

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/029structures.ipynb b/ch01python/029structures.ipynb new file mode 100644 index 000000000..997282842 --- /dev/null +++ b/ch01python/029structures.ipynb @@ -0,0 +1,254 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "90e36e3d", + "metadata": {}, + "source": [ + "## Data structures" + ] + }, + { + "cell_type": "markdown", + "id": "bc8935c4", + "metadata": {}, + "source": [ + "### Nested Lists and Dictionaries" + ] + }, + { + "cell_type": "markdown", + "id": "49cadcc0", + "metadata": {}, + "source": [ + "In research programming, one of our most common tasks is building an appropriate *structure* to model our complicated\n", + "data. Later in the course, we'll see how we can define our own types, with their own attributes, properties, and methods. But probably the most common approach is to use nested structures of lists, dictionaries, and sets to model our data. For example, an address might be modelled as a dictionary with appropriately named fields:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f795fe7", + "metadata": {}, + "outputs": [], + "source": [ + "UCL = {\n", + " 'City': 'London',\n", + " 'Street': 'Gower Street',\n", + " 'Postcode': 'WC1E 6BT'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6719408c", + "metadata": {}, + "outputs": [], + "source": [ + "Chapman = {\n", + " 'City': 'London',\n", + " 'Street': 'Southwood ln',\n", + " 'Postcode': 'N6 5TB'\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "0b964c59", + "metadata": {}, + "source": [ + "A collection of people's addresses is then a list of dictionaries:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce02b064", + "metadata": {}, + "outputs": [], + "source": [ + "addresses = [UCL, Chapman]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a4b211d", + "metadata": {}, + "outputs": [], + "source": [ + "addresses" + ] + }, + { + "cell_type": "markdown", + "id": "e0f45738", + "metadata": {}, + "source": [ + "A more complicated data structure, for example for a census database, might have a list of residents or employees at each address:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8753f500", + "metadata": {}, + "outputs": [], + "source": [ + "UCL['people'] = ['Jeremy','Leonard', 'James', 'Henry']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e74e061", + "metadata": {}, + "outputs": [], + "source": [ + "Chapman['people'] = ['Graham', 'David']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373cd63a", + "metadata": {}, + "outputs": [], + "source": [ + "addresses" + ] + }, + { + "cell_type": "markdown", + "id": "30c9a5bb", + "metadata": {}, + "source": [ + "Which is then a list of dictionaries, with keys which are strings or lists." + ] + }, + { + "cell_type": "markdown", + "id": "a192fac4", + "metadata": {}, + "source": [ + "We can go further, e.g.:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce986c4b", + "metadata": {}, + "outputs": [], + "source": [ + "UCL['Residential'] = False" + ] + }, + { + "cell_type": "markdown", + "id": "56f01c6d", + "metadata": {}, + "source": [ + "And we can write code against our structures:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "042be9ab", + "metadata": {}, + "outputs": [], + "source": [ + "leaders = [place['people'][0] for place in addresses]\n", + "leaders" + ] + }, + { + "cell_type": "markdown", + "id": "dc4d01d5", + "metadata": {}, + "source": [ + "This was an example of a 'list comprehension', which have used to get data of this structure, and which we'll see more of in a moment..." + ] + }, + { + "cell_type": "markdown", + "id": "3b0f5fe7", + "metadata": {}, + "source": [ + "### Exercise: a Maze Model." + ] + }, + { + "cell_type": "markdown", + "id": "c4f03409", + "metadata": {}, + "source": [ + "Work with a partner to design a data structure to represent a maze using dictionaries and lists." + ] + }, + { + "cell_type": "markdown", + "id": "8b747573", + "metadata": {}, + "source": [ + "* Each place in the maze has a name, which is a string.\n", + "* Each place in the maze has one or more people currently standing at it, by name.\n", + "* Each place in the maze has a maximum capacity of people that can fit in it.\n", + "* From each place in the maze, you can go from that place to a few other places, using a direction like 'up', 'north', \n", + "or 'sideways'" + ] + }, + { + "cell_type": "markdown", + "id": "c27303e9", + "metadata": {}, + "source": [ + "Create an example instance, in a notebook, of a simple structure for your maze:" + ] + }, + { + "cell_type": "markdown", + "id": "6b00eb1e", + "metadata": {}, + "source": [ + "* The front room can hold 2 people. Graham is currently there. You can go outside to the garden, or upstairs to the bedroom, or north to the kitchen.\n", + "* From the kitchen, you can go south to the front room. It fits 1 person.\n", + "* From the garden you can go inside to front room. It fits 3 people. David is currently there.\n", + "* From the bedroom, you can go downstairs to the front room. You can also jump out of the window to the garden. It fits 2 people." + ] + }, + { + "cell_type": "markdown", + "id": "f01f4d67", + "metadata": {}, + "source": [ + "Make sure that your model:\n", + "\n", + "* Allows empty rooms\n", + "* Allows you to jump out of the upstairs window, but not to fly back up.\n", + "* Allows rooms which people can't fit in." + ] + }, + { + "cell_type": "markdown", + "id": "25a722a5", + "metadata": {}, + "source": [ + "myhouse = [ \"Your answer here\" ]" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Structures" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/029structures.ipynb.py b/ch01python/029structures.ipynb.py new file mode 100644 index 000000000..8439ed97f --- /dev/null +++ b/ch01python/029structures.ipynb.py @@ -0,0 +1,108 @@ +# --- +# jupyter: +# jekyll: +# display_name: Structures +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Data structures + +# %% [markdown] +# ### Nested Lists and Dictionaries + +# %% [markdown] +# In research programming, one of our most common tasks is building an appropriate *structure* to model our complicated +# data. Later in the course, we'll see how we can define our own types, with their own attributes, properties, and methods. But probably the most common approach is to use nested structures of lists, dictionaries, and sets to model our data. For example, an address might be modelled as a dictionary with appropriately named fields: + +# %% +UCL = { + 'City': 'London', + 'Street': 'Gower Street', + 'Postcode': 'WC1E 6BT' +} + +# %% +Chapman = { + 'City': 'London', + 'Street': 'Southwood ln', + 'Postcode': 'N6 5TB' +} + +# %% [markdown] +# A collection of people's addresses is then a list of dictionaries: + +# %% +addresses = [UCL, Chapman] + +# %% +addresses + +# %% [markdown] +# A more complicated data structure, for example for a census database, might have a list of residents or employees at each address: + +# %% +UCL['people'] = ['Jeremy','Leonard', 'James', 'Henry'] + +# %% +Chapman['people'] = ['Graham', 'David'] + +# %% +addresses + +# %% [markdown] +# Which is then a list of dictionaries, with keys which are strings or lists. + +# %% [markdown] +# We can go further, e.g.: + +# %% +UCL['Residential'] = False + +# %% [markdown] +# And we can write code against our structures: + +# %% +leaders = [place['people'][0] for place in addresses] +leaders + +# %% [markdown] +# This was an example of a 'list comprehension', which have used to get data of this structure, and which we'll see more of in a moment... + +# %% [markdown] +# ### Exercise: a Maze Model. + +# %% [markdown] +# Work with a partner to design a data structure to represent a maze using dictionaries and lists. + +# %% [markdown] +# * Each place in the maze has a name, which is a string. +# * Each place in the maze has one or more people currently standing at it, by name. +# * Each place in the maze has a maximum capacity of people that can fit in it. +# * From each place in the maze, you can go from that place to a few other places, using a direction like 'up', 'north', +# or 'sideways' + +# %% [markdown] +# Create an example instance, in a notebook, of a simple structure for your maze: + +# %% [markdown] +# * The front room can hold 2 people. Graham is currently there. You can go outside to the garden, or upstairs to the bedroom, or north to the kitchen. +# * From the kitchen, you can go south to the front room. It fits 1 person. +# * From the garden you can go inside to front room. It fits 3 people. David is currently there. +# * From the bedroom, you can go downstairs to the front room. You can also jump out of the window to the garden. It fits 2 people. + +# %% [markdown] +# Make sure that your model: +# +# * Allows empty rooms +# * Allows you to jump out of the upstairs window, but not to fly back up. +# * Allows rooms which people can't fit in. + +# %% [markdown] +# myhouse = [ "Your answer here" ] diff --git a/ch01python/030MazeSolution.html b/ch01python/030MazeSolution.html new file mode 100644 index 000000000..87ba013f2 --- /dev/null +++ b/ch01python/030MazeSolution.html @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Maze Solution + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Solution: my Maze Model

+
+
+
+
+
+
+

Here's one possible solution to the Maze model. Yours will probably be different, and might be just as good. +That's the artistry of software engineering: some solutions will be faster, others use less memory, while others will +be easier for other people to understand. Optimising and balancing these factors is fun!

+
+
+
+
+
+
In [1]:
+
+
+
house = {
+    'living' : {
+        'exits': {
+            'north' : 'kitchen',
+            'outside' : 'garden',
+            'upstairs' : 'bedroom'
+        },
+        'people' : ['Graham'],
+        'capacity' : 2
+    },
+    'kitchen' : {
+        'exits': {
+            'south' : 'living'
+        },
+        'people' : [],
+        'capacity' : 1
+    },
+    'garden' : {
+        'exits': {
+            'inside' : 'living'
+        },
+        'people' : ['David'],
+        'capacity' : 3
+    },
+    'bedroom' : {
+        'exits': {
+            'downstairs' : 'living',
+            'jump' : 'garden'
+        },
+        'people' : [],
+        'capacity' : 1
+    }
+}
+
+
+
+
+
+
+
+
+

Some important points:

+
+
+
+
+
+
+
    +
  • The whole solution is a complete nested structure.
  • +
  • I used indenting to make the structure easier to read.
  • +
  • Python allows code to continue over multiple lines, so long as sets of brackets are not finished.
  • +
  • There is an empty person list in empty rooms, so the type structure is robust to potential movements of people.
  • +
  • We are nesting dictionaries and lists, with string and integer data.
  • +
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/030MazeSolution.ipynb b/ch01python/030MazeSolution.ipynb new file mode 100644 index 000000000..8b309593f --- /dev/null +++ b/ch01python/030MazeSolution.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2cc36544", + "metadata": {}, + "source": [ + "### Solution: my Maze Model" + ] + }, + { + "cell_type": "markdown", + "id": "648a3d99", + "metadata": {}, + "source": [ + "Here's one possible solution to the Maze model. Yours will probably be different, and might be just as good.\n", + "That's the artistry of software engineering: some solutions will be faster, others use less memory, while others will\n", + "be easier for other people to understand. Optimising and balancing these factors is fun!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1ccd6cb", + "metadata": {}, + "outputs": [], + "source": [ + "house = {\n", + " 'living' : {\n", + " 'exits': {\n", + " 'north' : 'kitchen',\n", + " 'outside' : 'garden',\n", + " 'upstairs' : 'bedroom'\n", + " },\n", + " 'people' : ['Graham'],\n", + " 'capacity' : 2\n", + " },\n", + " 'kitchen' : {\n", + " 'exits': {\n", + " 'south' : 'living'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " },\n", + " 'garden' : {\n", + " 'exits': {\n", + " 'inside' : 'living'\n", + " },\n", + " 'people' : ['David'],\n", + " 'capacity' : 3\n", + " },\n", + " 'bedroom' : {\n", + " 'exits': {\n", + " 'downstairs' : 'living',\n", + " 'jump' : 'garden'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "4f16eeb5", + "metadata": {}, + "source": [ + "Some important points:" + ] + }, + { + "cell_type": "markdown", + "id": "3a296c2e", + "metadata": {}, + "source": [ + "* The whole solution is a complete nested structure.\n", + "* I used indenting to make the structure easier to read.\n", + "* Python allows code to continue over multiple lines, so long as sets of brackets are not finished.\n", + "* There is an **empty** person list in empty rooms, so the type structure is robust to potential movements of people.\n", + "* We are nesting dictionaries and lists, with string and integer data." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Maze Solution" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/030MazeSolution.ipynb.py b/ch01python/030MazeSolution.ipynb.py new file mode 100644 index 000000000..0a933f2dd --- /dev/null +++ b/ch01python/030MazeSolution.ipynb.py @@ -0,0 +1,65 @@ +# --- +# jupyter: +# jekyll: +# display_name: Maze Solution +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ### Solution: my Maze Model + +# %% [markdown] +# Here's one possible solution to the Maze model. Yours will probably be different, and might be just as good. +# That's the artistry of software engineering: some solutions will be faster, others use less memory, while others will +# be easier for other people to understand. Optimising and balancing these factors is fun! + +# %% +house = { + 'living' : { + 'exits': { + 'north' : 'kitchen', + 'outside' : 'garden', + 'upstairs' : 'bedroom' + }, + 'people' : ['Graham'], + 'capacity' : 2 + }, + 'kitchen' : { + 'exits': { + 'south' : 'living' + }, + 'people' : [], + 'capacity' : 1 + }, + 'garden' : { + 'exits': { + 'inside' : 'living' + }, + 'people' : ['David'], + 'capacity' : 3 + }, + 'bedroom' : { + 'exits': { + 'downstairs' : 'living', + 'jump' : 'garden' + }, + 'people' : [], + 'capacity' : 1 + } +} + +# %% [markdown] +# Some important points: + +# %% [markdown] +# * The whole solution is a complete nested structure. +# * I used indenting to make the structure easier to read. +# * Python allows code to continue over multiple lines, so long as sets of brackets are not finished. +# * There is an **empty** person list in empty rooms, so the type structure is robust to potential movements of people. +# * We are nesting dictionaries and lists, with string and integer data. diff --git a/ch01python/032conditionality.html b/ch01python/032conditionality.html new file mode 100644 index 000000000..80fbb835f --- /dev/null +++ b/ch01python/032conditionality.html @@ -0,0 +1,1241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Conditionality + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Control and Flow

+
+
+
+
+
+
+

Turing completeness

+
+
+
+
+
+
+

Now that we understand how we can use objects to store and model our data, we only need to be able to control the flow of our +program in order to have a program that can, in principle, do anything!

+

Specifically we need to be able to:

+
    +
  • Control whether a program statement should be executed or not, based on a variable. "Conditionality"
  • +
  • Jump back to an earlier point in the program, and run some statements again. "Branching"
  • +
+
+
+
+
+
+
+

Once we have these, we can write computer programs to process information in arbitrary ways: we are Turing Complete!

+
+
+
+
+
+
+

Conditionality

+
+
+
+
+
+
+

Conditionality is achieved through Python's if statement:

+
+
+
+
+
+
In [1]:
+
+
+
x = 5
+
+if x < 0:
+    print(f"{x} is negative")
+
+
+
+
+
+
+
+
+

The absence of output here means the if clause prevented the print statement from running.

+
+
+
+
+
+
In [2]:
+
+
+
x = -10
+
+if x < 0:
+    print(f"{x} is negative")
+
+
+
+
+
+
+
+
+
+
-10 is negative
+
+
+
+
+
+
+
+
+
+

The first time through, the print statement never happened.

+
+
+
+
+
+
+

The controlled statements are indented. Once we remove the indent, the statements will once again happen regardless.

+
+
+
+
+
+
+

Else and Elif

+
+
+
+
+
+
+

Python's if statement has optional elif (else-if) and else clauses:

+
+
+
+
+
+
In [3]:
+
+
+
x = 5
+if x < 0:
+    print("x is negative")
+else:
+    print("x is positive")
+
+
+
+
+
+
+
+
+
+
x is positive
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
x = 5
+if x < 0:
+    print("x is negative")
+elif x == 0:
+    print("x is zero")
+else:
+    print("x is positive")
+
+
+
+
+
+
+
+
+
+
x is positive
+
+
+
+
+
+
+
+
+
+

Try editing the value of x here, and note that other sections are found.

+
+
+
+
+
+
In [5]:
+
+
+
choice = 'high'
+
+if choice == 'high':
+    print(1)
+elif choice == 'medium':
+    print(2)
+else:
+    print(3)
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
+
+
+
+

Comparison

+
+
+
+
+
+
+

True and False are used to represent boolean (true or false) values.

+
+
+
+
+
+
In [6]:
+
+
+
1 > 2
+
+
+
+
+
+
+
+
Out[6]:
+
+
False
+
+
+
+
+
+
+
+
+

Comparison on strings is alphabetical.

+
+
+
+
+
+
In [7]:
+
+
+
"UCL" > "KCL"
+
+
+
+
+
+
+
+
Out[7]:
+
+
True
+
+
+
+
+
+
+
+
+

But case sensitive:

+
+
+
+
+
+
In [8]:
+
+
+
"UCL" > "kcl"
+
+
+
+
+
+
+
+
Out[8]:
+
+
False
+
+
+
+
+
+
+
+
+

There's no automatic conversion of the string True to true:

+
+
+
+
+
+
In [9]:
+
+
+
True == "True"
+
+
+
+
+
+
+
+
Out[9]:
+
+
False
+
+
+
+
+
+
+
+
+

In python two there were subtle implied order comparisons between types, but it was bad style to rely on these. +In python three, you cannot compare these.

+
+
+
+
+
+
In [10]:
+
+
+
'1' < 2
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[10], line 1
+----> 1 '1' < 2
+
+TypeError: '<' not supported between instances of 'str' and 'int'
+
+
+
+
+
+
+
+
In [11]:
+
+
+
'5' < 2
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[11], line 1
+----> 1 '5' < 2
+
+TypeError: '<' not supported between instances of 'str' and 'int'
+
+
+
+
+
+
+
+
In [12]:
+
+
+
'1' > 2
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[12], line 1
+----> 1 '1' > 2
+
+TypeError: '>' not supported between instances of 'str' and 'int'
+
+
+
+
+
+
+
+
+

Any statement that evaluates to True or False can be used to control an if Statement.

+
+
+
+
+
+
+

Automatic Falsehood

+
+
+
+
+
+
+

Various other things automatically count as true or false, which can make life easier when coding:

+
+
+
+
+
+
In [13]:
+
+
+
mytext = "Hello"
+
+
+
+
+
+
+
+
In [14]:
+
+
+
if mytext:
+    print("Mytext is not empty")
+
+
+
+
+
+
+
+
+
+
Mytext is not empty
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
mytext2 = ""
+
+
+
+
+
+
+
+
In [16]:
+
+
+
if mytext2:
+    print("Mytext2 is not empty")
+
+
+
+
+
+
+
+
+

We can use logical not and logical and to combine true and false:

+
+
+
+
+
+
In [17]:
+
+
+
x = 3.2
+if not (x > 0 and isinstance(x, int)):
+    print(x,"is not a positive integer")
+
+
+
+
+
+
+
+
+
+
3.2 is not a positive integer
+
+
+
+
+
+
+
+
+
+

not also understands magic conversion from false-like things to True or False.

+
+
+
+
+
+
In [18]:
+
+
+
not not "Who's there!" # Thanks to Mysterious Student
+
+
+
+
+
+
+
+
Out[18]:
+
+
True
+
+
+
+
+
+
+
+
In [19]:
+
+
+
bool("")
+
+
+
+
+
+
+
+
Out[19]:
+
+
False
+
+
+
+
+
+
+
+
In [20]:
+
+
+
bool("Graham")
+
+
+
+
+
+
+
+
Out[20]:
+
+
True
+
+
+
+
+
+
+
+
In [21]:
+
+
+
bool([])
+
+
+
+
+
+
+
+
Out[21]:
+
+
False
+
+
+
+
+
+
+
+
In [22]:
+
+
+
bool(['a'])
+
+
+
+
+
+
+
+
Out[22]:
+
+
True
+
+
+
+
+
+
+
+
In [23]:
+
+
+
bool({})
+
+
+
+
+
+
+
+
Out[23]:
+
+
False
+
+
+
+
+
+
+
+
In [24]:
+
+
+
bool({'name': 'Graham'})
+
+
+
+
+
+
+
+
Out[24]:
+
+
True
+
+
+
+
+
+
+
+
In [25]:
+
+
+
bool(0)
+
+
+
+
+
+
+
+
Out[25]:
+
+
False
+
+
+
+
+
+
+
+
In [26]:
+
+
+
bool(1)
+
+
+
+
+
+
+
+
Out[26]:
+
+
True
+
+
+
+
+
+
+
+
+

But subtly, although these quantities evaluate True or False in an if statement, they're not themselves actually True or False under ==:

+
+
+
+
+
+
In [27]:
+
+
+
[] == False
+
+
+
+
+
+
+
+
Out[27]:
+
+
False
+
+
+
+
+
+
+
+
In [28]:
+
+
+
bool([]) == False
+
+
+
+
+
+
+
+
Out[28]:
+
+
True
+
+
+
+
+
+
+
+
+

Indentation

+
+
+
+
+
+
+

In Python, indentation is semantically significant. +You can choose how much indentation to use, so long as you +are consistent, but four spaces is +conventional. Please do not use tabs.

+

In the notebook, and most good editors, when you press <tab>, you get four spaces.

+
+
+
+
+
+
+

No indentation when it is expected, results in an error:

+
+
+
+
+
+
In [29]:
+
+
+
x = 2
+
+
+
+
+
+
+
+
In [30]:
+
+
+
if x > 0:
+print(x)
+
+
+
+
+
+
+
+
+
+
+  Cell In[30], line 2
+    print(x)
+    ^
+IndentationError: expected an indented block
+
+
+
+
+
+
+
+
+
+

but:

+
+
+
+
+
+
In [31]:
+
+
+
if x > 0:
+    print(x)
+
+
+
+
+
+
+
+
+
+
2
+
+
+
+
+
+
+
+
+
+

Pass

+
+
+
+
+
+
+

A statement expecting identation must have some indented code. +This can be annoying when commenting things out. (With #)

+
+
+
+
+
+
In [32]:
+
+
+
if x > 0:
+    # print x
+    
+print("Hello")
+
+
+
+
+
+
+
+
+
+
+  Cell In[32], line 4
+    print("Hello")
+    ^
+IndentationError: expected an indented block
+
+
+
+
+
+
+
+
+
+

So the pass statement is used to do nothing.

+
+
+
+
+
+
In [33]:
+
+
+
if x > 0:
+    # print x
+    pass
+
+print("Hello")
+
+
+
+
+
+
+
+
+
+
Hello
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/032conditionality.ipynb b/ch01python/032conditionality.ipynb new file mode 100644 index 000000000..148423a9b --- /dev/null +++ b/ch01python/032conditionality.ipynb @@ -0,0 +1,655 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2d18630b", + "metadata": {}, + "source": [ + "## Control and Flow" + ] + }, + { + "cell_type": "markdown", + "id": "948dcb31", + "metadata": {}, + "source": [ + "### Turing completeness" + ] + }, + { + "cell_type": "markdown", + "id": "1fdcd937", + "metadata": {}, + "source": [ + "Now that we understand how we can use objects to store and model our data, we only need to be able to control the flow of our\n", + "program in order to have a program that can, in principle, do anything!\n", + "\n", + "Specifically we need to be able to:\n", + "\n", + "* Control whether a program statement should be executed or not, based on a variable. \"Conditionality\"\n", + "* Jump back to an earlier point in the program, and run some statements again. \"Branching\"" + ] + }, + { + "cell_type": "markdown", + "id": "e0431c04", + "metadata": {}, + "source": [ + "Once we have these, we can write computer programs to process information in arbitrary ways: we are *Turing Complete*!" + ] + }, + { + "cell_type": "markdown", + "id": "96713267", + "metadata": {}, + "source": [ + "### Conditionality" + ] + }, + { + "cell_type": "markdown", + "id": "45df11e2", + "metadata": {}, + "source": [ + "Conditionality is achieved through Python's `if` statement:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19aaad78", + "metadata": {}, + "outputs": [], + "source": [ + "x = 5\n", + "\n", + "if x < 0:\n", + " print(f\"{x} is negative\")" + ] + }, + { + "cell_type": "markdown", + "id": "c56fca1e", + "metadata": {}, + "source": [ + "The absence of output here means the if clause prevented the print statement from running." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af485617", + "metadata": {}, + "outputs": [], + "source": [ + "x = -10\n", + "\n", + "if x < 0:\n", + " print(f\"{x} is negative\")" + ] + }, + { + "cell_type": "markdown", + "id": "07168633", + "metadata": {}, + "source": [ + "The first time through, the print statement never happened." + ] + }, + { + "cell_type": "markdown", + "id": "c7f32aeb", + "metadata": {}, + "source": [ + "The **controlled** statements are indented. Once we remove the indent, the statements will once again happen regardless. " + ] + }, + { + "cell_type": "markdown", + "id": "fd932200", + "metadata": {}, + "source": [ + "### Else and Elif" + ] + }, + { + "cell_type": "markdown", + "id": "e50f5a33", + "metadata": {}, + "source": [ + "Python's if statement has optional elif (else-if) and else clauses:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e0b22dc", + "metadata": {}, + "outputs": [], + "source": [ + "x = 5\n", + "if x < 0:\n", + " print(\"x is negative\")\n", + "else:\n", + " print(\"x is positive\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0f01f9d", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "x = 5\n", + "if x < 0:\n", + " print(\"x is negative\")\n", + "elif x == 0:\n", + " print(\"x is zero\")\n", + "else:\n", + " print(\"x is positive\")" + ] + }, + { + "cell_type": "markdown", + "id": "029a68c8", + "metadata": {}, + "source": [ + "Try editing the value of x here, and note that other sections are found." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "499b798c", + "metadata": {}, + "outputs": [], + "source": [ + "choice = 'high'\n", + "\n", + "if choice == 'high':\n", + " print(1)\n", + "elif choice == 'medium':\n", + " print(2)\n", + "else:\n", + " print(3)" + ] + }, + { + "cell_type": "markdown", + "id": "c6b92a48", + "metadata": {}, + "source": [ + "### Comparison" + ] + }, + { + "cell_type": "markdown", + "id": "60e3c01b", + "metadata": {}, + "source": [ + "`True` and `False` are used to represent **boolean** (true or false) values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ff2c2d1", + "metadata": {}, + "outputs": [], + "source": [ + "1 > 2" + ] + }, + { + "cell_type": "markdown", + "id": "64852022", + "metadata": {}, + "source": [ + "Comparison on strings is alphabetical." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03f3b469", + "metadata": {}, + "outputs": [], + "source": [ + "\"UCL\" > \"KCL\"" + ] + }, + { + "cell_type": "markdown", + "id": "8e2ae5af", + "metadata": {}, + "source": [ + "But case sensitive:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8e1ae56", + "metadata": {}, + "outputs": [], + "source": [ + "\"UCL\" > \"kcl\"" + ] + }, + { + "cell_type": "markdown", + "id": "3a5a269a", + "metadata": {}, + "source": [ + "There's no automatic conversion of the **string** True to true:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4b327fc", + "metadata": {}, + "outputs": [], + "source": [ + "True == \"True\"" + ] + }, + { + "cell_type": "markdown", + "id": "8981fbd0", + "metadata": {}, + "source": [ + "In python two there were subtle implied order comparisons between types, but it was bad style to rely on these.\n", + "In python three, you cannot compare these." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82d4b52e", + "metadata": {}, + "outputs": [], + "source": [ + "'1' < 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbd915f2", + "metadata": {}, + "outputs": [], + "source": [ + "'5' < 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a0f5a5b", + "metadata": {}, + "outputs": [], + "source": [ + "'1' > 2" + ] + }, + { + "cell_type": "markdown", + "id": "f4c4c05f", + "metadata": {}, + "source": [ + "Any statement that evaluates to `True` or `False` can be used to control an `if` Statement." + ] + }, + { + "cell_type": "markdown", + "id": "95e220fe", + "metadata": {}, + "source": [ + "### Automatic Falsehood" + ] + }, + { + "cell_type": "markdown", + "id": "3060f62c", + "metadata": {}, + "source": [ + "Various other things automatically count as true or false, which can make life easier when coding:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7900db27", + "metadata": {}, + "outputs": [], + "source": [ + "mytext = \"Hello\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "632c384f", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "if mytext:\n", + " print(\"Mytext is not empty\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bdf2365", + "metadata": {}, + "outputs": [], + "source": [ + "mytext2 = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe83fe81", + "metadata": {}, + "outputs": [], + "source": [ + "if mytext2:\n", + " print(\"Mytext2 is not empty\")" + ] + }, + { + "cell_type": "markdown", + "id": "014d0f39", + "metadata": {}, + "source": [ + "We can use logical not and logical and to combine true and false:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f92c3d2", + "metadata": {}, + "outputs": [], + "source": [ + "x = 3.2\n", + "if not (x > 0 and isinstance(x, int)):\n", + " print(x,\"is not a positive integer\")" + ] + }, + { + "cell_type": "markdown", + "id": "eee9d4f8", + "metadata": {}, + "source": [ + "`not` also understands magic conversion from false-like things to True or False." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bcb2bd7", + "metadata": {}, + "outputs": [], + "source": [ + "not not \"Who's there!\" # Thanks to Mysterious Student" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "961a1ce3", + "metadata": {}, + "outputs": [], + "source": [ + "bool(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c68cf12", + "metadata": {}, + "outputs": [], + "source": [ + "bool(\"Graham\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a0ea6f5", + "metadata": {}, + "outputs": [], + "source": [ + "bool([])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e675983a", + "metadata": {}, + "outputs": [], + "source": [ + "bool(['a'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45e64525", + "metadata": {}, + "outputs": [], + "source": [ + "bool({})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08d05710", + "metadata": {}, + "outputs": [], + "source": [ + "bool({'name': 'Graham'})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af78fed8", + "metadata": {}, + "outputs": [], + "source": [ + "bool(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e07e22d7", + "metadata": {}, + "outputs": [], + "source": [ + "bool(1)" + ] + }, + { + "cell_type": "markdown", + "id": "c2585317", + "metadata": {}, + "source": [ + "But subtly, although these quantities evaluate True or False in an if statement, they're not themselves actually True or False under ==:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67f70764", + "metadata": {}, + "outputs": [], + "source": [ + "[] == False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94b36357", + "metadata": {}, + "outputs": [], + "source": [ + "bool([]) == False" + ] + }, + { + "cell_type": "markdown", + "id": "22582201", + "metadata": {}, + "source": [ + "### Indentation" + ] + }, + { + "cell_type": "markdown", + "id": "dcf89363", + "metadata": {}, + "source": [ + "In Python, indentation is semantically significant.\n", + "You can choose how much indentation to use, so long as you\n", + "are consistent, but four spaces is\n", + "conventional. Please do not use tabs.\n", + "\n", + "In the notebook, and most good editors, when you press ``, you get four spaces.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "9a6da425", + "metadata": {}, + "source": [ + "No indentation when it is expected, results in an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e020150", + "metadata": {}, + "outputs": [], + "source": [ + "x = 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "114e43fe", + "metadata": {}, + "outputs": [], + "source": [ + "if x > 0:\n", + "print(x)" + ] + }, + { + "cell_type": "markdown", + "id": "298e28b4", + "metadata": {}, + "source": [ + "but:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed7a8a9d", + "metadata": {}, + "outputs": [], + "source": [ + "if x > 0:\n", + " print(x)" + ] + }, + { + "cell_type": "markdown", + "id": "79ee057a", + "metadata": {}, + "source": [ + "###  Pass" + ] + }, + { + "cell_type": "markdown", + "id": "f3f10ada", + "metadata": {}, + "source": [ + "\n", + "A statement expecting identation must have some indented code.\n", + "This can be annoying when commenting things out. (With `#`)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc8a3be1", + "metadata": {}, + "outputs": [], + "source": [ + "if x > 0:\n", + " # print x\n", + " \n", + "print(\"Hello\")" + ] + }, + { + "cell_type": "markdown", + "id": "12ad84a2", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "So the `pass` statement is used to do nothing.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d843124f", + "metadata": {}, + "outputs": [], + "source": [ + "if x > 0:\n", + " # print x\n", + " pass\n", + "\n", + "print(\"Hello\")" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Conditionality" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/032conditionality.ipynb.py b/ch01python/032conditionality.ipynb.py new file mode 100644 index 000000000..2bac8b8e8 --- /dev/null +++ b/ch01python/032conditionality.ipynb.py @@ -0,0 +1,266 @@ +# --- +# jupyter: +# jekyll: +# display_name: Conditionality +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Control and Flow + +# %% [markdown] +# ### Turing completeness + +# %% [markdown] +# Now that we understand how we can use objects to store and model our data, we only need to be able to control the flow of our +# program in order to have a program that can, in principle, do anything! +# +# Specifically we need to be able to: +# +# * Control whether a program statement should be executed or not, based on a variable. "Conditionality" +# * Jump back to an earlier point in the program, and run some statements again. "Branching" + +# %% [markdown] +# Once we have these, we can write computer programs to process information in arbitrary ways: we are *Turing Complete*! + +# %% [markdown] +# ### Conditionality + +# %% [markdown] +# Conditionality is achieved through Python's `if` statement: + +# %% +x = 5 + +if x < 0: + print(f"{x} is negative") + +# %% [markdown] +# The absence of output here means the if clause prevented the print statement from running. + +# %% +x = -10 + +if x < 0: + print(f"{x} is negative") + +# %% [markdown] +# The first time through, the print statement never happened. + +# %% [markdown] +# The **controlled** statements are indented. Once we remove the indent, the statements will once again happen regardless. + +# %% [markdown] +# ### Else and Elif + +# %% [markdown] +# Python's if statement has optional elif (else-if) and else clauses: + +# %% +x = 5 +if x < 0: + print("x is negative") +else: + print("x is positive") + +# %% +x = 5 +if x < 0: + print("x is negative") +elif x == 0: + print("x is zero") +else: + print("x is positive") + + +# %% [markdown] +# Try editing the value of x here, and note that other sections are found. + +# %% +choice = 'high' + +if choice == 'high': + print(1) +elif choice == 'medium': + print(2) +else: + print(3) + +# %% [markdown] +# ### Comparison + +# %% [markdown] +# `True` and `False` are used to represent **boolean** (true or false) values. + +# %% +1 > 2 + +# %% [markdown] +# Comparison on strings is alphabetical. + +# %% +"UCL" > "KCL" + +# %% [markdown] +# But case sensitive: + +# %% +"UCL" > "kcl" + +# %% [markdown] +# There's no automatic conversion of the **string** True to true: + +# %% +True == "True" + +# %% [markdown] +# In python two there were subtle implied order comparisons between types, but it was bad style to rely on these. +# In python three, you cannot compare these. + +# %% +'1' < 2 + +# %% +'5' < 2 + +# %% +'1' > 2 + +# %% [markdown] +# Any statement that evaluates to `True` or `False` can be used to control an `if` Statement. + +# %% [markdown] +# ### Automatic Falsehood + +# %% [markdown] +# Various other things automatically count as true or false, which can make life easier when coding: + +# %% +mytext = "Hello" + +# %% +if mytext: + print("Mytext is not empty") + + +# %% +mytext2 = "" + +# %% +if mytext2: + print("Mytext2 is not empty") + +# %% [markdown] +# We can use logical not and logical and to combine true and false: + +# %% +x = 3.2 +if not (x > 0 and isinstance(x, int)): + print(x,"is not a positive integer") + +# %% [markdown] +# `not` also understands magic conversion from false-like things to True or False. + +# %% +not not "Who's there!" # Thanks to Mysterious Student + +# %% +bool("") + +# %% +bool("Graham") + +# %% +bool([]) + +# %% +bool(['a']) + +# %% +bool({}) + +# %% +bool({'name': 'Graham'}) + +# %% +bool(0) + +# %% +bool(1) + +# %% [markdown] +# But subtly, although these quantities evaluate True or False in an if statement, they're not themselves actually True or False under ==: + +# %% +[] == False + +# %% +bool([]) == False + +# %% [markdown] +# ### Indentation + +# %% [markdown] +# In Python, indentation is semantically significant. +# You can choose how much indentation to use, so long as you +# are consistent, but four spaces is +# conventional. Please do not use tabs. +# +# In the notebook, and most good editors, when you press ``, you get four spaces. +# + +# %% [markdown] +# No indentation when it is expected, results in an error: + +# %% +x = 2 + +# %% +if x > 0: +print(x) + +# %% [markdown] +# but: + +# %% +if x > 0: + print(x) + +# %% [markdown] +# ###  Pass + +# %% [markdown] +# +# A statement expecting identation must have some indented code. +# This can be annoying when commenting things out. (With `#`) +# +# +# + +# %% +if x > 0: + # print x + +print("Hello") + +# %% [markdown] +# +# +# +# So the `pass` statement is used to do nothing. +# +# +# + +# %% +if x > 0: + # print x + pass + +print("Hello") diff --git a/ch01python/035looping.html b/ch01python/035looping.html new file mode 100644 index 000000000..4e46c4a47 --- /dev/null +++ b/ch01python/035looping.html @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Looping + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Iteration

+
+
+
+
+
+
+

Our other aspect of control is looping back on ourselves.

+

We use for ... in to "iterate" over lists:

+
+
+
+
+
+
In [1]:
+
+
+
mylist = [3, 7, 15, 2]
+
+
+
+
+
+
+
+
In [2]:
+
+
+
for whatever in mylist:
+    print(whatever ** 2)
+
+
+
+
+
+
+
+
+
+
9
+49
+225
+4
+
+
+
+
+
+
+
+
+
+

Each time through the loop, the variable in the value slot is updated to the next element of the sequence.

+
+
+
+
+
+
+

Iterables

+
+
+
+
+
+
+

Any sequence type is iterable:

+
+
+
+
+
+
In [3]:
+
+
+
vowels = "aeiou"
+sarcasm = []
+
+for letter in "Okay":
+    if letter.lower() in vowels:
+        repetition = 3
+    else:
+        repetition = 1
+
+    sarcasm.append(letter * repetition)
+
+"".join(sarcasm)
+
+
+
+
+
+
+
+
Out[3]:
+
+
'OOOkaaay'
+
+
+
+
+
+
+
+
+

The above is a little puzzle, work through it to understand why it does what it does.

+
+
+
+
+
+
+

Dictionaries are Iterables

+
+
+
+
+
+
+

All sequences are iterables. Some iterables (things you can for loop over) are not sequences (things with you can do x[5] to), for example sets and dictionaries.

+
+
+
+
+
+
In [4]:
+
+
+
import datetime
+now = datetime.datetime.now()
+
+founded = {"Eric": 1943, "UCL": 1826, "Cambridge": 1209}
+
+current_year = now.year
+
+for thing in founded:
+    print(thing, " is ", current_year -  founded[thing], "years old.")
+
+
+
+
+
+
+
+
+
+
Eric  is  80 years old.
+UCL  is  197 years old.
+Cambridge  is  814 years old.
+
+
+
+
+
+
+
+
+
+

Unpacking and Iteration

+
+
+
+
+
+
+

Unpacking can be useful with iteration:

+
+
+
+
+
+
In [5]:
+
+
+
triples = [
+    [4, 11, 15], 
+    [39, 4, 18]
+]
+
+
+
+
+
+
+
+
In [6]:
+
+
+
for whatever in triples:
+    print(whatever)
+
+
+
+
+
+
+
+
+
+
[4, 11, 15]
+[39, 4, 18]
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
for first, middle, last in triples:
+    print(middle)
+
+
+
+
+
+
+
+
+
+
11
+4
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
# A reminder that the words you use for variable names are arbitrary:
+for hedgehog, badger, fox in triples:
+    print(badger)
+
+
+
+
+
+
+
+
+
+
11
+4
+
+
+
+
+
+
+
+
+
+

for example, to iterate over the items in a dictionary as pairs:

+
+
+
+
+
+
In [9]:
+
+
+
things = {"Eric": [1943, 'South Shields'], 
+          "UCL": [1826, 'Bloomsbury'], 
+          "Cambridge": [1209, 'Cambridge']}
+
+print(things.items())
+
+
+
+
+
+
+
+
+
+
dict_items([('Eric', [1943, 'South Shields']), ('UCL', [1826, 'Bloomsbury']), ('Cambridge', [1209, 'Cambridge'])])
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
for name, year in founded.items():
+    print(name, " is ", current_year - year, "years old.")
+
+
+
+
+
+
+
+
+
+
Eric  is  80 years old.
+UCL  is  197 years old.
+Cambridge  is  814 years old.
+
+
+
+
+
+
+
+
+
+

Break, Continue

+
+
+
+
+
+
+
    +
  • Continue skips to the next turn of a loop
  • +
  • Break stops the loop early
  • +
+
+
+
+
+
+
In [11]:
+
+
+
for n in range(50):
+    if n == 20: 
+        break
+    if n % 2 == 0:
+        continue
+    print(n)
+
+
+
+
+
+
+
+
+
+
1
+3
+5
+7
+9
+11
+13
+15
+17
+19
+
+
+
+
+
+
+
+
+
+

These aren't useful that often, but are worth knowing about. There's also an optional else clause on loops, executed only if you don't break, but I've never found that useful.

+
+
+
+
+
+
+

Classroom exercise: the Maze Population

+
+
+
+
+
+
+

Take your maze data structure. Write a program to count the total number of people in the maze, and also determine the total possible occupants.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/035looping.ipynb b/ch01python/035looping.ipynb new file mode 100644 index 000000000..8f92973b1 --- /dev/null +++ b/ch01python/035looping.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "909dce05", + "metadata": {}, + "source": [ + "### Iteration" + ] + }, + { + "cell_type": "markdown", + "id": "03509075", + "metadata": {}, + "source": [ + "Our other aspect of control is looping back on ourselves.\n", + "\n", + "We use `for` ... `in` to \"iterate\" over lists:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b08f08a2", + "metadata": {}, + "outputs": [], + "source": [ + "mylist = [3, 7, 15, 2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "610eb051", + "metadata": {}, + "outputs": [], + "source": [ + "for whatever in mylist:\n", + " print(whatever ** 2)" + ] + }, + { + "cell_type": "markdown", + "id": "587ff241", + "metadata": {}, + "source": [ + "Each time through the loop, the variable in the `value` slot is updated to the **next** element of the sequence." + ] + }, + { + "cell_type": "markdown", + "id": "3f287358", + "metadata": {}, + "source": [ + "### Iterables" + ] + }, + { + "cell_type": "markdown", + "id": "7e0b0a8f", + "metadata": {}, + "source": [ + "\n", + "Any sequence type is iterable:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d1fcc26", + "metadata": {}, + "outputs": [], + "source": [ + "vowels = \"aeiou\"\n", + "sarcasm = []\n", + "\n", + "for letter in \"Okay\":\n", + " if letter.lower() in vowels:\n", + " repetition = 3\n", + " else:\n", + " repetition = 1\n", + "\n", + " sarcasm.append(letter * repetition)\n", + "\n", + "\"\".join(sarcasm)" + ] + }, + { + "cell_type": "markdown", + "id": "b2a947f8", + "metadata": {}, + "source": [ + "The above is a little puzzle, work through it to understand why it does what it does." + ] + }, + { + "cell_type": "markdown", + "id": "e8213604", + "metadata": {}, + "source": [ + "###  Dictionaries are Iterables" + ] + }, + { + "cell_type": "markdown", + "id": "65157a57", + "metadata": {}, + "source": [ + "All sequences are iterables. Some iterables (things you can `for` loop over) are not sequences (things with you can do `x[5]` to), for example sets and dictionaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c44ebb86", + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "now = datetime.datetime.now()\n", + "\n", + "founded = {\"Eric\": 1943, \"UCL\": 1826, \"Cambridge\": 1209}\n", + "\n", + "current_year = now.year\n", + "\n", + "for thing in founded:\n", + " print(thing, \" is \", current_year - founded[thing], \"years old.\")" + ] + }, + { + "cell_type": "markdown", + "id": "51e4846d", + "metadata": {}, + "source": [ + "### Unpacking and Iteration" + ] + }, + { + "cell_type": "markdown", + "id": "1b969ac2", + "metadata": {}, + "source": [ + "\n", + "Unpacking can be useful with iteration:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43398520", + "metadata": {}, + "outputs": [], + "source": [ + "triples = [\n", + " [4, 11, 15], \n", + " [39, 4, 18]\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcb687dc", + "metadata": {}, + "outputs": [], + "source": [ + "for whatever in triples:\n", + " print(whatever)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "936e39d2", + "metadata": {}, + "outputs": [], + "source": [ + "for first, middle, last in triples:\n", + " print(middle)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d156e52", + "metadata": {}, + "outputs": [], + "source": [ + "# A reminder that the words you use for variable names are arbitrary:\n", + "for hedgehog, badger, fox in triples:\n", + " print(badger)" + ] + }, + { + "cell_type": "markdown", + "id": "3b8f780a", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "for example, to iterate over the items in a dictionary as pairs:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e5ecdf2", + "metadata": {}, + "outputs": [], + "source": [ + "things = {\"Eric\": [1943, 'South Shields'], \n", + " \"UCL\": [1826, 'Bloomsbury'], \n", + " \"Cambridge\": [1209, 'Cambridge']}\n", + "\n", + "print(things.items())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19debdbc", + "metadata": {}, + "outputs": [], + "source": [ + "for name, year in founded.items():\n", + " print(name, \" is \", current_year - year, \"years old.\")" + ] + }, + { + "cell_type": "markdown", + "id": "8236c7c1", + "metadata": {}, + "source": [ + "### Break, Continue" + ] + }, + { + "cell_type": "markdown", + "id": "0b05903b", + "metadata": {}, + "source": [ + "\n", + "* Continue skips to the next turn of a loop\n", + "* Break stops the loop early\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cfb2f942", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "for n in range(50):\n", + " if n == 20: \n", + " break\n", + " if n % 2 == 0:\n", + " continue\n", + " print(n)" + ] + }, + { + "cell_type": "markdown", + "id": "a2b2ee00", + "metadata": {}, + "source": [ + "These aren't useful that often, but are worth knowing about. There's also an optional `else` clause on loops, executed only if you don't `break`, but I've never found that useful." + ] + }, + { + "cell_type": "markdown", + "id": "13a2d11e", + "metadata": {}, + "source": [ + "### Classroom exercise: the Maze Population" + ] + }, + { + "cell_type": "markdown", + "id": "17f12b56", + "metadata": {}, + "source": [ + "Take your maze data structure. Write a program to count the total number of people in the maze, and also determine the total possible occupants." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Looping" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/035looping.ipynb.py b/ch01python/035looping.ipynb.py new file mode 100644 index 000000000..df9b0fa8f --- /dev/null +++ b/ch01python/035looping.ipynb.py @@ -0,0 +1,152 @@ +# --- +# jupyter: +# jekyll: +# display_name: Looping +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ### Iteration + +# %% [markdown] +# Our other aspect of control is looping back on ourselves. +# +# We use `for` ... `in` to "iterate" over lists: + +# %% +mylist = [3, 7, 15, 2] + +# %% +for whatever in mylist: + print(whatever ** 2) + +# %% [markdown] +# Each time through the loop, the variable in the `value` slot is updated to the **next** element of the sequence. + +# %% [markdown] +# ### Iterables + +# %% [markdown] +# +# Any sequence type is iterable: +# +# +# + +# %% +vowels = "aeiou" +sarcasm = [] + +for letter in "Okay": + if letter.lower() in vowels: + repetition = 3 + else: + repetition = 1 + + sarcasm.append(letter * repetition) + +"".join(sarcasm) + +# %% [markdown] +# The above is a little puzzle, work through it to understand why it does what it does. + +# %% [markdown] +# ###  Dictionaries are Iterables + +# %% [markdown] +# All sequences are iterables. Some iterables (things you can `for` loop over) are not sequences (things with you can do `x[5]` to), for example sets and dictionaries. + +# %% +import datetime +now = datetime.datetime.now() + +founded = {"Eric": 1943, "UCL": 1826, "Cambridge": 1209} + +current_year = now.year + +for thing in founded: + print(thing, " is ", current_year - founded[thing], "years old.") + +# %% [markdown] +# ### Unpacking and Iteration + +# %% [markdown] +# +# Unpacking can be useful with iteration: +# +# +# + +# %% +triples = [ + [4, 11, 15], + [39, 4, 18] +] + +# %% +for whatever in triples: + print(whatever) + +# %% +for first, middle, last in triples: + print(middle) + +# %% +# A reminder that the words you use for variable names are arbitrary: +for hedgehog, badger, fox in triples: + print(badger) + +# %% [markdown] +# +# +# +# for example, to iterate over the items in a dictionary as pairs: +# +# +# + +# %% +things = {"Eric": [1943, 'South Shields'], + "UCL": [1826, 'Bloomsbury'], + "Cambridge": [1209, 'Cambridge']} + +print(things.items()) + +# %% +for name, year in founded.items(): + print(name, " is ", current_year - year, "years old.") + +# %% [markdown] +# ### Break, Continue + +# %% [markdown] +# +# * Continue skips to the next turn of a loop +# * Break stops the loop early +# +# +# + +# %% +for n in range(50): + if n == 20: + break + if n % 2 == 0: + continue + print(n) + + +# %% [markdown] +# These aren't useful that often, but are worth knowing about. There's also an optional `else` clause on loops, executed only if you don't `break`, but I've never found that useful. + +# %% [markdown] +# ### Classroom exercise: the Maze Population + +# %% [markdown] +# Take your maze data structure. Write a program to count the total number of people in the maze, and also determine the total possible occupants. diff --git a/ch01python/036MazeSolution2.html b/ch01python/036MazeSolution2.html new file mode 100644 index 000000000..42f0963a4 --- /dev/null +++ b/ch01python/036MazeSolution2.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Maze Control Solution + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Solution: counting people in the maze

+
+
+
+
+
+
+

With this maze structure:

+
+
+
+
+
+
In [1]:
+
+
+
house = {
+    'living' : {
+        'exits': {
+            'north' : 'kitchen',
+            'outside' : 'garden',
+            'upstairs' : 'bedroom'
+        },
+        'people' : ['Graham'],
+        'capacity' : 2
+    },
+    'kitchen' : {
+        'exits': {
+            'south' : 'living'
+        },
+        'people' : [],
+        'capacity' : 1
+    },
+    'garden' : {
+        'exits': {
+            'inside' : 'living'
+        },
+        'people' : ['David'],
+        'capacity' : 3
+    },
+    'bedroom' : {
+        'exits': {
+            'downstairs' : 'living',
+            'jump' : 'garden'
+        },
+        'people' : [],
+        'capacity' : 1
+    }
+}
+
+
+
+
+
+
+
+
+

We can count the occupants and capacity like this:

+
+
+
+
+
+
In [2]:
+
+
+
capacity = 0
+occupancy = 0
+for name, room in house.items():
+    capacity += room['capacity']
+    occupancy += len(room['people'])
+print(f"House can fit {capacity} people, and currently has: {occupancy}.")
+
+
+
+
+
+
+
+
+
+
House can fit 7 people, and currently has: 2.
+
+
+
+
+
+
+
+
+
+

As a side note, note how we included the values of capacity and occupancy in the last line. This is a handy syntax for building strings that contain the values of variables. You can read more about it at this Python String Formatting Best Practices guide or in the official documentation.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/036MazeSolution2.ipynb b/ch01python/036MazeSolution2.ipynb new file mode 100644 index 000000000..88c796eea --- /dev/null +++ b/ch01python/036MazeSolution2.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f746bc54", + "metadata": {}, + "source": [ + "### Solution: counting people in the maze" + ] + }, + { + "cell_type": "markdown", + "id": "2c887b97", + "metadata": {}, + "source": [ + "With this maze structure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7e27c1d", + "metadata": {}, + "outputs": [], + "source": [ + "house = {\n", + " 'living' : {\n", + " 'exits': {\n", + " 'north' : 'kitchen',\n", + " 'outside' : 'garden',\n", + " 'upstairs' : 'bedroom'\n", + " },\n", + " 'people' : ['Graham'],\n", + " 'capacity' : 2\n", + " },\n", + " 'kitchen' : {\n", + " 'exits': {\n", + " 'south' : 'living'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " },\n", + " 'garden' : {\n", + " 'exits': {\n", + " 'inside' : 'living'\n", + " },\n", + " 'people' : ['David'],\n", + " 'capacity' : 3\n", + " },\n", + " 'bedroom' : {\n", + " 'exits': {\n", + " 'downstairs' : 'living',\n", + " 'jump' : 'garden'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "8ff1784c", + "metadata": {}, + "source": [ + "We can count the occupants and capacity like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5be3a04", + "metadata": {}, + "outputs": [], + "source": [ + "capacity = 0\n", + "occupancy = 0\n", + "for name, room in house.items():\n", + " capacity += room['capacity']\n", + " occupancy += len(room['people'])\n", + "print(f\"House can fit {capacity} people, and currently has: {occupancy}.\")" + ] + }, + { + "cell_type": "markdown", + "id": "cfe367a4", + "metadata": {}, + "source": [ + "As a side note, note how we included the values of `capacity` and `occupancy` in the last line. This is a handy syntax for building strings that contain the values of variables. You can read more about it at this [Python String Formatting Best Practices guide](https://realpython.com/python-string-formatting/#2-new-style-string-formatting-strformat) or in the [official documentation](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Maze Control Solution" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/036MazeSolution2.ipynb.py b/ch01python/036MazeSolution2.ipynb.py new file mode 100644 index 000000000..d756deb56 --- /dev/null +++ b/ch01python/036MazeSolution2.ipynb.py @@ -0,0 +1,67 @@ +# --- +# jupyter: +# jekyll: +# display_name: Maze Control Solution +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ### Solution: counting people in the maze + +# %% [markdown] +# With this maze structure: + +# %% +house = { + 'living' : { + 'exits': { + 'north' : 'kitchen', + 'outside' : 'garden', + 'upstairs' : 'bedroom' + }, + 'people' : ['Graham'], + 'capacity' : 2 + }, + 'kitchen' : { + 'exits': { + 'south' : 'living' + }, + 'people' : [], + 'capacity' : 1 + }, + 'garden' : { + 'exits': { + 'inside' : 'living' + }, + 'people' : ['David'], + 'capacity' : 3 + }, + 'bedroom' : { + 'exits': { + 'downstairs' : 'living', + 'jump' : 'garden' + }, + 'people' : [], + 'capacity' : 1 + } +} + +# %% [markdown] +# We can count the occupants and capacity like this: + +# %% +capacity = 0 +occupancy = 0 +for name, room in house.items(): + capacity += room['capacity'] + occupancy += len(room['people']) +print(f"House can fit {capacity} people, and currently has: {occupancy}.") + +# %% [markdown] +# As a side note, note how we included the values of `capacity` and `occupancy` in the last line. This is a handy syntax for building strings that contain the values of variables. You can read more about it at this [Python String Formatting Best Practices guide](https://realpython.com/python-string-formatting/#2-new-style-string-formatting-strformat) or in the [official documentation](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals). diff --git a/ch01python/037comprehensions.html b/ch01python/037comprehensions.html new file mode 100644 index 000000000..65ecc7db3 --- /dev/null +++ b/ch01python/037comprehensions.html @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Comprehensions + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Comprehensions

+
+
+
+
+
+
+

The list comprehension

+
+
+
+
+
+
+

If you write a for loop inside a pair of square brackets for a list, you magic up a list as defined. +This can make for concise but hard to read code, so be careful.

+
+
+
+
+
+
In [1]:
+
+
+
[2 ** x for x in range(10)]
+
+
+
+
+
+
+
+
Out[1]:
+
+
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
+
+
+
+
+
+
+
+
+

Which is equivalent to the following code without using comprehensions:

+
+
+
+
+
+
In [2]:
+
+
+
result = []
+for x in range(10):
+    result.append(2 ** x)
+    
+result
+
+
+
+
+
+
+
+
Out[2]:
+
+
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
+
+
+
+
+
+
+
+
+

You can do quite weird and cool things with comprehensions:

+
+
+
+
+
+
In [3]:
+
+
+
[len(str(2 ** x)) for x in range(10)]
+
+
+
+
+
+
+
+
Out[3]:
+
+
[1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
+
+
+
+
+
+
+
+
+

Selection in comprehensions

+
+
+
+
+
+
+

You can write an if statement in comprehensions too:

+
+
+
+
+
+
In [4]:
+
+
+
[2 ** x for x in range(30) if x % 3 == 0]
+
+
+
+
+
+
+
+
Out[4]:
+
+
[1, 8, 64, 512, 4096, 32768, 262144, 2097152, 16777216, 134217728]
+
+
+
+
+
+
+
+
+

Consider the following, and make sure you understand why it works:

+
+
+
+
+
+
In [5]:
+
+
+
"".join([letter for letter in "Eric Idle" 
+         if letter.lower() not in 'aeiou'])
+
+
+
+
+
+
+
+
Out[5]:
+
+
'rc dl'
+
+
+
+
+
+
+
+
+

Comprehensions versus building lists with append:

+
+
+
+
+
+
+

This code:

+
+
+
+
+
+
In [6]:
+
+
+
result = []
+for x in range(30):
+    if x % 3 == 0:
+        result.append(2 ** x)
+result
+
+
+
+
+
+
+
+
Out[6]:
+
+
[1, 8, 64, 512, 4096, 32768, 262144, 2097152, 16777216, 134217728]
+
+
+
+
+
+
+
+
+

Does the same as the comprehension above. The comprehension is generally considered more readable.

+
+
+
+
+
+
+

Comprehensions are therefore an example of what we call 'syntactic sugar': they do not increase the capabilities of the language.

+
+
+
+
+
+
+

Instead, they make it possible to write the same thing in a more readable way.

+
+
+
+
+
+
+

Almost everything we learn from now on will be either syntactic sugar or interaction with something other than idealised memory, such as a storage device or the internet. Once you have variables, conditionality, and branching, your language can do anything. (And this can be proved.)

+
+
+
+
+
+
+

Nested comprehensions

+
+
+
+
+
+
+

If you write two for statements in a comprehension, you get a single array generated over all the pairs:

+
+
+
+
+
+
In [7]:
+
+
+
[x - y for x in range(4) for y in range(4)]
+
+
+
+
+
+
+
+
Out[7]:
+
+
[0, -1, -2, -3, 1, 0, -1, -2, 2, 1, 0, -1, 3, 2, 1, 0]
+
+
+
+
+
+
+
+
+

You can select on either, or on some combination:

+
+
+
+
+
+
In [8]:
+
+
+
[x - y for x in range(4) for y in range(4) if x >= y]
+
+
+
+
+
+
+
+
Out[8]:
+
+
[0, 1, 0, 2, 1, 0, 3, 2, 1, 0]
+
+
+
+
+
+
+
+
+

If you want something more like a matrix, you need to do two nested comprehensions!

+
+
+
+
+
+
In [9]:
+
+
+
[[x - y for x in range(4)] for y in range(4)]
+
+
+
+
+
+
+
+
Out[9]:
+
+
[[0, 1, 2, 3], [-1, 0, 1, 2], [-2, -1, 0, 1], [-3, -2, -1, 0]]
+
+
+
+
+
+
+
+
+

Note the subtly different square brackets.

+
+
+
+
+
+
+

Note that the list order for multiple or nested comprehensions can be confusing:

+
+
+
+
+
+
In [10]:
+
+
+
[x+y for x in ['a', 'b', 'c'] for y in ['1', '2', '3']]
+
+
+
+
+
+
+
+
Out[10]:
+
+
['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3']
+
+
+
+
+
+
+
+
In [11]:
+
+
+
[[x+y for x in ['a', 'b', 'c']] for y in ['1', '2', '3']]
+
+
+
+
+
+
+
+
Out[11]:
+
+
[['a1', 'b1', 'c1'], ['a2', 'b2', 'c2'], ['a3', 'b3', 'c3']]
+
+
+
+
+
+
+
+
+

Dictionary Comprehensions

+
+
+
+
+
+
+

You can automatically build dictionaries, by using a list comprehension syntax, but with curly brackets and a colon:

+
+
+
+
+
+
In [12]:
+
+
+
{(str(x)) * 3: x for x in range(3)}
+
+
+
+
+
+
+
+
Out[12]:
+
+
{'000': 0, '111': 1, '222': 2}
+
+
+
+
+
+
+
+
+

List-based thinking

+
+
+
+
+
+
+

Once you start to get comfortable with comprehensions, you find yourself working with containers, nested groups of lists +and dictionaries, as the 'things' in your program, not individual variables.

+
+
+
+
+
+
+

Given a way to analyse some dataset, we'll find ourselves writing stuff like:

+
analysed_data = [analyze(datum) for datum in data]
+
+
+
+
+
+
+

There are lots of built-in methods that provide actions on lists as a whole:

+
+
+
+
+
+
In [13]:
+
+
+
any([True, False, True])
+
+
+
+
+
+
+
+
Out[13]:
+
+
True
+
+
+
+
+
+
+
+
In [14]:
+
+
+
all([True, False, True])
+
+
+
+
+
+
+
+
Out[14]:
+
+
False
+
+
+
+
+
+
+
+
In [15]:
+
+
+
max([1, 2, 3])
+
+
+
+
+
+
+
+
Out[15]:
+
+
3
+
+
+
+
+
+
+
+
In [16]:
+
+
+
sum([1, 2, 3])
+
+
+
+
+
+
+
+
Out[16]:
+
+
6
+
+
+
+
+
+
+
+
+

My favourite is map, which, similar to a list comprehension, applies one function to every member of a list:

+
+
+
+
+
+
In [17]:
+
+
+
[str(x) for x in range(10)]
+
+
+
+
+
+
+
+
Out[17]:
+
+
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
+
+
+
+
+
+
+
+
In [18]:
+
+
+
list(map(str, range(10)))
+
+
+
+
+
+
+
+
Out[18]:
+
+
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
+
+
+
+
+
+
+
+
+

So I can write:

+
analysed_data = map(analyse, data)
+

We'll learn more about map and similar functions when we discuss functional programming later in the course.

+
+
+
+
+
+
+

Classroom Exercise: Occupancy Dictionary

+
+
+
+
+
+
+

Take your maze data structure. First write an expression to print out a new dictionary, which holds, for each room, that room's capacity. The output should look like:

+
+
+
+
+
+
In [19]:
+
+
+
{'bedroom': 1, 'garden': 3, 'kitchen': 1, 'living': 2}
+
+
+
+
+
+
+
+
Out[19]:
+
+
{'bedroom': 1, 'garden': 3, 'kitchen': 1, 'living': 2}
+
+
+
+
+
+
+
+
+

Now, write a program to print out a new dictionary, which gives, +for each room's name, the number of people in it. Don't add in a zero value in the dictionary for empty rooms.

+
+
+
+
+
+
+

The output should look similar to:

+
+
+
+
+
+
In [20]:
+
+
+
{'garden': 1, 'living': 1}
+
+
+
+
+
+
+
+
Out[20]:
+
+
{'garden': 1, 'living': 1}
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/037comprehensions.ipynb b/ch01python/037comprehensions.ipynb new file mode 100644 index 000000000..24f368249 --- /dev/null +++ b/ch01python/037comprehensions.ipynb @@ -0,0 +1,489 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "30ad1aa5", + "metadata": {}, + "source": [ + "## Comprehensions" + ] + }, + { + "cell_type": "markdown", + "id": "58063be5", + "metadata": {}, + "source": [ + "### The list comprehension" + ] + }, + { + "cell_type": "markdown", + "id": "44d7cc73", + "metadata": {}, + "source": [ + "If you write a for loop **inside** a pair of square brackets for a list, you magic up a list as defined.\n", + "This can make for concise but hard to read code, so be careful." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3d732d0", + "metadata": {}, + "outputs": [], + "source": [ + "[2 ** x for x in range(10)]" + ] + }, + { + "cell_type": "markdown", + "id": "275fff16", + "metadata": {}, + "source": [ + "Which is equivalent to the following code without using comprehensions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59b0b647", + "metadata": {}, + "outputs": [], + "source": [ + "result = []\n", + "for x in range(10):\n", + " result.append(2 ** x)\n", + " \n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "79d41276", + "metadata": {}, + "source": [ + "You can do quite weird and cool things with comprehensions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3918cc8e", + "metadata": {}, + "outputs": [], + "source": [ + "[len(str(2 ** x)) for x in range(10)]" + ] + }, + { + "cell_type": "markdown", + "id": "b23ec383", + "metadata": {}, + "source": [ + "### Selection in comprehensions" + ] + }, + { + "cell_type": "markdown", + "id": "da639ddc", + "metadata": {}, + "source": [ + "You can write an `if` statement in comprehensions too: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac2f6055", + "metadata": {}, + "outputs": [], + "source": [ + "[2 ** x for x in range(30) if x % 3 == 0]" + ] + }, + { + "cell_type": "markdown", + "id": "bec0892f", + "metadata": {}, + "source": [ + "Consider the following, and make sure you understand why it works:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c05ed86a", + "metadata": {}, + "outputs": [], + "source": [ + "\"\".join([letter for letter in \"Eric Idle\" \n", + " if letter.lower() not in 'aeiou'])" + ] + }, + { + "cell_type": "markdown", + "id": "2513ceec", + "metadata": {}, + "source": [ + "### Comprehensions versus building lists with `append`:" + ] + }, + { + "cell_type": "markdown", + "id": "cd2c8f8e", + "metadata": {}, + "source": [ + "This code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31b51e84", + "metadata": {}, + "outputs": [], + "source": [ + "result = []\n", + "for x in range(30):\n", + " if x % 3 == 0:\n", + " result.append(2 ** x)\n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "69af6743", + "metadata": {}, + "source": [ + "Does the same as the comprehension above. The comprehension is generally considered more readable." + ] + }, + { + "cell_type": "markdown", + "id": "0112331d", + "metadata": {}, + "source": [ + "Comprehensions are therefore an example of what we call 'syntactic sugar': they do not increase the capabilities of the language." + ] + }, + { + "cell_type": "markdown", + "id": "31369743", + "metadata": {}, + "source": [ + "Instead, they make it possible to write the same thing in a more readable way. " + ] + }, + { + "cell_type": "markdown", + "id": "2aa3c252", + "metadata": {}, + "source": [ + "Almost everything we learn from now on will be either syntactic sugar or interaction with something other than idealised memory, such as a storage device or the internet. Once you have variables, conditionality, and branching, your language can do anything. (And this can be proved.)" + ] + }, + { + "cell_type": "markdown", + "id": "4d557c01", + "metadata": {}, + "source": [ + "### Nested comprehensions" + ] + }, + { + "cell_type": "markdown", + "id": "b0d60da9", + "metadata": {}, + "source": [ + "If you write two `for` statements in a comprehension, you get a single array generated over all the pairs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ba23872", + "metadata": {}, + "outputs": [], + "source": [ + "[x - y for x in range(4) for y in range(4)]" + ] + }, + { + "cell_type": "markdown", + "id": "6206a900", + "metadata": {}, + "source": [ + "You can select on either, or on some combination:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4338e314", + "metadata": {}, + "outputs": [], + "source": [ + "[x - y for x in range(4) for y in range(4) if x >= y]" + ] + }, + { + "cell_type": "markdown", + "id": "bbd6d635", + "metadata": {}, + "source": [ + "If you want something more like a matrix, you need to do *two nested* comprehensions!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2446d15", + "metadata": {}, + "outputs": [], + "source": [ + "[[x - y for x in range(4)] for y in range(4)]" + ] + }, + { + "cell_type": "markdown", + "id": "96956a43", + "metadata": {}, + "source": [ + "Note the subtly different square brackets." + ] + }, + { + "cell_type": "markdown", + "id": "cfd131df", + "metadata": {}, + "source": [ + "Note that the list order for multiple or nested comprehensions can be confusing:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4a64f77", + "metadata": {}, + "outputs": [], + "source": [ + "[x+y for x in ['a', 'b', 'c'] for y in ['1', '2', '3']]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "789b15f9", + "metadata": {}, + "outputs": [], + "source": [ + "[[x+y for x in ['a', 'b', 'c']] for y in ['1', '2', '3']]" + ] + }, + { + "cell_type": "markdown", + "id": "0d5141e9", + "metadata": {}, + "source": [ + "### Dictionary Comprehensions" + ] + }, + { + "cell_type": "markdown", + "id": "c7f996ce", + "metadata": {}, + "source": [ + "You can automatically build dictionaries, by using a list comprehension syntax, but with curly brackets and a colon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45fde1d2", + "metadata": {}, + "outputs": [], + "source": [ + "{(str(x)) * 3: x for x in range(3)}" + ] + }, + { + "cell_type": "markdown", + "id": "fa358de8", + "metadata": {}, + "source": [ + "### List-based thinking" + ] + }, + { + "cell_type": "markdown", + "id": "5bba352c", + "metadata": {}, + "source": [ + "Once you start to get comfortable with comprehensions, you find yourself working with containers, nested groups of lists \n", + "and dictionaries, as the 'things' in your program, not individual variables. " + ] + }, + { + "cell_type": "markdown", + "id": "30be868c", + "metadata": {}, + "source": [ + "Given a way to analyse some dataset, we'll find ourselves writing stuff like:\n", + "\n", + " analysed_data = [analyze(datum) for datum in data]" + ] + }, + { + "cell_type": "markdown", + "id": "cf5b0f97", + "metadata": {}, + "source": [ + "There are lots of built-in methods that provide actions on lists as a whole:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49506101", + "metadata": {}, + "outputs": [], + "source": [ + "any([True, False, True])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be0fbecb", + "metadata": {}, + "outputs": [], + "source": [ + "all([True, False, True])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3028b00", + "metadata": {}, + "outputs": [], + "source": [ + "max([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7007d36", + "metadata": {}, + "outputs": [], + "source": [ + "sum([1, 2, 3])" + ] + }, + { + "cell_type": "markdown", + "id": "005daa96", + "metadata": {}, + "source": [ + "My favourite is `map`, which, similar to a list comprehension, applies one function to every member of a list:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91679392", + "metadata": {}, + "outputs": [], + "source": [ + "[str(x) for x in range(10)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6faeb6d5", + "metadata": {}, + "outputs": [], + "source": [ + "list(map(str, range(10)))" + ] + }, + { + "cell_type": "markdown", + "id": "a161e444", + "metadata": {}, + "source": [ + "So I can write:\n", + " \n", + " analysed_data = map(analyse, data)\n", + "\n", + "We'll learn more about `map` and similar functions when we discuss functional programming later in the course." + ] + }, + { + "cell_type": "markdown", + "id": "818f4d29", + "metadata": {}, + "source": [ + "### Classroom Exercise: Occupancy Dictionary" + ] + }, + { + "cell_type": "markdown", + "id": "77a8b283", + "metadata": {}, + "source": [ + "Take your maze data structure. First write an expression to print out a new dictionary, which holds, for each room, that room's capacity. The output should look like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61fdc2fc", + "metadata": {}, + "outputs": [], + "source": [ + "{'bedroom': 1, 'garden': 3, 'kitchen': 1, 'living': 2}" + ] + }, + { + "cell_type": "markdown", + "id": "051d36b0", + "metadata": {}, + "source": [ + "Now, write a program to print out a new dictionary, which gives,\n", + "for each room's name, the number of people in it. Don't add in a zero value in the dictionary for empty rooms." + ] + }, + { + "cell_type": "markdown", + "id": "8e2e1581", + "metadata": {}, + "source": [ + "The output should look similar to:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce702dd0", + "metadata": {}, + "outputs": [], + "source": [ + "{'garden': 1, 'living': 1}" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Comprehensions" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/037comprehensions.ipynb.py b/ch01python/037comprehensions.ipynb.py new file mode 100644 index 000000000..09e65960e --- /dev/null +++ b/ch01python/037comprehensions.ipynb.py @@ -0,0 +1,186 @@ +# --- +# jupyter: +# jekyll: +# display_name: Comprehensions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Comprehensions + +# %% [markdown] +# ### The list comprehension + +# %% [markdown] +# If you write a for loop **inside** a pair of square brackets for a list, you magic up a list as defined. +# This can make for concise but hard to read code, so be careful. + +# %% +[2 ** x for x in range(10)] + +# %% [markdown] +# Which is equivalent to the following code without using comprehensions: + +# %% +result = [] +for x in range(10): + result.append(2 ** x) + +result + +# %% [markdown] +# You can do quite weird and cool things with comprehensions: + +# %% +[len(str(2 ** x)) for x in range(10)] + +# %% [markdown] +# ### Selection in comprehensions + +# %% [markdown] +# You can write an `if` statement in comprehensions too: + +# %% +[2 ** x for x in range(30) if x % 3 == 0] + +# %% [markdown] +# Consider the following, and make sure you understand why it works: + +# %% +"".join([letter for letter in "Eric Idle" + if letter.lower() not in 'aeiou']) + +# %% [markdown] +# ### Comprehensions versus building lists with `append`: + +# %% [markdown] +# This code: + +# %% +result = [] +for x in range(30): + if x % 3 == 0: + result.append(2 ** x) +result + +# %% [markdown] +# Does the same as the comprehension above. The comprehension is generally considered more readable. + +# %% [markdown] +# Comprehensions are therefore an example of what we call 'syntactic sugar': they do not increase the capabilities of the language. + +# %% [markdown] +# Instead, they make it possible to write the same thing in a more readable way. + +# %% [markdown] +# Almost everything we learn from now on will be either syntactic sugar or interaction with something other than idealised memory, such as a storage device or the internet. Once you have variables, conditionality, and branching, your language can do anything. (And this can be proved.) + +# %% [markdown] +# ### Nested comprehensions + +# %% [markdown] +# If you write two `for` statements in a comprehension, you get a single array generated over all the pairs: + +# %% +[x - y for x in range(4) for y in range(4)] + +# %% [markdown] +# You can select on either, or on some combination: + +# %% +[x - y for x in range(4) for y in range(4) if x >= y] + +# %% [markdown] +# If you want something more like a matrix, you need to do *two nested* comprehensions! + +# %% +[[x - y for x in range(4)] for y in range(4)] + +# %% [markdown] +# Note the subtly different square brackets. + +# %% [markdown] +# Note that the list order for multiple or nested comprehensions can be confusing: + +# %% +[x+y for x in ['a', 'b', 'c'] for y in ['1', '2', '3']] + +# %% +[[x+y for x in ['a', 'b', 'c']] for y in ['1', '2', '3']] + +# %% [markdown] +# ### Dictionary Comprehensions + +# %% [markdown] +# You can automatically build dictionaries, by using a list comprehension syntax, but with curly brackets and a colon: + +# %% +{(str(x)) * 3: x for x in range(3)} + +# %% [markdown] +# ### List-based thinking + +# %% [markdown] +# Once you start to get comfortable with comprehensions, you find yourself working with containers, nested groups of lists +# and dictionaries, as the 'things' in your program, not individual variables. + +# %% [markdown] +# Given a way to analyse some dataset, we'll find ourselves writing stuff like: +# +# analysed_data = [analyze(datum) for datum in data] + +# %% [markdown] +# There are lots of built-in methods that provide actions on lists as a whole: + +# %% +any([True, False, True]) + +# %% +all([True, False, True]) + +# %% +max([1, 2, 3]) + +# %% +sum([1, 2, 3]) + +# %% [markdown] +# My favourite is `map`, which, similar to a list comprehension, applies one function to every member of a list: + +# %% +[str(x) for x in range(10)] + +# %% +list(map(str, range(10))) + +# %% [markdown] +# So I can write: +# +# analysed_data = map(analyse, data) +# +# We'll learn more about `map` and similar functions when we discuss functional programming later in the course. + +# %% [markdown] +# ### Classroom Exercise: Occupancy Dictionary + +# %% [markdown] +# Take your maze data structure. First write an expression to print out a new dictionary, which holds, for each room, that room's capacity. The output should look like: + +# %% +{'bedroom': 1, 'garden': 3, 'kitchen': 1, 'living': 2} + +# %% [markdown] +# Now, write a program to print out a new dictionary, which gives, +# for each room's name, the number of people in it. Don't add in a zero value in the dictionary for empty rooms. + +# %% [markdown] +# The output should look similar to: + +# %% +{'garden': 1, 'living': 1} diff --git a/ch01python/038SolutionComprehension.html b/ch01python/038SolutionComprehension.html new file mode 100644 index 000000000..404b79e87 --- /dev/null +++ b/ch01python/038SolutionComprehension.html @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Maze comprehension solution + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Solution

+
+
+
+
+
+
+

With this maze structure:

+
+
+
+
+
+
In [1]:
+
+
+
house = {
+    'living' : {
+        'exits': {
+            'north' : 'kitchen',
+            'outside' : 'garden',
+            'upstairs' : 'bedroom'
+        },
+        'people' : ['Graham'],
+        'capacity' : 2
+    },
+    'kitchen' : {
+        'exits': {
+            'south' : 'living'
+        },
+        'people' : [],
+        'capacity' : 1
+    },
+    'garden' : {
+        'exits': {
+            'inside' : 'living'
+        },
+        'people' : ['David'],
+        'capacity' : 3
+    },
+    'bedroom' : {
+        'exits': {
+            'downstairs' : 'living',
+            'jump' : 'garden'
+        },
+        'people' : [],
+        'capacity' : 1
+    }
+}
+
+
+
+
+
+
+
+
+

We can get a simpler dictionary with just capacities like this:

+
+
+
+
+
+
In [2]:
+
+
+
{name: room['capacity'] for name, room in house.items()}
+
+
+
+
+
+
+
+
Out[2]:
+
+
{'living': 2, 'kitchen': 1, 'garden': 3, 'bedroom': 1}
+
+
+
+
+
+
+
+
+

To get the current number of occupants, we can use a similar dictionary comprehension. Remember that we can filter (only keep certain rooms) by adding an if clause:

+
+
+
+
+
+
In [3]:
+
+
+
{name: len(room['people']) for name, room in house.items() if len(room['people']) > 0}
+
+
+
+
+
+
+
+
Out[3]:
+
+
{'living': 1, 'garden': 1}
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/038SolutionComprehension.ipynb b/ch01python/038SolutionComprehension.ipynb new file mode 100644 index 000000000..bba8875c9 --- /dev/null +++ b/ch01python/038SolutionComprehension.ipynb @@ -0,0 +1,109 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ca5e622", + "metadata": {}, + "source": [ + "### Solution" + ] + }, + { + "cell_type": "markdown", + "id": "2650c75b", + "metadata": {}, + "source": [ + "With this maze structure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c565cf8", + "metadata": {}, + "outputs": [], + "source": [ + "house = {\n", + " 'living' : {\n", + " 'exits': {\n", + " 'north' : 'kitchen',\n", + " 'outside' : 'garden',\n", + " 'upstairs' : 'bedroom'\n", + " },\n", + " 'people' : ['Graham'],\n", + " 'capacity' : 2\n", + " },\n", + " 'kitchen' : {\n", + " 'exits': {\n", + " 'south' : 'living'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " },\n", + " 'garden' : {\n", + " 'exits': {\n", + " 'inside' : 'living'\n", + " },\n", + " 'people' : ['David'],\n", + " 'capacity' : 3\n", + " },\n", + " 'bedroom' : {\n", + " 'exits': {\n", + " 'downstairs' : 'living',\n", + " 'jump' : 'garden'\n", + " },\n", + " 'people' : [],\n", + " 'capacity' : 1\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "524e5938", + "metadata": {}, + "source": [ + "We can get a simpler dictionary with just capacities like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c3db36c", + "metadata": {}, + "outputs": [], + "source": [ + "{name: room['capacity'] for name, room in house.items()}" + ] + }, + { + "cell_type": "markdown", + "id": "c2e01342", + "metadata": {}, + "source": [ + "To get the current number of occupants, we can use a similar dictionary comprehension. Remember that we can *filter* (only keep certain rooms) by adding an `if` clause:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8103d9e", + "metadata": {}, + "outputs": [], + "source": [ + "{name: len(room['people']) for name, room in house.items() if len(room['people']) > 0}" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Maze comprehension solution" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/038SolutionComprehension.ipynb.py b/ch01python/038SolutionComprehension.ipynb.py new file mode 100644 index 000000000..0f8edafb5 --- /dev/null +++ b/ch01python/038SolutionComprehension.ipynb.py @@ -0,0 +1,65 @@ +# --- +# jupyter: +# jekyll: +# display_name: Maze comprehension solution +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ### Solution + +# %% [markdown] +# With this maze structure: + +# %% +house = { + 'living' : { + 'exits': { + 'north' : 'kitchen', + 'outside' : 'garden', + 'upstairs' : 'bedroom' + }, + 'people' : ['Graham'], + 'capacity' : 2 + }, + 'kitchen' : { + 'exits': { + 'south' : 'living' + }, + 'people' : [], + 'capacity' : 1 + }, + 'garden' : { + 'exits': { + 'inside' : 'living' + }, + 'people' : ['David'], + 'capacity' : 3 + }, + 'bedroom' : { + 'exits': { + 'downstairs' : 'living', + 'jump' : 'garden' + }, + 'people' : [], + 'capacity' : 1 + } +} + +# %% [markdown] +# We can get a simpler dictionary with just capacities like this: + +# %% +{name: room['capacity'] for name, room in house.items()} + +# %% [markdown] +# To get the current number of occupants, we can use a similar dictionary comprehension. Remember that we can *filter* (only keep certain rooms) by adding an `if` clause: + +# %% +{name: len(room['people']) for name, room in house.items() if len(room['people']) > 0} diff --git a/ch01python/04functions.html b/ch01python/04functions.html new file mode 100644 index 000000000..34a640d29 --- /dev/null +++ b/ch01python/04functions.html @@ -0,0 +1,1042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Defining functions + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Functions

+
+
+
+
+
+
+

Definition

+
+
+
+
+
+
+

We use def to define a function, and return to pass back a value:

+
+
+
+
+
+
In [1]:
+
+
+
def double(x):
+    return x * 2
+
+print(double(5), double([5]), double('five'))
+
+
+
+
+
+
+
+
+
+
10 [5, 5] fivefive
+
+
+
+
+
+
+
+
+
+

Default Parameters

+
+
+
+
+
+
+

We can specify default values for parameters:

+
+
+
+
+
+
In [2]:
+
+
+
def jeeves(name = "Sir"):
+    return f"Very good, {name}"
+
+
+
+
+
+
+
+
In [3]:
+
+
+
jeeves()
+
+
+
+
+
+
+
+
Out[3]:
+
+
'Very good, Sir'
+
+
+
+
+
+
+
+
In [4]:
+
+
+
jeeves('John')
+
+
+
+
+
+
+
+
Out[4]:
+
+
'Very good, John'
+
+
+
+
+
+
+
+
+

If you have some parameters with defaults, and some without, those with defaults must go later.

+
+
+
+
+
+
+

If you have multiple default arguments, you can specify neither, one or both:

+
+
+
+
+
+
In [5]:
+
+
+
def jeeves(greeting="Very good", name="Sir"):
+    return f"{greeting}, {name}"
+
+
+
+
+
+
+
+
In [6]:
+
+
+
jeeves()
+
+
+
+
+
+
+
+
Out[6]:
+
+
'Very good, Sir'
+
+
+
+
+
+
+
+
In [7]:
+
+
+
jeeves("Hello")
+
+
+
+
+
+
+
+
Out[7]:
+
+
'Hello, Sir'
+
+
+
+
+
+
+
+
In [8]:
+
+
+
jeeves(name = "John")
+
+
+
+
+
+
+
+
Out[8]:
+
+
'Very good, John'
+
+
+
+
+
+
+
+
In [9]:
+
+
+
jeeves(greeting="Suits you")
+
+
+
+
+
+
+
+
Out[9]:
+
+
'Suits you, Sir'
+
+
+
+
+
+
+
+
In [10]:
+
+
+
jeeves("Hello", "Producer")
+
+
+
+
+
+
+
+
Out[10]:
+
+
'Hello, Producer'
+
+
+
+
+
+
+
+
+

Side effects

+
+
+
+
+
+
+

Functions can do things to change their mutable arguments, +so return is optional.

+

This is pretty awful style, in general, functions should normally be side-effect free.

+

Here is a contrived example of a function that makes plausible use of a side-effect

+
+
+
+
+
+
In [11]:
+
+
+
def double_inplace(vec):
+    vec[:] = [element * 2 for element in vec]
+
+z = list(range(4))
+double_inplace(z)
+print(z)
+
+
+
+
+
+
+
+
+
+
[0, 2, 4, 6]
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+letters[:] = []
+
+
+
+
+
+
+
+
+

In this example, we're using [:] to access into the same list, and write it's data.

+
vec = [element*2 for element in vec]
+

would just move a local label, not change the input.

+
+
+
+
+
+
+

But I'd usually just write this as a function which returned the output:

+
+
+
+
+
+
In [13]:
+
+
+
def double(vec):
+    return [element * 2 for element in vec]
+
+
+
+
+
+
+
+
+

Let's remind ourselves of the behaviour for modifying lists in-place using [:] with a simple array:

+
+
+
+
+
+
In [14]:
+
+
+
x = 5
+x = 7
+x = ['a', 'b', 'c']
+y = x
+
+
+
+
+
+
+
+
In [15]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[15]:
+
+
['a', 'b', 'c']
+
+
+
+
+
+
+
+
In [16]:
+
+
+
x[:] = ["Hooray!", "Yippee"]
+
+
+
+
+
+
+
+
In [17]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[17]:
+
+
['Hooray!', 'Yippee']
+
+
+
+
+
+
+
+
+

Early Return

+
+
+
+
+
+
+

Return without arguments can be used to exit early from a function

+
+
+
+
+
+
+

Here's a slightly more plausibly useful function-with-side-effects to extend a list with a specified padding datum.

+
+
+
+
+
+
In [18]:
+
+
+
def extend(to, vec, pad):
+    if len(vec) >= to:
+        return # Exit early, list is already long enough.
+    
+    vec[:] = vec + [pad] * (to - len(vec))
+
+
+
+
+
+
+
+
In [19]:
+
+
+
x = list(range(3))
+extend(6, x, 'a')
+print(x)
+
+
+
+
+
+
+
+
+
+
[0, 1, 2, 'a', 'a', 'a']
+
+
+
+
+
+
+
+
+
In [20]:
+
+
+
z = range(9)
+extend(6, z, 'a')
+print(z)
+
+
+
+
+
+
+
+
+
+
range(0, 9)
+
+
+
+
+
+
+
+
+
+

Unpacking arguments

+
+
+
+
+
+
+

If a vector is supplied to a function with a '*', its elements +are used to fill each of a function's arguments.

+
+
+
+
+
+
In [21]:
+
+
+
def arrow(before, after):
+    return f"{before} -> {after}"
+
+arrow(1, 3)
+
+
+
+
+
+
+
+
Out[21]:
+
+
'1 -> 3'
+
+
+
+
+
+
+
+
In [22]:
+
+
+
x = [1, -1]
+arrow(*x)
+
+
+
+
+
+
+
+
Out[22]:
+
+
'1 -> -1'
+
+
+
+
+
+
+
+
+

This can be quite powerful:

+
+
+
+
+
+
In [23]:
+
+
+
charges = {"neutron": 0, "proton": 1, "electron": -1}
+for particle in charges.items():
+    print(arrow(*particle))
+
+
+
+
+
+
+
+
+
+
neutron -> 0
+proton -> 1
+electron -> -1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Sequence Arguments

+
+
+
+
+
+
+

Similiarly, if a * is used in the definition of a function, multiple +arguments are absorbed into a list inside the function:

+
+
+
+
+
+
In [24]:
+
+
+
def doubler(*sequence):
+    return [x * 2 for x in sequence]
+
+
+
+
+
+
+
+
In [25]:
+
+
+
doubler(1, 2, 3)
+
+
+
+
+
+
+
+
Out[25]:
+
+
[2, 4, 6]
+
+
+
+
+
+
+
+
In [26]:
+
+
+
doubler(5, 2, "Wow!")
+
+
+
+
+
+
+
+
Out[26]:
+
+
[10, 4, 'Wow!Wow!']
+
+
+
+
+
+
+
+
+

Keyword Arguments

+
+
+
+
+
+
+

If two asterisks are used, named arguments are supplied inside the function as a dictionary:

+
+
+
+
+
+
In [27]:
+
+
+
def arrowify(**args):
+    for key, value in args.items():
+        print(f"{key} -> {value}")
+
+arrowify(neutron="n", proton="p", electron="e")
+
+
+
+
+
+
+
+
+
+
neutron -> n
+proton -> p
+electron -> e
+
+
+
+
+
+
+
+
+
+

These different approaches can be mixed:

+
+
+
+
+
+
In [28]:
+
+
+
def somefunc(a, b, *args, **kwargs):
+    print("A:", a)
+    print("B:", b)
+    print("args:", args)
+    print("keyword args", kwargs)
+
+
+
+
+
+
+
+
In [29]:
+
+
+
somefunc(1, 2, 3, 4, 5, fish="Haddock")
+
+
+
+
+
+
+
+
+
+
A: 1
+B: 2
+args: (3, 4, 5)
+keyword args {'fish': 'Haddock'}
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/04functions.ipynb b/ch01python/04functions.ipynb new file mode 100644 index 000000000..46fc413f1 --- /dev/null +++ b/ch01python/04functions.ipynb @@ -0,0 +1,561 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8200f015", + "metadata": {}, + "source": [ + "## Functions " + ] + }, + { + "cell_type": "markdown", + "id": "d486de36", + "metadata": {}, + "source": [ + "### Definition" + ] + }, + { + "cell_type": "markdown", + "id": "8b3343ca", + "metadata": {}, + "source": [ + "\n", + "We use `def` to define a function, and `return` to pass back a value:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8932f433", + "metadata": {}, + "outputs": [], + "source": [ + "def double(x):\n", + " return x * 2\n", + "\n", + "print(double(5), double([5]), double('five'))" + ] + }, + { + "cell_type": "markdown", + "id": "a5a873f9", + "metadata": {}, + "source": [ + "### Default Parameters" + ] + }, + { + "cell_type": "markdown", + "id": "01769774", + "metadata": {}, + "source": [ + "We can specify default values for parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebe7bae0", + "metadata": {}, + "outputs": [], + "source": [ + "def jeeves(name = \"Sir\"):\n", + " return f\"Very good, {name}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff6a9e98", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47bd92cd", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves('John')" + ] + }, + { + "cell_type": "markdown", + "id": "bbd4e17a", + "metadata": {}, + "source": [ + "If you have some parameters with defaults, and some without, those with defaults **must** go later." + ] + }, + { + "cell_type": "markdown", + "id": "817174bc", + "metadata": {}, + "source": [ + "If you have multiple default arguments, you can specify neither, one or both:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a998ae8b", + "metadata": {}, + "outputs": [], + "source": [ + "def jeeves(greeting=\"Very good\", name=\"Sir\"):\n", + " return f\"{greeting}, {name}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ede9394", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "060a32fc", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves(\"Hello\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adc48ab6", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves(name = \"John\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70ed38d8", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves(greeting=\"Suits you\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4eaa1a1e", + "metadata": {}, + "outputs": [], + "source": [ + "jeeves(\"Hello\", \"Producer\")" + ] + }, + { + "cell_type": "markdown", + "id": "571951c5", + "metadata": {}, + "source": [ + "### Side effects" + ] + }, + { + "cell_type": "markdown", + "id": "5076de72", + "metadata": {}, + "source": [ + "Functions can do things to change their **mutable** arguments,\n", + "so `return` is optional.\n", + "\n", + "This is pretty awful style, in general, functions should normally be side-effect free.\n", + "\n", + "Here is a contrived example of a function that makes plausible use of a side-effect" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e1ff433", + "metadata": {}, + "outputs": [], + "source": [ + "def double_inplace(vec):\n", + " vec[:] = [element * 2 for element in vec]\n", + "\n", + "z = list(range(4))\n", + "double_inplace(z)\n", + "print(z)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b778788f", + "metadata": {}, + "outputs": [], + "source": [ + "letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']\n", + "letters[:] = []" + ] + }, + { + "cell_type": "markdown", + "id": "4e135f76", + "metadata": {}, + "source": [ + "In this example, we're using `[:]` to access into the same list, and write it's data.\n", + "\n", + " vec = [element*2 for element in vec]\n", + "\n", + "would just move a local label, not change the input." + ] + }, + { + "cell_type": "markdown", + "id": "5b87bc14", + "metadata": {}, + "source": [ + "But I'd usually just write this as a function which **returned** the output:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2dd5ce5", + "metadata": {}, + "outputs": [], + "source": [ + "def double(vec):\n", + " return [element * 2 for element in vec]" + ] + }, + { + "cell_type": "markdown", + "id": "56bc943f", + "metadata": {}, + "source": [ + "Let's remind ourselves of the behaviour for modifying lists in-place using `[:]` with a simple array:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dd2816d", + "metadata": {}, + "outputs": [], + "source": [ + "x = 5\n", + "x = 7\n", + "x = ['a', 'b', 'c']\n", + "y = x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3efda9a", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db6bcc34", + "metadata": {}, + "outputs": [], + "source": [ + "x[:] = [\"Hooray!\", \"Yippee\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c34741f", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "markdown", + "id": "4e58323c", + "metadata": {}, + "source": [ + "### Early Return" + ] + }, + { + "cell_type": "markdown", + "id": "b7381520", + "metadata": {}, + "source": [ + "\n", + "Return without arguments can be used to exit early from a function\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "e277eebe", + "metadata": {}, + "source": [ + "Here's a slightly more plausibly useful function-with-side-effects to extend a list with a specified padding datum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd7c9c3b", + "metadata": {}, + "outputs": [], + "source": [ + "def extend(to, vec, pad):\n", + " if len(vec) >= to:\n", + " return # Exit early, list is already long enough.\n", + " \n", + " vec[:] = vec + [pad] * (to - len(vec))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f2a7fae", + "metadata": {}, + "outputs": [], + "source": [ + "x = list(range(3))\n", + "extend(6, x, 'a')\n", + "print(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3abd9bb", + "metadata": {}, + "outputs": [], + "source": [ + "z = range(9)\n", + "extend(6, z, 'a')\n", + "print(z)" + ] + }, + { + "cell_type": "markdown", + "id": "6dcd14ef", + "metadata": {}, + "source": [ + "### Unpacking arguments" + ] + }, + { + "cell_type": "markdown", + "id": "af8da5bc", + "metadata": {}, + "source": [ + "\n", + "If a vector is supplied to a function with a '*', its elements\n", + "are used to fill each of a function's arguments. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2286d2f2", + "metadata": {}, + "outputs": [], + "source": [ + "def arrow(before, after):\n", + " return f\"{before} -> {after}\"\n", + "\n", + "arrow(1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b09aa845", + "metadata": {}, + "outputs": [], + "source": [ + "x = [1, -1]\n", + "arrow(*x)" + ] + }, + { + "cell_type": "markdown", + "id": "d264a754", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "This can be quite powerful:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4cd72a6", + "metadata": {}, + "outputs": [], + "source": [ + "charges = {\"neutron\": 0, \"proton\": 1, \"electron\": -1}\n", + "for particle in charges.items():\n", + " print(arrow(*particle))" + ] + }, + { + "cell_type": "markdown", + "id": "1ea731cc", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2b7c46ab", + "metadata": {}, + "source": [ + "### Sequence Arguments" + ] + }, + { + "cell_type": "markdown", + "id": "7bad66fb", + "metadata": {}, + "source": [ + "Similiarly, if a `*` is used in the **definition** of a function, multiple\n", + "arguments are absorbed into a list **inside** the function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1968138", + "metadata": {}, + "outputs": [], + "source": [ + "def doubler(*sequence):\n", + " return [x * 2 for x in sequence]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98d01a4c", + "metadata": {}, + "outputs": [], + "source": [ + "doubler(1, 2, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be910563", + "metadata": {}, + "outputs": [], + "source": [ + "doubler(5, 2, \"Wow!\")" + ] + }, + { + "cell_type": "markdown", + "id": "c9fc9f6f", + "metadata": {}, + "source": [ + "### Keyword Arguments" + ] + }, + { + "cell_type": "markdown", + "id": "ab45c965", + "metadata": {}, + "source": [ + "If two asterisks are used, named arguments are supplied inside the function as a dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e731086", + "metadata": {}, + "outputs": [], + "source": [ + "def arrowify(**args):\n", + " for key, value in args.items():\n", + " print(f\"{key} -> {value}\")\n", + "\n", + "arrowify(neutron=\"n\", proton=\"p\", electron=\"e\")" + ] + }, + { + "cell_type": "markdown", + "id": "7336e886", + "metadata": {}, + "source": [ + "These different approaches can be mixed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81d85fb2", + "metadata": {}, + "outputs": [], + "source": [ + "def somefunc(a, b, *args, **kwargs):\n", + " print(\"A:\", a)\n", + " print(\"B:\", b)\n", + " print(\"args:\", args)\n", + " print(\"keyword args\", kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fede987", + "metadata": {}, + "outputs": [], + "source": [ + "somefunc(1, 2, 3, 4, 5, fish=\"Haddock\")" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Defining functions" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/04functions.ipynb.py b/ch01python/04functions.ipynb.py new file mode 100644 index 000000000..a1659feaa --- /dev/null +++ b/ch01python/04functions.ipynb.py @@ -0,0 +1,255 @@ +# --- +# jupyter: +# jekyll: +# display_name: Defining functions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Functions + +# %% [markdown] +# ### Definition + +# %% [markdown] +# +# We use `def` to define a function, and `return` to pass back a value: +# +# +# + +# %% +def double(x): + return x * 2 + +print(double(5), double([5]), double('five')) + + +# %% [markdown] +# ### Default Parameters + +# %% [markdown] +# We can specify default values for parameters: + +# %% +def jeeves(name = "Sir"): + return f"Very good, {name}" + + +# %% +jeeves() + +# %% +jeeves('John') + + +# %% [markdown] +# If you have some parameters with defaults, and some without, those with defaults **must** go later. + +# %% [markdown] +# If you have multiple default arguments, you can specify neither, one or both: + +# %% +def jeeves(greeting="Very good", name="Sir"): + return f"{greeting}, {name}" + + +# %% +jeeves() + +# %% +jeeves("Hello") + +# %% +jeeves(name = "John") + +# %% +jeeves(greeting="Suits you") + +# %% +jeeves("Hello", "Producer") + + +# %% [markdown] +# ### Side effects + +# %% [markdown] +# Functions can do things to change their **mutable** arguments, +# so `return` is optional. +# +# This is pretty awful style, in general, functions should normally be side-effect free. +# +# Here is a contrived example of a function that makes plausible use of a side-effect + +# %% +def double_inplace(vec): + vec[:] = [element * 2 for element in vec] + +z = list(range(4)) +double_inplace(z) +print(z) + +# %% +letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] +letters[:] = [] + + +# %% [markdown] +# In this example, we're using `[:]` to access into the same list, and write it's data. +# +# vec = [element*2 for element in vec] +# +# would just move a local label, not change the input. + +# %% [markdown] +# But I'd usually just write this as a function which **returned** the output: + +# %% +def double(vec): + return [element * 2 for element in vec] + + +# %% [markdown] +# Let's remind ourselves of the behaviour for modifying lists in-place using `[:]` with a simple array: + +# %% +x = 5 +x = 7 +x = ['a', 'b', 'c'] +y = x + +# %% +x + +# %% +x[:] = ["Hooray!", "Yippee"] + +# %% +y + + +# %% [markdown] +# ### Early Return + +# %% [markdown] +# +# Return without arguments can be used to exit early from a function +# +# +# + +# %% [markdown] +# Here's a slightly more plausibly useful function-with-side-effects to extend a list with a specified padding datum. + +# %% +def extend(to, vec, pad): + if len(vec) >= to: + return # Exit early, list is already long enough. + + vec[:] = vec + [pad] * (to - len(vec)) + + +# %% +x = list(range(3)) +extend(6, x, 'a') +print(x) + +# %% +z = range(9) +extend(6, z, 'a') +print(z) + + +# %% [markdown] +# ### Unpacking arguments + +# %% [markdown] +# +# If a vector is supplied to a function with a '*', its elements +# are used to fill each of a function's arguments. +# +# +# + +# %% +def arrow(before, after): + return f"{before} -> {after}" + +arrow(1, 3) + +# %% +x = [1, -1] +arrow(*x) + +# %% [markdown] +# +# +# +# This can be quite powerful: +# +# +# + +# %% +charges = {"neutron": 0, "proton": 1, "electron": -1} +for particle in charges.items(): + print(arrow(*particle)) + + +# %% [markdown] +# +# +# + +# %% [markdown] +# ### Sequence Arguments + +# %% [markdown] +# Similiarly, if a `*` is used in the **definition** of a function, multiple +# arguments are absorbed into a list **inside** the function: + +# %% +def doubler(*sequence): + return [x * 2 for x in sequence] + + +# %% +doubler(1, 2, 3) + +# %% +doubler(5, 2, "Wow!") + + +# %% [markdown] +# ### Keyword Arguments + +# %% [markdown] +# If two asterisks are used, named arguments are supplied inside the function as a dictionary: + +# %% +def arrowify(**args): + for key, value in args.items(): + print(f"{key} -> {value}") + +arrowify(neutron="n", proton="p", electron="e") + + +# %% [markdown] +# These different approaches can be mixed: + +# %% +def somefunc(a, b, *args, **kwargs): + print("A:", a) + print("B:", b) + print("args:", args) + print("keyword args", kwargs) + + +# %% +somefunc(1, 2, 3, 4, 5, fish="Haddock") diff --git a/ch01python/050import.html b/ch01python/050import.html new file mode 100644 index 000000000..356490e86 --- /dev/null +++ b/ch01python/050import.html @@ -0,0 +1,746 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Modules + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Using Libraries

+
+
+
+
+
+
+

Import

+
+
+
+
+
+
+

To use a function or type from a python library, rather than a built-in function or type, we have to import the library.

+
+
+
+
+
+
In [1]:
+
+
+
math.sin(1.6)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+NameError                                 Traceback (most recent call last)
+Cell In[1], line 1
+----> 1 math.sin(1.6)
+
+NameError: name 'math' is not defined
+
+
+
+
+
+
+
+
In [2]:
+
+
+
import math
+
+
+
+
+
+
+
+
In [3]:
+
+
+
math.sin(1.6)
+
+
+
+
+
+
+
+
Out[3]:
+
+
0.9995736030415051
+
+
+
+
+
+
+
+
+

We call these libraries modules:

+
+
+
+
+
+
In [4]:
+
+
+
type(math)
+
+
+
+
+
+
+
+
Out[4]:
+
+
module
+
+
+
+
+
+
+
+
+

The tools supplied by a module are attributes of the module, and as such, are accessed with a dot.

+
+
+
+
+
+
In [5]:
+
+
+
dir(math)
+
+
+
+
+
+
+
+
Out[5]:
+
+
['__doc__',
+ '__file__',
+ '__loader__',
+ '__name__',
+ '__package__',
+ '__spec__',
+ 'acos',
+ 'acosh',
+ 'asin',
+ 'asinh',
+ 'atan',
+ 'atan2',
+ 'atanh',
+ 'ceil',
+ 'comb',
+ 'copysign',
+ 'cos',
+ 'cosh',
+ 'degrees',
+ 'dist',
+ 'e',
+ 'erf',
+ 'erfc',
+ 'exp',
+ 'expm1',
+ 'fabs',
+ 'factorial',
+ 'floor',
+ 'fmod',
+ 'frexp',
+ 'fsum',
+ 'gamma',
+ 'gcd',
+ 'hypot',
+ 'inf',
+ 'isclose',
+ 'isfinite',
+ 'isinf',
+ 'isnan',
+ 'isqrt',
+ 'ldexp',
+ 'lgamma',
+ 'log',
+ 'log10',
+ 'log1p',
+ 'log2',
+ 'modf',
+ 'nan',
+ 'perm',
+ 'pi',
+ 'pow',
+ 'prod',
+ 'radians',
+ 'remainder',
+ 'sin',
+ 'sinh',
+ 'sqrt',
+ 'tan',
+ 'tanh',
+ 'tau',
+ 'trunc']
+
+
+
+
+
+
+
+
+

They include properties as well as functions:

+
+
+
+
+
+
In [6]:
+
+
+
math.pi
+
+
+
+
+
+
+
+
Out[6]:
+
+
3.141592653589793
+
+
+
+
+
+
+
+
+

You can always find out where on your storage medium a library has been imported from:

+
+
+
+
+
+
In [7]:
+
+
+
print(math.__file__[0:50])
+print(math.__file__[50:])
+
+
+
+
+
+
+
+
+
+
/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3
+.8/lib-dynload/math.cpython-38-x86_64-linux-gnu.so
+
+
+
+
+
+
+
+
+
+

Note that import does not install libraries. It just makes them available to your current notebook session, assuming they are already installed. Installing libraries is harder, and we'll cover it later. +So what libraries are available? Until you install more, you might have just the modules that come with Python, the standard library.

+
+
+
+
+
+
+

Supplementary Materials: Review the list of standard library modules.

+
+
+
+
+
+
+

If you installed via Anaconda, then you also have access to a bunch of modules that are commonly used in research.

+

Supplementary Materials: Review the list of modules that are packaged with Anaconda by default on different architectures (modules installed by default are shown with ticks).

+

We'll see later how to add more libraries to our setup.

+
+
+
+
+
+
+

Why bother?

+
+
+
+
+
+
+

Why bother with modules? Why not just have everything available all the time?

+

The answer is that there are only so many names available! Without a module system, every time I made a variable whose name matched a function in a library, I'd lose access to it. In the olden days, people ended up having to make really long variable names, thinking their names would be unique, and they still ended up with "name clashes". The module mechanism avoids this.

+
+
+
+
+
+
+

Importing from modules

+
+
+
+
+
+
+

Still, it can be annoying to have to write math.sin(math.pi) instead of sin(pi). +Things can be imported from modules to become part of the current module:

+
+
+
+
+
+
In [8]:
+
+
+
import math
+math.sin(math.pi)
+
+
+
+
+
+
+
+
Out[8]:
+
+
1.2246467991473532e-16
+
+
+
+
+
+
+
+
In [9]:
+
+
+
from math import sin
+sin(math.pi)
+
+
+
+
+
+
+
+
Out[9]:
+
+
1.2246467991473532e-16
+
+
+
+
+
+
+
+
+

Importing one-by-one like this is a nice compromise between typing and risk of name clashes.

+
+
+
+
+
+
+

It is possible to import everything from a module, but you risk name clashes.

+
+
+
+
+
+
In [10]:
+
+
+
from math import *
+sin(pi)
+
+
+
+
+
+
+
+
Out[10]:
+
+
1.2246467991473532e-16
+
+
+
+
+
+
+
+
+

Import and rename

+
+
+
+
+
+
+

You can rename things as you import them to avoid clashes or for typing convenience

+
+
+
+
+
+
In [11]:
+
+
+
import math as m
+m.cos(0)
+
+
+
+
+
+
+
+
Out[11]:
+
+
1.0
+
+
+
+
+
+
+
+
In [12]:
+
+
+
pi = 3
+from math import pi as realpi
+print(sin(pi), sin(realpi))
+
+
+
+
+
+
+
+
+
+
0.1411200080598672 1.2246467991473532e-16
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/050import.ipynb b/ch01python/050import.ipynb new file mode 100644 index 000000000..e76890f61 --- /dev/null +++ b/ch01python/050import.ipynb @@ -0,0 +1,294 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1fcfb1c1", + "metadata": {}, + "source": [ + "## Using Libraries" + ] + }, + { + "cell_type": "markdown", + "id": "5d0f73d7", + "metadata": {}, + "source": [ + "### Import" + ] + }, + { + "cell_type": "markdown", + "id": "8fc7e752", + "metadata": {}, + "source": [ + "To use a function or type from a python library, rather than a **built-in** function or type, we have to import the library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e998b7be", + "metadata": {}, + "outputs": [], + "source": [ + "math.sin(1.6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45b15c90", + "metadata": {}, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba2760dc", + "metadata": {}, + "outputs": [], + "source": [ + "math.sin(1.6)" + ] + }, + { + "cell_type": "markdown", + "id": "566be681", + "metadata": {}, + "source": [ + "We call these libraries **modules**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a801379", + "metadata": {}, + "outputs": [], + "source": [ + "type(math)" + ] + }, + { + "cell_type": "markdown", + "id": "73fbffb8", + "metadata": {}, + "source": [ + "The tools supplied by a module are *attributes* of the module, and as such, are accessed with a dot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e875a89", + "metadata": {}, + "outputs": [], + "source": [ + "dir(math)" + ] + }, + { + "cell_type": "markdown", + "id": "12ebcb55", + "metadata": {}, + "source": [ + "They include properties as well as functions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7fddb97", + "metadata": {}, + "outputs": [], + "source": [ + "math.pi" + ] + }, + { + "cell_type": "markdown", + "id": "c9926bcb", + "metadata": {}, + "source": [ + "You can always find out where on your storage medium a library has been imported from:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efdb5dbc", + "metadata": {}, + "outputs": [], + "source": [ + "print(math.__file__[0:50])\n", + "print(math.__file__[50:])" + ] + }, + { + "cell_type": "markdown", + "id": "a232d324", + "metadata": {}, + "source": [ + "Note that `import` does *not* install libraries. It just makes them available to your current notebook session, assuming they are already installed. Installing libraries is harder, and we'll cover it later.\n", + "So what libraries are available? Until you install more, you might have just the modules that come with Python, the *standard library*." + ] + }, + { + "cell_type": "markdown", + "id": "99cc4699", + "metadata": {}, + "source": [ + "**Supplementary Materials**: Review the [list of standard library modules](https://docs.python.org/library/)." + ] + }, + { + "cell_type": "markdown", + "id": "20733449", + "metadata": {}, + "source": [ + "If you installed via Anaconda, then you also have access to a bunch of modules that are commonly used in research.\n", + "\n", + "**Supplementary Materials**: Review the [list of modules that are packaged with Anaconda by default on different architectures](https://docs.anaconda.com/anaconda/packages/pkg-docs/) (modules installed by default are shown with ticks).\n", + "\n", + "We'll see later how to add more libraries to our setup." + ] + }, + { + "cell_type": "markdown", + "id": "40a7ca5b", + "metadata": {}, + "source": [ + "### Why bother?" + ] + }, + { + "cell_type": "markdown", + "id": "011d0a6b", + "metadata": {}, + "source": [ + "Why bother with modules? Why not just have everything available all the time?\n", + "\n", + "The answer is that there are only so many names available! Without a module system, every time I made a variable whose name matched a function in a library, I'd lose access to it. In the olden days, people ended up having to make really long variable names, thinking their names would be unique, and they still ended up with \"name clashes\". The module mechanism avoids this." + ] + }, + { + "cell_type": "markdown", + "id": "243c2c8b", + "metadata": {}, + "source": [ + "### Importing from modules" + ] + }, + { + "cell_type": "markdown", + "id": "9a35bdb4", + "metadata": {}, + "source": [ + "Still, it can be annoying to have to write `math.sin(math.pi)` instead of `sin(pi)`.\n", + "Things can be imported *from* modules to become part of the current module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45277f88", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "math.sin(math.pi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1aff9fcc", + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin\n", + "sin(math.pi)" + ] + }, + { + "cell_type": "markdown", + "id": "4c65cdb1", + "metadata": {}, + "source": [ + "Importing one-by-one like this is a nice compromise between typing and risk of name clashes." + ] + }, + { + "cell_type": "markdown", + "id": "252cc2f8", + "metadata": {}, + "source": [ + "It *is* possible to import **everything** from a module, but you risk name clashes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c00c541b", + "metadata": {}, + "outputs": [], + "source": [ + "from math import *\n", + "sin(pi)" + ] + }, + { + "cell_type": "markdown", + "id": "7ee72aec", + "metadata": {}, + "source": [ + "###  Import and rename" + ] + }, + { + "cell_type": "markdown", + "id": "f67693ac", + "metadata": {}, + "source": [ + "You can rename things as you import them to avoid clashes or for typing convenience" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8710174b", + "metadata": {}, + "outputs": [], + "source": [ + "import math as m\n", + "m.cos(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "684773dd", + "metadata": {}, + "outputs": [], + "source": [ + "pi = 3\n", + "from math import pi as realpi\n", + "print(sin(pi), sin(realpi))" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Modules" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/050import.ipynb.py b/ch01python/050import.ipynb.py new file mode 100644 index 000000000..0196103fc --- /dev/null +++ b/ch01python/050import.ipynb.py @@ -0,0 +1,117 @@ +# --- +# jupyter: +# jekyll: +# display_name: Modules +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Using Libraries + +# %% [markdown] +# ### Import + +# %% [markdown] +# To use a function or type from a python library, rather than a **built-in** function or type, we have to import the library. + +# %% +math.sin(1.6) + +# %% +import math + +# %% +math.sin(1.6) + +# %% [markdown] +# We call these libraries **modules**: + +# %% +type(math) + +# %% [markdown] +# The tools supplied by a module are *attributes* of the module, and as such, are accessed with a dot. + +# %% +dir(math) + +# %% [markdown] +# They include properties as well as functions: + +# %% +math.pi + +# %% [markdown] +# You can always find out where on your storage medium a library has been imported from: + +# %% +print(math.__file__[0:50]) +print(math.__file__[50:]) + +# %% [markdown] +# Note that `import` does *not* install libraries. It just makes them available to your current notebook session, assuming they are already installed. Installing libraries is harder, and we'll cover it later. +# So what libraries are available? Until you install more, you might have just the modules that come with Python, the *standard library*. + +# %% [markdown] +# **Supplementary Materials**: Review the [list of standard library modules](https://docs.python.org/library/). + +# %% [markdown] +# If you installed via Anaconda, then you also have access to a bunch of modules that are commonly used in research. +# +# **Supplementary Materials**: Review the [list of modules that are packaged with Anaconda by default on different architectures](https://docs.anaconda.com/anaconda/packages/pkg-docs/) (modules installed by default are shown with ticks). +# +# We'll see later how to add more libraries to our setup. + +# %% [markdown] +# ### Why bother? + +# %% [markdown] +# Why bother with modules? Why not just have everything available all the time? +# +# The answer is that there are only so many names available! Without a module system, every time I made a variable whose name matched a function in a library, I'd lose access to it. In the olden days, people ended up having to make really long variable names, thinking their names would be unique, and they still ended up with "name clashes". The module mechanism avoids this. + +# %% [markdown] +# ### Importing from modules + +# %% [markdown] +# Still, it can be annoying to have to write `math.sin(math.pi)` instead of `sin(pi)`. +# Things can be imported *from* modules to become part of the current module: + +# %% +import math +math.sin(math.pi) + +# %% +from math import sin +sin(math.pi) + +# %% [markdown] +# Importing one-by-one like this is a nice compromise between typing and risk of name clashes. + +# %% [markdown] +# It *is* possible to import **everything** from a module, but you risk name clashes. + +# %% +from math import * +sin(pi) + +# %% [markdown] +# ###  Import and rename + +# %% [markdown] +# You can rename things as you import them to avoid clashes or for typing convenience + +# %% +import math as m +m.cos(0) + +# %% +pi = 3 +from math import pi as realpi +print(sin(pi), sin(realpi)) diff --git a/ch01python/101Classes.html b/ch01python/101Classes.html new file mode 100644 index 000000000..eb1487c43 --- /dev/null +++ b/ch01python/101Classes.html @@ -0,0 +1,1302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Classes + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Defining your own classes

+
+
+
+
+
+
+

User Defined Types

+
+
+
+
+
+
+

A class is a user-programmed Python type (since Python 2.2!)

+
+
+
+
+
+
+

It can be defined like:

+
+
+
+
+
+
In [1]:
+
+
+
class Room(object):
+    pass
+
+
+
+
+
+
+
+
+

Or:

+
+
+
+
+
+
In [2]:
+
+
+
class Room():
+    pass
+
+
+
+
+
+
+
+
+

Or:

+
+
+
+
+
+
In [3]:
+
+
+
class Room:
+    pass
+
+
+
+
+
+
+
+
+

What's the difference? Before Python 2.2 a class was distinct from all other Python types, which caused some odd behaviour. To fix this, classes were redefined as user programmed types by extending object, e.g., class room(object).

+

So most Python 2 code will use this syntax as very few people want to use old style python classes. Python 3 has formalised this by removing old-style classes, so they can be defined without extending object, or indeed without braces.

+
+
+
+
+
+
+

Just as with other python types, you use the name of the type as a function to make a variable of that type:

+
+
+
+
+
+
In [4]:
+
+
+
zero = int()
+type(zero)
+
+
+
+
+
+
+
+
Out[4]:
+
+
int
+
+
+
+
+
+
+
+
In [5]:
+
+
+
myroom = Room()
+type(myroom)
+
+
+
+
+
+
+
+
Out[5]:
+
+
__main__.Room
+
+
+
+
+
+
+
+
+

In the jargon, we say that an object is an instance of a particular class.

+

__main__ is the name of the scope in which top-level code executes, where we've defined the class Room.

+
+
+
+
+
+
+

Once we have an object with a type of our own devising, we can add properties at will:

+
+
+
+
+
+
In [6]:
+
+
+
myroom.name = "Living"
+
+
+
+
+
+
+
+
In [7]:
+
+
+
myroom.name
+
+
+
+
+
+
+
+
Out[7]:
+
+
'Living'
+
+
+
+
+
+
+
+
+

The most common use of a class is to allow us to group data into an object in a way that is +easier to read and understand than organising data into lists and dictionaries.

+
+
+
+
+
+
In [8]:
+
+
+
myroom.capacity = 3
+myroom.occupants = ["Graham", "Eric"]
+
+
+
+
+
+
+
+
+

Methods

+
+
+
+
+
+
+

So far, our class doesn't do much!

+
+
+
+
+
+
+

We define functions inside the definition of a class, in order to give them capabilities, just like the methods on built-in +types.

+
+
+
+
+
+
In [9]:
+
+
+
class Room:
+    def overfull(self):
+        return len(self.occupants) > self.capacity
+
+
+
+
+
+
+
+
In [10]:
+
+
+
myroom = Room()
+myroom.capacity = 3
+myroom.occupants = ["Graham", "Eric"]
+
+
+
+
+
+
+
+
In [11]:
+
+
+
myroom.overfull()
+
+
+
+
+
+
+
+
Out[11]:
+
+
False
+
+
+
+
+
+
+
+
In [12]:
+
+
+
myroom.occupants.append(['TerryG'])
+
+
+
+
+
+
+
+
In [13]:
+
+
+
myroom.occupants.append(['John'])
+
+
+
+
+
+
+
+
In [14]:
+
+
+
myroom.overfull()
+
+
+
+
+
+
+
+
Out[14]:
+
+
True
+
+
+
+
+
+
+
+
+

When we write methods, we always write the first function argument as self, to refer to the object instance itself, +the argument that goes "before the dot".

+
+
+
+
+
+
+

This is just a convention for this variable name, not a keyword. You could call it something else if you wanted.

+
+
+
+
+
+
+

Constructors

+
+
+
+
+
+
+

Normally, though, we don't want to add data to the class attributes on the fly like that. +Instead, we define a constructor that converts input data into an object.

+
+
+
+
+
+
In [15]:
+
+
+
class Room:
+    def __init__(self, name, exits, capacity, occupants=[]):
+        self.name = name
+        self.occupants = occupants  # Note the default argument, occupants start empty
+        self.exits = exits
+        self.capacity = capacity
+
+    def overfull(self):
+        return len(self.occupants) > self.capacity
+
+
+
+
+
+
+
+
In [16]:
+
+
+
living = Room("Living Room", {'north': 'garden'}, 3)
+
+
+
+
+
+
+
+
In [17]:
+
+
+
living.capacity
+
+
+
+
+
+
+
+
Out[17]:
+
+
3
+
+
+
+
+
+
+
+
+

Methods which begin and end with two underscores in their names fulfil special capabilities in Python, such as +constructors.

+
+
+
+
+
+
+

Object-oriented design

+
+
+
+
+
+
+

In building a computer system to model a problem, therefore, we often want to make:

+
    +
  • classes for each kind of thing in our system
  • +
  • methods for each capability of that kind
  • +
  • properties (defined in a constructor) for each piece of information describing that kind
  • +
+
+
+
+
+
+
+

For example, the below program might describe our "Maze of Rooms" system:

+
+
+
+
+
+
+

We define a "Maze" class which can hold rooms:

+
+
+
+
+
+
In [18]:
+
+
+
class Maze:
+    def __init__(self, name):
+        self.name = name
+        self.rooms = {}
+
+    def add_room(self, room):
+        room.maze = self  # The Room needs to know which Maze it is a part of
+        self.rooms[room.name] = room
+
+    def occupants(self):
+        return [occupant for room in self.rooms.values()
+                for occupant in room.occupants.values()]
+
+    def wander(self):
+        """Move all the people in a random direction"""
+        for occupant in self.occupants():
+            occupant.wander()
+
+    def describe(self):
+        for room in self.rooms.values():
+            room.describe()
+
+    def step(self):
+        self.describe()
+        print("")
+        self.wander()
+        print("")
+
+    def simulate(self, steps):
+        for _ in range(steps):
+            self.step()
+
+
+
+
+
+
+
+
+

And a "Room" class with exits, and people:

+
+
+
+
+
+
In [19]:
+
+
+
class Room:
+    def __init__(self, name, exits, capacity, maze=None):
+        self.maze = maze
+        self.name = name
+        self.occupants = {}  # Note the default argument, occupants start empty
+        self.exits = exits  # Should be a dictionary from directions to room names
+        self.capacity = capacity
+
+    def has_space(self):
+        return len(self.occupants) < self.capacity
+
+    def available_exits(self):
+        return [exit for exit, target in self.exits.items()
+                if self.maze.rooms[target].has_space()]
+
+    def random_valid_exit(self):
+        import random
+        if not self.available_exits():
+            return None
+        return random.choice(self.available_exits())
+
+    def destination(self, exit):
+        return self.maze.rooms[self.exits[exit]]
+
+    def add_occupant(self, occupant):
+        occupant.room = self  # The person needs to know which room it is in
+        self.occupants[occupant.name] = occupant
+
+    def delete_occupant(self, occupant):
+        del self.occupants[occupant.name]
+
+    def describe(self):
+        if self.occupants:
+            print(f"{self.name}: " + " ".join(self.occupants.keys()))
+
+
+
+
+
+
+
+
+

We define a "Person" class for room occupants:

+
+
+
+
+
+
In [20]:
+
+
+
class Person:
+    def __init__(self, name, room=None):
+        self.name = name
+
+    def use(self, exit):
+        self.room.delete_occupant(self)
+        destination = self.room.destination(exit)
+        destination.add_occupant(self)
+        print("{some} goes {action} to the {where}".format(some=self.name,
+                                                           action=exit,
+                                                           where=destination.name))
+
+    def wander(self):
+        exit = self.room.random_valid_exit()
+        if exit:
+            self.use(exit)
+
+
+
+
+
+
+
+
+

And we use these classes to define our people, rooms, and their relationships:

+
+
+
+
+
+
In [21]:
+
+
+
graham = Person('Graham')
+eric = Person('Eric')
+terryg = Person('TerryG')
+john = Person('John')
+
+
+
+
+
+
+
+
In [22]:
+
+
+
living = Room('livingroom', {'outside': 'garden',
+                             'upstairs': 'bedroom', 'north': 'kitchen'}, 2)
+kitchen = Room('kitchen', {'south': 'livingroom'}, 1)
+garden = Room('garden', {'inside': 'livingroom'}, 3)
+bedroom = Room('bedroom', {'jump': 'garden', 'downstairs': 'livingroom'}, 1)
+
+
+
+
+
+
+
+
In [23]:
+
+
+
house = Maze('My House')
+
+
+
+
+
+
+
+
In [24]:
+
+
+
for room in [living, kitchen, garden, bedroom]:
+    house.add_room(room)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
living.add_occupant(graham)
+
+
+
+
+
+
+
+
In [26]:
+
+
+
garden.add_occupant(eric)
+garden.add_occupant(terryg)
+
+
+
+
+
+
+
+
In [27]:
+
+
+
bedroom.add_occupant(john)
+
+
+
+
+
+
+
+
+

And we can run a "simulation" of our model:

+
+
+
+
+
+
In [28]:
+
+
+
house.simulate(3)
+
+
+
+
+
+
+
+
+
+
livingroom: Graham
+garden: Eric TerryG
+bedroom: John
+
+Graham goes outside to the garden
+Eric goes inside to the livingroom
+TerryG goes inside to the livingroom
+John goes jump to the garden
+
+livingroom: Eric TerryG
+garden: Graham John
+
+Eric goes upstairs to the bedroom
+TerryG goes north to the kitchen
+Graham goes inside to the livingroom
+John goes inside to the livingroom
+
+livingroom: Graham John
+kitchen: TerryG
+bedroom: Eric
+
+Graham goes outside to the garden
+John goes outside to the garden
+TerryG goes south to the livingroom
+Eric goes jump to the garden
+
+
+
+
+
+
+
+
+
+
+

Object oriented design

+
+
+
+
+
+
+

There are many choices for how to design programs to do this. Another choice would be to separately define exits as a different class from rooms. This way, +we can use arrays instead of dictionaries, but we have to first define all our rooms, then define all our exits.

+
+
+
+
+
+
In [29]:
+
+
+
class Maze:
+    def __init__(self, name):
+        self.name = name
+        self.rooms = []
+        self.occupants = []
+
+    def add_room(self, name, capacity):
+        result = Room(name, capacity)
+        self.rooms.append(result)
+        return result
+
+    def add_exit(self, name, source, target, reverse=None):
+        source.add_exit(name, target)
+        if reverse:
+            target.add_exit(reverse, source)
+
+    def add_occupant(self, name, room):
+        self.occupants.append(Person(name, room))
+        room.occupancy += 1
+
+    def wander(self):
+        "Move all the people in a random direction"
+        for occupant in self.occupants:
+            occupant.wander()
+
+    def describe(self):
+        for occupant in self.occupants:
+            occupant.describe()
+
+    def step(self):
+        house.describe()
+        print("")
+        house.wander()
+        print("")
+
+    def simulate(self, steps):
+        for _ in range(steps):
+            self.step()
+
+
+
+
+
+
+
+
In [30]:
+
+
+
class Room:
+    def __init__(self, name, capacity):
+        self.name = name
+        self.capacity = capacity
+        self.occupancy = 0
+        self.exits = []
+
+    def has_space(self):
+        return self.occupancy < self.capacity
+
+    def available_exits(self):
+        return [exit for exit in self.exits if exit.valid()]
+
+    def random_valid_exit(self):
+        import random
+        if not self.available_exits():
+            return None
+        return random.choice(self.available_exits())
+
+    def add_exit(self, name, target):
+        self.exits.append(Exit(name, target))
+
+
+
+
+
+
+
+
In [31]:
+
+
+
class Person:
+    def __init__(self, name, room=None):
+        self.name = name
+        self.room = room
+
+    def use(self, exit):
+        self.room.occupancy -= 1
+        destination = exit.target
+        destination.occupancy += 1
+        self.room = destination
+        print("{some} goes {action} to the {where}".format(some=self.name,
+                                                           action=exit.name,
+                                                           where=destination.name))
+
+    def wander(self):
+        exit = self.room.random_valid_exit()
+        if exit:
+            self.use(exit)
+
+    def describe(self):
+        print("{who} is in the {where}".format(who=self.name,
+                                               where=self.room.name))
+
+
+
+
+
+
+
+
In [32]:
+
+
+
class Exit:
+    def __init__(self, name, target):
+        self.name = name
+        self.target = target
+
+    def valid(self):
+        return self.target.has_space()
+
+
+
+
+
+
+
+
In [33]:
+
+
+
house = Maze('My New House')
+
+
+
+
+
+
+
+
In [34]:
+
+
+
living = house.add_room('livingroom', 2)
+bed = house.add_room('bedroom', 1)
+garden = house.add_room('garden', 3)
+kitchen = house.add_room('kitchen', 1)
+
+
+
+
+
+
+
+
In [35]:
+
+
+
house.add_exit('north', living, kitchen, 'south')
+
+
+
+
+
+
+
+
In [36]:
+
+
+
house.add_exit('upstairs', living, bed, 'downstairs')
+
+
+
+
+
+
+
+
In [37]:
+
+
+
house.add_exit('outside', living, garden, 'inside')
+
+
+
+
+
+
+
+
In [38]:
+
+
+
house.add_exit('jump', bed, garden)
+
+
+
+
+
+
+
+
In [39]:
+
+
+
house.add_occupant('Graham', living)
+house.add_occupant('Eric', garden)
+house.add_occupant('TerryJ', bed)
+house.add_occupant('John', garden)
+
+
+
+
+
+
+
+
In [40]:
+
+
+
house.simulate(3)
+
+
+
+
+
+
+
+
+
+
Graham is in the livingroom
+Eric is in the garden
+TerryJ is in the bedroom
+John is in the garden
+
+Graham goes outside to the garden
+Eric goes inside to the livingroom
+TerryJ goes downstairs to the livingroom
+
+Graham is in the garden
+Eric is in the livingroom
+TerryJ is in the livingroom
+John is in the garden
+
+Eric goes upstairs to the bedroom
+TerryJ goes north to the kitchen
+John goes inside to the livingroom
+
+Graham is in the garden
+Eric is in the bedroom
+TerryJ is in the kitchen
+John is in the livingroom
+
+Graham goes inside to the livingroom
+Eric goes jump to the garden
+John goes outside to the garden
+
+
+
+
+
+
+
+
+
+
+

This is a huge topic, about which many books have been written. The differences between these two designs are important, and will have long-term consequences for the project. That is the how we start to think about software engineering, as opposed to learning to program, and is an important part of this course.

+
+
+
+
+
+
+

Exercise: Your own solution

+
+
+
+
+
+
+

Compare the two solutions above. Discuss with a partner which you like better, and why. Then, starting from scratch, design your own. What choices did you make that are different from mine?

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch01python/101Classes.ipynb b/ch01python/101Classes.ipynb new file mode 100644 index 000000000..c1a75dd19 --- /dev/null +++ b/ch01python/101Classes.ipynb @@ -0,0 +1,880 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a2413304", + "metadata": {}, + "source": [ + "## Defining your own classes" + ] + }, + { + "cell_type": "markdown", + "id": "8fa1f58b", + "metadata": {}, + "source": [ + "### User Defined Types" + ] + }, + { + "cell_type": "markdown", + "id": "684c6c21", + "metadata": {}, + "source": [ + "A **class** is a user-programmed Python type (since Python 2.2!)" + ] + }, + { + "cell_type": "markdown", + "id": "b7b6c334", + "metadata": {}, + "source": [ + "It can be defined like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84f9659d", + "metadata": {}, + "outputs": [], + "source": [ + "class Room(object):\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "98d1f654", + "metadata": {}, + "source": [ + "Or:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97a365f6", + "metadata": {}, + "outputs": [], + "source": [ + "class Room():\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "e6f14c62", + "metadata": {}, + "source": [ + "Or:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3390874e", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "c4b014c7", + "metadata": {}, + "source": [ + "What's the difference? Before Python 2.2 a class was distinct from all other Python types, which caused some odd behaviour. To fix this, classes were redefined as user programmed types by extending `object`, e.g., class `room(object)`.\n", + "\n", + "So most Python 2 code will use this syntax as very few people want to use old style python classes. Python 3 has formalised this by removing old-style classes, so they can be defined without extending `object`, or indeed without braces.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5497c363", + "metadata": {}, + "source": [ + "Just as with other python types, you use the name of the type as a function to make a variable of that type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ba843fd", + "metadata": {}, + "outputs": [], + "source": [ + "zero = int()\n", + "type(zero)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e943db01", + "metadata": {}, + "outputs": [], + "source": [ + "myroom = Room()\n", + "type(myroom)" + ] + }, + { + "cell_type": "markdown", + "id": "62ddbbbf", + "metadata": {}, + "source": [ + "In the jargon, we say that an **object** is an **instance** of a particular **class**.\n", + "\n", + "`__main__` is the name of the scope in which top-level code executes, where we've defined the class `Room`." + ] + }, + { + "cell_type": "markdown", + "id": "87f37501", + "metadata": {}, + "source": [ + "Once we have an object with a type of our own devising, we can add properties at will:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ab5074f", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.name = \"Living\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f3e103d", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.name" + ] + }, + { + "cell_type": "markdown", + "id": "f9cb2d49", + "metadata": {}, + "source": [ + "The most common use of a class is to allow us to group data into an object in a way that is \n", + "easier to read and understand than organising data into lists and dictionaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1c5894c", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.capacity = 3\n", + "myroom.occupants = [\"Graham\", \"Eric\"]" + ] + }, + { + "cell_type": "markdown", + "id": "d36ac120", + "metadata": {}, + "source": [ + "### Methods" + ] + }, + { + "cell_type": "markdown", + "id": "8965a02b", + "metadata": {}, + "source": [ + "So far, our class doesn't do much!" + ] + }, + { + "cell_type": "markdown", + "id": "ad9bb2ac", + "metadata": {}, + "source": [ + "We define functions **inside** the definition of a class, in order to give them capabilities, just like the methods on built-in\n", + "types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c8cd8ac", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def overfull(self):\n", + " return len(self.occupants) > self.capacity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c22b9e", + "metadata": {}, + "outputs": [], + "source": [ + "myroom = Room()\n", + "myroom.capacity = 3\n", + "myroom.occupants = [\"Graham\", \"Eric\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58888b31", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.overfull()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "feafebc7", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.occupants.append(['TerryG'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dfe8026", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.occupants.append(['John'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92456898", + "metadata": {}, + "outputs": [], + "source": [ + "myroom.overfull()" + ] + }, + { + "cell_type": "markdown", + "id": "4b8d8374", + "metadata": {}, + "source": [ + "When we write methods, we always write the first function argument as `self`, to refer to the object instance itself,\n", + "the argument that goes \"before the dot\"." + ] + }, + { + "cell_type": "markdown", + "id": "f67d9092", + "metadata": {}, + "source": [ + "This is just a convention for this variable name, not a keyword. You could call it something else if you wanted." + ] + }, + { + "cell_type": "markdown", + "id": "e5d13e58", + "metadata": {}, + "source": [ + "### Constructors" + ] + }, + { + "cell_type": "markdown", + "id": "d9a7de9d", + "metadata": {}, + "source": [ + "Normally, though, we don't want to add data to the class attributes on the fly like that. \n", + "Instead, we define a **constructor** that converts input data into an object. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "777dbb1d", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, exits, capacity, occupants=[]):\n", + " self.name = name\n", + " self.occupants = occupants # Note the default argument, occupants start empty\n", + " self.exits = exits\n", + " self.capacity = capacity\n", + "\n", + " def overfull(self):\n", + " return len(self.occupants) > self.capacity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62fbaea6", + "metadata": {}, + "outputs": [], + "source": [ + "living = Room(\"Living Room\", {'north': 'garden'}, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "115d0935", + "metadata": {}, + "outputs": [], + "source": [ + "living.capacity" + ] + }, + { + "cell_type": "markdown", + "id": "363239ba", + "metadata": {}, + "source": [ + "Methods which begin and end with **two underscores** in their names fulfil special capabilities in Python, such as\n", + "constructors." + ] + }, + { + "cell_type": "markdown", + "id": "241d4124", + "metadata": {}, + "source": [ + "### Object-oriented design" + ] + }, + { + "cell_type": "markdown", + "id": "4ed7058e", + "metadata": {}, + "source": [ + "In building a computer system to model a problem, therefore, we often want to make:\n", + "\n", + "* classes for each *kind of thing* in our system\n", + "* methods for each *capability* of that kind\n", + "* properties (defined in a constructor) for each *piece of information describing* that kind\n" + ] + }, + { + "cell_type": "markdown", + "id": "b7e0eb03", + "metadata": {}, + "source": [ + "For example, the below program might describe our \"Maze of Rooms\" system:" + ] + }, + { + "cell_type": "markdown", + "id": "8569a4d6", + "metadata": {}, + "source": [ + "We define a \"Maze\" class which can hold rooms:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96b6edac", + "metadata": {}, + "outputs": [], + "source": [ + "class Maze:\n", + " def __init__(self, name):\n", + " self.name = name\n", + " self.rooms = {}\n", + "\n", + " def add_room(self, room):\n", + " room.maze = self # The Room needs to know which Maze it is a part of\n", + " self.rooms[room.name] = room\n", + "\n", + " def occupants(self):\n", + " return [occupant for room in self.rooms.values()\n", + " for occupant in room.occupants.values()]\n", + "\n", + " def wander(self):\n", + " \"\"\"Move all the people in a random direction\"\"\"\n", + " for occupant in self.occupants():\n", + " occupant.wander()\n", + "\n", + " def describe(self):\n", + " for room in self.rooms.values():\n", + " room.describe()\n", + "\n", + " def step(self):\n", + " self.describe()\n", + " print(\"\")\n", + " self.wander()\n", + " print(\"\")\n", + "\n", + " def simulate(self, steps):\n", + " for _ in range(steps):\n", + " self.step()" + ] + }, + { + "cell_type": "markdown", + "id": "479ad666", + "metadata": {}, + "source": [ + "And a \"Room\" class with exits, and people:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64ccea64", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, exits, capacity, maze=None):\n", + " self.maze = maze\n", + " self.name = name\n", + " self.occupants = {} # Note the default argument, occupants start empty\n", + " self.exits = exits # Should be a dictionary from directions to room names\n", + " self.capacity = capacity\n", + "\n", + " def has_space(self):\n", + " return len(self.occupants) < self.capacity\n", + "\n", + " def available_exits(self):\n", + " return [exit for exit, target in self.exits.items()\n", + " if self.maze.rooms[target].has_space()]\n", + "\n", + " def random_valid_exit(self):\n", + " import random\n", + " if not self.available_exits():\n", + " return None\n", + " return random.choice(self.available_exits())\n", + "\n", + " def destination(self, exit):\n", + " return self.maze.rooms[self.exits[exit]]\n", + "\n", + " def add_occupant(self, occupant):\n", + " occupant.room = self # The person needs to know which room it is in\n", + " self.occupants[occupant.name] = occupant\n", + "\n", + " def delete_occupant(self, occupant):\n", + " del self.occupants[occupant.name]\n", + "\n", + " def describe(self):\n", + " if self.occupants:\n", + " print(f\"{self.name}: \" + \" \".join(self.occupants.keys()))" + ] + }, + { + "cell_type": "markdown", + "id": "acf4d19a", + "metadata": {}, + "source": [ + "We define a \"Person\" class for room occupants:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90776db7", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, room=None):\n", + " self.name = name\n", + "\n", + " def use(self, exit):\n", + " self.room.delete_occupant(self)\n", + " destination = self.room.destination(exit)\n", + " destination.add_occupant(self)\n", + " print(\"{some} goes {action} to the {where}\".format(some=self.name,\n", + " action=exit,\n", + " where=destination.name))\n", + "\n", + " def wander(self):\n", + " exit = self.room.random_valid_exit()\n", + " if exit:\n", + " self.use(exit)" + ] + }, + { + "cell_type": "markdown", + "id": "583cccac", + "metadata": {}, + "source": [ + "And we use these classes to define our people, rooms, and their relationships:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1f94e18", + "metadata": {}, + "outputs": [], + "source": [ + "graham = Person('Graham')\n", + "eric = Person('Eric')\n", + "terryg = Person('TerryG')\n", + "john = Person('John')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc5f7694", + "metadata": {}, + "outputs": [], + "source": [ + "living = Room('livingroom', {'outside': 'garden',\n", + " 'upstairs': 'bedroom', 'north': 'kitchen'}, 2)\n", + "kitchen = Room('kitchen', {'south': 'livingroom'}, 1)\n", + "garden = Room('garden', {'inside': 'livingroom'}, 3)\n", + "bedroom = Room('bedroom', {'jump': 'garden', 'downstairs': 'livingroom'}, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c6d183b", + "metadata": {}, + "outputs": [], + "source": [ + "house = Maze('My House')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ae9349e", + "metadata": {}, + "outputs": [], + "source": [ + "for room in [living, kitchen, garden, bedroom]:\n", + " house.add_room(room)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fbf990c", + "metadata": {}, + "outputs": [], + "source": [ + "living.add_occupant(graham)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f78fe7a8", + "metadata": {}, + "outputs": [], + "source": [ + "garden.add_occupant(eric)\n", + "garden.add_occupant(terryg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33c07ced", + "metadata": {}, + "outputs": [], + "source": [ + "bedroom.add_occupant(john)" + ] + }, + { + "cell_type": "markdown", + "id": "7141e218", + "metadata": {}, + "source": [ + "And we can run a \"simulation\" of our model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d2bd649", + "metadata": {}, + "outputs": [], + "source": [ + "house.simulate(3)" + ] + }, + { + "cell_type": "markdown", + "id": "546225d6", + "metadata": {}, + "source": [ + "### Object oriented design" + ] + }, + { + "cell_type": "markdown", + "id": "96d3837d", + "metadata": {}, + "source": [ + "There are many choices for how to design programs to do this. Another choice would be to separately define exits as a different class from rooms. This way, \n", + "we can use arrays instead of dictionaries, but we have to first define all our rooms, then define all our exits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aee11e16", + "metadata": {}, + "outputs": [], + "source": [ + "class Maze:\n", + " def __init__(self, name):\n", + " self.name = name\n", + " self.rooms = []\n", + " self.occupants = []\n", + "\n", + " def add_room(self, name, capacity):\n", + " result = Room(name, capacity)\n", + " self.rooms.append(result)\n", + " return result\n", + "\n", + " def add_exit(self, name, source, target, reverse=None):\n", + " source.add_exit(name, target)\n", + " if reverse:\n", + " target.add_exit(reverse, source)\n", + "\n", + " def add_occupant(self, name, room):\n", + " self.occupants.append(Person(name, room))\n", + " room.occupancy += 1\n", + "\n", + " def wander(self):\n", + " \"Move all the people in a random direction\"\n", + " for occupant in self.occupants:\n", + " occupant.wander()\n", + "\n", + " def describe(self):\n", + " for occupant in self.occupants:\n", + " occupant.describe()\n", + "\n", + " def step(self):\n", + " house.describe()\n", + " print(\"\")\n", + " house.wander()\n", + " print(\"\")\n", + "\n", + " def simulate(self, steps):\n", + " for _ in range(steps):\n", + " self.step()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e901968", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, capacity):\n", + " self.name = name\n", + " self.capacity = capacity\n", + " self.occupancy = 0\n", + " self.exits = []\n", + "\n", + " def has_space(self):\n", + " return self.occupancy < self.capacity\n", + "\n", + " def available_exits(self):\n", + " return [exit for exit in self.exits if exit.valid()]\n", + "\n", + " def random_valid_exit(self):\n", + " import random\n", + " if not self.available_exits():\n", + " return None\n", + " return random.choice(self.available_exits())\n", + "\n", + " def add_exit(self, name, target):\n", + " self.exits.append(Exit(name, target))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31822cbe", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, room=None):\n", + " self.name = name\n", + " self.room = room\n", + "\n", + " def use(self, exit):\n", + " self.room.occupancy -= 1\n", + " destination = exit.target\n", + " destination.occupancy += 1\n", + " self.room = destination\n", + " print(\"{some} goes {action} to the {where}\".format(some=self.name,\n", + " action=exit.name,\n", + " where=destination.name))\n", + "\n", + " def wander(self):\n", + " exit = self.room.random_valid_exit()\n", + " if exit:\n", + " self.use(exit)\n", + "\n", + " def describe(self):\n", + " print(\"{who} is in the {where}\".format(who=self.name,\n", + " where=self.room.name))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "072e53e8", + "metadata": {}, + "outputs": [], + "source": [ + "class Exit:\n", + " def __init__(self, name, target):\n", + " self.name = name\n", + " self.target = target\n", + "\n", + " def valid(self):\n", + " return self.target.has_space()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38f7a307", + "metadata": {}, + "outputs": [], + "source": [ + "house = Maze('My New House')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ed8e2f4", + "metadata": {}, + "outputs": [], + "source": [ + "living = house.add_room('livingroom', 2)\n", + "bed = house.add_room('bedroom', 1)\n", + "garden = house.add_room('garden', 3)\n", + "kitchen = house.add_room('kitchen', 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f52555a", + "metadata": {}, + "outputs": [], + "source": [ + "house.add_exit('north', living, kitchen, 'south')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b361f21e", + "metadata": {}, + "outputs": [], + "source": [ + "house.add_exit('upstairs', living, bed, 'downstairs')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f6d5ca0", + "metadata": {}, + "outputs": [], + "source": [ + "house.add_exit('outside', living, garden, 'inside')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c80c9f", + "metadata": {}, + "outputs": [], + "source": [ + "house.add_exit('jump', bed, garden)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8255402c", + "metadata": {}, + "outputs": [], + "source": [ + "house.add_occupant('Graham', living)\n", + "house.add_occupant('Eric', garden)\n", + "house.add_occupant('TerryJ', bed)\n", + "house.add_occupant('John', garden)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f31db7b7", + "metadata": {}, + "outputs": [], + "source": [ + "house.simulate(3)" + ] + }, + { + "cell_type": "markdown", + "id": "60867dfd", + "metadata": {}, + "source": [ + "This is a huge topic, about which many books have been written. The differences between these two designs are important, and will have long-term consequences for the project. That is the how we start to think about **software engineering**, as opposed to learning to program, and is an important part of this course." + ] + }, + { + "cell_type": "markdown", + "id": "eedaa998", + "metadata": {}, + "source": [ + "### Exercise: Your own solution" + ] + }, + { + "cell_type": "markdown", + "id": "9ee0e1b2", + "metadata": {}, + "source": [ + "Compare the two solutions above. Discuss with a partner which you like better, and why. Then, starting from scratch, design your own. What choices did you make that are different from mine?" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Classes" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch01python/101Classes.ipynb.py b/ch01python/101Classes.ipynb.py new file mode 100644 index 000000000..32be9d516 --- /dev/null +++ b/ch01python/101Classes.ipynb.py @@ -0,0 +1,455 @@ +# --- +# jupyter: +# jekyll: +# display_name: Classes +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Defining your own classes + +# %% [markdown] +# ### User Defined Types + +# %% [markdown] +# A **class** is a user-programmed Python type (since Python 2.2!) + +# %% [markdown] +# It can be defined like: + +# %% +class Room(object): + pass + + +# %% [markdown] +# Or: + +# %% +class Room(): + pass + + +# %% [markdown] +# Or: + +# %% +class Room: + pass + + +# %% [markdown] +# What's the difference? Before Python 2.2 a class was distinct from all other Python types, which caused some odd behaviour. To fix this, classes were redefined as user programmed types by extending `object`, e.g., class `room(object)`. +# +# So most Python 2 code will use this syntax as very few people want to use old style python classes. Python 3 has formalised this by removing old-style classes, so they can be defined without extending `object`, or indeed without braces. +# + +# %% [markdown] +# Just as with other python types, you use the name of the type as a function to make a variable of that type: + +# %% +zero = int() +type(zero) + +# %% +myroom = Room() +type(myroom) + +# %% [markdown] +# In the jargon, we say that an **object** is an **instance** of a particular **class**. +# +# `__main__` is the name of the scope in which top-level code executes, where we've defined the class `Room`. + +# %% [markdown] +# Once we have an object with a type of our own devising, we can add properties at will: + +# %% +myroom.name = "Living" + +# %% +myroom.name + +# %% [markdown] +# The most common use of a class is to allow us to group data into an object in a way that is +# easier to read and understand than organising data into lists and dictionaries. + +# %% +myroom.capacity = 3 +myroom.occupants = ["Graham", "Eric"] + + +# %% [markdown] +# ### Methods + +# %% [markdown] +# So far, our class doesn't do much! + +# %% [markdown] +# We define functions **inside** the definition of a class, in order to give them capabilities, just like the methods on built-in +# types. + +# %% +class Room: + def overfull(self): + return len(self.occupants) > self.capacity + + +# %% +myroom = Room() +myroom.capacity = 3 +myroom.occupants = ["Graham", "Eric"] + +# %% +myroom.overfull() + +# %% +myroom.occupants.append(['TerryG']) + +# %% +myroom.occupants.append(['John']) + +# %% +myroom.overfull() + + +# %% [markdown] +# When we write methods, we always write the first function argument as `self`, to refer to the object instance itself, +# the argument that goes "before the dot". + +# %% [markdown] +# This is just a convention for this variable name, not a keyword. You could call it something else if you wanted. + +# %% [markdown] +# ### Constructors + +# %% [markdown] +# Normally, though, we don't want to add data to the class attributes on the fly like that. +# Instead, we define a **constructor** that converts input data into an object. + +# %% +class Room: + def __init__(self, name, exits, capacity, occupants=[]): + self.name = name + self.occupants = occupants # Note the default argument, occupants start empty + self.exits = exits + self.capacity = capacity + + def overfull(self): + return len(self.occupants) > self.capacity + + +# %% +living = Room("Living Room", {'north': 'garden'}, 3) + +# %% +living.capacity + + +# %% [markdown] +# Methods which begin and end with **two underscores** in their names fulfil special capabilities in Python, such as +# constructors. + +# %% [markdown] +# ### Object-oriented design + +# %% [markdown] +# In building a computer system to model a problem, therefore, we often want to make: +# +# * classes for each *kind of thing* in our system +# * methods for each *capability* of that kind +# * properties (defined in a constructor) for each *piece of information describing* that kind +# + +# %% [markdown] +# For example, the below program might describe our "Maze of Rooms" system: + +# %% [markdown] +# We define a "Maze" class which can hold rooms: + +# %% +class Maze: + def __init__(self, name): + self.name = name + self.rooms = {} + + def add_room(self, room): + room.maze = self # The Room needs to know which Maze it is a part of + self.rooms[room.name] = room + + def occupants(self): + return [occupant for room in self.rooms.values() + for occupant in room.occupants.values()] + + def wander(self): + """Move all the people in a random direction""" + for occupant in self.occupants(): + occupant.wander() + + def describe(self): + for room in self.rooms.values(): + room.describe() + + def step(self): + self.describe() + print("") + self.wander() + print("") + + def simulate(self, steps): + for _ in range(steps): + self.step() + + +# %% [markdown] +# And a "Room" class with exits, and people: + +# %% +class Room: + def __init__(self, name, exits, capacity, maze=None): + self.maze = maze + self.name = name + self.occupants = {} # Note the default argument, occupants start empty + self.exits = exits # Should be a dictionary from directions to room names + self.capacity = capacity + + def has_space(self): + return len(self.occupants) < self.capacity + + def available_exits(self): + return [exit for exit, target in self.exits.items() + if self.maze.rooms[target].has_space()] + + def random_valid_exit(self): + import random + if not self.available_exits(): + return None + return random.choice(self.available_exits()) + + def destination(self, exit): + return self.maze.rooms[self.exits[exit]] + + def add_occupant(self, occupant): + occupant.room = self # The person needs to know which room it is in + self.occupants[occupant.name] = occupant + + def delete_occupant(self, occupant): + del self.occupants[occupant.name] + + def describe(self): + if self.occupants: + print(f"{self.name}: " + " ".join(self.occupants.keys())) + + +# %% [markdown] +# We define a "Person" class for room occupants: + +# %% +class Person: + def __init__(self, name, room=None): + self.name = name + + def use(self, exit): + self.room.delete_occupant(self) + destination = self.room.destination(exit) + destination.add_occupant(self) + print("{some} goes {action} to the {where}".format(some=self.name, + action=exit, + where=destination.name)) + + def wander(self): + exit = self.room.random_valid_exit() + if exit: + self.use(exit) + + +# %% [markdown] +# And we use these classes to define our people, rooms, and their relationships: + +# %% +graham = Person('Graham') +eric = Person('Eric') +terryg = Person('TerryG') +john = Person('John') + +# %% +living = Room('livingroom', {'outside': 'garden', + 'upstairs': 'bedroom', 'north': 'kitchen'}, 2) +kitchen = Room('kitchen', {'south': 'livingroom'}, 1) +garden = Room('garden', {'inside': 'livingroom'}, 3) +bedroom = Room('bedroom', {'jump': 'garden', 'downstairs': 'livingroom'}, 1) + +# %% +house = Maze('My House') + +# %% +for room in [living, kitchen, garden, bedroom]: + house.add_room(room) + +# %% +living.add_occupant(graham) + +# %% +garden.add_occupant(eric) +garden.add_occupant(terryg) + +# %% +bedroom.add_occupant(john) + +# %% [markdown] +# And we can run a "simulation" of our model: + +# %% +house.simulate(3) + + +# %% [markdown] +# ### Object oriented design + +# %% [markdown] +# There are many choices for how to design programs to do this. Another choice would be to separately define exits as a different class from rooms. This way, +# we can use arrays instead of dictionaries, but we have to first define all our rooms, then define all our exits. + +# %% +class Maze: + def __init__(self, name): + self.name = name + self.rooms = [] + self.occupants = [] + + def add_room(self, name, capacity): + result = Room(name, capacity) + self.rooms.append(result) + return result + + def add_exit(self, name, source, target, reverse=None): + source.add_exit(name, target) + if reverse: + target.add_exit(reverse, source) + + def add_occupant(self, name, room): + self.occupants.append(Person(name, room)) + room.occupancy += 1 + + def wander(self): + "Move all the people in a random direction" + for occupant in self.occupants: + occupant.wander() + + def describe(self): + for occupant in self.occupants: + occupant.describe() + + def step(self): + house.describe() + print("") + house.wander() + print("") + + def simulate(self, steps): + for _ in range(steps): + self.step() + + +# %% +class Room: + def __init__(self, name, capacity): + self.name = name + self.capacity = capacity + self.occupancy = 0 + self.exits = [] + + def has_space(self): + return self.occupancy < self.capacity + + def available_exits(self): + return [exit for exit in self.exits if exit.valid()] + + def random_valid_exit(self): + import random + if not self.available_exits(): + return None + return random.choice(self.available_exits()) + + def add_exit(self, name, target): + self.exits.append(Exit(name, target)) + + +# %% +class Person: + def __init__(self, name, room=None): + self.name = name + self.room = room + + def use(self, exit): + self.room.occupancy -= 1 + destination = exit.target + destination.occupancy += 1 + self.room = destination + print("{some} goes {action} to the {where}".format(some=self.name, + action=exit.name, + where=destination.name)) + + def wander(self): + exit = self.room.random_valid_exit() + if exit: + self.use(exit) + + def describe(self): + print("{who} is in the {where}".format(who=self.name, + where=self.room.name)) + + +# %% +class Exit: + def __init__(self, name, target): + self.name = name + self.target = target + + def valid(self): + return self.target.has_space() + + +# %% +house = Maze('My New House') + +# %% +living = house.add_room('livingroom', 2) +bed = house.add_room('bedroom', 1) +garden = house.add_room('garden', 3) +kitchen = house.add_room('kitchen', 1) + +# %% +house.add_exit('north', living, kitchen, 'south') + +# %% +house.add_exit('upstairs', living, bed, 'downstairs') + +# %% +house.add_exit('outside', living, garden, 'inside') + +# %% +house.add_exit('jump', bed, garden) + +# %% +house.add_occupant('Graham', living) +house.add_occupant('Eric', garden) +house.add_occupant('TerryJ', bed) +house.add_occupant('John', garden) + +# %% +house.simulate(3) + +# %% [markdown] +# This is a huge topic, about which many books have been written. The differences between these two designs are important, and will have long-term consequences for the project. That is the how we start to think about **software engineering**, as opposed to learning to program, and is an important part of this course. + +# %% [markdown] +# ### Exercise: Your own solution + +# %% [markdown] +# Compare the two solutions above. Discuss with a partner which you like better, and why. Then, starting from scratch, design your own. What choices did you make that are different from mine? diff --git a/ch01python/draw_eight.py b/ch01python/draw_eight.py new file mode 100644 index 000000000..bc815a622 --- /dev/null +++ b/ch01python/draw_eight.py @@ -0,0 +1,17 @@ +# Above line tells the notebook to treat the rest of this +# cell as content for a file on disk. +import math + +import numpy as np +import matplotlib.pyplot as plt + +def make_figure(): + """Plot a figure of eight.""" + + theta = np.arange(0, 4 * math.pi, 0.1) + eight = plt.figure() + axes = eight.add_axes([0, 0, 1, 1]) + axes.plot(0.5 * np.sin(theta), np.cos(theta / 2)) + + return eight + diff --git a/ch01python/eight.py b/ch01python/eight.py new file mode 100644 index 000000000..0699bf8a8 --- /dev/null +++ b/ch01python/eight.py @@ -0,0 +1 @@ +print(2 * 4) diff --git a/ch01python/fourteen.py b/ch01python/fourteen.py new file mode 100755 index 000000000..f5cc0679c --- /dev/null +++ b/ch01python/fourteen.py @@ -0,0 +1,2 @@ +#! /usr/bin/env python +print(2 * 7) diff --git a/ch01python/index.html b/ch01python/index.html new file mode 100644 index 000000000..ef01619d2 --- /dev/null +++ b/ch01python/index.html @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Introduction to Python + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Why use scripting languages?
  • +
  • Python. IPython and the Jupyter notebook.
  • +
  • Data structures: list, dictionaries, and sets.
  • +
  • List comprehensions
  • +
  • Functions in Python
  • +
  • Modules in Python
  • +
  • An introduction to classes
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/060files.html b/ch02data/060files.html new file mode 100644 index 000000000..846607dcc --- /dev/null +++ b/ch02data/060files.html @@ -0,0 +1,1240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Working with files + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Working with Data

+
+
+
+
+
+
+

Loading data from files

+
+
+
+
+
+
+

Loading data

+
+
+
+
+
+
+

An important part of this course is about using Python to analyse and visualise data. +Most data, of course, is supplied to us in various formats: spreadsheets, database dumps, or text files in various formats (csv, tsv, json, yaml, hdf5, netcdf) +It is also stored in some medium: on a local disk, a network drive, or on the internet in various ways. +It is important to distinguish the data format, how the data is structured into a file, from the data's storage, where it is put.

+

We'll look first at the question of data transport: loading data from a disk, and at downloading data from the internet. +Then we'll look at data parsing: building Python structures from the data. +These are related, but separate questions.

+
+
+
+
+
+
+

An example datafile

+
+
+
+
+
+
+

Let's write an example datafile to disk so we can investigate it. We'll just use a plain-text file. Jupyter notebook provides a way to do this: if we put +%%writefile at the top of a cell, instead of being interpreted as python, the cell contents are saved to disk.

+
+
+
+
+
+
In [1]:
+
+
+
%%writefile mydata.txt
+A poet once said, 'The whole universe is in a glass of wine.'
+We will probably never know in what sense he meant it, 
+for poets do not write to be understood. 
+But it is true that if we look at a glass of wine closely enough we see the entire universe. 
+There are the things of physics: the twisting liquid which evaporates depending
+on the wind and weather, the reflection in the glass;
+and our imagination adds atoms.
+The glass is a distillation of the earth's rocks,
+and in its composition we see the secrets of the universe's age, and the evolution of stars. 
+What strange array of chemicals are in the wine? How did they come to be? 
+There are the ferments, the enzymes, the substrates, and the products.
+There in wine is found the great generalization; all life is fermentation.
+Nobody can discover the chemistry of wine without discovering, 
+as did Louis Pasteur, the cause of much disease.
+How vivid is the claret, pressing its existence into the consciousness that watches it!
+If our small minds, for some convenience, divide this glass of wine, this universe, 
+into parts -- 
+physics, biology, geology, astronomy, psychology, and so on -- 
+remember that nature does not know it!
+
+So let us put it all back together, not forgetting ultimately what it is for.
+Let it give us one more final pleasure; drink it and forget it all!
+   - Richard Feynman
+
+
+
+
+
+
+
+
+
+
Writing mydata.txt
+
+
+
+
+
+
+
+
+
+

Where did that go? It went to the current folder, which for a notebook, by default, is where the notebook is on disk.

+
+
+
+
+
+
In [2]:
+
+
+
import os # The 'os' module gives us all the tools we need to search in the file system
+os.getcwd() # Use the 'getcwd' function from the 'os' module to find where we are on disk.
+
+
+
+
+
+
+
+
Out[2]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch02data'
+
+
+
+
+
+
+
+
+

Can we see if it is there?

+
+
+
+
+
+
In [3]:
+
+
+
import os
+[x for x in os.listdir(os.getcwd()) if ".txt" in x]
+
+
+
+
+
+
+
+
Out[3]:
+
+
['mydata.txt']
+
+
+
+
+
+
+
+
+

Yep! Note how we used a list comprehension to filter all the extraneous files.

+
+
+
+
+
+
+

Path independence and os

+
+
+
+
+
+
+

We can use dirname to get the parent folder for a folder, in a platform independent-way.

+
+
+
+
+
+
In [4]:
+
+
+
os.path.dirname(os.getcwd())
+
+
+
+
+
+
+
+
Out[4]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse'
+
+
+
+
+
+
+
+
+

We could do this manually using split:

+
+
+
+
+
+
In [5]:
+
+
+
"/".join(os.getcwd().split("/")[:-1])
+
+
+
+
+
+
+
+
Out[5]:
+
+
'/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse'
+
+
+
+
+
+
+
+
+

But this would not work on Windows, where path elements are separated with a \ instead of a /. So it's important +to use os.path for this stuff.

+
+
+
+
+
+
+

Supplementary Materials: If you're not already comfortable with how files fit into folders, and folders form a tree, +with folders containing subfolders, then look at this Software Carpentry lesson on navigating the file system.

+

Satisfy yourself that after using %%writefile, you can then find the file on disk with Windows Explorer, OSX Finder, or the Linux Shell.

+
+
+
+
+
+
+

We can see how in Python we can investigate the file system with functions in the os module, using just the same programming approaches as for anything else.

+
+
+
+
+
+
+

We'll gradually learn more features of the os module as we go, allowing us to move around the disk, walk around the +disk looking for relevant files, and so on. These will be important to master for automating our data analyses.

+
+
+
+
+
+
+

Opening files in Python

+
+
+
+
+
+
+

So, let's read our file:

+
+
+
+
+
+
In [6]:
+
+
+
myfile = open('mydata.txt')
+
+
+
+
+
+
+
+
In [7]:
+
+
+
type(myfile)
+
+
+
+
+
+
+
+
Out[7]:
+
+
_io.TextIOWrapper
+
+
+
+
+
+
+
+
+

Even though the name of this type is not very clear, it offers various ways of accessing the file.

+

We can go line-by-line, by treating the file as an iterable:

+
+
+
+
+
+
In [8]:
+
+
+
[x for x in myfile]
+
+
+
+
+
+
+
+
Out[8]:
+
+
["A poet once said, 'The whole universe is in a glass of wine.'\n",
+ 'We will probably never know in what sense he meant it, \n',
+ 'for poets do not write to be understood. \n',
+ 'But it is true that if we look at a glass of wine closely enough we see the entire universe. \n',
+ 'There are the things of physics: the twisting liquid which evaporates depending\n',
+ 'on the wind and weather, the reflection in the glass;\n',
+ 'and our imagination adds atoms.\n',
+ "The glass is a distillation of the earth's rocks,\n",
+ "and in its composition we see the secrets of the universe's age, and the evolution of stars. \n",
+ 'What strange array of chemicals are in the wine? How did they come to be? \n',
+ 'There are the ferments, the enzymes, the substrates, and the products.\n',
+ 'There in wine is found the great generalization; all life is fermentation.\n',
+ 'Nobody can discover the chemistry of wine without discovering, \n',
+ 'as did Louis Pasteur, the cause of much disease.\n',
+ 'How vivid is the claret, pressing its existence into the consciousness that watches it!\n',
+ 'If our small minds, for some convenience, divide this glass of wine, this universe, \n',
+ 'into parts -- \n',
+ 'physics, biology, geology, astronomy, psychology, and so on -- \n',
+ 'remember that nature does not know it!\n',
+ '\n',
+ 'So let us put it all back together, not forgetting ultimately what it is for.\n',
+ 'Let it give us one more final pleasure; drink it and forget it all!\n',
+ '   - Richard Feynman\n']
+
+
+
+
+
+
+
+
+

If we do that again, the file has already finished, there is no more data.

+
+
+
+
+
+
In [9]:
+
+
+
[x for x in myfile]
+
+
+
+
+
+
+
+
Out[9]:
+
+
[]
+
+
+
+
+
+
+
+
+

We need to 'rewind' it!

+
+
+
+
+
+
In [10]:
+
+
+
myfile.seek(0)
+[len(x) for x in myfile if 'know' in x]
+
+
+
+
+
+
+
+
Out[10]:
+
+
[56, 39]
+
+
+
+
+
+
+
+
+

It's really important to remember that a file is a different built in type than a string.

+
+
+
+
+
+
+

Working with files

+
+
+
+
+
+
+

We can read one line at a time with readline:

+
+
+
+
+
+
In [11]:
+
+
+
myfile.seek(0)
+first = myfile.readline()
+
+
+
+
+
+
+
+
In [12]:
+
+
+
first
+
+
+
+
+
+
+
+
Out[12]:
+
+
"A poet once said, 'The whole universe is in a glass of wine.'\n"
+
+
+
+
+
+
+
+
In [13]:
+
+
+
second = myfile.readline()
+
+
+
+
+
+
+
+
In [14]:
+
+
+
second
+
+
+
+
+
+
+
+
Out[14]:
+
+
'We will probably never know in what sense he meant it, \n'
+
+
+
+
+
+
+
+
+

We can read the whole remaining file with read:

+
+
+
+
+
+
In [15]:
+
+
+
rest = myfile.read()
+
+
+
+
+
+
+
+
In [16]:
+
+
+
rest
+
+
+
+
+
+
+
+
Out[16]:
+
+
"for poets do not write to be understood. \nBut it is true that if we look at a glass of wine closely enough we see the entire universe. \nThere are the things of physics: the twisting liquid which evaporates depending\non the wind and weather, the reflection in the glass;\nand our imagination adds atoms.\nThe glass is a distillation of the earth's rocks,\nand in its composition we see the secrets of the universe's age, and the evolution of stars. \nWhat strange array of chemicals are in the wine? How did they come to be? \nThere are the ferments, the enzymes, the substrates, and the products.\nThere in wine is found the great generalization; all life is fermentation.\nNobody can discover the chemistry of wine without discovering, \nas did Louis Pasteur, the cause of much disease.\nHow vivid is the claret, pressing its existence into the consciousness that watches it!\nIf our small minds, for some convenience, divide this glass of wine, this universe, \ninto parts -- \nphysics, biology, geology, astronomy, psychology, and so on -- \nremember that nature does not know it!\n\nSo let us put it all back together, not forgetting ultimately what it is for.\nLet it give us one more final pleasure; drink it and forget it all!\n   - Richard Feynman\n"
+
+
+
+
+
+
+
+
+

Which means that when a file is first opened, read is useful to just get the whole thing as a string:

+
+
+
+
+
+
In [17]:
+
+
+
open('mydata.txt').read()
+
+
+
+
+
+
+
+
Out[17]:
+
+
"A poet once said, 'The whole universe is in a glass of wine.'\nWe will probably never know in what sense he meant it, \nfor poets do not write to be understood. \nBut it is true that if we look at a glass of wine closely enough we see the entire universe. \nThere are the things of physics: the twisting liquid which evaporates depending\non the wind and weather, the reflection in the glass;\nand our imagination adds atoms.\nThe glass is a distillation of the earth's rocks,\nand in its composition we see the secrets of the universe's age, and the evolution of stars. \nWhat strange array of chemicals are in the wine? How did they come to be? \nThere are the ferments, the enzymes, the substrates, and the products.\nThere in wine is found the great generalization; all life is fermentation.\nNobody can discover the chemistry of wine without discovering, \nas did Louis Pasteur, the cause of much disease.\nHow vivid is the claret, pressing its existence into the consciousness that watches it!\nIf our small minds, for some convenience, divide this glass of wine, this universe, \ninto parts -- \nphysics, biology, geology, astronomy, psychology, and so on -- \nremember that nature does not know it!\n\nSo let us put it all back together, not forgetting ultimately what it is for.\nLet it give us one more final pleasure; drink it and forget it all!\n   - Richard Feynman\n"
+
+
+
+
+
+
+
+
+

You can also read just a few characters:

+
+
+
+
+
+
In [18]:
+
+
+
myfile.seek(1335)
+
+
+
+
+
+
+
+
Out[18]:
+
+
1335
+
+
+
+
+
+
+
+
In [19]:
+
+
+
myfile.read(15)
+
+
+
+
+
+
+
+
Out[19]:
+
+
'\n   - Richard F'
+
+
+
+
+
+
+
+
+

Converting strings to files

+
+
+
+
+
+
+

Because files and strings are different types, we CANNOT just treat strings as if they were files:

+
+
+
+
+
+
In [20]:
+
+
+
mystring = "Hello World\n My name is James"
+
+
+
+
+
+
+
+
In [21]:
+
+
+
mystring
+
+
+
+
+
+
+
+
Out[21]:
+
+
'Hello World\n My name is James'
+
+
+
+
+
+
+
+
In [22]:
+
+
+
mystring.readline()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[22], line 1
+----> 1 mystring.readline()
+
+AttributeError: 'str' object has no attribute 'readline'
+
+
+
+
+
+
+
+
+

This is important, because some file format parsers expect input from a file and not a string. +We can convert between them using the StringIO class of the io module in the standard library:

+
+
+
+
+
+
In [23]:
+
+
+
from io import StringIO
+
+
+
+
+
+
+
+
In [24]:
+
+
+
mystringasafile = StringIO(mystring)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
mystringasafile.readline()
+
+
+
+
+
+
+
+
Out[25]:
+
+
'Hello World\n'
+
+
+
+
+
+
+
+
In [26]:
+
+
+
mystringasafile.readline()
+
+
+
+
+
+
+
+
Out[26]:
+
+
' My name is James'
+
+
+
+
+
+
+
+
+

Note that in a string, \n is used to represent a newline.

+
+
+
+
+
+
+

Closing files

+
+
+
+
+
+
+

We really ought to close files when we've finished with them, as it makes our work more efficient and safer. (On a shared computer, +this is particularly important)

+
+
+
+
+
+
In [27]:
+
+
+
myfile.close()
+
+
+
+
+
+
+
+
+

Because it's so easy to forget this, python provides a context manager to open a file, then close it automatically at +the end of an indented block:

+
+
+
+
+
+
In [28]:
+
+
+
with open('mydata.txt') as somefile:
+    content = somefile.read()
+
+content
+
+
+
+
+
+
+
+
Out[28]:
+
+
"A poet once said, 'The whole universe is in a glass of wine.'\nWe will probably never know in what sense he meant it, \nfor poets do not write to be understood. \nBut it is true that if we look at a glass of wine closely enough we see the entire universe. \nThere are the things of physics: the twisting liquid which evaporates depending\non the wind and weather, the reflection in the glass;\nand our imagination adds atoms.\nThe glass is a distillation of the earth's rocks,\nand in its composition we see the secrets of the universe's age, and the evolution of stars. \nWhat strange array of chemicals are in the wine? How did they come to be? \nThere are the ferments, the enzymes, the substrates, and the products.\nThere in wine is found the great generalization; all life is fermentation.\nNobody can discover the chemistry of wine without discovering, \nas did Louis Pasteur, the cause of much disease.\nHow vivid is the claret, pressing its existence into the consciousness that watches it!\nIf our small minds, for some convenience, divide this glass of wine, this universe, \ninto parts -- \nphysics, biology, geology, astronomy, psychology, and so on -- \nremember that nature does not know it!\n\nSo let us put it all back together, not forgetting ultimately what it is for.\nLet it give us one more final pleasure; drink it and forget it all!\n   - Richard Feynman\n"
+
+
+
+
+
+
+
+
+

The code to be done while the file is open is indented, just like for an if statement.

+
+
+
+
+
+
+

You should pretty much always use this syntax for working with files. +We will see more about context managers in a later chapter.

+
+
+
+
+
+
+

Writing files

+
+
+
+
+
+
+

We might want to create a file from a string in memory. We can't do this with the notebook's %%writefile -- this is +just a notebook convenience, and isn't very programmable.

+
+
+
+
+
+
+

When we open a file, we can specify a 'mode', in this case, 'w' for writing. ('r' for reading is the default.)

+
+
+
+
+
+
In [29]:
+
+
+
with open('mywrittenfile', 'w') as target:
+    target.write('Hello')
+    target.write('World')
+
+
+
+
+
+
+
+
In [30]:
+
+
+
with open('mywrittenfile','r') as source:
+    print(source.read())
+
+
+
+
+
+
+
+
+
+
HelloWorld
+
+
+
+
+
+
+
+
+
+

And we can "append" to a file with mode 'a':

+
+
+
+
+
+
In [31]:
+
+
+
with open('mywrittenfile', 'a') as target:
+    target.write('Hello')
+    target.write('James')
+
+
+
+
+
+
+
+
In [32]:
+
+
+
with open('mywrittenfile','r') as source:
+    print(source.read())
+
+
+
+
+
+
+
+
+
+
HelloWorldHelloJames
+
+
+
+
+
+
+
+
+
+

If a file already exists, mode 'w' will overwrite it.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/060files.ipynb b/ch02data/060files.ipynb new file mode 100644 index 000000000..30b2452fd --- /dev/null +++ b/ch02data/060files.ipynb @@ -0,0 +1,719 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "391e0634", + "metadata": {}, + "source": [ + "# Working with Data" + ] + }, + { + "cell_type": "markdown", + "id": "9d7aa54d", + "metadata": {}, + "source": [ + "## Loading data from files" + ] + }, + { + "cell_type": "markdown", + "id": "60e8b470", + "metadata": {}, + "source": [ + "### Loading data" + ] + }, + { + "cell_type": "markdown", + "id": "3d32c4aa", + "metadata": {}, + "source": [ + "An important part of this course is about using Python to analyse and visualise data.\n", + "Most data, of course, is supplied to us in various *formats*: spreadsheets, database dumps, or text files in various formats (csv, tsv, json, yaml, hdf5, netcdf)\n", + "It is also stored in some *medium*: on a local disk, a network drive, or on the internet in various ways.\n", + "It is important to distinguish the data format, how the data is structured into a file, from the data's storage, where it is put. \n", + "\n", + "We'll look first at the question of data *transport*: loading data from a disk, and at downloading data from the internet.\n", + "Then we'll look at data *parsing*: building Python structures from the data.\n", + "These are related, but separate questions." + ] + }, + { + "cell_type": "markdown", + "id": "af7ae75e", + "metadata": {}, + "source": [ + "### An example datafile" + ] + }, + { + "cell_type": "markdown", + "id": "a0df49e0", + "metadata": {}, + "source": [ + "Let's write an example datafile to disk so we can investigate it. We'll just use a plain-text file. Jupyter notebook provides a way to do this: if we put\n", + "`%%writefile` at the top of a cell, instead of being interpreted as python, the cell contents are saved to disk." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf07f9e0", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mydata.txt\n", + "A poet once said, 'The whole universe is in a glass of wine.'\n", + "We will probably never know in what sense he meant it, \n", + "for poets do not write to be understood. \n", + "But it is true that if we look at a glass of wine closely enough we see the entire universe. \n", + "There are the things of physics: the twisting liquid which evaporates depending\n", + "on the wind and weather, the reflection in the glass;\n", + "and our imagination adds atoms.\n", + "The glass is a distillation of the earth's rocks,\n", + "and in its composition we see the secrets of the universe's age, and the evolution of stars. \n", + "What strange array of chemicals are in the wine? How did they come to be? \n", + "There are the ferments, the enzymes, the substrates, and the products.\n", + "There in wine is found the great generalization; all life is fermentation.\n", + "Nobody can discover the chemistry of wine without discovering, \n", + "as did Louis Pasteur, the cause of much disease.\n", + "How vivid is the claret, pressing its existence into the consciousness that watches it!\n", + "If our small minds, for some convenience, divide this glass of wine, this universe, \n", + "into parts -- \n", + "physics, biology, geology, astronomy, psychology, and so on -- \n", + "remember that nature does not know it!\n", + "\n", + "So let us put it all back together, not forgetting ultimately what it is for.\n", + "Let it give us one more final pleasure; drink it and forget it all!\n", + " - Richard Feynman" + ] + }, + { + "cell_type": "markdown", + "id": "b0e7174d", + "metadata": {}, + "source": [ + "Where did that go? It went to the current folder, which for a notebook, by default, is where the notebook is on disk." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c592ee5", + "metadata": {}, + "outputs": [], + "source": [ + "import os # The 'os' module gives us all the tools we need to search in the file system\n", + "os.getcwd() # Use the 'getcwd' function from the 'os' module to find where we are on disk." + ] + }, + { + "cell_type": "markdown", + "id": "bedc92fa", + "metadata": {}, + "source": [ + "Can we see if it is there?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1660878b", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "[x for x in os.listdir(os.getcwd()) if \".txt\" in x]" + ] + }, + { + "cell_type": "markdown", + "id": "da83b14a", + "metadata": {}, + "source": [ + "Yep! Note how we used a list comprehension to filter all the extraneous files." + ] + }, + { + "cell_type": "markdown", + "id": "75fc2810", + "metadata": {}, + "source": [ + "### Path independence and `os`" + ] + }, + { + "cell_type": "markdown", + "id": "d4e94a27", + "metadata": {}, + "source": [ + "We can use `dirname` to get the parent folder for a folder, in a platform independent-way." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d90e0ff", + "metadata": {}, + "outputs": [], + "source": [ + "os.path.dirname(os.getcwd())" + ] + }, + { + "cell_type": "markdown", + "id": "f1b8bd0f", + "metadata": {}, + "source": [ + "We could do this manually using `split`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b3783b8", + "metadata": {}, + "outputs": [], + "source": [ + "\"/\".join(os.getcwd().split(\"/\")[:-1])" + ] + }, + { + "cell_type": "markdown", + "id": "5d2d60a8", + "metadata": {}, + "source": [ + "But this would not work on Windows, where path elements are separated with a `\\` instead of a `/`. So it's important \n", + "to use `os.path` for this stuff." + ] + }, + { + "cell_type": "markdown", + "id": "17b8b3f2", + "metadata": {}, + "source": [ + "**Supplementary Materials**: If you're not already comfortable with how files fit into folders, and folders form a tree,\n", + " with folders containing subfolders, then look at [this Software Carpentry lesson on navigating the file system](http://swcarpentry.github.io/shell-novice/02-filedir/index.html). \n", + "\n", + "Satisfy yourself that after using `%%writefile`, you can then find the file on disk with Windows Explorer, OSX Finder, or the Linux Shell." + ] + }, + { + "cell_type": "markdown", + "id": "922e0455", + "metadata": {}, + "source": [ + "We can see how in Python we can investigate the file system with functions in the `os` module, using just the same programming approaches as for anything else." + ] + }, + { + "cell_type": "markdown", + "id": "5317e3f7", + "metadata": {}, + "source": [ + "We'll gradually learn more features of the `os` module as we go, allowing us to move around the disk, `walk` around the\n", + "disk looking for relevant files, and so on. These will be important to master for automating our data analyses." + ] + }, + { + "cell_type": "markdown", + "id": "28b99de4", + "metadata": {}, + "source": [ + "### Opening files in Python" + ] + }, + { + "cell_type": "markdown", + "id": "94b54b33", + "metadata": {}, + "source": [ + "So, let's read our file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d058555", + "metadata": {}, + "outputs": [], + "source": [ + "myfile = open('mydata.txt')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c675c293", + "metadata": {}, + "outputs": [], + "source": [ + "type(myfile)" + ] + }, + { + "cell_type": "markdown", + "id": "21340d05", + "metadata": {}, + "source": [ + "Even though the name of this type is not very clear, it offers various ways of accessing the file.\n", + "\n", + "We can go line-by-line, by treating the file as an iterable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aba86ae0", + "metadata": {}, + "outputs": [], + "source": [ + "[x for x in myfile]" + ] + }, + { + "cell_type": "markdown", + "id": "bc75503c", + "metadata": {}, + "source": [ + "If we do that again, the file has already finished, there is no more data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fdd9154", + "metadata": {}, + "outputs": [], + "source": [ + "[x for x in myfile]" + ] + }, + { + "cell_type": "markdown", + "id": "2f2332a4", + "metadata": {}, + "source": [ + "We need to 'rewind' it!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "972f12e2", + "metadata": {}, + "outputs": [], + "source": [ + "myfile.seek(0)\n", + "[len(x) for x in myfile if 'know' in x]" + ] + }, + { + "cell_type": "markdown", + "id": "80b1aca8", + "metadata": {}, + "source": [ + "It's really important to remember that a file is a *different* built in type than a string." + ] + }, + { + "cell_type": "markdown", + "id": "09b3f977", + "metadata": {}, + "source": [ + "### Working with files" + ] + }, + { + "cell_type": "markdown", + "id": "3aabd600", + "metadata": {}, + "source": [ + "We can read one line at a time with `readline`: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acb56e54", + "metadata": {}, + "outputs": [], + "source": [ + "myfile.seek(0)\n", + "first = myfile.readline()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c81f108", + "metadata": {}, + "outputs": [], + "source": [ + "first" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97b6ca41", + "metadata": {}, + "outputs": [], + "source": [ + "second = myfile.readline()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f15b9a1", + "metadata": {}, + "outputs": [], + "source": [ + "second" + ] + }, + { + "cell_type": "markdown", + "id": "9ed887b2", + "metadata": {}, + "source": [ + "We can read the whole remaining file with `read`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9f616b1", + "metadata": {}, + "outputs": [], + "source": [ + "rest = myfile.read()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5896a39", + "metadata": {}, + "outputs": [], + "source": [ + "rest" + ] + }, + { + "cell_type": "markdown", + "id": "95885e01", + "metadata": {}, + "source": [ + "Which means that when a file is first opened, read is useful to just get the whole thing as a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b87d38e", + "metadata": {}, + "outputs": [], + "source": [ + "open('mydata.txt').read()" + ] + }, + { + "cell_type": "markdown", + "id": "65c9b913", + "metadata": {}, + "source": [ + "You can also read just a few characters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8e2b27a", + "metadata": {}, + "outputs": [], + "source": [ + "myfile.seek(1335)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "631d4184", + "metadata": {}, + "outputs": [], + "source": [ + "myfile.read(15)" + ] + }, + { + "cell_type": "markdown", + "id": "94f1c696", + "metadata": {}, + "source": [ + "### Converting strings to files" + ] + }, + { + "cell_type": "markdown", + "id": "a7b59014", + "metadata": {}, + "source": [ + "Because files and strings are different types, we CANNOT just treat strings as if they were files:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0822c4df", + "metadata": {}, + "outputs": [], + "source": [ + "mystring = \"Hello World\\n My name is James\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fe97a29", + "metadata": {}, + "outputs": [], + "source": [ + "mystring" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f04ca28c", + "metadata": {}, + "outputs": [], + "source": [ + "mystring.readline()" + ] + }, + { + "cell_type": "markdown", + "id": "10218df9", + "metadata": {}, + "source": [ + "This is important, because some file format parsers expect input from a **file** and not a string. \n", + "We can convert between them using the `StringIO` class of the [io module](https://docs.python.org/3/library/io.html) in the standard library:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1594f4a", + "metadata": {}, + "outputs": [], + "source": [ + "from io import StringIO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8e8bbd2", + "metadata": {}, + "outputs": [], + "source": [ + "mystringasafile = StringIO(mystring)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dcb113a", + "metadata": {}, + "outputs": [], + "source": [ + "mystringasafile.readline()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d08103b3", + "metadata": {}, + "outputs": [], + "source": [ + "mystringasafile.readline()" + ] + }, + { + "cell_type": "markdown", + "id": "72f30413", + "metadata": {}, + "source": [ + "Note that in a string, `\\n` is used to represent a newline." + ] + }, + { + "cell_type": "markdown", + "id": "7131f435", + "metadata": {}, + "source": [ + "### Closing files" + ] + }, + { + "cell_type": "markdown", + "id": "d956ec96", + "metadata": {}, + "source": [ + "We really ought to close files when we've finished with them, as it makes our work more efficient and safer. (On a shared computer,\n", + "this is particularly important)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44390086", + "metadata": {}, + "outputs": [], + "source": [ + "myfile.close()" + ] + }, + { + "cell_type": "markdown", + "id": "00170602", + "metadata": {}, + "source": [ + "Because it's so easy to forget this, python provides a **context manager** to open a file, then close it automatically at\n", + "the end of an indented block:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85c8884a", + "metadata": {}, + "outputs": [], + "source": [ + "with open('mydata.txt') as somefile:\n", + " content = somefile.read()\n", + "\n", + "content" + ] + }, + { + "cell_type": "markdown", + "id": "aab08fcb", + "metadata": {}, + "source": [ + "The code to be done while the file is open is indented, just like for an `if` statement." + ] + }, + { + "cell_type": "markdown", + "id": "e473541a", + "metadata": {}, + "source": [ + "You should pretty much **always** use this syntax for working with files.\n", + "We will see more about context managers in a [later chapter](../ch07dry/025Iterators.html)." + ] + }, + { + "cell_type": "markdown", + "id": "f9da9067", + "metadata": {}, + "source": [ + "### Writing files" + ] + }, + { + "cell_type": "markdown", + "id": "6b61355a", + "metadata": {}, + "source": [ + "We might want to create a file from a string in memory. We can't do this with the notebook's `%%writefile` -- this is\n", + "just a notebook convenience, and isn't very programmable." + ] + }, + { + "cell_type": "markdown", + "id": "6b49af48", + "metadata": {}, + "source": [ + "When we open a file, we can specify a 'mode', in this case, 'w' for writing. ('r' for reading is the default.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c39688e7", + "metadata": {}, + "outputs": [], + "source": [ + "with open('mywrittenfile', 'w') as target:\n", + " target.write('Hello')\n", + " target.write('World')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2fa4313", + "metadata": {}, + "outputs": [], + "source": [ + "with open('mywrittenfile','r') as source:\n", + " print(source.read())" + ] + }, + { + "cell_type": "markdown", + "id": "faa62cc7", + "metadata": {}, + "source": [ + "And we can \"append\" to a file with mode 'a':" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a06f49f2", + "metadata": {}, + "outputs": [], + "source": [ + "with open('mywrittenfile', 'a') as target:\n", + " target.write('Hello')\n", + " target.write('James')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93ca4e89", + "metadata": {}, + "outputs": [], + "source": [ + "with open('mywrittenfile','r') as source:\n", + " print(source.read())" + ] + }, + { + "cell_type": "markdown", + "id": "ff8ab832", + "metadata": {}, + "source": [ + "If a file already exists, mode 'w' will overwrite it." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Working with files" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/060files.ipynb.py b/ch02data/060files.ipynb.py new file mode 100644 index 000000000..7f685c128 --- /dev/null +++ b/ch02data/060files.ipynb.py @@ -0,0 +1,287 @@ +# --- +# jupyter: +# jekyll: +# display_name: Working with files +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Working with Data + +# %% [markdown] +# ## Loading data from files + +# %% [markdown] +# ### Loading data + +# %% [markdown] +# An important part of this course is about using Python to analyse and visualise data. +# Most data, of course, is supplied to us in various *formats*: spreadsheets, database dumps, or text files in various formats (csv, tsv, json, yaml, hdf5, netcdf) +# It is also stored in some *medium*: on a local disk, a network drive, or on the internet in various ways. +# It is important to distinguish the data format, how the data is structured into a file, from the data's storage, where it is put. +# +# We'll look first at the question of data *transport*: loading data from a disk, and at downloading data from the internet. +# Then we'll look at data *parsing*: building Python structures from the data. +# These are related, but separate questions. + +# %% [markdown] +# ### An example datafile + +# %% [markdown] +# Let's write an example datafile to disk so we can investigate it. We'll just use a plain-text file. Jupyter notebook provides a way to do this: if we put +# `%%writefile` at the top of a cell, instead of being interpreted as python, the cell contents are saved to disk. + +# %% +# %%writefile mydata.txt +A poet once said, 'The whole universe is in a glass of wine.' +We will probably never know in what sense he meant it, +for poets do not write to be understood. +But it is true that if we look at a glass of wine closely enough we see the entire universe. +There are the things of physics: the twisting liquid which evaporates depending +on the wind and weather, the reflection in the glass; +and our imagination adds atoms. +The glass is a distillation of the earth's rocks, +and in its composition we see the secrets of the universe's age, and the evolution of stars. +What strange array of chemicals are in the wine? How did they come to be? +There are the ferments, the enzymes, the substrates, and the products. +There in wine is found the great generalization; all life is fermentation. +Nobody can discover the chemistry of wine without discovering, +as did Louis Pasteur, the cause of much disease. +How vivid is the claret, pressing its existence into the consciousness that watches it! +If our small minds, for some convenience, divide this glass of wine, this universe, +into parts -- +physics, biology, geology, astronomy, psychology, and so on -- +remember that nature does not know it! + +So let us put it all back together, not forgetting ultimately what it is for. +Let it give us one more final pleasure; drink it and forget it all! + - Richard Feynman + +# %% [markdown] +# Where did that go? It went to the current folder, which for a notebook, by default, is where the notebook is on disk. + +# %% +import os # The 'os' module gives us all the tools we need to search in the file system +os.getcwd() # Use the 'getcwd' function from the 'os' module to find where we are on disk. + +# %% [markdown] +# Can we see if it is there? + +# %% +import os +[x for x in os.listdir(os.getcwd()) if ".txt" in x] + +# %% [markdown] +# Yep! Note how we used a list comprehension to filter all the extraneous files. + +# %% [markdown] +# ### Path independence and `os` + +# %% [markdown] +# We can use `dirname` to get the parent folder for a folder, in a platform independent-way. + +# %% +os.path.dirname(os.getcwd()) + +# %% [markdown] +# We could do this manually using `split`: + +# %% +"/".join(os.getcwd().split("/")[:-1]) + +# %% [markdown] +# But this would not work on Windows, where path elements are separated with a `\` instead of a `/`. So it's important +# to use `os.path` for this stuff. + +# %% [markdown] +# **Supplementary Materials**: If you're not already comfortable with how files fit into folders, and folders form a tree, +# with folders containing subfolders, then look at [this Software Carpentry lesson on navigating the file system](http://swcarpentry.github.io/shell-novice/02-filedir/index.html). +# +# Satisfy yourself that after using `%%writefile`, you can then find the file on disk with Windows Explorer, OSX Finder, or the Linux Shell. + +# %% [markdown] +# We can see how in Python we can investigate the file system with functions in the `os` module, using just the same programming approaches as for anything else. + +# %% [markdown] +# We'll gradually learn more features of the `os` module as we go, allowing us to move around the disk, `walk` around the +# disk looking for relevant files, and so on. These will be important to master for automating our data analyses. + +# %% [markdown] +# ### Opening files in Python + +# %% [markdown] +# So, let's read our file: + +# %% +myfile = open('mydata.txt') + +# %% +type(myfile) + +# %% [markdown] +# Even though the name of this type is not very clear, it offers various ways of accessing the file. +# +# We can go line-by-line, by treating the file as an iterable: + +# %% +[x for x in myfile] + +# %% [markdown] +# If we do that again, the file has already finished, there is no more data. + +# %% +[x for x in myfile] + +# %% [markdown] +# We need to 'rewind' it! + +# %% +myfile.seek(0) +[len(x) for x in myfile if 'know' in x] + +# %% [markdown] +# It's really important to remember that a file is a *different* built in type than a string. + +# %% [markdown] +# ### Working with files + +# %% [markdown] +# We can read one line at a time with `readline`: + +# %% +myfile.seek(0) +first = myfile.readline() + +# %% +first + +# %% +second = myfile.readline() + +# %% +second + +# %% [markdown] +# We can read the whole remaining file with `read`: + +# %% +rest = myfile.read() + +# %% +rest + +# %% [markdown] +# Which means that when a file is first opened, read is useful to just get the whole thing as a string: + +# %% +open('mydata.txt').read() + +# %% [markdown] +# You can also read just a few characters: + +# %% +myfile.seek(1335) + +# %% +myfile.read(15) + +# %% [markdown] +# ### Converting strings to files + +# %% [markdown] +# Because files and strings are different types, we CANNOT just treat strings as if they were files: + +# %% +mystring = "Hello World\n My name is James" + +# %% +mystring + +# %% +mystring.readline() + +# %% [markdown] +# This is important, because some file format parsers expect input from a **file** and not a string. +# We can convert between them using the `StringIO` class of the [io module](https://docs.python.org/3/library/io.html) in the standard library: + +# %% +from io import StringIO + +# %% +mystringasafile = StringIO(mystring) + +# %% +mystringasafile.readline() + +# %% +mystringasafile.readline() + +# %% [markdown] +# Note that in a string, `\n` is used to represent a newline. + +# %% [markdown] +# ### Closing files + +# %% [markdown] +# We really ought to close files when we've finished with them, as it makes our work more efficient and safer. (On a shared computer, +# this is particularly important) + +# %% +myfile.close() + +# %% [markdown] +# Because it's so easy to forget this, python provides a **context manager** to open a file, then close it automatically at +# the end of an indented block: + +# %% +with open('mydata.txt') as somefile: + content = somefile.read() + +content + +# %% [markdown] +# The code to be done while the file is open is indented, just like for an `if` statement. + +# %% [markdown] +# You should pretty much **always** use this syntax for working with files. +# We will see more about context managers in a [later chapter](../ch07dry/025Iterators.html). + +# %% [markdown] +# ### Writing files + +# %% [markdown] +# We might want to create a file from a string in memory. We can't do this with the notebook's `%%writefile` -- this is +# just a notebook convenience, and isn't very programmable. + +# %% [markdown] +# When we open a file, we can specify a 'mode', in this case, 'w' for writing. ('r' for reading is the default.) + +# %% +with open('mywrittenfile', 'w') as target: + target.write('Hello') + target.write('World') + +# %% +with open('mywrittenfile','r') as source: + print(source.read()) + +# %% [markdown] +# And we can "append" to a file with mode 'a': + +# %% +with open('mywrittenfile', 'a') as target: + target.write('Hello') + target.write('James') + +# %% +with open('mywrittenfile','r') as source: + print(source.read()) + +# %% [markdown] +# If a file already exists, mode 'w' will overwrite it. diff --git a/ch02data/061internet.html b/ch02data/061internet.html new file mode 100644 index 000000000..afad414cb --- /dev/null +++ b/ch02data/061internet.html @@ -0,0 +1,722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Internet + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Getting data from the Internet

+
+
+
+
+
+
+

We've seen about obtaining data from our local file system.

+
+
+
+
+
+
+

The other common place today that we might want to obtain data is from the internet.

+
+
+
+
+
+
+

It's very common today to treat the web as a source and store of information; we need to be able to programmatically +download data, and place it in Python objects.

+
+
+
+
+
+
+

We may also want to be able to programmatically upload data, for example, to automatically fill in forms.

+
+
+
+
+
+
+

This can be really powerful if we want to, for example, perform an automated meta-analysis across a selection of research papers.

+
+
+
+
+
+
+

URLs

+
+
+
+
+
+
+

All internet resources are defined by a Uniform Resource Locator.

+
+
+
+
+
+
In [1]:
+
+
+
"https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US"
+
+
+
+
+
+
+
+
Out[1]:
+
+
'https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US'
+
+
+
+
+
+
+
+
+

A url consists of:

+
    +
  • A scheme (http, https, ssh, ...)
  • +
  • A host (static-maps.yandex.ru, the name of the remote computer you want to talk to)
  • +
  • A port (optional, most protocols have a typical port associated with them, e.g. 80 for http, 443 for https)
  • +
  • A path (Like a file path on the machine, here it is 1.x/)
  • +
  • A query part after a ?, (optional, usually ampersand-separated parameters e.g. size=400x400, or z=10)
  • +
+
+
+
+
+
+
+

Supplementary materials: These can actually be different for different protocols, the above is a simplification. You can see more, for example, at +the wikipedia article about the URI scheme.

+
+
+
+
+
+
+

URLs are not allowed to include all characters; we need to, for example, "escape" a space that appears inside the URL, +replacing it with %20, so e.g. a request of http://some example.com/ would need to be http://some%20example.com/

+
+
+
+
+
+
+

Supplementary materials: The code used to replace each character is the ASCII code for it.

+
+
+
+
+
+
+

Supplementary materials: The escaping rules are quite subtle. See the wikipedia article for more detail. The standard library provides the urlencode function that can take care of this for you.

+
+
+
+
+
+
+

Requests

+
+
+
+
+
+
+

The python requests library can help us manage and manipulate URLs. It is easier to use than the urllib library that is part of the standard library, and is included with anaconda and canopy. It sorts out escaping, parameter encoding, and so on for us.

+
+
+
+
+
+
+

To request the above URL, for example, we write:

+
+
+
+
+
+
In [2]:
+
+
+
import requests
+
+
+
+
+
+
+
+
In [3]:
+
+
+
response = requests.get("https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US",
+                        params={
+                            'size': '400,400',
+                            'll': '-0.1275,51.51',
+                            'zoom': 10,
+                            'l': 'sat',
+                            'lang': 'en_US'
+                        })
+
+
+
+
+
+
+
+
In [4]:
+
+
+
response.content[0:50]
+
+
+
+
+
+
+
+
Out[4]:
+
+
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f'
+
+
+
+
+
+
+
+
+

When we do a request, the result comes back as text. For the png image in the above, this isn't very readable.

+
+
+
+
+
+
+

Just as for file access, therefore, we will need to send the text we get to a python module which understands that file format.

+
+
+
+
+
+
+

Again, it is important to separate the transport model (e.g. a file system, or an "http request" for the web) from the data model of the data that is returned.

+
+
+
+
+
+
+

Example: Sunspots

+
+
+
+
+
+
+

Let's try to get something scientific: the sunspot cycle data from SILSO:

+
+
+
+
+
+
In [5]:
+
+
+
spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text
+
+
+
+
+
+
+
+
In [6]:
+
+
+
spots[0:80]
+
+
+
+
+
+
+
+
Out[6]:
+
+
'1749;01;1749.042;  96.7; -1.0;   -1;1\n1749;02;1749.123; 104.3; -1.0;   -1;1\n1749'
+
+
+
+
+
+
+
+
+

This looks like semicolon-separated data, with different records on different lines. (Line separators come out as \n)

+
+
+
+
+
+
+

There are many many scientific datasets which can now be downloaded like this - integrating the download into your data +pipeline can help to keep your data flows organised.

+
+
+
+
+
+
+

Writing our own Parser

+
+
+
+
+
+
+

We'll need a python library to handle semicolon-separated data like the sunspot data.

+
+
+
+
+
+
+

You might be thinking: "But I can do that myself!":

+
+
+
+
+
+
In [7]:
+
+
+
lines = spots.split("\n")
+lines[0:5]
+
+
+
+
+
+
+
+
Out[7]:
+
+
['1749;01;1749.042;  96.7; -1.0;   -1;1',
+ '1749;02;1749.123; 104.3; -1.0;   -1;1',
+ '1749;03;1749.204; 116.7; -1.0;   -1;1',
+ '1749;04;1749.288;  92.8; -1.0;   -1;1',
+ '1749;05;1749.371; 141.7; -1.0;   -1;1']
+
+
+
+
+
+
+
+
In [8]:
+
+
+
years = [line.split(";")[0] for line in lines]
+
+
+
+
+
+
+
+
In [9]:
+
+
+
years[0:15]
+
+
+
+
+
+
+
+
Out[9]:
+
+
['1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1749',
+ '1750',
+ '1750',
+ '1750']
+
+
+
+
+
+
+
+
+

But don't: what if, for example, one of the records contains a separator inside it; most computers will put the content in quotes, +so that, for example,

+
"something; something"; something; something
+

has three fields, the first of which is

+
something; something
+

The naive code above would give four fields, of which the first is

+
"something
+
+
+
+
+
+
+

You'll never manage to get all that right; so you'll be better off using a library to do it.

+
+
+
+
+
+
+

Writing data to the internet

+
+
+
+
+
+
+

Note that we're using requests.get. get is used to receive data from the web. +You can also use post to fill in a web-form programmatically.

+
+
+
+
+
+
+

Supplementary material: Learn about using post with requests.

+
+
+
+
+
+
+

Supplementary material: Learn about the different kinds of http request: Get, Post, Put, Delete...

+
+
+
+
+
+
+

This can be used for all kinds of things, for example, to programmatically add data to a web resource. It's all well beyond +our scope for this course, but it's important to know it's possible, and start to think about the scientific possibilities.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/061internet.ipynb b/ch02data/061internet.ipynb new file mode 100644 index 000000000..415faa1dd --- /dev/null +++ b/ch02data/061internet.ipynb @@ -0,0 +1,400 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "69492042", + "metadata": {}, + "source": [ + "## Getting data from the Internet" + ] + }, + { + "cell_type": "markdown", + "id": "830ea901", + "metadata": {}, + "source": [ + "We've seen about obtaining data from our local file system." + ] + }, + { + "cell_type": "markdown", + "id": "1313e092", + "metadata": {}, + "source": [ + "The other common place today that we might want to obtain data is from the internet." + ] + }, + { + "cell_type": "markdown", + "id": "e3ce1b82", + "metadata": {}, + "source": [ + "It's very common today to treat the web as a source and store of information; we need to be able to programmatically\n", + "download data, and place it in Python objects." + ] + }, + { + "cell_type": "markdown", + "id": "5e77a72c", + "metadata": {}, + "source": [ + "We may also want to be able to programmatically *upload* data, for example, to automatically fill in forms." + ] + }, + { + "cell_type": "markdown", + "id": "8960f5ba", + "metadata": {}, + "source": [ + "This can be really powerful if we want to, for example, perform an automated meta-analysis across a selection of research papers." + ] + }, + { + "cell_type": "markdown", + "id": "e6516c8a", + "metadata": {}, + "source": [ + "### URLs" + ] + }, + { + "cell_type": "markdown", + "id": "47ed9888", + "metadata": {}, + "source": [ + "All internet resources are defined by a Uniform Resource Locator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d811237", + "metadata": {}, + "outputs": [], + "source": [ + "\"https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US\"" + ] + }, + { + "cell_type": "markdown", + "id": "8c1cba46", + "metadata": {}, + "source": [ + "A url consists of:\n", + "\n", + "* A *scheme* (`http`, `https`, `ssh`, ...)\n", + "* A *host* (`static-maps.yandex.ru`, the name of the remote computer you want to talk to)\n", + "* A *port* (optional, most protocols have a typical port associated with them, e.g. 80 for http, 443 for https)\n", + "* A *path* (Like a file path on the machine, here it is `1.x/`)\n", + "* A *query* part after a `?`, (optional, usually ampersand-separated *parameters* e.g. `size=400x400`, or `z=10`)" + ] + }, + { + "cell_type": "markdown", + "id": "94322e4c", + "metadata": {}, + "source": [ + "**Supplementary materials**: These can actually be different for different protocols, the above is a simplification. You can see more, for example, at\n", + " [the wikipedia article about the URI scheme](https://en.wikipedia.org/wiki/URI_scheme)." + ] + }, + { + "cell_type": "markdown", + "id": "e3edc0c1", + "metadata": {}, + "source": [ + "URLs are not allowed to include all characters; we need to, for example, \"escape\" a space that appears inside the URL,\n", + "replacing it with `%20`, so e.g. a request of `http://some example.com/` would need to be `http://some%20example.com/`\n" + ] + }, + { + "cell_type": "markdown", + "id": "e1e66af2", + "metadata": {}, + "source": [ + "**Supplementary materials**: The code used to replace each character is the [ASCII](http://www.asciitable.com) code for it." + ] + }, + { + "cell_type": "markdown", + "id": "1e293bb3", + "metadata": {}, + "source": [ + "**Supplementary materials**: The escaping rules are quite subtle. See [the wikipedia article for more detail](https://en.wikipedia.org/wiki/Percent-encoding). The standard library provides the [urlencode](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode) function that can take care of this for you. " + ] + }, + { + "cell_type": "markdown", + "id": "00b9db35", + "metadata": {}, + "source": [ + "### Requests" + ] + }, + { + "cell_type": "markdown", + "id": "122102c1", + "metadata": {}, + "source": [ + "The python [requests](http://docs.python-requests.org/en/latest/) library can help us manage and manipulate URLs. It is easier to use than the `urllib` library that is part of the standard library, and is included with anaconda and canopy. It sorts out escaping, parameter encoding, and so on for us." + ] + }, + { + "cell_type": "markdown", + "id": "dbb0b462", + "metadata": {}, + "source": [ + "To request the above URL, for example, we write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8a1ff16", + "metadata": {}, + "outputs": [], + "source": [ + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d679ccf3", + "metadata": {}, + "outputs": [], + "source": [ + "response = requests.get(\"https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US\",\n", + " params={\n", + " 'size': '400,400',\n", + " 'll': '-0.1275,51.51',\n", + " 'zoom': 10,\n", + " 'l': 'sat',\n", + " 'lang': 'en_US'\n", + " })" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c07cbed", + "metadata": {}, + "outputs": [], + "source": [ + "response.content[0:50]" + ] + }, + { + "cell_type": "markdown", + "id": "97c2ff76", + "metadata": {}, + "source": [ + "When we do a request, the result comes back as text. For the png image in the above, this isn't very readable." + ] + }, + { + "cell_type": "markdown", + "id": "ae2f487e", + "metadata": {}, + "source": [ + "Just as for file access, therefore, we will need to send the text we get to a python module which understands that file format." + ] + }, + { + "cell_type": "markdown", + "id": "885d4531", + "metadata": {}, + "source": [ + "Again, it is important to separate the *transport* model (e.g. a file system, or an \"http request\" for the web) from the data model of the data that is returned." + ] + }, + { + "cell_type": "markdown", + "id": "cc756622", + "metadata": {}, + "source": [ + "### Example: Sunspots" + ] + }, + { + "cell_type": "markdown", + "id": "ade30427", + "metadata": {}, + "source": [ + "Let's try to get something scientific: the sunspot cycle data from [SILSO](http://sidc.be/silso/home):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2c0b5a5", + "metadata": {}, + "outputs": [], + "source": [ + "spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce110ab5", + "metadata": {}, + "outputs": [], + "source": [ + "spots[0:80]" + ] + }, + { + "cell_type": "markdown", + "id": "59d12f81", + "metadata": {}, + "source": [ + "This looks like semicolon-separated data, with different records on different lines. (Line separators come out as `\\n`)" + ] + }, + { + "cell_type": "markdown", + "id": "08f13c93", + "metadata": {}, + "source": [ + "There are many many scientific datasets which can now be downloaded like this - integrating the download into your data\n", + "pipeline can help to keep your data flows organised." + ] + }, + { + "cell_type": "markdown", + "id": "e2188597", + "metadata": {}, + "source": [ + "### Writing our own Parser" + ] + }, + { + "cell_type": "markdown", + "id": "e395685f", + "metadata": {}, + "source": [ + "We'll need a python library to handle semicolon-separated data like the sunspot data." + ] + }, + { + "cell_type": "markdown", + "id": "545ce5a5", + "metadata": {}, + "source": [ + "You might be thinking: \"But I can do that myself!\":" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "688ebf7c", + "metadata": {}, + "outputs": [], + "source": [ + "lines = spots.split(\"\\n\")\n", + "lines[0:5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ff9c10e", + "metadata": {}, + "outputs": [], + "source": [ + "years = [line.split(\";\")[0] for line in lines]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d505493", + "metadata": {}, + "outputs": [], + "source": [ + "years[0:15]" + ] + }, + { + "cell_type": "markdown", + "id": "c95fc3f2", + "metadata": {}, + "source": [ + "But **don't**: what if, for example, one of the records contains a separator inside it; most computers will put the content in quotes,\n", + "so that, for example,\n", + "\n", + " \"something; something\"; something; something\n", + " \n", + "has three fields, the first of which is\n", + "\n", + " something; something\n", + " \n", + " The naive code above would give four fields, of which the first is \n", + " \n", + " \"something" + ] + }, + { + "cell_type": "markdown", + "id": "299461ab", + "metadata": {}, + "source": [ + "You'll never manage to get all that right; so you'll be better off using a library to do it." + ] + }, + { + "cell_type": "markdown", + "id": "777077cb", + "metadata": {}, + "source": [ + "### Writing data to the internet" + ] + }, + { + "cell_type": "markdown", + "id": "03d41413", + "metadata": {}, + "source": [ + "Note that we're using `requests.get`. `get` is used to receive data from the web.\n", + "You can also use `post` to fill in a web-form programmatically." + ] + }, + { + "cell_type": "markdown", + "id": "baa8ea9e", + "metadata": {}, + "source": [ + "**Supplementary material**: Learn about using `post` with [requests](http://docs.python-requests.org/en/latest/user/quickstart/)." + ] + }, + { + "cell_type": "markdown", + "id": "5093e694", + "metadata": {}, + "source": [ + "**Supplementary material**: Learn about the different kinds of [http request](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods): [Get, Post, Put, Delete](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)..." + ] + }, + { + "cell_type": "markdown", + "id": "9f06899d", + "metadata": {}, + "source": [ + "This can be used for all kinds of things, for example, to programmatically add data to a web resource. It's all well beyond\n", + "our scope for this course, but it's important to know it's possible, and start to think about the scientific possibilities." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Internet" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/061internet.ipynb.py b/ch02data/061internet.ipynb.py new file mode 100644 index 000000000..06ac2cc10 --- /dev/null +++ b/ch02data/061internet.ipynb.py @@ -0,0 +1,170 @@ +# --- +# jupyter: +# jekyll: +# display_name: Internet +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Getting data from the Internet + +# %% [markdown] +# We've seen about obtaining data from our local file system. + +# %% [markdown] +# The other common place today that we might want to obtain data is from the internet. + +# %% [markdown] +# It's very common today to treat the web as a source and store of information; we need to be able to programmatically +# download data, and place it in Python objects. + +# %% [markdown] +# We may also want to be able to programmatically *upload* data, for example, to automatically fill in forms. + +# %% [markdown] +# This can be really powerful if we want to, for example, perform an automated meta-analysis across a selection of research papers. + +# %% [markdown] +# ### URLs + +# %% [markdown] +# All internet resources are defined by a Uniform Resource Locator. + +# %% +"https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US" + +# %% [markdown] +# A url consists of: +# +# * A *scheme* (`http`, `https`, `ssh`, ...) +# * A *host* (`static-maps.yandex.ru`, the name of the remote computer you want to talk to) +# * A *port* (optional, most protocols have a typical port associated with them, e.g. 80 for http, 443 for https) +# * A *path* (Like a file path on the machine, here it is `1.x/`) +# * A *query* part after a `?`, (optional, usually ampersand-separated *parameters* e.g. `size=400x400`, or `z=10`) + +# %% [markdown] +# **Supplementary materials**: These can actually be different for different protocols, the above is a simplification. You can see more, for example, at +# [the wikipedia article about the URI scheme](https://en.wikipedia.org/wiki/URI_scheme). + +# %% [markdown] +# URLs are not allowed to include all characters; we need to, for example, "escape" a space that appears inside the URL, +# replacing it with `%20`, so e.g. a request of `http://some example.com/` would need to be `http://some%20example.com/` +# + +# %% [markdown] +# **Supplementary materials**: The code used to replace each character is the [ASCII](http://www.asciitable.com) code for it. + +# %% [markdown] +# **Supplementary materials**: The escaping rules are quite subtle. See [the wikipedia article for more detail](https://en.wikipedia.org/wiki/Percent-encoding). The standard library provides the [urlencode](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode) function that can take care of this for you. + +# %% [markdown] +# ### Requests + +# %% [markdown] +# The python [requests](http://docs.python-requests.org/en/latest/) library can help us manage and manipulate URLs. It is easier to use than the `urllib` library that is part of the standard library, and is included with anaconda and canopy. It sorts out escaping, parameter encoding, and so on for us. + +# %% [markdown] +# To request the above URL, for example, we write: + +# %% +import requests + +# %% +response = requests.get("https://static-maps.yandex.ru/1.x/?size=400,400&ll=-0.1275,51.51&z=10&l=sat&lang=en_US", + params={ + 'size': '400,400', + 'll': '-0.1275,51.51', + 'zoom': 10, + 'l': 'sat', + 'lang': 'en_US' + }) + +# %% +response.content[0:50] + +# %% [markdown] +# When we do a request, the result comes back as text. For the png image in the above, this isn't very readable. + +# %% [markdown] +# Just as for file access, therefore, we will need to send the text we get to a python module which understands that file format. + +# %% [markdown] +# Again, it is important to separate the *transport* model (e.g. a file system, or an "http request" for the web) from the data model of the data that is returned. + +# %% [markdown] +# ### Example: Sunspots + +# %% [markdown] +# Let's try to get something scientific: the sunspot cycle data from [SILSO](http://sidc.be/silso/home): + +# %% +spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text + +# %% +spots[0:80] + +# %% [markdown] +# This looks like semicolon-separated data, with different records on different lines. (Line separators come out as `\n`) + +# %% [markdown] +# There are many many scientific datasets which can now be downloaded like this - integrating the download into your data +# pipeline can help to keep your data flows organised. + +# %% [markdown] +# ### Writing our own Parser + +# %% [markdown] +# We'll need a python library to handle semicolon-separated data like the sunspot data. + +# %% [markdown] +# You might be thinking: "But I can do that myself!": + +# %% +lines = spots.split("\n") +lines[0:5] + +# %% +years = [line.split(";")[0] for line in lines] + +# %% +years[0:15] + +# %% [markdown] +# But **don't**: what if, for example, one of the records contains a separator inside it; most computers will put the content in quotes, +# so that, for example, +# +# "something; something"; something; something +# +# has three fields, the first of which is +# +# something; something +# +# The naive code above would give four fields, of which the first is +# +# "something + +# %% [markdown] +# You'll never manage to get all that right; so you'll be better off using a library to do it. + +# %% [markdown] +# ### Writing data to the internet + +# %% [markdown] +# Note that we're using `requests.get`. `get` is used to receive data from the web. +# You can also use `post` to fill in a web-form programmatically. + +# %% [markdown] +# **Supplementary material**: Learn about using `post` with [requests](http://docs.python-requests.org/en/latest/user/quickstart/). + +# %% [markdown] +# **Supplementary material**: Learn about the different kinds of [http request](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods): [Get, Post, Put, Delete](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)... + +# %% [markdown] +# This can be used for all kinds of things, for example, to programmatically add data to a web resource. It's all well beyond +# our scope for this course, but it's important to know it's possible, and start to think about the scientific possibilities. diff --git a/ch02data/062csv.html b/ch02data/062csv.html new file mode 100644 index 000000000..095fad577 --- /dev/null +++ b/ch02data/062csv.html @@ -0,0 +1,1296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSV + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Field and Record Data (Tabular data)

Tabular data, that is data that is formatted as a table with a fixed number of rows and columns, is very common in a research context. A particularly simple and also popular file format for such data is delimited-separated value files.

+
+
+
+
+
+
+

Delimiter-separated values

+
+
+
+
+
+
+

Let's carry on with our sunspots example. As we saw previously the data is semicolon-separated.

+

We can request the CSV file text from the URL we used previously:

+
+
+
+
+
+
In [1]:
+
+
+
import requests
+# Request sunspots data from URL and extract response content as text
+sunspots_csv_text = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text
+# Strip any leading or trailing whitespace from CSV text to skip any empty rows
+sunspots_csv_text = sunspots_csv_text.strip()
+
+
+
+
+
+
+
+
+

As a quick check we can split the text on the newline character \n and print the last five entries in the resulting list

+
+
+
+
+
+
In [2]:
+
+
+
print(sunspots_csv_text.split('\n')[-5:])
+
+
+
+
+
+
+
+
+
+
['2023;06;2023.453; 160.5; 20.0; 1248;1', '2023;07;2023.538; 159.1; 17.3; 1039;0', '2023;08;2023.623; 114.8; 15.4; 1095;0', '2023;09;2023.705; 133.6; 17.6; 1140;0', '2023;10;2023.790;  99.4; 16.0;  958;0']
+
+
+
+
+
+
+
+
+
+

We see that each line is a string with numeric calues separted by semicolon delimiters. We want to work programmatically with such delimited-separated value files.

+
+
+
+
+
+
+

These are files which (typically) have

+
    +
  • One record per line (row)
  • +
  • Each record has multiple fields (columns)
  • +
  • Fields are separated by some delimiter
  • +
+
+
+
+
+
+
+

Typical separators are the space, tab, comma, and semicolon, leading to correspondingly-named file formats, e.g.:

+
    +
  • Space-separated value (e.g. field1 "field two" field3 )
  • +
  • Comma-separated value (e.g. field1, another field, "wow, another field")
  • +
+
+
+
+
+
+
+

Comma-separated value is abbreviated CSV, and tab-separated value TSV.

+
+
+
+
+
+
+

CSV is also sometimes used to refer to all the different sub-kinds of separated value files, i.e. some people use CSV to refer to tab-, space- and semicolon-separated files.

+
+
+
+
+
+
+

CSV is not a particularly superb data format, because it forces your data model to only have two 'axes', records and fields, with each record a flat object. As we will see in the next notebook, structured file formats can be used to represent a richer array of data formats, including for example hierarchically structured data where each record may itself have an internal structure.

+
+
+
+
+
+
+

Nevertheless, because you can always export spreadsheets as CSV files (each cell is a field, each row is a record), CSV files are very popular.

+
+
+
+
+
+
+

CSV variants

+
+
+
+
+
+
+

Some CSV formats define a comment character, so that rows beginning with, e.g., a #, are not treated as data, but give a human comment.

+
+
+
+
+
+
+

Some CSV formats define a three-deep list structure, where a double-newline separates records into blocks.

+
+
+
+
+
+
+

Some CSV formats assume that the first line (also called a header) defines the names of the fields, e.g.:

+
name, age
+James, 39
+Will, 2
+
+
+
+
+
+
+
+

Python csv module

+
+
+
+
+
+
+

The Python standard library provides a csv module for reading and writing delimited-separated value files, including, as the name suggests, CSV files. As it is built-in to all Python installations, it is useful to be familiar with the csv module as an option for loading and saving CSV formatted data, though the CSV capabilities in third-party libraries such as NumPy (which we will cover later in the course) and Pandas are more powerful and will often be better options in practice.

+
+
+
+
+
+
In [3]:
+
+
+
import csv
+
+
+
+
+
+
+
+
+

The most straightforward way to read CSV files using the csv module is with the csv.reader function. This accepts an iterable object as its first argument which returns a line of delimited input for each iteration. Commonly this will be an opened file object however it can also for example be a sequence of strings which is what we will use here by using the split method to convert the sunspot CSV text object into a list of per-line strings. The csv.reader function also accepts various optional keyword arguments including importantly a delimiter argument to specify the character used as the delimiter separating the values in each line, with we setting this to a semicolon here.

+
+
+
+
+
+
In [4]:
+
+
+
csv_reader = csv.reader(sunspots_csv_text.split('\n'), delimiter=';')
+
+
+
+
+
+
+
+
+

The object returned by the csv.reader function is an iterator over the rows of the CSV file, with each row being returned as a list of the separated values in the row (with all values being read as strings by default). We can read all of the data in to a nested list-of-lists using a list comprehension:

+
+
+
+
+
+
In [5]:
+
+
+
sunspots_data = [row for row in csv_reader]
+print(sunspots_data[-5:])
+
+
+
+
+
+
+
+
+
+
[['2023', '06', '2023.453', ' 160.5', ' 20.0', ' 1248', '1'], ['2023', '07', '2023.538', ' 159.1', ' 17.3', ' 1039', '0'], ['2023', '08', '2023.623', ' 114.8', ' 15.4', ' 1095', '0'], ['2023', '09', '2023.705', ' 133.6', ' 17.6', ' 1140', '0'], ['2023', '10', '2023.790', '  99.4', ' 16.0', '  958', '0']]
+
+
+
+
+
+
+
+
+
+

For this particular CSV file the first column corresponds to the measurement year, the second the measurement month number and the third the measurement date as a 'fractional year'. We can extract a list of the just the (fractional) years converted to floating point values using another list comprehension

+
+
+
+
+
+
In [6]:
+
+
+
fractional_years = [float(row[2]) for row in sunspots_data]
+
+
+
+
+
+
+
+
+

Similarly the fourth column in the CSV file contains the monthly mean total sunspot number, which we can extract with another list comprehension

+
+
+
+
+
+
In [7]:
+
+
+
monthly_mean_total_sunspot_numbers = [float(row[3]) for row in sunspots_data]
+
+
+
+
+
+
+
+
+

We can then for example use Matplotlib to create a plot of how the monthly average sunspot number varies over time, with this highlighting the cyclic nature of sunspot activity

+
+
+
+
+
+
In [8]:
+
+
+
from matplotlib import pyplot as plt
+plt.plot(fractional_years, monthly_mean_total_sunspot_numbers)
+plt.xlabel('Year')
+plt.ylabel('Monthly mean total sunspot number');
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Reading rows as dictionaries

+
+
+
+
+
+
+

Accessing the values in each row by an index corresponding to their column can be unclear and prone to bugs. The csv module also provides the csv.DictReader class to allow reading each record (line) in the CSV file as a dictionary keyed by a set of field names. For the dataset we are using we have the columns correspond to

+
Column 1-2: Gregorian calendar date
+- Year
+- Month
+Column 3: Date in fraction of year.
+Column 4: Monthly mean total sunspot number.
+Column 5: Monthly mean standard deviation of the input sunspot numbers.
+Column 6: Number of observations used to compute the monthly mean total sunspot number.
+Column 7: Definitive/provisional marker. '1' indicates that the value is definitive. '0' indicates that the value is still provisional.
+
+

We can create an instance of the csv.DictReader class very similarly to how we called the csv.reader function, but with an additional keyword argument fieldnames specifying a sequence of strings corresponding to the keys to associate the values in each row with. Below we also set the optional keyword argument quoting to the special constant csv.QUOTE_NONNUMERIC which causes all non-quoted values in each line to be automatically converted to floating point values.

+
+
+
+
+
+
In [9]:
+
+
+
csv_reader = csv.DictReader(
+    sunspots_csv_text.split('\n'),
+    fieldnames=['year', 'month', 'fractional_year', 'mean', 'deviation', 'observations', 'definitive'],
+    delimiter=';',
+    quoting=csv.QUOTE_NONNUMERIC
+)
+
+
+
+
+
+
+
+
+

Similarly to previously we can now extract all the data using a list comprehension, with the difference being that each item in the constructed list is now a dictionary keyed by the field names:

+
+
+
+
+
+
In [10]:
+
+
+
sunspots_data = [record for record in csv_reader]
+print(sunspots_data[-1])
+
+
+
+
+
+
+
+
+
+
{'year': 2023.0, 'month': 10.0, 'fractional_year': 2023.79, 'mean': 99.4, 'deviation': 16.0, 'observations': 958.0, 'definitive': 0.0}
+
+
+
+
+
+
+
+
+
+

We can then recreate the same plot as previously as follows, with the intention of the list comprehensions extracting the year and mean values now much more apparent

+
+
+
+
+
+
In [11]:
+
+
+
from matplotlib import pyplot as plt
+
+plt.plot([r['fractional_year'] for r in sunspots_data], [r['mean'] for r in sunspots_data])
+plt.xlabel('Year')
+plt.ylabel('Monthly mean total sunspot number');
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Writing CSV files

The csv module also provides functionality for writing delimiter-separated value files. The csv.writer function provides a simple interface for writing CSV files row by row, with the function accepting a file object (and optional keyword arugments specifying formatting options such as delimiter) and the returned object providing a writerow method to write a sequence of values to the file as a delimiter-separated string. For example we can save a table of data about the planets in the solar system as a comma-separated values file using the following code snippet

+
+
+
+
+
+
In [12]:
+
+
+
planets_data = [
+    ['Name', 'Mean distance from sun / AU', 'Orbit period / years', 'Mass / M🜨', 'Radius / R🜨', 'Number of satellites'],
+    ['Mercury', 0.39, 0.24, 0.06, 0.38, 0],
+    ['Venus', 0.72, 0.62, 0.82, 0.95, 0],
+    ['Earth', 1., 1., 1., 1., 1],
+    ['Mars', 1.5, 1.9, 0.11, 0.53, 2],
+    ['Jupiter', 5.2, 12., 320., 11., 63],
+    ['Saturn', 9.5, 29., 95., 9.4, 61],
+    ['Uranus', 19., 84., 15., 4.1, 27],
+    ['Neptune', 30., 170., 17., 3.9, 14],
+]
+
+with open('planets_data.csv', 'w', encoding='utf-8') as f:
+    csv_writer = csv.writer(f, delimiter=',')
+    for row in planets_data:
+        csv_writer.writerow(row)
+
+
+
+
+
+
+
+
+

A csv.DictWriter class is also provided which analogously to the csv.DictReader class, allows writing a CSV file using rows specified by dictionaries mapping from field names to values.

+
+
+
+
+
+
+

NumPy's CSV readers

+
+
+
+
+
+
+

The Python standard library csv module seen in the preceding section is less powerful than the CSV capabilities in NumPy, +the main scientific Python library for handling data. NumPy is distributed with Anaconda and Canopy, so we recommend use that when available.

+
+
+
+
+
+
+

NumPy has powerful capabilities for handling matrices, and other fun stuff, and we'll learn about these later in the course, +but for now, we'll use NumPy's CSV reader, and assume it gives us lists and dictionaries, rather than its more exciting array type.

+
+
+
+
+
+
In [13]:
+
+
+
import numpy as np
+
+
+
+
+
+
+
+
In [14]:
+
+
+
spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)
+
+
+
+
+
+
+
+
+

stream=True delays loading all of the data until it is required.

+
+
+
+
+
+
In [15]:
+
+
+
sunspots = np.genfromtxt(spots.raw, delimiter=';')
+
+
+
+
+
+
+
+
+

genfromtxt is a powerful CSV reader. We used the delimiter optional argument to specify the delimeter. We could also specify +names=True if we had a first line naming fields, and comments=# if we had comment lines.

+
+
+
+
+
+
In [16]:
+
+
+
sunspots[0][3]
+
+
+
+
+
+
+
+
Out[16]:
+
+
96.7
+
+
+
+
+
+
+
+
+

As before, we can now plot the "Sunspot cycle", note how we can specify the column directly from the data we've read.

+
+
+
+
+
+
In [17]:
+
+
+
%matplotlib inline
+
+from matplotlib import pyplot as plt
+plt.plot(sunspots[:,2], sunspots[:,3]) # Numpy syntax to access all 
+                                       # rows, specified column.
+
+
+
+
+
+
+
+
Out[17]:
+
+
[<matplotlib.lines.Line2D at 0x7f5e7afedd00>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The plot command accepted an array of 'X' values and an array of 'Y' values. We used a special NumPy ":" syntax, +which we'll learn more about later. Don't worry about the %matplotlib magic command for now - we'll also look at this later.

+
+
+
+
+
+
+

genfromtxt also allows naming the columns. Similarly of what we've done with the csv.DictReader. We do that by specifying the column information to the formatter:

+
+
+
+
+
+
In [18]:
+
+
+
spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)
+
+sunspots = np.genfromtxt(spots.raw, delimiter=';', 
+                         names=['year','month','date',
+                         'mean','deviation','observations','definitive'])
+
+
+
+
+
+
+
+
In [19]:
+
+
+
sunspots
+
+
+
+
+
+
+
+
Out[19]:
+
+
array([(1749.,  1., 1749.042,  96.7, -1. , -1.000e+00, 1.),
+       (1749.,  2., 1749.123, 104.3, -1. , -1.000e+00, 1.),
+       (1749.,  3., 1749.204, 116.7, -1. , -1.000e+00, 1.), ...,
+       (2023.,  8., 2023.623, 114.8, 15.4,  1.095e+03, 0.),
+       (2023.,  9., 2023.705, 133.6, 17.6,  1.140e+03, 0.),
+       (2023., 10., 2023.79 ,  99.4, 16. ,  9.580e+02, 0.)],
+      dtype=[('year', '<f8'), ('month', '<f8'), ('date', '<f8'), ('mean', '<f8'), ('deviation', '<f8'), ('observations', '<f8'), ('definitive', '<f8')])
+
+
+
+
+
+
+
+
+

Going a step further, as we know what's expected on each column, it's then good to specify the datatype of each field.

+
+
+
+
+
+
In [20]:
+
+
+
spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)
+
+sunspots = np.genfromtxt(spots.raw, delimiter=';', 
+                         names=['year','month','date',
+                         'mean','deviation','observations','definitive'],
+                         dtype=[int, int, float, float, float, int, int])
+
+
+
+
+
+
+
+
In [21]:
+
+
+
sunspots
+
+
+
+
+
+
+
+
Out[21]:
+
+
array([(1749,  1, 1749.042,  96.7, -1. ,   -1, 1),
+       (1749,  2, 1749.123, 104.3, -1. ,   -1, 1),
+       (1749,  3, 1749.204, 116.7, -1. ,   -1, 1), ...,
+       (2023,  8, 2023.623, 114.8, 15.4, 1095, 0),
+       (2023,  9, 2023.705, 133.6, 17.6, 1140, 0),
+       (2023, 10, 2023.79 ,  99.4, 16. ,  958, 0)],
+      dtype=[('year', '<i8'), ('month', '<i8'), ('date', '<f8'), ('mean', '<f8'), ('deviation', '<f8'), ('observations', '<i8'), ('definitive', '<i8')])
+
+
+
+
+
+
+
+
+

Now, NumPy understands the names of the columns, so our plot command is more readable:

+
+
+
+
+
+
In [22]:
+
+
+
sunspots['year']
+
+
+
+
+
+
+
+
Out[22]:
+
+
array([1749, 1749, 1749, ..., 2023, 2023, 2023])
+
+
+
+
+
+
+
+
In [23]:
+
+
+
plt.plot(sunspots['year'], sunspots['mean'])
+
+
+
+
+
+
+
+
Out[23]:
+
+
[<matplotlib.lines.Line2D at 0x7f5ea8fd8910>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Pandas - a more powerful CSV reader

+
+
+
+
+
+
+

If most of your work is going to be working with CSV files, then, most probably you will enjoy the powers that pandas provide. Whereas numpy uses arrays, pandas work with DataFrames, that's how they name to their representation of data in a table (2D arrays).

+

Let's see how we would use it for this example.

+
+
+
+
+
+
In [24]:
+
+
+
import pandas as pd
+
+spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)
+
+sunspots = pd.read_csv(spots.raw, delimiter=';',
+                       names=['year','month','date',
+                               'mean','deviation','observations','definitive'])
+sunspots
+
+
+
+
+
+
+
+
Out[24]:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
yearmonthdatemeandeviationobservationsdefinitive
0174911749.04296.7-1.0-11
1174921749.123104.3-1.0-11
2174931749.204116.7-1.0-11
3174941749.28892.8-1.0-11
4174951749.371141.7-1.0-11
........................
3293202362023.453160.520.012481
3294202372023.538159.117.310390
3295202382023.623114.815.410950
3296202392023.705133.617.611400
32972023102023.79099.416.09580
+

3298 rows × 7 columns

+
+
+
+
+
+
+
+
+

In pandas, then you can access to the data of their columns as with a dictionaries.

+
+
+
+
+
+
In [25]:
+
+
+
sunspots['year']
+
+
+
+
+
+
+
+
Out[25]:
+
+
0       1749
+1       1749
+2       1749
+3       1749
+4       1749
+        ... 
+3293    2023
+3294    2023
+3295    2023
+3296    2023
+3297    2023
+Name: year, Length: 3298, dtype: int64
+
+
+
+
+
+
+
+
+

And ploting the data is directly available from the object.

+
+
+
+
+
+
In [26]:
+
+
+
sunspots.plot('year', 'mean')
+
+
+
+
+
+
+
+
Out[26]:
+
+
<Axes: xlabel='year'>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Note how, automatically, pandas sets the axis labels and the legend. We will learn how to set these up later in this chapter.

+
+
+
+
+
+
+

You can learn more about pandas with the Software carpentry's Plotting and Programming with Python lesson.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/062csv.ipynb b/ch02data/062csv.ipynb new file mode 100644 index 000000000..df31a23eb --- /dev/null +++ b/ch02data/062csv.ipynb @@ -0,0 +1,718 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "247623ba", + "metadata": {}, + "source": [ + "## Field and Record Data (Tabular data)\n", + "\n", + "Tabular data, that is data that is formatted as a table with a fixed number of rows and columns, is very common in a research context. A particularly simple and also popular file format for such data is [_delimited-separated value_ files](https://en.wikipedia.org/wiki/Delimiter-separated_values)." + ] + }, + { + "cell_type": "markdown", + "id": "e4f459e4", + "metadata": {}, + "source": [ + "## Delimiter-separated values" + ] + }, + { + "cell_type": "markdown", + "id": "8ad31bb4", + "metadata": {}, + "source": [ + "Let's carry on with our sunspots example. As we saw previously the data is semicolon-separated. \n", + "\n", + "We can request the CSV file text from the URL we used previously:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1739cdf7", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "# Request sunspots data from URL and extract response content as text\n", + "sunspots_csv_text = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text\n", + "# Strip any leading or trailing whitespace from CSV text to skip any empty rows\n", + "sunspots_csv_text = sunspots_csv_text.strip()" + ] + }, + { + "cell_type": "markdown", + "id": "521f8bf2", + "metadata": {}, + "source": [ + "As a quick check we can split the text on the newline character `\\n` and print the last five entries in the resulting list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9cd8475", + "metadata": {}, + "outputs": [], + "source": [ + "print(sunspots_csv_text.split('\\n')[-5:])" + ] + }, + { + "cell_type": "markdown", + "id": "74eff84e", + "metadata": {}, + "source": [ + "We see that each line is a string with numeric calues separted by semicolon delimiters. We want to work programmatically with such *delimited-separated value* files." + ] + }, + { + "cell_type": "markdown", + "id": "13fe1c45", + "metadata": {}, + "source": [ + "These are files which (typically) have\n", + "\n", + "* One *record* per line (row)\n", + "* Each record has multiple *fields* (columns)\n", + "* Fields are separated by some *delimiter*" + ] + }, + { + "cell_type": "markdown", + "id": "93844312", + "metadata": {}, + "source": [ + "Typical separators are the `space`, `tab`, `comma`, and `semicolon`, leading to correspondingly-named file formats, e.g.:\n", + "\n", + "* Space-separated value (e.g. `field1 \"field two\" field3` )\n", + "* Comma-separated value (e.g. `field1, another field, \"wow, another field\"`)" + ] + }, + { + "cell_type": "markdown", + "id": "77cfc6b1", + "metadata": {}, + "source": [ + "Comma-separated value is abbreviated CSV, and tab-separated value TSV." + ] + }, + { + "cell_type": "markdown", + "id": "a78bee1f", + "metadata": {}, + "source": [ + "CSV is also sometimes used to refer to all the different sub-kinds of separated value files, i.e. some people use CSV to refer to tab-, space- and semicolon-separated files." + ] + }, + { + "cell_type": "markdown", + "id": "00fadf85", + "metadata": {}, + "source": [ + "CSV is not a particularly superb data format, because it forces your data model to only have two 'axes', records and fields, with each record a flat object. As we will see in the next notebook, structured file formats can be used to represent a richer array of data formats, including for example hierarchically structured data where each record may itself have an internal structure." + ] + }, + { + "cell_type": "markdown", + "id": "faf92eeb", + "metadata": {}, + "source": [ + "Nevertheless, because you can always export *spreadsheets* as CSV files (each cell is a field, each row is a record), CSV files are very popular. " + ] + }, + { + "cell_type": "markdown", + "id": "ffb71623", + "metadata": {}, + "source": [ + "### CSV variants" + ] + }, + { + "cell_type": "markdown", + "id": "54bca7f9", + "metadata": {}, + "source": [ + "Some CSV formats define a comment character, so that rows beginning with, e.g., a `#`, are not treated as data, but give a human comment." + ] + }, + { + "cell_type": "markdown", + "id": "8b57f8bf", + "metadata": {}, + "source": [ + "Some CSV formats define a three-deep list structure, where a double-newline separates records into blocks." + ] + }, + { + "cell_type": "markdown", + "id": "d1a6ef66", + "metadata": {}, + "source": [ + "Some CSV formats assume that the first line (also called a header) defines the names of the fields, e.g.:\n", + "\n", + "```\n", + "name, age\n", + "James, 39\n", + "Will, 2\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a616d708", + "metadata": {}, + "source": [ + "### Python `csv` module" + ] + }, + { + "cell_type": "markdown", + "id": "5a1f8c2d", + "metadata": {}, + "source": [ + "The Python standard library provides a `csv` module for reading and writing delimited-separated value files, including, as the name suggests, CSV files. As it is built-in to all Python installations, it is useful to be familiar with the `csv` module as an option for loading and saving CSV formatted data, though the CSV capabilities in third-party libraries such as [NumPy](https://numpy.org/doc/stable/reference/generated/numpy.loadtxt.html) (which we will cover later in the course) and [Pandas](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html) are more powerful and will often be better options in practice." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a07b7d5", + "metadata": {}, + "outputs": [], + "source": [ + "import csv" + ] + }, + { + "cell_type": "markdown", + "id": "a10278bb", + "metadata": {}, + "source": [ + "The most straightforward way to read CSV files using the `csv` module is with the [`csv.reader` function](https://docs.python.org/3/library/csv.html#csv.reader). This accepts an _iterable_ object as its first argument which returns a line of delimited input for each iteration. Commonly this will be an opened file object however it can also for example be a sequence of strings which is what we will use here by using the `split` method to convert the sunspot CSV text object into a list of per-line strings. The `csv.reader` function also accepts various optional keyword arguments including importantly a `delimiter` argument to specify the character used as the delimiter separating the values in each line, with we setting this to a semicolon here. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d16e410e", + "metadata": {}, + "outputs": [], + "source": [ + "csv_reader = csv.reader(sunspots_csv_text.split('\\n'), delimiter=';')" + ] + }, + { + "cell_type": "markdown", + "id": "c95f925e", + "metadata": {}, + "source": [ + "The object returned by the `csv.reader` function is an iterator over the rows of the CSV file, with each row being returned as a list of the separated values in the row (with all values being read as strings by default). We can read all of the data in to a nested list-of-lists using a list comprehension:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d5cd0fe", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots_data = [row for row in csv_reader]\n", + "print(sunspots_data[-5:])" + ] + }, + { + "cell_type": "markdown", + "id": "7cca44d9", + "metadata": {}, + "source": [ + "For this particular CSV file the first column corresponds to the measurement year, the second the measurement month number and the third the measurement date as a 'fractional year'. We can extract a list of the just the (fractional) years converted to floating point values using another list comprehension" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c0d228d", + "metadata": {}, + "outputs": [], + "source": [ + "fractional_years = [float(row[2]) for row in sunspots_data]" + ] + }, + { + "cell_type": "markdown", + "id": "ee1de144", + "metadata": {}, + "source": [ + "Similarly the fourth column in the CSV file contains the monthly mean total sunspot number, which we can extract with another list comprehension" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca67474c", + "metadata": {}, + "outputs": [], + "source": [ + "monthly_mean_total_sunspot_numbers = [float(row[3]) for row in sunspots_data]" + ] + }, + { + "cell_type": "markdown", + "id": "7bd491ae", + "metadata": {}, + "source": [ + "We can then for example use Matplotlib to create a plot of how the monthly average sunspot number varies over time, with this highlighting [the cyclic nature of sunspot activity](https://en.wikipedia.org/wiki/Solar_cycle)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84176a06", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "plt.plot(fractional_years, monthly_mean_total_sunspot_numbers)\n", + "plt.xlabel('Year')\n", + "plt.ylabel('Monthly mean total sunspot number');" + ] + }, + { + "cell_type": "markdown", + "id": "c401766e", + "metadata": {}, + "source": [ + "#### Reading rows as dictionaries" + ] + }, + { + "cell_type": "markdown", + "id": "8af77823", + "metadata": {}, + "source": [ + "Accessing the values in each row by an index corresponding to their column can be unclear and prone to bugs. The `csv` module also provides the `csv.DictReader` class to allow reading each record (line) in the CSV file as a dictionary keyed by a set of field names. For the dataset we are using [we have the columns correspond to](http://www.sidc.be/silso/infosnmtot)\n", + "\n", + "```\n", + "Column 1-2: Gregorian calendar date\n", + "- Year\n", + "- Month\n", + "Column 3: Date in fraction of year.\n", + "Column 4: Monthly mean total sunspot number.\n", + "Column 5: Monthly mean standard deviation of the input sunspot numbers.\n", + "Column 6: Number of observations used to compute the monthly mean total sunspot number.\n", + "Column 7: Definitive/provisional marker. '1' indicates that the value is definitive. '0' indicates that the value is still provisional.\n", + "```\n", + "\n", + "We can create an instance of the `csv.DictReader` class very similarly to how we called the `csv.reader` function, but with an additional keyword argument `fieldnames` specifying a sequence of strings corresponding to the keys to associate the values in each row with. Below we also set the optional keyword argument `quoting` to the special constant `csv.QUOTE_NONNUMERIC` which causes all non-quoted values in each line to be automatically converted to floating point values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26cc7319", + "metadata": {}, + "outputs": [], + "source": [ + "csv_reader = csv.DictReader(\n", + " sunspots_csv_text.split('\\n'),\n", + " fieldnames=['year', 'month', 'fractional_year', 'mean', 'deviation', 'observations', 'definitive'],\n", + " delimiter=';',\n", + " quoting=csv.QUOTE_NONNUMERIC\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7b385382", + "metadata": {}, + "source": [ + "Similarly to previously we can now extract all the data using a list comprehension, with the difference being that each item in the constructed list is now a dictionary keyed by the field names:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35988de2", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots_data = [record for record in csv_reader]\n", + "print(sunspots_data[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "8b41871b", + "metadata": {}, + "source": [ + "We can then recreate the same plot as previously as follows, with the intention of the list comprehensions extracting the year and mean values now much more apparent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9adb0a5", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "plt.plot([r['fractional_year'] for r in sunspots_data], [r['mean'] for r in sunspots_data])\n", + "plt.xlabel('Year')\n", + "plt.ylabel('Monthly mean total sunspot number');" + ] + }, + { + "cell_type": "markdown", + "id": "1628bf68", + "metadata": {}, + "source": [ + "#### Writing CSV files\n", + "\n", + "The `csv` module also provides functionality for writing delimiter-separated value files. The `csv.writer` function provides a simple interface for writing CSV files row by row, with the function accepting a file object (and optional keyword arugments specifying formatting options such as `delimiter`) and the returned object providing a `writerow` method to write a sequence of values to the file as a delimiter-separated string. For example we can save a table of data about the planets in the solar system as a comma-separated values file using the following code snippet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07e89c1c", + "metadata": {}, + "outputs": [], + "source": [ + "planets_data = [\n", + " ['Name', 'Mean distance from sun / AU', 'Orbit period / years', 'Mass / M🜨', 'Radius / R🜨', 'Number of satellites'],\n", + " ['Mercury', 0.39, 0.24, 0.06, 0.38, 0],\n", + " ['Venus', 0.72, 0.62, 0.82, 0.95, 0],\n", + " ['Earth', 1., 1., 1., 1., 1],\n", + " ['Mars', 1.5, 1.9, 0.11, 0.53, 2],\n", + " ['Jupiter', 5.2, 12., 320., 11., 63],\n", + " ['Saturn', 9.5, 29., 95., 9.4, 61],\n", + " ['Uranus', 19., 84., 15., 4.1, 27],\n", + " ['Neptune', 30., 170., 17., 3.9, 14],\n", + "]\n", + "\n", + "with open('planets_data.csv', 'w', encoding='utf-8') as f:\n", + " csv_writer = csv.writer(f, delimiter=',')\n", + " for row in planets_data:\n", + " csv_writer.writerow(row)" + ] + }, + { + "cell_type": "markdown", + "id": "ef8650e9", + "metadata": {}, + "source": [ + "A [`csv.DictWriter`](https://docs.python.org/3/library/csv.html#csv.DictWriter) class is also provided which analogously to the `csv.DictReader` class, allows writing a CSV file using rows specified by dictionaries mapping from field names to values." + ] + }, + { + "cell_type": "markdown", + "id": "ba433fe2", + "metadata": {}, + "source": [ + "### NumPy's CSV readers" + ] + }, + { + "cell_type": "markdown", + "id": "e6727db9", + "metadata": {}, + "source": [ + "The Python standard library `csv` module seen in the preceding section is less powerful than the CSV capabilities in NumPy,\n", + "the main scientific Python library for handling data. NumPy is distributed with Anaconda and Canopy, so we recommend use that when available." + ] + }, + { + "cell_type": "markdown", + "id": "ef94c04d", + "metadata": {}, + "source": [ + "NumPy has powerful capabilities for handling matrices, and other fun stuff, and we'll learn about these later in the course,\n", + "but for now, we'll use NumPy's CSV reader, and assume it gives us lists and dictionaries, rather than its more exciting `array` type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f02752de", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d443716", + "metadata": {}, + "outputs": [], + "source": [ + "spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)" + ] + }, + { + "cell_type": "markdown", + "id": "005d8190", + "metadata": {}, + "source": [ + "`stream=True` delays loading all of the data until it is required." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97238dd2", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots = np.genfromtxt(spots.raw, delimiter=';')" + ] + }, + { + "cell_type": "markdown", + "id": "315ecd8d", + "metadata": {}, + "source": [ + "`genfromtxt` is a powerful CSV reader. We used the `delimiter` optional argument to specify the delimeter. We could also specify\n", + "`names=True` if we had a first line naming fields, and `comments=#` if we had comment lines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "307349d6", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots[0][3]" + ] + }, + { + "cell_type": "markdown", + "id": "7df472f4", + "metadata": {}, + "source": [ + "As before, we can now plot the \"Sunspot cycle\", note how we can specify the column directly from the data we've read." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82391d26", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "from matplotlib import pyplot as plt\n", + "plt.plot(sunspots[:,2], sunspots[:,3]) # Numpy syntax to access all \n", + " # rows, specified column." + ] + }, + { + "cell_type": "markdown", + "id": "b42e2f30", + "metadata": {}, + "source": [ + "The plot command accepted an array of 'X' values and an array of 'Y' values. We used a special NumPy \"`:`\" syntax,\n", + "which we'll learn more about later. Don't worry about the `%matplotlib` magic command for now - we'll also look at this later." + ] + }, + { + "cell_type": "markdown", + "id": "620430b8", + "metadata": {}, + "source": [ + "`genfromtxt` also allows naming the columns. Similarly of what we've done with the `csv.DictReader`. We do that by specifying the column information to the formatter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4b19db0", + "metadata": {}, + "outputs": [], + "source": [ + "spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)\n", + "\n", + "sunspots = np.genfromtxt(spots.raw, delimiter=';', \n", + " names=['year','month','date',\n", + " 'mean','deviation','observations','definitive'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c197647", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots" + ] + }, + { + "cell_type": "markdown", + "id": "b0d16870", + "metadata": {}, + "source": [ + "Going a step further, as we know what's expected on each column, it's then good to specify the datatype of each field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4994a243", + "metadata": {}, + "outputs": [], + "source": [ + "spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)\n", + "\n", + "sunspots = np.genfromtxt(spots.raw, delimiter=';', \n", + " names=['year','month','date',\n", + " 'mean','deviation','observations','definitive'],\n", + " dtype=[int, int, float, float, float, int, int])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee0c8767", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots" + ] + }, + { + "cell_type": "markdown", + "id": "94106860", + "metadata": {}, + "source": [ + "Now, NumPy understands the names of the columns, so our plot command is more readable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af8d7170", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots['year']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc0b1fe7", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(sunspots['year'], sunspots['mean'])" + ] + }, + { + "cell_type": "markdown", + "id": "beee9482", + "metadata": {}, + "source": [ + "### Pandas - a more powerful CSV reader" + ] + }, + { + "cell_type": "markdown", + "id": "8013edf5", + "metadata": {}, + "source": [ + "If most of your work is going to be working with CSV files, then, most probably you will enjoy the powers that [pandas](https://pandas.pydata.org/) provide. Whereas numpy uses arrays, pandas work with [DataFrames](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/01_table_oriented.html), that's how they name to their representation of data in a table (2D arrays).\n", + "\n", + "Let's see how we would use it for this example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9faaa216", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True)\n", + "\n", + "sunspots = pd.read_csv(spots.raw, delimiter=';',\n", + " names=['year','month','date',\n", + " 'mean','deviation','observations','definitive'])\n", + "sunspots" + ] + }, + { + "cell_type": "markdown", + "id": "cd0fd2ea", + "metadata": {}, + "source": [ + "In pandas, then you can access to the data of their columns as with a dictionaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7b745d1", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots['year']" + ] + }, + { + "cell_type": "markdown", + "id": "efa6b4f1", + "metadata": {}, + "source": [ + "And ploting the data is directly available from the object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1826c0ba", + "metadata": {}, + "outputs": [], + "source": [ + "sunspots.plot('year', 'mean')" + ] + }, + { + "cell_type": "markdown", + "id": "c90f6ecd", + "metadata": {}, + "source": [ + "Note how, automatically, pandas sets the axis labels and the legend. We will learn how to set these up later in this chapter." + ] + }, + { + "cell_type": "markdown", + "id": "fd7a36a4", + "metadata": {}, + "source": [ + "You can learn more about pandas with the [Software carpentry's Plotting and Programming with Python](https://swcarpentry.github.io/python-novice-gapminder/) lesson." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "CSV" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/062csv.ipynb.py b/ch02data/062csv.ipynb.py new file mode 100644 index 000000000..8de8dc098 --- /dev/null +++ b/ch02data/062csv.ipynb.py @@ -0,0 +1,313 @@ +# --- +# jupyter: +# jekyll: +# display_name: CSV +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Field and Record Data (Tabular data) +# +# Tabular data, that is data that is formatted as a table with a fixed number of rows and columns, is very common in a research context. A particularly simple and also popular file format for such data is [_delimited-separated value_ files](https://en.wikipedia.org/wiki/Delimiter-separated_values). + +# %% [markdown] +# ## Delimiter-separated values + +# %% [markdown] +# Let's carry on with our sunspots example. As we saw previously the data is semicolon-separated. +# +# We can request the CSV file text from the URL we used previously: + +# %% +import requests +# Request sunspots data from URL and extract response content as text +sunspots_csv_text = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php').text +# Strip any leading or trailing whitespace from CSV text to skip any empty rows +sunspots_csv_text = sunspots_csv_text.strip() + +# %% [markdown] +# As a quick check we can split the text on the newline character `\n` and print the last five entries in the resulting list + +# %% +print(sunspots_csv_text.split('\n')[-5:]) + +# %% [markdown] +# We see that each line is a string with numeric calues separted by semicolon delimiters. We want to work programmatically with such *delimited-separated value* files. + +# %% [markdown] +# These are files which (typically) have +# +# * One *record* per line (row) +# * Each record has multiple *fields* (columns) +# * Fields are separated by some *delimiter* + +# %% [markdown] +# Typical separators are the `space`, `tab`, `comma`, and `semicolon`, leading to correspondingly-named file formats, e.g.: +# +# * Space-separated value (e.g. `field1 "field two" field3` ) +# * Comma-separated value (e.g. `field1, another field, "wow, another field"`) + +# %% [markdown] +# Comma-separated value is abbreviated CSV, and tab-separated value TSV. + +# %% [markdown] +# CSV is also sometimes used to refer to all the different sub-kinds of separated value files, i.e. some people use CSV to refer to tab-, space- and semicolon-separated files. + +# %% [markdown] +# CSV is not a particularly superb data format, because it forces your data model to only have two 'axes', records and fields, with each record a flat object. As we will see in the next notebook, structured file formats can be used to represent a richer array of data formats, including for example hierarchically structured data where each record may itself have an internal structure. + +# %% [markdown] +# Nevertheless, because you can always export *spreadsheets* as CSV files (each cell is a field, each row is a record), CSV files are very popular. + +# %% [markdown] +# ### CSV variants + +# %% [markdown] +# Some CSV formats define a comment character, so that rows beginning with, e.g., a `#`, are not treated as data, but give a human comment. + +# %% [markdown] +# Some CSV formats define a three-deep list structure, where a double-newline separates records into blocks. + +# %% [markdown] +# Some CSV formats assume that the first line (also called a header) defines the names of the fields, e.g.: +# +# ``` +# name, age +# James, 39 +# Will, 2 +# ``` + +# %% [markdown] +# ### Python `csv` module + +# %% [markdown] +# The Python standard library provides a `csv` module for reading and writing delimited-separated value files, including, as the name suggests, CSV files. As it is built-in to all Python installations, it is useful to be familiar with the `csv` module as an option for loading and saving CSV formatted data, though the CSV capabilities in third-party libraries such as [NumPy](https://numpy.org/doc/stable/reference/generated/numpy.loadtxt.html) (which we will cover later in the course) and [Pandas](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html) are more powerful and will often be better options in practice. + +# %% +import csv + +# %% [markdown] +# The most straightforward way to read CSV files using the `csv` module is with the [`csv.reader` function](https://docs.python.org/3/library/csv.html#csv.reader). This accepts an _iterable_ object as its first argument which returns a line of delimited input for each iteration. Commonly this will be an opened file object however it can also for example be a sequence of strings which is what we will use here by using the `split` method to convert the sunspot CSV text object into a list of per-line strings. The `csv.reader` function also accepts various optional keyword arguments including importantly a `delimiter` argument to specify the character used as the delimiter separating the values in each line, with we setting this to a semicolon here. + +# %% +csv_reader = csv.reader(sunspots_csv_text.split('\n'), delimiter=';') + +# %% [markdown] +# The object returned by the `csv.reader` function is an iterator over the rows of the CSV file, with each row being returned as a list of the separated values in the row (with all values being read as strings by default). We can read all of the data in to a nested list-of-lists using a list comprehension: + +# %% +sunspots_data = [row for row in csv_reader] +print(sunspots_data[-5:]) + +# %% [markdown] +# For this particular CSV file the first column corresponds to the measurement year, the second the measurement month number and the third the measurement date as a 'fractional year'. We can extract a list of the just the (fractional) years converted to floating point values using another list comprehension + +# %% +fractional_years = [float(row[2]) for row in sunspots_data] + +# %% [markdown] +# Similarly the fourth column in the CSV file contains the monthly mean total sunspot number, which we can extract with another list comprehension + +# %% +monthly_mean_total_sunspot_numbers = [float(row[3]) for row in sunspots_data] + +# %% [markdown] +# We can then for example use Matplotlib to create a plot of how the monthly average sunspot number varies over time, with this highlighting [the cyclic nature of sunspot activity](https://en.wikipedia.org/wiki/Solar_cycle) + +# %% +from matplotlib import pyplot as plt +plt.plot(fractional_years, monthly_mean_total_sunspot_numbers) +plt.xlabel('Year') +plt.ylabel('Monthly mean total sunspot number'); + +# %% [markdown] +# #### Reading rows as dictionaries + +# %% [markdown] +# Accessing the values in each row by an index corresponding to their column can be unclear and prone to bugs. The `csv` module also provides the `csv.DictReader` class to allow reading each record (line) in the CSV file as a dictionary keyed by a set of field names. For the dataset we are using [we have the columns correspond to](http://www.sidc.be/silso/infosnmtot) +# +# ``` +# Column 1-2: Gregorian calendar date +# - Year +# - Month +# Column 3: Date in fraction of year. +# Column 4: Monthly mean total sunspot number. +# Column 5: Monthly mean standard deviation of the input sunspot numbers. +# Column 6: Number of observations used to compute the monthly mean total sunspot number. +# Column 7: Definitive/provisional marker. '1' indicates that the value is definitive. '0' indicates that the value is still provisional. +# ``` +# +# We can create an instance of the `csv.DictReader` class very similarly to how we called the `csv.reader` function, but with an additional keyword argument `fieldnames` specifying a sequence of strings corresponding to the keys to associate the values in each row with. Below we also set the optional keyword argument `quoting` to the special constant `csv.QUOTE_NONNUMERIC` which causes all non-quoted values in each line to be automatically converted to floating point values. + +# %% +csv_reader = csv.DictReader( + sunspots_csv_text.split('\n'), + fieldnames=['year', 'month', 'fractional_year', 'mean', 'deviation', 'observations', 'definitive'], + delimiter=';', + quoting=csv.QUOTE_NONNUMERIC +) + +# %% [markdown] +# Similarly to previously we can now extract all the data using a list comprehension, with the difference being that each item in the constructed list is now a dictionary keyed by the field names: + +# %% +sunspots_data = [record for record in csv_reader] +print(sunspots_data[-1]) + +# %% [markdown] +# We can then recreate the same plot as previously as follows, with the intention of the list comprehensions extracting the year and mean values now much more apparent + +# %% +from matplotlib import pyplot as plt + +plt.plot([r['fractional_year'] for r in sunspots_data], [r['mean'] for r in sunspots_data]) +plt.xlabel('Year') +plt.ylabel('Monthly mean total sunspot number'); + +# %% [markdown] +# #### Writing CSV files +# +# The `csv` module also provides functionality for writing delimiter-separated value files. The `csv.writer` function provides a simple interface for writing CSV files row by row, with the function accepting a file object (and optional keyword arugments specifying formatting options such as `delimiter`) and the returned object providing a `writerow` method to write a sequence of values to the file as a delimiter-separated string. For example we can save a table of data about the planets in the solar system as a comma-separated values file using the following code snippet + +# %% +planets_data = [ + ['Name', 'Mean distance from sun / AU', 'Orbit period / years', 'Mass / M🜨', 'Radius / R🜨', 'Number of satellites'], + ['Mercury', 0.39, 0.24, 0.06, 0.38, 0], + ['Venus', 0.72, 0.62, 0.82, 0.95, 0], + ['Earth', 1., 1., 1., 1., 1], + ['Mars', 1.5, 1.9, 0.11, 0.53, 2], + ['Jupiter', 5.2, 12., 320., 11., 63], + ['Saturn', 9.5, 29., 95., 9.4, 61], + ['Uranus', 19., 84., 15., 4.1, 27], + ['Neptune', 30., 170., 17., 3.9, 14], +] + +with open('planets_data.csv', 'w', encoding='utf-8') as f: + csv_writer = csv.writer(f, delimiter=',') + for row in planets_data: + csv_writer.writerow(row) + +# %% [markdown] +# A [`csv.DictWriter`](https://docs.python.org/3/library/csv.html#csv.DictWriter) class is also provided which analogously to the `csv.DictReader` class, allows writing a CSV file using rows specified by dictionaries mapping from field names to values. + +# %% [markdown] +# ### NumPy's CSV readers + +# %% [markdown] +# The Python standard library `csv` module seen in the preceding section is less powerful than the CSV capabilities in NumPy, +# the main scientific Python library for handling data. NumPy is distributed with Anaconda and Canopy, so we recommend use that when available. + +# %% [markdown] +# NumPy has powerful capabilities for handling matrices, and other fun stuff, and we'll learn about these later in the course, +# but for now, we'll use NumPy's CSV reader, and assume it gives us lists and dictionaries, rather than its more exciting `array` type. + +# %% +import numpy as np + +# %% +spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True) + +# %% [markdown] +# `stream=True` delays loading all of the data until it is required. + +# %% +sunspots = np.genfromtxt(spots.raw, delimiter=';') + +# %% [markdown] +# `genfromtxt` is a powerful CSV reader. We used the `delimiter` optional argument to specify the delimeter. We could also specify +# `names=True` if we had a first line naming fields, and `comments=#` if we had comment lines. + +# %% +sunspots[0][3] + +# %% [markdown] +# As before, we can now plot the "Sunspot cycle", note how we can specify the column directly from the data we've read. + +# %% +# %matplotlib inline + +from matplotlib import pyplot as plt +plt.plot(sunspots[:,2], sunspots[:,3]) # Numpy syntax to access all + # rows, specified column. + +# %% [markdown] +# The plot command accepted an array of 'X' values and an array of 'Y' values. We used a special NumPy "`:`" syntax, +# which we'll learn more about later. Don't worry about the `%matplotlib` magic command for now - we'll also look at this later. + +# %% [markdown] +# `genfromtxt` also allows naming the columns. Similarly of what we've done with the `csv.DictReader`. We do that by specifying the column information to the formatter: + +# %% +spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True) + +sunspots = np.genfromtxt(spots.raw, delimiter=';', + names=['year','month','date', + 'mean','deviation','observations','definitive']) + +# %% +sunspots + +# %% [markdown] +# Going a step further, as we know what's expected on each column, it's then good to specify the datatype of each field. + +# %% +spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True) + +sunspots = np.genfromtxt(spots.raw, delimiter=';', + names=['year','month','date', + 'mean','deviation','observations','definitive'], + dtype=[int, int, float, float, float, int, int]) + +# %% +sunspots + +# %% [markdown] +# Now, NumPy understands the names of the columns, so our plot command is more readable: + +# %% +sunspots['year'] + +# %% +plt.plot(sunspots['year'], sunspots['mean']) + +# %% [markdown] +# ### Pandas - a more powerful CSV reader + +# %% [markdown] +# If most of your work is going to be working with CSV files, then, most probably you will enjoy the powers that [pandas](https://pandas.pydata.org/) provide. Whereas numpy uses arrays, pandas work with [DataFrames](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/01_table_oriented.html), that's how they name to their representation of data in a table (2D arrays). +# +# Let's see how we would use it for this example. + +# %% +import pandas as pd + +spots = requests.get('http://www.sidc.be/silso/INFO/snmtotcsv.php', stream=True) + +sunspots = pd.read_csv(spots.raw, delimiter=';', + names=['year','month','date', + 'mean','deviation','observations','definitive']) +sunspots + +# %% [markdown] +# In pandas, then you can access to the data of their columns as with a dictionaries. + +# %% +sunspots['year'] + +# %% [markdown] +# And ploting the data is directly available from the object. + +# %% +sunspots.plot('year', 'mean') + +# %% [markdown] +# Note how, automatically, pandas sets the axis labels and the legend. We will learn how to set these up later in this chapter. + +# %% [markdown] +# You can learn more about pandas with the [Software carpentry's Plotting and Programming with Python](https://swcarpentry.github.io/python-novice-gapminder/) lesson. diff --git a/ch02data/064JsonYamlXML.html b/ch02data/064JsonYamlXML.html new file mode 100644 index 000000000..ff70bdb88 --- /dev/null +++ b/ch02data/064JsonYamlXML.html @@ -0,0 +1,742 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Structured data files + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Structured Data

+
+
+
+
+
+
+

Structured data

+
+
+
+
+
+
+

CSV files can only model data where each record has several fields, and each field is a simple datatype, +a string or number.

+
+
+
+
+
+
+

We often want to store data which is more complicated than this, with nested structures of lists and dictionaries. +Structured data formats like JSON, YAML, and XML are designed for this.

+
+
+
+
+
+
+

JSON

+
+
+
+
+
+
+

JSON is a very common open-standard data format that is used to store structured data in a human-readable way.

+
+
+
+
+
+
+

This allows us to represent data which is combinations of lists and dictionaries as a text file which +looks a bit like a Javascript (or Python) data literal.

+
+
+
+
+
+
In [1]:
+
+
+
import json
+
+
+
+
+
+
+
+
+

Any nested group of dictionaries and lists can be saved:

+
+
+
+
+
+
In [2]:
+
+
+
mydata = {'key': ['value1', 'value2'], 
+          'key2': {'key4':'value3'}}
+
+
+
+
+
+
+
+
In [3]:
+
+
+
json.dumps(mydata)
+
+
+
+
+
+
+
+
Out[3]:
+
+
'{"key": ["value1", "value2"], "key2": {"key4": "value3"}}'
+
+
+
+
+
+
+
+
+

If you would like a more readable output, you can use the indent argument.

+
+
+
+
+
+
In [4]:
+
+
+
print(json.dumps(mydata, indent=4))
+
+
+
+
+
+
+
+
+
+
{
+    "key": [
+        "value1",
+        "value2"
+    ],
+    "key2": {
+        "key4": "value3"
+    }
+}
+
+
+
+
+
+
+
+
+
+

Loading data is also really easy:

+
+
+
+
+
+
In [5]:
+
+
+
%%writefile myfile.json
+{
+    "somekey": ["a list", "with values"]
+}
+
+
+
+
+
+
+
+
+
+
Writing myfile.json
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
with open('myfile.json', 'r') as json_file:
+    my_data_as_string = json_file.read()
+
+
+
+
+
+
+
+
In [7]:
+
+
+
my_data_as_string
+
+
+
+
+
+
+
+
Out[7]:
+
+
'{\n    "somekey": ["a list", "with values"]\n}\n'
+
+
+
+
+
+
+
+
In [8]:
+
+
+
mydata = json.loads(my_data_as_string)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
mydata['somekey']
+
+
+
+
+
+
+
+
Out[9]:
+
+
['a list', 'with values']
+
+
+
+
+
+
+
+
+

This is a very nice solution for loading and saving Python data structures.

+
+
+
+
+
+
+

It's a very common way of transferring data on the internet, and of saving datasets to disk.

+
+
+
+
+
+
+

There's good support in most languages, so it's a nice inter-language file interchange format.

+
+
+
+
+
+
+

YAML

+
+
+
+
+
+
+

YAML is a very similar data format to JSON, with some nice additions:

+
+
+
+
+
+
+
    +
  • You don't need to quote strings if they don't have funny characters in
  • +
  • You can have comment lines, beginning with a #
  • +
  • You can write dictionaries without the curly brackets: it just notices the colons.
  • +
  • You can write lists like this:
  • +
+
+
+
+
+
+
In [10]:
+
+
+
%%writefile myfile.yaml
+somekey:
+    - a list # Look, this is a list
+    - with values
+
+
+
+
+
+
+
+
+
+
Writing myfile.yaml
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
import yaml  # This may need installed as pyyaml
+
+
+
+
+
+
+
+
In [12]:
+
+
+
with open('myfile.yaml') as yaml_file:
+    my_data = yaml.safe_load(yaml_file)
+print(mydata)
+
+
+
+
+
+
+
+
+
+
{'somekey': ['a list', 'with values']}
+
+
+
+
+
+
+
+
+
+

YAML is a popular format for ad-hoc data files, but the library doesn't ship with default Python (though it is part +of Anaconda and Canopy), so some people still prefer JSON for its universality.

+
+
+
+
+
+
+

Because YAML gives the option of serialising a list either as newlines with dashes or with square brackets, +you can control this choice:

+
+
+
+
+
+
In [13]:
+
+
+
print(yaml.safe_dump(mydata))
+
+
+
+
+
+
+
+
+
+
somekey:
+- a list
+- with values
+
+
+
+
+
+
+
+
+
+
In [14]:
+
+
+
print(yaml.safe_dump(mydata, default_flow_style=True))
+
+
+
+
+
+
+
+
+
+
{somekey: [a list, with values]}
+
+
+
+
+
+
+
+
+
+
+

default_flow_style=False (the default) uses a "block style" (rather than an "inline" or "flow style") to delineate data structures. See the YAML docs for more details.

+
+
+
+
+
+
+

XML

+
+
+
+
+
+
+

Supplementary material: XML is another popular choice when saving nested data structures. +It's very careful, but verbose. If your field uses XML data, you'll need to learn a python XML parser +(there are a few), and about how XML works.

+
+
+
+
+
+
+

Exercise: Saving and loading data

+
+
+
+
+
+
+

Use YAML or JSON to save your maze data structure to disk and load it again.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/064JsonYamlXML.ipynb b/ch02data/064JsonYamlXML.ipynb new file mode 100644 index 000000000..68c47c6d5 --- /dev/null +++ b/ch02data/064JsonYamlXML.ipynb @@ -0,0 +1,359 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "98d33caa", + "metadata": {}, + "source": [ + "## Structured Data" + ] + }, + { + "cell_type": "markdown", + "id": "eb01bf06", + "metadata": {}, + "source": [ + "### Structured data" + ] + }, + { + "cell_type": "markdown", + "id": "ba8ba873", + "metadata": {}, + "source": [ + "CSV files can only model data where each record has several fields, and each field is a simple datatype,\n", + "a string or number." + ] + }, + { + "cell_type": "markdown", + "id": "7daa20bf", + "metadata": {}, + "source": [ + "We often want to store data which is more complicated than this, with nested structures of lists and dictionaries.\n", + "Structured data formats like JSON, YAML, and XML are designed for this." + ] + }, + { + "cell_type": "markdown", + "id": "bdae639b", + "metadata": {}, + "source": [ + "### JSON" + ] + }, + { + "cell_type": "markdown", + "id": "6d8cd28b", + "metadata": {}, + "source": [ + "[JSON](https://en.wikipedia.org/wiki/JSON) is a very common open-standard data format that is used to store structured data in a human-readable way." + ] + }, + { + "cell_type": "markdown", + "id": "095391d0", + "metadata": {}, + "source": [ + "This allows us to represent data which is combinations of lists and dictionaries as a text file which\n", + "looks a bit like a Javascript (or Python) data literal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a224254f", + "metadata": {}, + "outputs": [], + "source": [ + "import json" + ] + }, + { + "cell_type": "markdown", + "id": "afa4db78", + "metadata": {}, + "source": [ + "Any nested group of dictionaries and lists can be saved:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7abc2cf4", + "metadata": {}, + "outputs": [], + "source": [ + "mydata = {'key': ['value1', 'value2'], \n", + " 'key2': {'key4':'value3'}}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74381e2f", + "metadata": {}, + "outputs": [], + "source": [ + "json.dumps(mydata)" + ] + }, + { + "cell_type": "markdown", + "id": "33ac1bf1", + "metadata": {}, + "source": [ + "If you would like a more readable output, you can use the `indent` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2753f331", + "metadata": {}, + "outputs": [], + "source": [ + "print(json.dumps(mydata, indent=4))" + ] + }, + { + "cell_type": "markdown", + "id": "2c544246", + "metadata": {}, + "source": [ + "Loading data is also really easy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae7326bc", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile myfile.json\n", + "{\n", + " \"somekey\": [\"a list\", \"with values\"]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6f70df7", + "metadata": {}, + "outputs": [], + "source": [ + "with open('myfile.json', 'r') as json_file:\n", + " my_data_as_string = json_file.read()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b18e1a5", + "metadata": {}, + "outputs": [], + "source": [ + "my_data_as_string" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a689fcc7", + "metadata": {}, + "outputs": [], + "source": [ + "mydata = json.loads(my_data_as_string)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7521b048", + "metadata": {}, + "outputs": [], + "source": [ + "mydata['somekey']" + ] + }, + { + "cell_type": "markdown", + "id": "47c9c158", + "metadata": {}, + "source": [ + "This is a very nice solution for loading and saving Python data structures." + ] + }, + { + "cell_type": "markdown", + "id": "1fd73287", + "metadata": {}, + "source": [ + "It's a very common way of transferring data on the internet, and of saving datasets to disk." + ] + }, + { + "cell_type": "markdown", + "id": "5b1ce651", + "metadata": {}, + "source": [ + "There's good support in most languages, so it's a nice inter-language file interchange format." + ] + }, + { + "cell_type": "markdown", + "id": "cb4679b5", + "metadata": {}, + "source": [ + "### YAML" + ] + }, + { + "cell_type": "markdown", + "id": "e656a58d", + "metadata": {}, + "source": [ + "[YAML](https://en.wikipedia.org/wiki/YAML) is a very similar data format to JSON, with some nice additions:" + ] + }, + { + "cell_type": "markdown", + "id": "bc186e4a", + "metadata": {}, + "source": [ + "* You don't need to quote strings if they don't have funny characters in\n", + "* You can have comment lines, beginning with a `#`\n", + "* You can write dictionaries without the curly brackets: it just notices the colons.\n", + "* You can write lists like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a83ac7d4", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile myfile.yaml\n", + "somekey:\n", + " - a list # Look, this is a list\n", + " - with values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59df9f3e", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml # This may need installed as pyyaml" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcac990b", + "metadata": {}, + "outputs": [], + "source": [ + "with open('myfile.yaml') as yaml_file:\n", + " my_data = yaml.safe_load(yaml_file)\n", + "print(mydata)" + ] + }, + { + "cell_type": "markdown", + "id": "11d1492a", + "metadata": {}, + "source": [ + "YAML is a popular format for ad-hoc data files, but the library doesn't ship with default Python (though it is part\n", + "of Anaconda and Canopy), so some people still prefer JSON for its universality." + ] + }, + { + "cell_type": "markdown", + "id": "b1a1cc1a", + "metadata": {}, + "source": [ + "Because YAML gives the option of serialising a list either as newlines with dashes or with square brackets,\n", + "you can control this choice:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5b0e514", + "metadata": {}, + "outputs": [], + "source": [ + "print(yaml.safe_dump(mydata))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b0b6ef1", + "metadata": {}, + "outputs": [], + "source": [ + "print(yaml.safe_dump(mydata, default_flow_style=True))" + ] + }, + { + "cell_type": "markdown", + "id": "1ab240ad", + "metadata": {}, + "source": [ + "`default_flow_style=False` (the default) uses a \"block style\" (rather than an \"inline\" or \"flow style\") to delineate data structures. See [the YAML docs](http://yaml.org/spec/1.2/spec.html) for more details." + ] + }, + { + "cell_type": "markdown", + "id": "79cbbf94", + "metadata": {}, + "source": [ + "### XML" + ] + }, + { + "cell_type": "markdown", + "id": "9de1851c", + "metadata": {}, + "source": [ + "*Supplementary material*: [XML](http://www.w3schools.com/xml/) is another popular choice when saving nested data structures. \n", + "It's very careful, but verbose. If your field uses XML data, you'll need to learn a [python XML parser](https://docs.python.org/3/library/xml.etree.elementtree.html)\n", + "(there are a few), and about how XML works." + ] + }, + { + "cell_type": "markdown", + "id": "709db874", + "metadata": {}, + "source": [ + "### Exercise: Saving and loading data" + ] + }, + { + "cell_type": "markdown", + "id": "8aa36506", + "metadata": {}, + "source": [ + "Use YAML or JSON to save your maze data structure to disk and load it again." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Structured data files" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/064JsonYamlXML.ipynb.py b/ch02data/064JsonYamlXML.ipynb.py new file mode 100644 index 000000000..397bdbd42 --- /dev/null +++ b/ch02data/064JsonYamlXML.ipynb.py @@ -0,0 +1,143 @@ +# --- +# jupyter: +# jekyll: +# display_name: Structured data files +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Structured Data + +# %% [markdown] +# ### Structured data + +# %% [markdown] +# CSV files can only model data where each record has several fields, and each field is a simple datatype, +# a string or number. + +# %% [markdown] +# We often want to store data which is more complicated than this, with nested structures of lists and dictionaries. +# Structured data formats like JSON, YAML, and XML are designed for this. + +# %% [markdown] +# ### JSON + +# %% [markdown] +# [JSON](https://en.wikipedia.org/wiki/JSON) is a very common open-standard data format that is used to store structured data in a human-readable way. + +# %% [markdown] +# This allows us to represent data which is combinations of lists and dictionaries as a text file which +# looks a bit like a Javascript (or Python) data literal. + +# %% +import json + +# %% [markdown] +# Any nested group of dictionaries and lists can be saved: + +# %% +mydata = {'key': ['value1', 'value2'], + 'key2': {'key4':'value3'}} + +# %% +json.dumps(mydata) + +# %% [markdown] +# If you would like a more readable output, you can use the `indent` argument. + +# %% +print(json.dumps(mydata, indent=4)) + +# %% [markdown] +# Loading data is also really easy: + +# %% +# %%writefile myfile.json +{ + "somekey": ["a list", "with values"] +} + +# %% +with open('myfile.json', 'r') as json_file: + my_data_as_string = json_file.read() + +# %% +my_data_as_string + +# %% +mydata = json.loads(my_data_as_string) + +# %% +mydata['somekey'] + +# %% [markdown] +# This is a very nice solution for loading and saving Python data structures. + +# %% [markdown] +# It's a very common way of transferring data on the internet, and of saving datasets to disk. + +# %% [markdown] +# There's good support in most languages, so it's a nice inter-language file interchange format. + +# %% [markdown] +# ### YAML + +# %% [markdown] +# [YAML](https://en.wikipedia.org/wiki/YAML) is a very similar data format to JSON, with some nice additions: + +# %% [markdown] +# * You don't need to quote strings if they don't have funny characters in +# * You can have comment lines, beginning with a `#` +# * You can write dictionaries without the curly brackets: it just notices the colons. +# * You can write lists like this: + +# %% +# %%writefile myfile.yaml +somekey: + - a list # Look, this is a list + - with values + +# %% +import yaml # This may need installed as pyyaml + +# %% +with open('myfile.yaml') as yaml_file: + my_data = yaml.safe_load(yaml_file) +print(mydata) + +# %% [markdown] +# YAML is a popular format for ad-hoc data files, but the library doesn't ship with default Python (though it is part +# of Anaconda and Canopy), so some people still prefer JSON for its universality. + +# %% [markdown] +# Because YAML gives the option of serialising a list either as newlines with dashes or with square brackets, +# you can control this choice: + +# %% +print(yaml.safe_dump(mydata)) + +# %% +print(yaml.safe_dump(mydata, default_flow_style=True)) + +# %% [markdown] +# `default_flow_style=False` (the default) uses a "block style" (rather than an "inline" or "flow style") to delineate data structures. See [the YAML docs](http://yaml.org/spec/1.2/spec.html) for more details. + +# %% [markdown] +# ### XML + +# %% [markdown] +# *Supplementary material*: [XML](http://www.w3schools.com/xml/) is another popular choice when saving nested data structures. +# It's very careful, but verbose. If your field uses XML data, you'll need to learn a [python XML parser](https://docs.python.org/3/library/xml.etree.elementtree.html) +# (there are a few), and about how XML works. + +# %% [markdown] +# ### Exercise: Saving and loading data + +# %% [markdown] +# Use YAML or JSON to save your maze data structure to disk and load it again. diff --git a/ch02data/065MazeSaved.html b/ch02data/065MazeSaved.html new file mode 100644 index 000000000..6708c9d01 --- /dev/null +++ b/ch02data/065MazeSaved.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Maze Files Solution + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
In [1]:
+
+
+
house = {
+    'living': {
+        'exits': {
+            'north': 'kitchen',
+            'outside': 'garden',
+            'upstairs': 'bedroom'
+        },
+        'people': ['James'],
+        'capacity': 2
+    },
+    'kitchen': {
+        'exits': {
+            'south': 'living'
+        },
+        'people': [],
+        'capacity': 1
+    },
+    'garden': {
+        'exits': {
+            'inside': 'living'
+        },
+        'people': ['Sue'],
+        'capacity': 3
+    },
+    'bedroom': {
+        'exits': {
+            'downstairs': 'living',
+            'jump': 'garden'
+        },
+        'people': [],
+        'capacity': 1
+    }
+}
+
+
+
+
+
+
+
+
+

Save the maze with json:

+
+
+
+
+
+
In [2]:
+
+
+
import json
+
+
+
+
+
+
+
+
In [3]:
+
+
+
with open('maze.json', 'w') as json_maze_out:
+    json_maze_out.write(json.dumps(house))
+
+
+
+
+
+
+
+
+

Consider the file on the disk:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+cat 'maze.json'
+
+
+
+
+
+
+
+
+
+
{"living": {"exits": {"north": "kitchen", "outside": "garden", "upstairs": "bedroom"}, "people": ["James"], "capacity": 2}, "kitchen": {"exits": {"south": "living"}, "people": [], "capacity": 1}, "garden": {"exits": {"inside": "living"}, "people": ["Sue"], "capacity": 3}, "bedroom": {"exits": {"downstairs": "living", "jump": "garden"}, "people": [], "capacity": 1}}
+
+
+
+
+
+
+
+
+

and now load it into a different variable:

+
+
+
+
+
+
In [5]:
+
+
+
with open('maze.json') as json_maze_in:
+    maze_again = json.load(json_maze_in)
+
+
+
+
+
+
+
+
In [6]:
+
+
+
maze_again
+
+
+
+
+
+
+
+
Out[6]:
+
+
{'living': {'exits': {'north': 'kitchen',
+   'outside': 'garden',
+   'upstairs': 'bedroom'},
+  'people': ['James'],
+  'capacity': 2},
+ 'kitchen': {'exits': {'south': 'living'}, 'people': [], 'capacity': 1},
+ 'garden': {'exits': {'inside': 'living'}, 'people': ['Sue'], 'capacity': 3},
+ 'bedroom': {'exits': {'downstairs': 'living', 'jump': 'garden'},
+  'people': [],
+  'capacity': 1}}
+
+
+
+
+
+
+
+
+

Or with YAML:

+
+
+
+
+
+
In [7]:
+
+
+
import yaml
+
+
+
+
+
+
+
+
In [8]:
+
+
+
with open('maze.yaml', 'w') as yaml_maze_out:
+    yaml_maze_out.write(yaml.dump(house))
+
+
+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+cat 'maze.yaml'
+
+
+
+
+
+
+
+
+
+
bedroom:
+  capacity: 1
+  exits:
+    downstairs: living
+    jump: garden
+  people: []
+garden:
+  capacity: 3
+  exits:
+    inside: living
+  people:
+  - Sue
+kitchen:
+  capacity: 1
+  exits:
+    south: living
+  people: []
+living:
+  capacity: 2
+  exits:
+    north: kitchen
+    outside: garden
+    upstairs: bedroom
+  people:
+  - James
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
with open('maze.yaml') as yaml_maze_in:
+    maze_again = yaml.safe_load(yaml_maze_in)
+
+
+
+
+
+
+
+
In [11]:
+
+
+
maze_again
+
+
+
+
+
+
+
+
Out[11]:
+
+
{'bedroom': {'capacity': 1,
+  'exits': {'downstairs': 'living', 'jump': 'garden'},
+  'people': []},
+ 'garden': {'capacity': 3, 'exits': {'inside': 'living'}, 'people': ['Sue']},
+ 'kitchen': {'capacity': 1, 'exits': {'south': 'living'}, 'people': []},
+ 'living': {'capacity': 2,
+  'exits': {'north': 'kitchen', 'outside': 'garden', 'upstairs': 'bedroom'},
+  'people': ['James']}}
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/065MazeSaved.ipynb b/ch02data/065MazeSaved.ipynb new file mode 100644 index 000000000..caddc2bae --- /dev/null +++ b/ch02data/065MazeSaved.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7315278c", + "metadata": {}, + "outputs": [], + "source": [ + "house = {\n", + " 'living': {\n", + " 'exits': {\n", + " 'north': 'kitchen',\n", + " 'outside': 'garden',\n", + " 'upstairs': 'bedroom'\n", + " },\n", + " 'people': ['James'],\n", + " 'capacity': 2\n", + " },\n", + " 'kitchen': {\n", + " 'exits': {\n", + " 'south': 'living'\n", + " },\n", + " 'people': [],\n", + " 'capacity': 1\n", + " },\n", + " 'garden': {\n", + " 'exits': {\n", + " 'inside': 'living'\n", + " },\n", + " 'people': ['Sue'],\n", + " 'capacity': 3\n", + " },\n", + " 'bedroom': {\n", + " 'exits': {\n", + " 'downstairs': 'living',\n", + " 'jump': 'garden'\n", + " },\n", + " 'people': [],\n", + " 'capacity': 1\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "9fb45f2d", + "metadata": {}, + "source": [ + "Save the maze with json:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1078dc8b", + "metadata": {}, + "outputs": [], + "source": [ + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9fa453d", + "metadata": {}, + "outputs": [], + "source": [ + "with open('maze.json', 'w') as json_maze_out:\n", + " json_maze_out.write(json.dumps(house))" + ] + }, + { + "cell_type": "markdown", + "id": "ece53bbe", + "metadata": {}, + "source": [ + "Consider the file on the disk:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "981a2389", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cat 'maze.json'" + ] + }, + { + "cell_type": "markdown", + "id": "51f69338", + "metadata": {}, + "source": [ + "and now load it into a different variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8268242", + "metadata": {}, + "outputs": [], + "source": [ + "with open('maze.json') as json_maze_in:\n", + " maze_again = json.load(json_maze_in)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85ddd16b", + "metadata": {}, + "outputs": [], + "source": [ + "maze_again" + ] + }, + { + "cell_type": "markdown", + "id": "4ad999cc", + "metadata": {}, + "source": [ + "Or with YAML:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "982ae36c", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1460d5c", + "metadata": {}, + "outputs": [], + "source": [ + "with open('maze.yaml', 'w') as yaml_maze_out:\n", + " yaml_maze_out.write(yaml.dump(house))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "459acc5f", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cat 'maze.yaml'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "853d25e7", + "metadata": {}, + "outputs": [], + "source": [ + "with open('maze.yaml') as yaml_maze_in:\n", + " maze_again = yaml.safe_load(yaml_maze_in)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbb8d9b3", + "metadata": {}, + "outputs": [], + "source": [ + "maze_again" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Maze Files Solution" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/065MazeSaved.ipynb.py b/ch02data/065MazeSaved.ipynb.py new file mode 100644 index 000000000..545996625 --- /dev/null +++ b/ch02data/065MazeSaved.ipynb.py @@ -0,0 +1,93 @@ +# --- +# jupyter: +# jekyll: +# display_name: Maze Files Solution +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% +house = { + 'living': { + 'exits': { + 'north': 'kitchen', + 'outside': 'garden', + 'upstairs': 'bedroom' + }, + 'people': ['James'], + 'capacity': 2 + }, + 'kitchen': { + 'exits': { + 'south': 'living' + }, + 'people': [], + 'capacity': 1 + }, + 'garden': { + 'exits': { + 'inside': 'living' + }, + 'people': ['Sue'], + 'capacity': 3 + }, + 'bedroom': { + 'exits': { + 'downstairs': 'living', + 'jump': 'garden' + }, + 'people': [], + 'capacity': 1 + } +} + +# %% [markdown] +# Save the maze with json: + +# %% +import json + +# %% +with open('maze.json', 'w') as json_maze_out: + json_maze_out.write(json.dumps(house)) + +# %% [markdown] +# Consider the file on the disk: + +# %% language="bash" +# cat 'maze.json' + +# %% [markdown] +# and now load it into a different variable: + +# %% +with open('maze.json') as json_maze_in: + maze_again = json.load(json_maze_in) + +# %% +maze_again + +# %% [markdown] +# Or with YAML: + +# %% +import yaml + +# %% +with open('maze.yaml', 'w') as yaml_maze_out: + yaml_maze_out.write(yaml.dump(house)) + +# %% language="bash" +# cat 'maze.yaml' + +# %% +with open('maze.yaml') as yaml_maze_in: + maze_again = yaml.safe_load(yaml_maze_in) + +# %% +maze_again diff --git a/ch02data/066QuakeExercise.html b/ch02data/066QuakeExercise.html new file mode 100644 index 000000000..75ac86a22 --- /dev/null +++ b/ch02data/066QuakeExercise.html @@ -0,0 +1,388 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Earthquakes Exercise + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Classroom exercise: the biggest earthquake in the UK this century

+
+
+
+
+
+
+

The Problem

+
+
+
+
+
+
+

GeoJSON is a JSON-based file format for sharing geographic data. One example dataset is the USGS earthquake data:

+
+
+
+
+
+
In [1]:
+
+
+
import requests
+quakes = requests.get("http://earthquake.usgs.gov/fdsnws/event/1/query.geojson",
+                      params={
+                          'starttime': "2000-01-01",
+                          "maxlatitude": "58.723",
+                          "minlatitude": "50.008",
+                          "maxlongitude": "1.67",
+                          "minlongitude": "-9.756",
+                          "minmagnitude": "1",
+                          "endtime": "2018-10-11",
+                          "orderby": "time-asc"}
+                      )
+
+
+
+
+
+
+
+
In [2]:
+
+
+
quakes.text[0:100]
+
+
+
+
+
+
+
+
Out[2]:
+
+
'{"type":"FeatureCollection","metadata":{"generated":1700667939000,"url":"https://earthquake.usgs.gov'
+
+
+
+
+
+
+
+
+

Your exercise: determine the location of the largest magnitude earthquake in the UK this century.

+
+
+
+
+
+
+

You'll need to:

+
    +
  • Get the text of the web result
  • +
  • Parse the data as JSON
  • +
  • Understand how the data is structured into dictionaries and lists
      +
    • Where is the magnitude?
    • +
    • Where is the place description or coordinates?
    • +
    +
  • +
  • Program a search through all the quakes to find the biggest quake
  • +
  • Find the place of the biggest quake
  • +
  • Form a URL for an online map service at that latitude and longitude: look back at the introductory example
  • +
  • Display that image
  • +
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/066QuakeExercise.ipynb b/ch02data/066QuakeExercise.ipynb new file mode 100644 index 000000000..19b686306 --- /dev/null +++ b/ch02data/066QuakeExercise.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fb5e61b7", + "metadata": {}, + "source": [ + "## Classroom exercise: the biggest earthquake in the UK this century" + ] + }, + { + "cell_type": "markdown", + "id": "74efa9bf", + "metadata": {}, + "source": [ + "### The Problem" + ] + }, + { + "cell_type": "markdown", + "id": "4a475d89", + "metadata": {}, + "source": [ + "GeoJSON is a JSON-based file format for sharing geographic data. One example dataset is the USGS earthquake data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99acff76", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "quakes = requests.get(\"http://earthquake.usgs.gov/fdsnws/event/1/query.geojson\",\n", + " params={\n", + " 'starttime': \"2000-01-01\",\n", + " \"maxlatitude\": \"58.723\",\n", + " \"minlatitude\": \"50.008\",\n", + " \"maxlongitude\": \"1.67\",\n", + " \"minlongitude\": \"-9.756\",\n", + " \"minmagnitude\": \"1\",\n", + " \"endtime\": \"2018-10-11\",\n", + " \"orderby\": \"time-asc\"}\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bed94bf", + "metadata": {}, + "outputs": [], + "source": [ + "quakes.text[0:100]" + ] + }, + { + "cell_type": "markdown", + "id": "d60fe441", + "metadata": {}, + "source": [ + "Your exercise: determine the location of the largest magnitude earthquake in the UK this century." + ] + }, + { + "cell_type": "markdown", + "id": "ab35aea6", + "metadata": {}, + "source": [ + "You'll need to:\n", + "* Get the text of the web result\n", + "* Parse the data as JSON\n", + "* Understand how the data is structured into dictionaries and lists\n", + " * Where is the magnitude?\n", + " * Where is the place description or coordinates?\n", + "* Program a search through all the quakes to find the biggest quake\n", + "* Find the place of the biggest quake\n", + "* Form a URL for an online map service at that latitude and longitude: look back at the introductory example\n", + "* Display that image" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Earthquakes Exercise" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/066QuakeExercise.ipynb.py b/ch02data/066QuakeExercise.ipynb.py new file mode 100644 index 000000000..f799e2fff --- /dev/null +++ b/ch02data/066QuakeExercise.ipynb.py @@ -0,0 +1,53 @@ +# --- +# jupyter: +# jekyll: +# display_name: Earthquakes Exercise +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Classroom exercise: the biggest earthquake in the UK this century + +# %% [markdown] +# ### The Problem + +# %% [markdown] +# GeoJSON is a JSON-based file format for sharing geographic data. One example dataset is the USGS earthquake data: + +# %% +import requests +quakes = requests.get("http://earthquake.usgs.gov/fdsnws/event/1/query.geojson", + params={ + 'starttime': "2000-01-01", + "maxlatitude": "58.723", + "minlatitude": "50.008", + "maxlongitude": "1.67", + "minlongitude": "-9.756", + "minmagnitude": "1", + "endtime": "2018-10-11", + "orderby": "time-asc"} + ) + +# %% +quakes.text[0:100] + +# %% [markdown] +# Your exercise: determine the location of the largest magnitude earthquake in the UK this century. + +# %% [markdown] +# You'll need to: +# * Get the text of the web result +# * Parse the data as JSON +# * Understand how the data is structured into dictionaries and lists +# * Where is the magnitude? +# * Where is the place description or coordinates? +# * Program a search through all the quakes to find the biggest quake +# * Find the place of the biggest quake +# * Form a URL for an online map service at that latitude and longitude: look back at the introductory example +# * Display that image diff --git a/ch02data/068QuakesSolution.html b/ch02data/068QuakesSolution.html new file mode 100644 index 000000000..0c4e26337 --- /dev/null +++ b/ch02data/068QuakesSolution.html @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Quakes Solution + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Solution to the earthquake exercise

NOTE: This is intended as a reference for after you have attempted the problem (notebook version) yourself!

+
+
+
+
+
+
+

Download the data

+
+
+
+
+
+
In [1]:
+
+
+
import requests
+quakes = requests.get("http://earthquake.usgs.gov/fdsnws/event/1/query.geojson",
+                      params={
+                          'starttime': "2000-01-01",
+                          "maxlatitude": "58.723",
+                          "minlatitude": "50.008",
+                          "maxlongitude": "1.67",
+                          "minlongitude": "-9.756",
+                          "minmagnitude": "1",
+                          "endtime": "2018-10-11",
+                          "orderby": "time-asc"}
+                      )
+
+
+
+
+
+
+
+
+

Parse the data as JSON

+
+
+
+
+
+
In [2]:
+
+
+
import json
+
+
+
+
+
+
+
+
In [3]:
+
+
+
requests_json = json.loads(quakes.text)
+
+
+
+
+
+
+
+
+

Investigate the data to discover how it is structured

+
+
+
+
+
+
+

There is no foolproof way of doing this. A good first step is to see the type of our data!

+
+
+
+
+
+
In [4]:
+
+
+
type(requests_json)
+
+
+
+
+
+
+
+
Out[4]:
+
+
dict
+
+
+
+
+
+
+
+
+

Now we can navigate through this dictionary to see how the information is stored in the nested dictionaries and lists. The keys method can indicate what kind of information each dictionary holds, and the len function tells us how many entries are contained in a list. How you explore is up to you!

+
+
+
+
+
+
In [5]:
+
+
+
requests_json.keys()
+
+
+
+
+
+
+
+
Out[5]:
+
+
dict_keys(['type', 'metadata', 'features', 'bbox'])
+
+
+
+
+
+
+
+
In [6]:
+
+
+
len(requests_json['features'])
+
+
+
+
+
+
+
+
Out[6]:
+
+
120
+
+
+
+
+
+
+
+
In [7]:
+
+
+
requests_json['features'][0].keys()
+
+
+
+
+
+
+
+
Out[7]:
+
+
dict_keys(['type', 'properties', 'geometry', 'id'])
+
+
+
+
+
+
+
+
In [8]:
+
+
+
requests_json['features'][0]['properties'].keys()
+
+
+
+
+
+
+
+
Out[8]:
+
+
dict_keys(['mag', 'place', 'time', 'updated', 'tz', 'url', 'detail', 'felt', 'cdi', 'mmi', 'alert', 'status', 'tsunami', 'sig', 'net', 'code', 'ids', 'sources', 'types', 'nst', 'dmin', 'rms', 'gap', 'magType', 'type', 'title'])
+
+
+
+
+
+
+
+
In [9]:
+
+
+
requests_json['features'][0]['properties']['mag']
+
+
+
+
+
+
+
+
Out[9]:
+
+
2.6
+
+
+
+
+
+
+
+
In [10]:
+
+
+
requests_json['features'][0]['geometry']
+
+
+
+
+
+
+
+
Out[10]:
+
+
{'type': 'Point', 'coordinates': [-2.81, 54.77, 14]}
+
+
+
+
+
+
+
+
+

Also note that some IDEs display JSON in a way that makes its structure easier to understand. Try saving this data in a text file and opening it in an IDE or a browser.

+
+
+
+
+
+
+

Find the largest quake

+
+
+
+
+
+
In [11]:
+
+
+
quakes = requests_json['features']
+
+
+
+
+
+
+
+
In [12]:
+
+
+
largest_so_far = quakes[0]
+for quake in quakes:
+    if quake['properties']['mag'] > largest_so_far['properties']['mag']:
+        largest_so_far = quake
+largest_so_far['properties']['mag']
+
+
+
+
+
+
+
+
Out[12]:
+
+
4.8
+
+
+
+
+
+
+
+
In [13]:
+
+
+
lat = largest_so_far['geometry']['coordinates'][1]
+long = largest_so_far['geometry']['coordinates'][0]
+print("Latitude: {} Longitude: {}".format(lat, long))
+
+
+
+
+
+
+
+
+
+
Latitude: 52.52 Longitude: -2.15
+
+
+
+
+
+
+
+
+
+

Get a map at the point of the quake

+
+
+
+
+
+
+

We saw something similar in the Greengraph example (notebook version) of the previous chapter.

+
+
+
+
+
+
In [14]:
+
+
+
import requests
+
+
+def request_map_at(lat, long, satellite=True,
+                   zoom=10, size=(400, 400)):
+    base = "https://static-maps.yandex.ru/1.x/?"
+
+    params = dict(
+        z=zoom,
+        size="{},{}".format(size[0], size[1]),
+        ll="{},{}".format(long, lat),
+        l="sat" if satellite else "map",
+        lang="en_US"
+    )
+
+    return requests.get(base, params=params)
+
+
+
+
+
+
+
+
In [15]:
+
+
+
map_png = request_map_at(lat, long, zoom=10, satellite=False)
+
+
+
+
+
+
+
+
+

Display the map

+
+
+
+
+
+
In [16]:
+
+
+
from IPython.display import Image
+Image(map_png.content)
+
+
+
+
+
+
+
+
Out[16]:
+
+No description has been provided for this image +
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/068QuakesSolution.ipynb b/ch02data/068QuakesSolution.ipynb new file mode 100644 index 000000000..781719778 --- /dev/null +++ b/ch02data/068QuakesSolution.ipynb @@ -0,0 +1,298 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c3601ff5", + "metadata": {}, + "source": [ + "## Solution to the earthquake exercise\n", + "\n", + "**NOTE:** This is intended as a reference for **after** you have attempted [the problem](./066QuakeExercise.html) [(notebook version)](./066QuakeExercise.ipynb) yourself!" + ] + }, + { + "cell_type": "markdown", + "id": "7ca49d8d", + "metadata": {}, + "source": [ + "### Download the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e60ef32", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "quakes = requests.get(\"http://earthquake.usgs.gov/fdsnws/event/1/query.geojson\",\n", + " params={\n", + " 'starttime': \"2000-01-01\",\n", + " \"maxlatitude\": \"58.723\",\n", + " \"minlatitude\": \"50.008\",\n", + " \"maxlongitude\": \"1.67\",\n", + " \"minlongitude\": \"-9.756\",\n", + " \"minmagnitude\": \"1\",\n", + " \"endtime\": \"2018-10-11\",\n", + " \"orderby\": \"time-asc\"}\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "15debd0f", + "metadata": {}, + "source": [ + "### Parse the data as JSON" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "377f78e2", + "metadata": {}, + "outputs": [], + "source": [ + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7111cd7", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json = json.loads(quakes.text)" + ] + }, + { + "cell_type": "markdown", + "id": "262a52ca", + "metadata": {}, + "source": [ + "### Investigate the data to discover how it is structured" + ] + }, + { + "cell_type": "markdown", + "id": "caa9302b", + "metadata": {}, + "source": [ + "There is no foolproof way of doing this. A good first step is to see the type of our data!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "630a3433", + "metadata": {}, + "outputs": [], + "source": [ + "type(requests_json)" + ] + }, + { + "cell_type": "markdown", + "id": "199956bd", + "metadata": {}, + "source": [ + "Now we can navigate through this dictionary to see how the information is stored in the nested dictionaries and lists. The `keys` method can indicate what kind of information each dictionary holds, and the `len` function tells us how many entries are contained in a list. How you explore is up to you!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d84a812", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27d8dada", + "metadata": {}, + "outputs": [], + "source": [ + "len(requests_json['features'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1e0bc4b", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json['features'][0].keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "489844de", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json['features'][0]['properties'].keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8644b28f", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json['features'][0]['properties']['mag']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2ef8bfd", + "metadata": {}, + "outputs": [], + "source": [ + "requests_json['features'][0]['geometry']" + ] + }, + { + "cell_type": "markdown", + "id": "16829006", + "metadata": {}, + "source": [ + "Also note that some IDEs display JSON in a way that makes its structure easier to understand. Try saving this data in a text file and opening it in an IDE or a browser." + ] + }, + { + "cell_type": "markdown", + "id": "90d6ddbd", + "metadata": {}, + "source": [ + "### Find the largest quake" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a01f0da", + "metadata": {}, + "outputs": [], + "source": [ + "quakes = requests_json['features']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff319545", + "metadata": {}, + "outputs": [], + "source": [ + "largest_so_far = quakes[0]\n", + "for quake in quakes:\n", + " if quake['properties']['mag'] > largest_so_far['properties']['mag']:\n", + " largest_so_far = quake\n", + "largest_so_far['properties']['mag']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc6f0313", + "metadata": {}, + "outputs": [], + "source": [ + "lat = largest_so_far['geometry']['coordinates'][1]\n", + "long = largest_so_far['geometry']['coordinates'][0]\n", + "print(\"Latitude: {} Longitude: {}\".format(lat, long))" + ] + }, + { + "cell_type": "markdown", + "id": "7cef11ca", + "metadata": {}, + "source": [ + "### Get a map at the point of the quake" + ] + }, + { + "cell_type": "markdown", + "id": "6fcceef2", + "metadata": {}, + "source": [ + "We saw something similar in the [Greengraph example](../ch01python/010exemplar.html#More-complex-functions) [(notebook version)](../ch01python/010exemplar.ipynb#More-complex-functions) of the previous chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3739961", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "\n", + "def request_map_at(lat, long, satellite=True,\n", + " zoom=10, size=(400, 400)):\n", + " base = \"https://static-maps.yandex.ru/1.x/?\"\n", + "\n", + " params = dict(\n", + " z=zoom,\n", + " size=\"{},{}\".format(size[0], size[1]),\n", + " ll=\"{},{}\".format(long, lat),\n", + " l=\"sat\" if satellite else \"map\",\n", + " lang=\"en_US\"\n", + " )\n", + "\n", + " return requests.get(base, params=params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "103e54c8", + "metadata": {}, + "outputs": [], + "source": [ + "map_png = request_map_at(lat, long, zoom=10, satellite=False)" + ] + }, + { + "cell_type": "markdown", + "id": "0108125e", + "metadata": {}, + "source": [ + "### Display the map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71b0da72", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Image\n", + "Image(map_png.content)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Quakes Solution" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/068QuakesSolution.ipynb.py b/ch02data/068QuakesSolution.ipynb.py new file mode 100644 index 000000000..03f23debc --- /dev/null +++ b/ch02data/068QuakesSolution.ipynb.py @@ -0,0 +1,129 @@ +# --- +# jupyter: +# jekyll: +# display_name: Quakes Solution +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Solution to the earthquake exercise +# +# **NOTE:** This is intended as a reference for **after** you have attempted [the problem](./066QuakeExercise.html) [(notebook version)](./066QuakeExercise.ipynb) yourself! + +# %% [markdown] +# ### Download the data + +# %% +import requests +quakes = requests.get("http://earthquake.usgs.gov/fdsnws/event/1/query.geojson", + params={ + 'starttime': "2000-01-01", + "maxlatitude": "58.723", + "minlatitude": "50.008", + "maxlongitude": "1.67", + "minlongitude": "-9.756", + "minmagnitude": "1", + "endtime": "2018-10-11", + "orderby": "time-asc"} + ) + +# %% [markdown] +# ### Parse the data as JSON + +# %% +import json + +# %% +requests_json = json.loads(quakes.text) + +# %% [markdown] +# ### Investigate the data to discover how it is structured + +# %% [markdown] +# There is no foolproof way of doing this. A good first step is to see the type of our data! + +# %% +type(requests_json) + +# %% [markdown] +# Now we can navigate through this dictionary to see how the information is stored in the nested dictionaries and lists. The `keys` method can indicate what kind of information each dictionary holds, and the `len` function tells us how many entries are contained in a list. How you explore is up to you! + +# %% +requests_json.keys() + +# %% +len(requests_json['features']) + +# %% +requests_json['features'][0].keys() + +# %% +requests_json['features'][0]['properties'].keys() + +# %% +requests_json['features'][0]['properties']['mag'] + +# %% +requests_json['features'][0]['geometry'] + +# %% [markdown] +# Also note that some IDEs display JSON in a way that makes its structure easier to understand. Try saving this data in a text file and opening it in an IDE or a browser. + +# %% [markdown] +# ### Find the largest quake + +# %% +quakes = requests_json['features'] + +# %% +largest_so_far = quakes[0] +for quake in quakes: + if quake['properties']['mag'] > largest_so_far['properties']['mag']: + largest_so_far = quake +largest_so_far['properties']['mag'] + +# %% +lat = largest_so_far['geometry']['coordinates'][1] +long = largest_so_far['geometry']['coordinates'][0] +print("Latitude: {} Longitude: {}".format(lat, long)) + +# %% [markdown] +# ### Get a map at the point of the quake + +# %% [markdown] +# We saw something similar in the [Greengraph example](../ch01python/010exemplar.html#More-complex-functions) [(notebook version)](../ch01python/010exemplar.ipynb#More-complex-functions) of the previous chapter. + +# %% +import requests + + +def request_map_at(lat, long, satellite=True, + zoom=10, size=(400, 400)): + base = "https://static-maps.yandex.ru/1.x/?" + + params = dict( + z=zoom, + size="{},{}".format(size[0], size[1]), + ll="{},{}".format(long, lat), + l="sat" if satellite else "map", + lang="en_US" + ) + + return requests.get(base, params=params) + + +# %% +map_png = request_map_at(lat, long, zoom=10, satellite=False) + +# %% [markdown] +# ### Display the map + +# %% +from IPython.display import Image +Image(map_png.content) diff --git a/ch02data/070hdf5.html b/ch02data/070hdf5.html new file mode 100644 index 000000000..9522839fe --- /dev/null +++ b/ch02data/070hdf5.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Scientific File Formats + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Scientific File Formats

CSV, JSON and YAML are very common formats for representing general-purpose data, but their simplicity sometimes makes then inconvenient for scientific applications. A common drawback, for example, is that reading very large amounts of data from a CSV or JSON file can be inefficient. This has led to to the use of more targeted file formats which better address scientists' requirements for storing, accessing or manipulating data.

+

In this section, we will see an example of such a file format, and how to interact with files written in it programmatically.

+
+
+
+
+
+
+

HDF5

HDF5 is the current version of the Hierachical Data Format (HDF), and is commonly used to store large volumes of scientific data, such as experimental results or measurements. An HDF5 file contains two kinds of entities organised in a hierarchy, similar to a filesystem.

+
    +
  • Datasets contain scalar or array values. Each dataset has a type, such as integer, floating-point or string.

    +
  • +
  • Groups contain datasets or other groups, much like directories contain files and directories.

    +
  • +
+

Both datasets and groups can have attributes associated with them, which provide metadata about the contents.

+

For example, let's imagine we are trying to store some measurements of sea level at different locations and dates. One way to organise it is shown in the image below:

+
+
+
+
+
+
+

Structure of an example HDF5 file, including a dataset called locations and a group called measurements, which in turns contains another dataset

+
+
+
+
+
+
+

We will store the locations of our sampling points in a dataset called locations, and the actual results in a group called measurements. Within that group, we will have a dataset for each date we took samples on, which will contain results for all locations on that date. For instance, if we are collecting data from $N$ locations at $T$ times per day, each dataset will be a $N \times T$ array of numerical values (integer or floating-point, depending on how we want to record it).

+

One of the strengths of the HDF5 format is that a file can contain disparate kinds of data, of arbitrary size and types. The attributes provide additional information about the meaning or provenance of the data, and can even link to other datasets and groups within the file.

+
+
+
+
+
+
+

Working with HDF5 files

Unlike CSV or JSON files, which contain plain text, HDF5 is a binary file format. This means that the information stored there is encoded in a more complex way, and cannot be shown or edited using a simple text editor. Instead, to inspect the contents of an HDF5 file, we must use a more specialised application which "knows" how to to read the encoded information. One such application is HDFView.

+

An alternative is to interact with files programmatically - that is, use some code to read or write HDF5 files. Doing this from scratch would be tricky, but there are various libraries that let you interact with an HDF5 file from within your program. You can see examples of basic tasks in various programming languages, including Python, in the documentation pages of the HDF5 standard.

+
+
+
+
+
+
+

Accessing HDF5 files with Python

Let's now see an example of creating and reading an HDF5 file with Python. In line with the above, we will use the h5py library that gives us all the functionality we need.

+

We'll be creating a file that follows the structure of the climate example mentioned earlier.

+
+
+
+
+
+
+

The first thing we need to do is install the library. This can be done from the terminal, with the command

+
pip install h5py
+
+

Some distributions (like Anaconda) already include this library by default, in which case this command will not do anything except report that the library is already installed.

+

Once installed, we must import it in our file like any other library:

+
+
+
+
+
+
In [1]:
+
+
+
import h5py
+
+
+
+
+
+
+
+
+

Let's create a new HDF5 file that mirrors the structure of the above example. We start by creating an object that will represent this file in our program.

+
+
+
+
+
+
In [2]:
+
+
+
new_file = h5py.File('my_file.hdf5', 'w')
+
+
+
+
+
+
+
+
+

In the example, the file contains a dataset named locations and a group called measurements at the root level. We can add these to our empty file using some of the methods that the file object provides.

+
+
+
+
+
+
In [3]:
+
+
+
new_file.create_dataset('locations', data=[[55.9548, -3.11], [38.045, 23.999]])
+
+
+
+
+
+
+
+
Out[3]:
+
+
<HDF5 dataset "locations": shape (2, 2), type "<f8">
+
+
+
+
+
+
+
+
In [4]:
+
+
+
new_file.create_group('measurements')
+
+
+
+
+
+
+
+
Out[4]:
+
+
<HDF5 group "/measurements" (0 members)>
+
+
+
+
+
+
+
+
+

Note that the library lets us create empty datasets, which can be populated later. In this case, however, we initialise the dataset with some values at creation using the data argument.

+
+
+
+
+
+
+

The HDF5 file objects behave somewhat like Python dictionaries: we can access the new group with the usual indexing syntax ([...]). This next section shows how to do that and how to add a dataset to the group. Here, we add 4 measurements for each location for that day.

+
+
+
+
+
+
In [5]:
+
+
+
group = new_file['measurements']
+group.create_dataset("sea_level_20191012", data=[[10, 12, 7, 9], [20, 18, 23, 22]])
+
+
+
+
+
+
+
+
Out[5]:
+
+
<HDF5 dataset "sea_level_20191012": shape (2, 4), type "<i8">
+
+
+
+
+
+
+
+
+

When we are done with writing to the file, we must make sure to close it, so that all the changes are written to it (if they have not been already) and any used memory is released:

+
+
+
+
+
+
In [6]:
+
+
+
new_file.close()
+
+
+
+
+
+
+
+
+

There is a different style for reading and writing files, which is safer and saves you the need to close the file after you are finished. We can use this to read a file and iterate over its contents:

+
+
+
+
+
+
In [7]:
+
+
+
with h5py.File('my_file.hdf5', 'r') as hdf_file:
+    # Print out contents of root group
+    print("/ contains...")
+    for name in hdf_file:
+        print(name)
+    # Now print out the contents of the measurements group:
+    print("/measurements contains...")
+    for name in hdf_file['/measurements']:
+        print(name)
+
+
+
+
+
+
+
+
+
+
/ contains...
+locations
+measurements
+/measurements contains...
+sea_level_20191012
+
+
+
+
+
+
+
+
+
+

This is similar to the with open(...) syntax we use to work with text files - it is another example of a context manager.

+
+
+
+
+
+
+

There are many more ways you can access a file with h5py. If you are interested, you can look at the quick-start guide from its documentation for an overview.

+
+
+
+
+
+
+

Other formats

HDF5 is used across various scientific fields to store data, but some disciplines tend to use other file formats. Examples of such formats (and the python libraries) that are popular in particular disciplines are DICOM (pydicom) for medical imaging, FITS (astropy.io.fits) in astronomy, and NetCDF (netCDF4) in the geosciences.

+

The overall points that we have made about HDF5 generally apply to these formats as well. They are binary files which require specific applications, but you can also use various libraries to interact with them programmatically. Some libraries even offer support for multiple related types of files, such as different image formats.

+

If you often need to work with a particular type of files, try finding a relevant library in your chosen language. If you have not used it before, are you able to read or write a file using it?

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/070hdf5.ipynb b/ch02data/070hdf5.ipynb new file mode 100644 index 000000000..3b7e9dcfc --- /dev/null +++ b/ch02data/070hdf5.ipynb @@ -0,0 +1,258 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "114902ec", + "metadata": {}, + "source": [ + "## Scientific File Formats\n", + "\n", + "CSV, JSON and YAML are very common formats for representing general-purpose data, but their simplicity sometimes makes then inconvenient for scientific applications. A common drawback, for example, is that reading very large amounts of data from a CSV or JSON file can be inefficient. This has led to to the use of more targeted file formats which better address scientists' requirements for storing, accessing or manipulating data.\n", + "\n", + "In this section, we will see an example of such a file format, and how to interact with files written in it programmatically." + ] + }, + { + "cell_type": "markdown", + "id": "ff7b99df", + "metadata": {}, + "source": [ + "### HDF5 \n", + "\n", + "HDF5 is the current version of the [Hierachical Data Format](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) (HDF), and is commonly used to store large volumes of scientific data, such as experimental results or measurements. An HDF5 file contains two kinds of entities organised in a hierarchy, similar to a filesystem.\n", + "\n", + "- **Datasets** contain scalar or array values. Each dataset has a type, such as integer, floating-point or string.\n", + "\n", + "- **Groups** contain datasets or other groups, much like directories contain files and directories.\n", + "\n", + "Both datasets and groups can have **attributes** associated with them, which provide metadata about the contents.\n", + "\n", + "For example, let's imagine we are trying to store some measurements of sea level at different locations and dates. One way to organise it is shown in the image below:" + ] + }, + { + "cell_type": "markdown", + "id": "6fc3f854", + "metadata": {}, + "source": [ + "![Structure of an example HDF5 file, including a dataset called locations and a group called measurements, which in turns contains another dataset](hdf5_example.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "1b6ec8d2", + "metadata": {}, + "source": [ + "We will store the locations of our sampling points in a dataset called `locations`, and the actual results in a group called `measurements`. Within that group, we will have a dataset for each date we took samples on, which will contain results for all locations on that date. For instance, if we are collecting data from $N$ locations at $T$ times per day, each dataset will be a $N \\times T$ array of numerical values (integer or floating-point, depending on how we want to record it).\n", + "\n", + "One of the strengths of the HDF5 format is that a file can contain disparate kinds of data, of arbitrary size and types. The attributes provide additional information about the meaning or provenance of the data, and can even link to other datasets and groups within the file." + ] + }, + { + "cell_type": "markdown", + "id": "7d3360be", + "metadata": {}, + "source": [ + "#### Working with HDF5 files\n", + "\n", + "Unlike CSV or JSON files, which contain plain text, HDF5 is a **binary file** format. This means that the information stored there is encoded in a more complex way, and cannot be shown or edited using a simple text editor. Instead, to inspect the contents of an HDF5 file, we must use a more specialised application which \"knows\" how to to read the encoded information. One such application is [HDFView](https://www.hdfgroup.org/downloads/hdfview/).\n", + "\n", + "An alternative is to interact with files **programmatically** - that is, use some code to read or write HDF5 files. Doing this from scratch would be tricky, but there are various libraries that let you interact with an HDF5 file from within your program. You can see [examples of basic tasks](https://portal.hdfgroup.org/display/HDF5/Examples+from+Learning+the+Basics) in various programming languages, including Python, in the documentation pages of the HDF5 standard." + ] + }, + { + "cell_type": "markdown", + "id": "447f1a30", + "metadata": {}, + "source": [ + "#### Accessing HDF5 files with Python\n", + "\n", + "Let's now see an example of creating and reading an HDF5 file with Python. In line with the above, we will use the [`h5py` library](http://docs.h5py.org/en/stable/) that gives us all the functionality we need.\n", + "\n", + "We'll be creating a file that follows the structure of the climate example mentioned earlier." + ] + }, + { + "cell_type": "markdown", + "id": "ff024c01", + "metadata": {}, + "source": [ + "The first thing we need to do is install the library. This can be done from the terminal, with the command\n", + "```\n", + "pip install h5py\n", + "```\n", + "Some distributions (like Anaconda) already include this library by default, in which case this command will not do anything except report that the library is already installed.\n", + "\n", + "Once installed, we must import it in our file like any other library:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de4d3441", + "metadata": {}, + "outputs": [], + "source": [ + "import h5py" + ] + }, + { + "cell_type": "markdown", + "id": "9f127353", + "metadata": {}, + "source": [ + "Let's create a new HDF5 file that mirrors the structure of the above example. We start by creating an object that will represent this file in our program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e38429cd", + "metadata": {}, + "outputs": [], + "source": [ + "new_file = h5py.File('my_file.hdf5', 'w')" + ] + }, + { + "cell_type": "markdown", + "id": "d4328ff3", + "metadata": {}, + "source": [ + "In the example, the file contains a dataset named `locations` and a group called `measurements` at the root level. We can add these to our empty file using some of the methods that the file object provides." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f930cfb3", + "metadata": {}, + "outputs": [], + "source": [ + "new_file.create_dataset('locations', data=[[55.9548, -3.11], [38.045, 23.999]])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df770b75", + "metadata": {}, + "outputs": [], + "source": [ + "new_file.create_group('measurements')" + ] + }, + { + "cell_type": "markdown", + "id": "272ef443", + "metadata": {}, + "source": [ + "Note that the library lets us create empty datasets, which can be populated later. In this case, however, we initialise the dataset with some values at creation using the `data` argument." + ] + }, + { + "cell_type": "markdown", + "id": "ef2de8cb", + "metadata": {}, + "source": [ + "The HDF5 file objects behave somewhat like Python dictionaries: we can access the new group with the usual indexing syntax (`[...`]). This next section shows how to do that and how to add a dataset to the group. Here, we add 4 measurements for each location for that day." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "118fcc24", + "metadata": {}, + "outputs": [], + "source": [ + "group = new_file['measurements']\n", + "group.create_dataset(\"sea_level_20191012\", data=[[10, 12, 7, 9], [20, 18, 23, 22]])" + ] + }, + { + "cell_type": "markdown", + "id": "8047a30a", + "metadata": {}, + "source": [ + "When we are done with writing to the file, we must make sure to close it, so that all the changes are written to it (if they have not been already) and any used memory is released:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e49e5695", + "metadata": {}, + "outputs": [], + "source": [ + "new_file.close()" + ] + }, + { + "cell_type": "markdown", + "id": "dce505d7", + "metadata": {}, + "source": [ + "There is a different style for reading and writing files, which is safer and saves you the need to close the file after you are finished. We can use this to read a file and iterate over its contents:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4e8b40c", + "metadata": {}, + "outputs": [], + "source": [ + "with h5py.File('my_file.hdf5', 'r') as hdf_file:\n", + " # Print out contents of root group\n", + " print(\"/ contains...\")\n", + " for name in hdf_file:\n", + " print(name)\n", + " # Now print out the contents of the measurements group:\n", + " print(\"/measurements contains...\")\n", + " for name in hdf_file['/measurements']:\n", + " print(name)" + ] + }, + { + "cell_type": "markdown", + "id": "029eb319", + "metadata": {}, + "source": [ + "This is similar to the `with open(...)` syntax we use to work with text files - it is another example of a context manager." + ] + }, + { + "cell_type": "markdown", + "id": "31d6e59b", + "metadata": {}, + "source": [ + "There are many more ways you can access a file with `h5py`. If you are interested, you can look at [the quick-start guide](http://docs.h5py.org/en/stable/quick.html) from its documentation for an overview." + ] + }, + { + "cell_type": "markdown", + "id": "77c6fec7", + "metadata": {}, + "source": [ + "### Other formats\n", + "\n", + "HDF5 is used across various scientific fields to store data, but some disciplines tend to use other file formats. Examples of such formats (and the python libraries) that are popular in particular disciplines are [DICOM](https://en.wikipedia.org/wiki/DICOM) ([`pydicom`](https://pydicom.github.io/pydicom/stable/)) for medical imaging, [FITS](https://en.wikipedia.org/wiki/FITS) ([`astropy.io.fits`](http://docs.astropy.org/en/stable/io/fits/)) in astronomy, and [NetCDF](https://en.wikipedia.org/wiki/NetCDF) ([`netCDF4`](https://unidata.github.io/netcdf4-python/netCDF4/index.html)) in the geosciences.\n", + "\n", + "The overall points that we have made about HDF5 generally apply to these formats as well. They are binary files which require specific applications, but you can also use various libraries to interact with them programmatically. Some libraries even offer support for multiple related types of files, such as different image formats.\n", + "\n", + "If you often need to work with a particular type of files, try finding a relevant library in your chosen language. If you have not used it before, are you able to read or write a file using it?" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Scientific File Formats" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/070hdf5.ipynb.py b/ch02data/070hdf5.ipynb.py new file mode 100644 index 000000000..87f57cca3 --- /dev/null +++ b/ch02data/070hdf5.ipynb.py @@ -0,0 +1,126 @@ +# --- +# jupyter: +# jekyll: +# display_name: Scientific File Formats +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Scientific File Formats +# +# CSV, JSON and YAML are very common formats for representing general-purpose data, but their simplicity sometimes makes then inconvenient for scientific applications. A common drawback, for example, is that reading very large amounts of data from a CSV or JSON file can be inefficient. This has led to to the use of more targeted file formats which better address scientists' requirements for storing, accessing or manipulating data. +# +# In this section, we will see an example of such a file format, and how to interact with files written in it programmatically. + +# %% [markdown] +# ### HDF5 +# +# HDF5 is the current version of the [Hierachical Data Format](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) (HDF), and is commonly used to store large volumes of scientific data, such as experimental results or measurements. An HDF5 file contains two kinds of entities organised in a hierarchy, similar to a filesystem. +# +# - **Datasets** contain scalar or array values. Each dataset has a type, such as integer, floating-point or string. +# +# - **Groups** contain datasets or other groups, much like directories contain files and directories. +# +# Both datasets and groups can have **attributes** associated with them, which provide metadata about the contents. +# +# For example, let's imagine we are trying to store some measurements of sea level at different locations and dates. One way to organise it is shown in the image below: + +# %% [markdown] +# ![Structure of an example HDF5 file, including a dataset called locations and a group called measurements, which in turns contains another dataset](hdf5_example.svg) + +# %% [markdown] +# We will store the locations of our sampling points in a dataset called `locations`, and the actual results in a group called `measurements`. Within that group, we will have a dataset for each date we took samples on, which will contain results for all locations on that date. For instance, if we are collecting data from $N$ locations at $T$ times per day, each dataset will be a $N \times T$ array of numerical values (integer or floating-point, depending on how we want to record it). +# +# One of the strengths of the HDF5 format is that a file can contain disparate kinds of data, of arbitrary size and types. The attributes provide additional information about the meaning or provenance of the data, and can even link to other datasets and groups within the file. + +# %% [markdown] +# #### Working with HDF5 files +# +# Unlike CSV or JSON files, which contain plain text, HDF5 is a **binary file** format. This means that the information stored there is encoded in a more complex way, and cannot be shown or edited using a simple text editor. Instead, to inspect the contents of an HDF5 file, we must use a more specialised application which "knows" how to to read the encoded information. One such application is [HDFView](https://www.hdfgroup.org/downloads/hdfview/). +# +# An alternative is to interact with files **programmatically** - that is, use some code to read or write HDF5 files. Doing this from scratch would be tricky, but there are various libraries that let you interact with an HDF5 file from within your program. You can see [examples of basic tasks](https://portal.hdfgroup.org/display/HDF5/Examples+from+Learning+the+Basics) in various programming languages, including Python, in the documentation pages of the HDF5 standard. + +# %% [markdown] +# #### Accessing HDF5 files with Python +# +# Let's now see an example of creating and reading an HDF5 file with Python. In line with the above, we will use the [`h5py` library](http://docs.h5py.org/en/stable/) that gives us all the functionality we need. +# +# We'll be creating a file that follows the structure of the climate example mentioned earlier. + +# %% [markdown] +# The first thing we need to do is install the library. This can be done from the terminal, with the command +# ``` +# pip install h5py +# ``` +# Some distributions (like Anaconda) already include this library by default, in which case this command will not do anything except report that the library is already installed. +# +# Once installed, we must import it in our file like any other library: + +# %% +import h5py + +# %% [markdown] +# Let's create a new HDF5 file that mirrors the structure of the above example. We start by creating an object that will represent this file in our program. + +# %% +new_file = h5py.File('my_file.hdf5', 'w') + +# %% [markdown] +# In the example, the file contains a dataset named `locations` and a group called `measurements` at the root level. We can add these to our empty file using some of the methods that the file object provides. + +# %% +new_file.create_dataset('locations', data=[[55.9548, -3.11], [38.045, 23.999]]) + +# %% +new_file.create_group('measurements') + +# %% [markdown] +# Note that the library lets us create empty datasets, which can be populated later. In this case, however, we initialise the dataset with some values at creation using the `data` argument. + +# %% [markdown] +# The HDF5 file objects behave somewhat like Python dictionaries: we can access the new group with the usual indexing syntax (`[...`]). This next section shows how to do that and how to add a dataset to the group. Here, we add 4 measurements for each location for that day. + +# %% +group = new_file['measurements'] +group.create_dataset("sea_level_20191012", data=[[10, 12, 7, 9], [20, 18, 23, 22]]) + +# %% [markdown] +# When we are done with writing to the file, we must make sure to close it, so that all the changes are written to it (if they have not been already) and any used memory is released: + +# %% +new_file.close() + +# %% [markdown] +# There is a different style for reading and writing files, which is safer and saves you the need to close the file after you are finished. We can use this to read a file and iterate over its contents: + +# %% +with h5py.File('my_file.hdf5', 'r') as hdf_file: + # Print out contents of root group + print("/ contains...") + for name in hdf_file: + print(name) + # Now print out the contents of the measurements group: + print("/measurements contains...") + for name in hdf_file['/measurements']: + print(name) + +# %% [markdown] +# This is similar to the `with open(...)` syntax we use to work with text files - it is another example of a context manager. + +# %% [markdown] +# There are many more ways you can access a file with `h5py`. If you are interested, you can look at [the quick-start guide](http://docs.h5py.org/en/stable/quick.html) from its documentation for an overview. + +# %% [markdown] +# ### Other formats +# +# HDF5 is used across various scientific fields to store data, but some disciplines tend to use other file formats. Examples of such formats (and the python libraries) that are popular in particular disciplines are [DICOM](https://en.wikipedia.org/wiki/DICOM) ([`pydicom`](https://pydicom.github.io/pydicom/stable/)) for medical imaging, [FITS](https://en.wikipedia.org/wiki/FITS) ([`astropy.io.fits`](http://docs.astropy.org/en/stable/io/fits/)) in astronomy, and [NetCDF](https://en.wikipedia.org/wiki/NetCDF) ([`netCDF4`](https://unidata.github.io/netcdf4-python/netCDF4/index.html)) in the geosciences. +# +# The overall points that we have made about HDF5 generally apply to these formats as well. They are binary files which require specific applications, but you can also use various libraries to interact with them programmatically. Some libraries even offer support for multiple related types of files, such as different image formats. +# +# If you often need to work with a particular type of files, try finding a relevant library in your chosen language. If you have not used it before, are you able to read or write a file using it? diff --git a/ch02data/072plotting.html b/ch02data/072plotting.html new file mode 100644 index 000000000..f919f6784 --- /dev/null +++ b/ch02data/072plotting.html @@ -0,0 +1,1099 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Plotting + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Plotting with Matplotlib

Plotting data is very common and useful in scientific work. Python does not include any plotting functionality in the language itself, but there are various frameworks available for producing plots and visualisations.

+

In this section, we will look at Matplotlib, one of those frameworks. As the name indicates, it was conceived to provide an interface similar to the MATLAB programming language, but no knowledge of MATLAB is required!

+
+
+
+
+
+
+

Importing Matplotlib

+
+
+
+
+
+
+

We import the pyplot object from Matplotlib, which provides us with an interface for making figures. We usually abbreviate it.

+
+
+
+
+
+
In [1]:
+
+
+
from matplotlib import pyplot as plt
+
+
+
+
+
+
+
+
+

Notebook magics

+
+
+
+
+
+
+

When we write:

+
+
+
+
+
+
In [2]:
+
+
+
%matplotlib inline
+
+
+
+
+
+
+
+
+

We tell the Jupyter notebook to show figures we generate alongside the code that created it, rather than in a +separate window. Lines beginning with a single percent are not python code: they control how the notebook deals with python code.

+
+
+
+
+
+
+

Lines beginning with two percents are "cell magics", that tell Jupyter notebook how to interpret the particular cell; +we've seen %%writefile, for example.

+
+
+
+
+
+
+

A basic plot

+
+
+
+
+
+
+

When we write:

+
+
+
+
+
+
In [3]:
+
+
+
from math import sin, cos, pi
+my_fig = plt.plot([sin(pi * x / 100.0) for x in range(100)])
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The plot command returns a figure, just like the return value of any function. The notebook then displays this.

+
+
+
+
+
+
+

To add a title, axis labels etc, we need to get that figure object, and manipulate it. +For convenience, matplotlib allows us to do this just by issuing commands to change the "current figure":

+
+
+
+
+
+
In [4]:
+
+
+
plt.plot([sin(pi * x / 100.0) for x in range(100)])
+plt.title("Hello")
+
+
+
+
+
+
+
+
Out[4]:
+
+
Text(0.5, 1.0, 'Hello')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

But this requires us to keep all our commands together in a single cell, and makes use of a "global" single "current plot", +which, while convenient for quick exploratory sketches, is a bit cumbersome. To produce from our notebook proper plots +to use in papers, the library defines some types we can use to treat individual figures as variables, +and manipulate these.

+
+
+
+
+
+
+

Figures and Axes

+
+
+
+
+
+
+

We often want multiple graphs in a single figure (e.g. for figures which display a matrix of graphs of different variables for comparison).

+
+
+
+
+
+
+

So Matplotlib divides a figure object up into axes: each pair of axes is one 'subplot'. +To make a boring figure with just one pair of axes, however, we can just ask for a default new figure, with +brand new axes. The relevant function returns a (figure, axis) pair, which we can deal out with parallel assignment (unpacking).

+
+
+
+
+
+
In [5]:
+
+
+
sine_graph, sine_graph_axes = plt.subplots()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Once we have some axes, we can plot a graph on them:

+
+
+
+
+
+
In [6]:
+
+
+
sine_graph_axes.plot([sin(pi * x / 100.0) for x in range(100)], label='sin(x)')
+
+
+
+
+
+
+
+
Out[6]:
+
+
[<matplotlib.lines.Line2D at 0x7f296c37f9a0>]
+
+
+
+
+
+
+
+
+

We can add a title to a pair of axes:

+
+
+
+
+
+
In [7]:
+
+
+
sine_graph_axes.set_title("My graph")
+
+
+
+
+
+
+
+
Out[7]:
+
+
Text(0.5, 1.0, 'My graph')
+
+
+
+
+
+
+
+
In [8]:
+
+
+
sine_graph_axes.set_ylabel("f(x)")
+
+
+
+
+
+
+
+
Out[8]:
+
+
Text(4.444444444444445, 0.5, 'f(x)')
+
+
+
+
+
+
+
+
In [9]:
+
+
+
sine_graph_axes.set_xlabel("100 x")
+
+
+
+
+
+
+
+
Out[9]:
+
+
Text(0.5, 4.444444444444445, '100 x')
+
+
+
+
+
+
+
+
+

Now we need to actually display the figure. As always with the notebook, if we make a variable be returned by the last +line of a code cell, it gets displayed:

+
+
+
+
+
+
In [10]:
+
+
+
sine_graph
+
+
+
+
+
+
+
+
Out[10]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We can add another curve:

+
+
+
+
+
+
In [11]:
+
+
+
sine_graph_axes.plot([cos(pi * x / 100.0) for x in range(100)], label='cos(x)')
+
+
+
+
+
+
+
+
Out[11]:
+
+
[<matplotlib.lines.Line2D at 0x7f296c30aa60>]
+
+
+
+
+
+
+
+
In [12]:
+
+
+
sine_graph
+
+
+
+
+
+
+
+
Out[12]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

A legend will help us distinguish the curves:

+
+
+
+
+
+
In [13]:
+
+
+
sine_graph_axes.legend()
+
+
+
+
+
+
+
+
Out[13]:
+
+
<matplotlib.legend.Legend at 0x7f296c745280>
+
+
+
+
+
+
+
+
In [14]:
+
+
+
sine_graph
+
+
+
+
+
+
+
+
Out[14]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Saving figures

+
+
+
+
+
+
+

We must be able to save figures to disk, in order to use them in papers. This is really easy:

+
+
+
+
+
+
In [15]:
+
+
+
sine_graph.savefig('my_graph.png')
+
+
+
+
+
+
+
+
+

In order to be able to check that it worked, we need to know how to display an arbitrary image in the notebook.

+
+
+
+
+
+
+

The programmatic way is like this:

+
+
+
+
+
+
In [16]:
+
+
+
from IPython.display import Image # Get the notebook's own library for manipulating itself.
+Image(filename='my_graph.png')
+
+
+
+
+
+
+
+
Out[16]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Subplots

+
+
+
+
+
+
+

We might have wanted the $\sin$ and $\cos$ graphs on separate axes:

+
+
+
+
+
+
In [17]:
+
+
+
double_graph = plt.figure()
+
+
+
+
+
+
+
+
+
+
<Figure size 640x480 with 0 Axes>
+
+
+
+
+
+
+
+
In [18]:
+
+
+
sin_axes = double_graph.add_subplot(2, 1, 1) # 2 rows, 1 column, 1st subplot
+
+
+
+
+
+
+
+
In [19]:
+
+
+
cos_axes = double_graph.add_subplot(2, 1, 2) # 2 rows, 1 column, 2nd subplot
+
+
+
+
+
+
+
+
In [20]:
+
+
+
double_graph
+
+
+
+
+
+
+
+
Out[20]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [21]:
+
+
+
sin_axes.plot([sin(pi * x / 100.0) for x in range(100)])
+
+
+
+
+
+
+
+
Out[21]:
+
+
[<matplotlib.lines.Line2D at 0x7f296c21a0d0>]
+
+
+
+
+
+
+
+
In [22]:
+
+
+
sin_axes.set_ylabel("sin(x)")
+
+
+
+
+
+
+
+
Out[22]:
+
+
Text(4.444444444444445, 0.5, 'sin(x)')
+
+
+
+
+
+
+
+
In [23]:
+
+
+
cos_axes.plot([cos(pi * x / 100.0) for x in range(100)])
+
+
+
+
+
+
+
+
Out[23]:
+
+
[<matplotlib.lines.Line2D at 0x7f296c21ab20>]
+
+
+
+
+
+
+
+
In [24]:
+
+
+
cos_axes.set_ylabel("cos(x)")
+
+
+
+
+
+
+
+
Out[24]:
+
+
Text(4.444444444444445, 0.5, 'cos(x)')
+
+
+
+
+
+
+
+
In [25]:
+
+
+
cos_axes.set_xlabel("100 x")
+
+
+
+
+
+
+
+
Out[25]:
+
+
Text(0.5, 4.444444444444445, '100 x')
+
+
+
+
+
+
+
+
In [26]:
+
+
+
double_graph
+
+
+
+
+
+
+
+
Out[26]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Versus plots

+
+
+
+
+
+
+

When we specify a single list to plot, the x-values are just the array index number. We usually want to plot something +more meaningful:

+
+
+
+
+
+
In [27]:
+
+
+
double_graph = plt.figure()
+sin_axes = double_graph.add_subplot(2, 1, 1)
+cos_axes = double_graph.add_subplot(2, 1, 2)
+cos_axes.set_ylabel("cos(x)")
+sin_axes.set_ylabel("sin(x)")
+cos_axes.set_xlabel("x")
+
+
+
+
+
+
+
+
Out[27]:
+
+
Text(0.5, 0, 'x')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [28]:
+
+
+
sin_axes.plot([x / 100.0 for x in range(100)], [sin(pi * x / 100.0) for x in range(100)])
+cos_axes.plot([x / 100.0 for x in range(100)], [cos(pi * x / 100.0) for x in range(100)])
+
+
+
+
+
+
+
+
Out[28]:
+
+
[<matplotlib.lines.Line2D at 0x7f296c1143d0>]
+
+
+
+
+
+
+
+
In [29]:
+
+
+
double_graph
+
+
+
+
+
+
+
+
Out[29]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Learning More

+
+
+
+
+
+
+

There's so much more to learn about matplotlib: pie charts, bar charts, heat maps, 3-d plotting, animated plots, and so on. You can learn all this via the Matplotlib Website. +You should try to get comfortable with all this, so please use some time in class, or at home, to work your way through a bunch of the examples.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/072plotting.ipynb b/ch02data/072plotting.ipynb new file mode 100644 index 000000000..952fd9970 --- /dev/null +++ b/ch02data/072plotting.ipynb @@ -0,0 +1,569 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cf3d2f30", + "metadata": {}, + "source": [ + "## Plotting with Matplotlib\n", + "\n", + "Plotting data is very common and useful in scientific work. Python does not include any plotting functionality in the language itself, but there are various frameworks available for producing plots and visualisations.\n", + "\n", + "In this section, we will look at Matplotlib, one of those frameworks. As the name indicates, it was conceived to provide an interface similar to the MATLAB programming language, but no knowledge of MATLAB is required!" + ] + }, + { + "cell_type": "markdown", + "id": "760fbb66", + "metadata": {}, + "source": [ + "### Importing Matplotlib" + ] + }, + { + "cell_type": "markdown", + "id": "8e9a18a3", + "metadata": {}, + "source": [ + "We import the `pyplot` object from Matplotlib, which provides us with an interface for making figures. We usually abbreviate it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75df931b", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "5b4490c1", + "metadata": {}, + "source": [ + "### Notebook magics" + ] + }, + { + "cell_type": "markdown", + "id": "f9552f9a", + "metadata": {}, + "source": [ + "When we write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea2b0c7b", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "9aea6f2f", + "metadata": {}, + "source": [ + "We tell the Jupyter notebook to show figures we generate alongside the code that created it, rather than in a\n", + "separate window. Lines beginning with a single percent are not python code: they control how the notebook deals with python code." + ] + }, + { + "cell_type": "markdown", + "id": "c23c914b", + "metadata": {}, + "source": [ + "Lines beginning with two percents are \"cell magics\", that tell Jupyter notebook how to interpret the particular cell;\n", + "we've seen `%%writefile`, for example." + ] + }, + { + "cell_type": "markdown", + "id": "09cd8230", + "metadata": {}, + "source": [ + "### A basic plot" + ] + }, + { + "cell_type": "markdown", + "id": "21124e5e", + "metadata": {}, + "source": [ + "When we write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "415a29b4", + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos, pi\n", + "my_fig = plt.plot([sin(pi * x / 100.0) for x in range(100)])" + ] + }, + { + "cell_type": "markdown", + "id": "72d80347", + "metadata": {}, + "source": [ + "The plot command *returns* a figure, just like the return value of any function. The notebook then displays this." + ] + }, + { + "cell_type": "markdown", + "id": "b634d096", + "metadata": {}, + "source": [ + "To add a title, axis labels etc, we need to get that figure object, and manipulate it. \n", + "For convenience, matplotlib allows us to do this just by issuing commands to change the \"current figure\":" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0770b1cc", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot([sin(pi * x / 100.0) for x in range(100)])\n", + "plt.title(\"Hello\")" + ] + }, + { + "cell_type": "markdown", + "id": "56fe5bfb", + "metadata": {}, + "source": [ + "But this requires us to keep all our commands together in a single cell, and makes use of a \"global\" single \"current plot\",\n", + "which, while convenient for quick exploratory sketches, is a bit cumbersome. To produce from our notebook proper plots\n", + "to use in papers, the library defines some types we can use to treat individual figures as variables,\n", + "and manipulate these." + ] + }, + { + "cell_type": "markdown", + "id": "3342621b", + "metadata": {}, + "source": [ + "### Figures and Axes" + ] + }, + { + "cell_type": "markdown", + "id": "5750ddd8", + "metadata": {}, + "source": [ + "We often want multiple graphs in a single figure (e.g. for figures which display a matrix of graphs of different variables for comparison)." + ] + }, + { + "cell_type": "markdown", + "id": "141dedc5", + "metadata": {}, + "source": [ + "So Matplotlib divides a `figure` object up into axes: each pair of axes is one 'subplot'. \n", + "To make a boring figure with just one pair of axes, however, we can just ask for a default new figure, with\n", + "brand new axes. The relevant function returns a (figure, axis) pair, which we can deal out with parallel assignment (unpacking). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1a9d66d", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph, sine_graph_axes = plt.subplots()" + ] + }, + { + "cell_type": "markdown", + "id": "41f2b2be", + "metadata": {}, + "source": [ + "Once we have some axes, we can plot a graph on them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5df8d02", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.plot([sin(pi * x / 100.0) for x in range(100)], label='sin(x)')" + ] + }, + { + "cell_type": "markdown", + "id": "9a4b56a0", + "metadata": {}, + "source": [ + "We can add a title to a pair of axes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e27b9f7", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.set_title(\"My graph\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44c07432", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.set_ylabel(\"f(x)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aac78a7e", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.set_xlabel(\"100 x\")" + ] + }, + { + "cell_type": "markdown", + "id": "7cb38637", + "metadata": {}, + "source": [ + "Now we need to actually display the figure. As always with the notebook, if we make a variable be returned by the last\n", + "line of a code cell, it gets displayed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba5772b8", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph" + ] + }, + { + "cell_type": "markdown", + "id": "46183e1c", + "metadata": {}, + "source": [ + "We can add another curve:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8917bd45", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.plot([cos(pi * x / 100.0) for x in range(100)], label='cos(x)')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0d5c83b", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph" + ] + }, + { + "cell_type": "markdown", + "id": "eb045b84", + "metadata": {}, + "source": [ + "A legend will help us distinguish the curves:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "470e64d5", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph_axes.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baefe3d1", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph" + ] + }, + { + "cell_type": "markdown", + "id": "80d36479", + "metadata": {}, + "source": [ + "### Saving figures" + ] + }, + { + "cell_type": "markdown", + "id": "9cbdf930", + "metadata": {}, + "source": [ + "We must be able to save figures to disk, in order to use them in papers. This is really easy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5284bffe", + "metadata": {}, + "outputs": [], + "source": [ + "sine_graph.savefig('my_graph.png')" + ] + }, + { + "cell_type": "markdown", + "id": "ccc53f75", + "metadata": {}, + "source": [ + "In order to be able to check that it worked, we need to know how to display an arbitrary image in the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "088c182a", + "metadata": {}, + "source": [ + "The programmatic way is like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b99af09f", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Image # Get the notebook's own library for manipulating itself.\n", + "Image(filename='my_graph.png')" + ] + }, + { + "cell_type": "markdown", + "id": "50eb1cc7", + "metadata": {}, + "source": [ + "### Subplots" + ] + }, + { + "cell_type": "markdown", + "id": "d831fdbc", + "metadata": {}, + "source": [ + "We might have wanted the $\\sin$ and $\\cos$ graphs on separate axes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8a0aade", + "metadata": {}, + "outputs": [], + "source": [ + "double_graph = plt.figure()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d71594d", + "metadata": {}, + "outputs": [], + "source": [ + "sin_axes = double_graph.add_subplot(2, 1, 1) # 2 rows, 1 column, 1st subplot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "755c7245", + "metadata": {}, + "outputs": [], + "source": [ + "cos_axes = double_graph.add_subplot(2, 1, 2) # 2 rows, 1 column, 2nd subplot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5090a3a5", + "metadata": {}, + "outputs": [], + "source": [ + "double_graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b6c2870", + "metadata": {}, + "outputs": [], + "source": [ + "sin_axes.plot([sin(pi * x / 100.0) for x in range(100)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5dfc017", + "metadata": {}, + "outputs": [], + "source": [ + "sin_axes.set_ylabel(\"sin(x)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bebde72", + "metadata": {}, + "outputs": [], + "source": [ + "cos_axes.plot([cos(pi * x / 100.0) for x in range(100)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ad466fc", + "metadata": {}, + "outputs": [], + "source": [ + "cos_axes.set_ylabel(\"cos(x)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d937a4b", + "metadata": {}, + "outputs": [], + "source": [ + "cos_axes.set_xlabel(\"100 x\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "863a0a68", + "metadata": {}, + "outputs": [], + "source": [ + "double_graph" + ] + }, + { + "cell_type": "markdown", + "id": "d31fa337", + "metadata": {}, + "source": [ + "### Versus plots" + ] + }, + { + "cell_type": "markdown", + "id": "1be4f8aa", + "metadata": {}, + "source": [ + "When we specify a single `list` to `plot`, the x-values are just the array index number. We usually want to plot something\n", + "more meaningful:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "069ec098", + "metadata": {}, + "outputs": [], + "source": [ + "double_graph = plt.figure()\n", + "sin_axes = double_graph.add_subplot(2, 1, 1)\n", + "cos_axes = double_graph.add_subplot(2, 1, 2)\n", + "cos_axes.set_ylabel(\"cos(x)\")\n", + "sin_axes.set_ylabel(\"sin(x)\")\n", + "cos_axes.set_xlabel(\"x\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f6ecd99", + "metadata": {}, + "outputs": [], + "source": [ + "sin_axes.plot([x / 100.0 for x in range(100)], [sin(pi * x / 100.0) for x in range(100)])\n", + "cos_axes.plot([x / 100.0 for x in range(100)], [cos(pi * x / 100.0) for x in range(100)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90296821", + "metadata": {}, + "outputs": [], + "source": [ + "double_graph" + ] + }, + { + "cell_type": "markdown", + "id": "6d501319", + "metadata": {}, + "source": [ + "### Learning More" + ] + }, + { + "cell_type": "markdown", + "id": "8dab2c1a", + "metadata": {}, + "source": [ + "There's so much more to learn about matplotlib: pie charts, bar charts, heat maps, 3-d plotting, animated plots, and so on. You can learn all this via the [Matplotlib Website](http://matplotlib.org/).\n", + "You should try to get comfortable with all this, so please use some time in class, or at home, to work your way through a bunch of the [examples](https://matplotlib.org/stable/gallery/index)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Plotting" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/072plotting.ipynb.py b/ch02data/072plotting.ipynb.py new file mode 100644 index 000000000..0414b879b --- /dev/null +++ b/ch02data/072plotting.ipynb.py @@ -0,0 +1,213 @@ +# --- +# jupyter: +# jekyll: +# display_name: Plotting +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Plotting with Matplotlib +# +# Plotting data is very common and useful in scientific work. Python does not include any plotting functionality in the language itself, but there are various frameworks available for producing plots and visualisations. +# +# In this section, we will look at Matplotlib, one of those frameworks. As the name indicates, it was conceived to provide an interface similar to the MATLAB programming language, but no knowledge of MATLAB is required! + +# %% [markdown] +# ### Importing Matplotlib + +# %% [markdown] +# We import the `pyplot` object from Matplotlib, which provides us with an interface for making figures. We usually abbreviate it. + +# %% +from matplotlib import pyplot as plt + +# %% [markdown] +# ### Notebook magics + +# %% [markdown] +# When we write: + +# %% +# %matplotlib inline + +# %% [markdown] +# We tell the Jupyter notebook to show figures we generate alongside the code that created it, rather than in a +# separate window. Lines beginning with a single percent are not python code: they control how the notebook deals with python code. + +# %% [markdown] +# Lines beginning with two percents are "cell magics", that tell Jupyter notebook how to interpret the particular cell; +# we've seen `%%writefile`, for example. + +# %% [markdown] +# ### A basic plot + +# %% [markdown] +# When we write: + +# %% +from math import sin, cos, pi +my_fig = plt.plot([sin(pi * x / 100.0) for x in range(100)]) + +# %% [markdown] +# The plot command *returns* a figure, just like the return value of any function. The notebook then displays this. + +# %% [markdown] +# To add a title, axis labels etc, we need to get that figure object, and manipulate it. +# For convenience, matplotlib allows us to do this just by issuing commands to change the "current figure": + +# %% +plt.plot([sin(pi * x / 100.0) for x in range(100)]) +plt.title("Hello") + +# %% [markdown] +# But this requires us to keep all our commands together in a single cell, and makes use of a "global" single "current plot", +# which, while convenient for quick exploratory sketches, is a bit cumbersome. To produce from our notebook proper plots +# to use in papers, the library defines some types we can use to treat individual figures as variables, +# and manipulate these. + +# %% [markdown] +# ### Figures and Axes + +# %% [markdown] +# We often want multiple graphs in a single figure (e.g. for figures which display a matrix of graphs of different variables for comparison). + +# %% [markdown] +# So Matplotlib divides a `figure` object up into axes: each pair of axes is one 'subplot'. +# To make a boring figure with just one pair of axes, however, we can just ask for a default new figure, with +# brand new axes. The relevant function returns a (figure, axis) pair, which we can deal out with parallel assignment (unpacking). + +# %% +sine_graph, sine_graph_axes = plt.subplots() + +# %% [markdown] +# Once we have some axes, we can plot a graph on them: + +# %% +sine_graph_axes.plot([sin(pi * x / 100.0) for x in range(100)], label='sin(x)') + +# %% [markdown] +# We can add a title to a pair of axes: + +# %% +sine_graph_axes.set_title("My graph") + +# %% +sine_graph_axes.set_ylabel("f(x)") + +# %% +sine_graph_axes.set_xlabel("100 x") + +# %% [markdown] +# Now we need to actually display the figure. As always with the notebook, if we make a variable be returned by the last +# line of a code cell, it gets displayed: + +# %% +sine_graph + +# %% [markdown] +# We can add another curve: + +# %% +sine_graph_axes.plot([cos(pi * x / 100.0) for x in range(100)], label='cos(x)') + +# %% +sine_graph + +# %% [markdown] +# A legend will help us distinguish the curves: + +# %% +sine_graph_axes.legend() + +# %% +sine_graph + +# %% [markdown] +# ### Saving figures + +# %% [markdown] +# We must be able to save figures to disk, in order to use them in papers. This is really easy: + +# %% +sine_graph.savefig('my_graph.png') + +# %% [markdown] +# In order to be able to check that it worked, we need to know how to display an arbitrary image in the notebook. + +# %% [markdown] +# The programmatic way is like this: + +# %% +from IPython.display import Image # Get the notebook's own library for manipulating itself. +Image(filename='my_graph.png') + +# %% [markdown] +# ### Subplots + +# %% [markdown] +# We might have wanted the $\sin$ and $\cos$ graphs on separate axes: + +# %% +double_graph = plt.figure() + +# %% +sin_axes = double_graph.add_subplot(2, 1, 1) # 2 rows, 1 column, 1st subplot + +# %% +cos_axes = double_graph.add_subplot(2, 1, 2) # 2 rows, 1 column, 2nd subplot + +# %% +double_graph + +# %% +sin_axes.plot([sin(pi * x / 100.0) for x in range(100)]) + +# %% +sin_axes.set_ylabel("sin(x)") + +# %% +cos_axes.plot([cos(pi * x / 100.0) for x in range(100)]) + +# %% +cos_axes.set_ylabel("cos(x)") + +# %% +cos_axes.set_xlabel("100 x") + +# %% +double_graph + +# %% [markdown] +# ### Versus plots + +# %% [markdown] +# When we specify a single `list` to `plot`, the x-values are just the array index number. We usually want to plot something +# more meaningful: + +# %% +double_graph = plt.figure() +sin_axes = double_graph.add_subplot(2, 1, 1) +cos_axes = double_graph.add_subplot(2, 1, 2) +cos_axes.set_ylabel("cos(x)") +sin_axes.set_ylabel("sin(x)") +cos_axes.set_xlabel("x") + +# %% +sin_axes.plot([x / 100.0 for x in range(100)], [sin(pi * x / 100.0) for x in range(100)]) +cos_axes.plot([x / 100.0 for x in range(100)], [cos(pi * x / 100.0) for x in range(100)]) + +# %% +double_graph + +# %% [markdown] +# ### Learning More + +# %% [markdown] +# There's so much more to learn about matplotlib: pie charts, bar charts, heat maps, 3-d plotting, animated plots, and so on. You can learn all this via the [Matplotlib Website](http://matplotlib.org/). +# You should try to get comfortable with all this, so please use some time in class, or at home, to work your way through a bunch of the [examples](https://matplotlib.org/stable/gallery/index). diff --git a/ch02data/082NumPy.html b/ch02data/082NumPy.html new file mode 100644 index 000000000..1d14465bd --- /dev/null +++ b/ch02data/082NumPy.html @@ -0,0 +1,2902 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Numerical Python + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

NumPy

+
+
+
+
+
+
+

The Scientific Python Trilogy

+
+
+
+
+
+
+

Why is Python so popular for research work?

+
+
+
+
+
+
+

MATLAB has typically been the most popular "language of technical computing", with strong built-in support for efficient numerical analysis with matrices (the mat in MATLAB is for Matrix, not Maths), and plotting.

+
+
+
+
+
+
+

Other dynamic languages have cleaner, more logical syntax (Ruby, Haskell)

+
+
+
+
+
+
+

But Python users developed three critical libraries, matching the power of MATLAB for scientific work:

+
+
+
+
+
+
+ +
+
+
+
+
+
+

By combining a plotting library, a matrix maths library, and an easy-to-use interface allowing live plotting commands +in a persistent environment, the powerful capabilities of MATLAB were matched by a free and open toolchain.

+
+
+
+
+
+
+

We've learned about Matplotlib and IPython in this course already. NumPy is the last part of the trilogy.

+
+
+
+
+
+
+

Limitations of Python Lists

+
+
+
+
+
+
+

The normal Python list is just one dimensional. To make a matrix, we have to nest Python lists:

+
+
+
+
+
+
In [1]:
+
+
+
x = [list(range(5)) for N in range(5)]
+
+
+
+
+
+
+
+
In [2]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[2]:
+
+
[[0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4]]
+
+
+
+
+
+
+
+
In [3]:
+
+
+
x[2][2]
+
+
+
+
+
+
+
+
Out[3]:
+
+
2
+
+
+
+
+
+
+
+
+

Applying an operation to every element is a pain:

+
+
+
+
+
+
In [4]:
+
+
+
x + 5
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[4], line 1
+----> 1 x + 5
+
+TypeError: can only concatenate list (not "int") to list
+
+
+
+
+
+
+
+
In [5]:
+
+
+
[[elem + 5 for elem in row] for row in x]
+
+
+
+
+
+
+
+
Out[5]:
+
+
[[5, 6, 7, 8, 9],
+ [5, 6, 7, 8, 9],
+ [5, 6, 7, 8, 9],
+ [5, 6, 7, 8, 9],
+ [5, 6, 7, 8, 9]]
+
+
+
+
+
+
+
+
+

Common useful operations like transposing a matrix or reshaping a 10 by 10 matrix into a 20 by 5 matrix are not easy to code in raw Python lists.

+
+
+
+
+
+
+

The NumPy array

+
+
+
+
+
+
+

NumPy's array type represents a multidimensional matrix $M_{i,j,k...n}$

+
+
+
+
+
+
+

The NumPy array seems at first to be just like a list. For example, we can index it and iterate over it:

+
+
+
+
+
+
In [6]:
+
+
+
import numpy as np
+my_array = np.array(range(5))
+
+
+
+
+
+
+
+
In [7]:
+
+
+
my_array
+
+
+
+
+
+
+
+
Out[7]:
+
+
array([0, 1, 2, 3, 4])
+
+
+
+
+
+
+
+
In [8]:
+
+
+
my_array[2]
+
+
+
+
+
+
+
+
Out[8]:
+
+
2
+
+
+
+
+
+
+
+
In [9]:
+
+
+
for element in my_array:
+    print("Hello" * element)
+
+
+
+
+
+
+
+
+
+
+Hello
+HelloHello
+HelloHelloHello
+HelloHelloHelloHello
+
+
+
+
+
+
+
+
+
+

We can also see our first weakness of NumPy arrays versus Python lists:

+
+
+
+
+
+
In [10]:
+
+
+
my_array.append(4)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[10], line 1
+----> 1 my_array.append(4)
+
+AttributeError: 'numpy.ndarray' object has no attribute 'append'
+
+
+
+
+
+
+
+
+

For NumPy arrays, you typically don't change the data size once you've defined your array, +whereas for Python lists, you can do this efficiently. However, you get back lots of goodies in return...

+
+
+
+
+
+
+

Elementwise Operations

+
+
+
+
+
+
+

Most operations can be applied element-wise automatically!

+
+
+
+
+
+
In [11]:
+
+
+
my_array + 2
+
+
+
+
+
+
+
+
Out[11]:
+
+
array([2, 3, 4, 5, 6])
+
+
+
+
+
+
+
+
+

These "vectorized" operations are very fast: (the %%timeit magic reports how long it takes to run a cell; there is more information available if interested)

+
+
+
+
+
+
In [12]:
+
+
+
import numpy as np
+big_list = range(10000)
+big_array = np.arange(10000)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%timeit
+[x**2 for x in big_list]
+
+
+
+
+
+
+
+
+
+
3.06 ms ± 14.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
+
+
+
+
+
+
+
+
+
In [14]:
+
+
+
%%timeit
+big_array**2
+
+
+
+
+
+
+
+
+
+
3.33 µs ± 27.3 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
+
+
+
+
+
+
+
+
+
+

arange and linspace

+
+
+
+
+
+
+

NumPy has two methods for quickly defining evenly-spaced arrays of (floating-point) numbers. These can be useful, for example, in plotting.

+

The first method is arange:

+
+
+
+
+
+
In [15]:
+
+
+
x = np.arange(0, 10, 0.1)  # Start, stop, step size
+
+
+
+
+
+
+
+
+

This is similar to Python's range, although note that we can't use non-integer steps with the latter!

+
+
+
+
+
+
In [16]:
+
+
+
y = list(range(0, 10, 0.1))
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[16], line 1
+----> 1 y = list(range(0, 10, 0.1))
+
+TypeError: 'float' object cannot be interpreted as an integer
+
+
+
+
+
+
+
+
+

The second method is linspace:

+
+
+
+
+
+
In [17]:
+
+
+
import math
+values = np.linspace(0, math.pi, 100)  # Start, stop, number of steps
+
+
+
+
+
+
+
+
In [18]:
+
+
+
values
+
+
+
+
+
+
+
+
Out[18]:
+
+
array([0.        , 0.03173326, 0.06346652, 0.09519978, 0.12693304,
+       0.1586663 , 0.19039955, 0.22213281, 0.25386607, 0.28559933,
+       0.31733259, 0.34906585, 0.38079911, 0.41253237, 0.44426563,
+       0.47599889, 0.50773215, 0.53946541, 0.57119866, 0.60293192,
+       0.63466518, 0.66639844, 0.6981317 , 0.72986496, 0.76159822,
+       0.79333148, 0.82506474, 0.856798  , 0.88853126, 0.92026451,
+       0.95199777, 0.98373103, 1.01546429, 1.04719755, 1.07893081,
+       1.11066407, 1.14239733, 1.17413059, 1.20586385, 1.23759711,
+       1.26933037, 1.30106362, 1.33279688, 1.36453014, 1.3962634 ,
+       1.42799666, 1.45972992, 1.49146318, 1.52319644, 1.5549297 ,
+       1.58666296, 1.61839622, 1.65012947, 1.68186273, 1.71359599,
+       1.74532925, 1.77706251, 1.80879577, 1.84052903, 1.87226229,
+       1.90399555, 1.93572881, 1.96746207, 1.99919533, 2.03092858,
+       2.06266184, 2.0943951 , 2.12612836, 2.15786162, 2.18959488,
+       2.22132814, 2.2530614 , 2.28479466, 2.31652792, 2.34826118,
+       2.37999443, 2.41172769, 2.44346095, 2.47519421, 2.50692747,
+       2.53866073, 2.57039399, 2.60212725, 2.63386051, 2.66559377,
+       2.69732703, 2.72906028, 2.76079354, 2.7925268 , 2.82426006,
+       2.85599332, 2.88772658, 2.91945984, 2.9511931 , 2.98292636,
+       3.01465962, 3.04639288, 3.07812614, 3.10985939, 3.14159265])
+
+
+
+
+
+
+
+
+

Regardless of the method used, the array of values that we get can be used in the same way.

+

In fact, NumPy comes with "vectorised" versions of common functions which work element-by-element when applied to arrays:

+
+
+
+
+
+
In [19]:
+
+
+
%matplotlib inline
+
+from matplotlib import pyplot as plt
+plt.plot(values, np.sin(values))
+
+
+
+
+
+
+
+
Out[19]:
+
+
[<matplotlib.lines.Line2D at 0x7f716c002a30>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

So we don't have to use awkward list comprehensions when using these.

+
+
+
+
+
+
+

Multi-Dimensional Arrays

+
+
+
+
+
+
+

NumPy's true power comes from multi-dimensional arrays:

+
+
+
+
+
+
In [20]:
+
+
+
np.zeros([3, 4, 2])  # 3 arrays with 4 rows and 2 columns each
+
+
+
+
+
+
+
+
Out[20]:
+
+
array([[[0., 0.],
+        [0., 0.],
+        [0., 0.],
+        [0., 0.]],
+
+       [[0., 0.],
+        [0., 0.],
+        [0., 0.],
+        [0., 0.]],
+
+       [[0., 0.],
+        [0., 0.],
+        [0., 0.],
+        [0., 0.]]])
+
+
+
+
+
+
+
+
+

Unlike a list-of-lists in Python, we can reshape arrays:

+
+
+
+
+
+
In [21]:
+
+
+
x = np.array(range(40))
+x
+
+
+
+
+
+
+
+
Out[21]:
+
+
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
+       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
+       34, 35, 36, 37, 38, 39])
+
+
+
+
+
+
+
+
In [22]:
+
+
+
y = x.reshape([4, 5, 2])
+y
+
+
+
+
+
+
+
+
Out[22]:
+
+
array([[[ 0,  1],
+        [ 2,  3],
+        [ 4,  5],
+        [ 6,  7],
+        [ 8,  9]],
+
+       [[10, 11],
+        [12, 13],
+        [14, 15],
+        [16, 17],
+        [18, 19]],
+
+       [[20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29]],
+
+       [[30, 31],
+        [32, 33],
+        [34, 35],
+        [36, 37],
+        [38, 39]]])
+
+
+
+
+
+
+
+
+

And index multiple columns at once:

+
+
+
+
+
+
In [23]:
+
+
+
y[3, 2, 1]
+
+
+
+
+
+
+
+
Out[23]:
+
+
35
+
+
+
+
+
+
+
+
+

Including selecting on inner axes while taking all from the outermost:

+
+
+
+
+
+
In [24]:
+
+
+
y[:, 2, 1]
+
+
+
+
+
+
+
+
Out[24]:
+
+
array([ 5, 15, 25, 35])
+
+
+
+
+
+
+
+
+

And subselecting ranges:

+
+
+
+
+
+
In [25]:
+
+
+
y[2:, :1, :]  # Last 2 axes, 1st row, all columns
+
+
+
+
+
+
+
+
Out[25]:
+
+
array([[[20, 21]],
+
+       [[30, 31]]])
+
+
+
+
+
+
+
+
+

And transpose arrays:

+
+
+
+
+
+
In [26]:
+
+
+
y.transpose()
+
+
+
+
+
+
+
+
Out[26]:
+
+
array([[[ 0, 10, 20, 30],
+        [ 2, 12, 22, 32],
+        [ 4, 14, 24, 34],
+        [ 6, 16, 26, 36],
+        [ 8, 18, 28, 38]],
+
+       [[ 1, 11, 21, 31],
+        [ 3, 13, 23, 33],
+        [ 5, 15, 25, 35],
+        [ 7, 17, 27, 37],
+        [ 9, 19, 29, 39]]])
+
+
+
+
+
+
+
+
+

You can get the dimensions of an array with shape:

+
+
+
+
+
+
In [27]:
+
+
+
y.shape
+
+
+
+
+
+
+
+
Out[27]:
+
+
(4, 5, 2)
+
+
+
+
+
+
+
+
In [28]:
+
+
+
y.transpose().shape
+
+
+
+
+
+
+
+
Out[28]:
+
+
(2, 5, 4)
+
+
+
+
+
+
+
+
+

Some numpy functions apply by default to the whole array, but can be chosen to act only on certain axes:

+
+
+
+
+
+
In [29]:
+
+
+
x = np.arange(12).reshape(4,3)
+x
+
+
+
+
+
+
+
+
Out[29]:
+
+
array([[ 0,  1,  2],
+       [ 3,  4,  5],
+       [ 6,  7,  8],
+       [ 9, 10, 11]])
+
+
+
+
+
+
+
+
In [30]:
+
+
+
x.mean(1)  # Mean along the second axis, leaving the first.
+
+
+
+
+
+
+
+
Out[30]:
+
+
array([ 1.,  4.,  7., 10.])
+
+
+
+
+
+
+
+
In [31]:
+
+
+
x.mean(0)  # Mean along the first axis, leaving the second.
+
+
+
+
+
+
+
+
Out[31]:
+
+
array([4.5, 5.5, 6.5])
+
+
+
+
+
+
+
+
In [32]:
+
+
+
x.mean()  # mean of all axes
+
+
+
+
+
+
+
+
Out[32]:
+
+
5.5
+
+
+
+
+
+
+
+
+

Array Datatypes

+
+
+
+
+
+
+

A Python list can contain data of mixed type:

+
+
+
+
+
+
In [33]:
+
+
+
x = ['hello', 2, 3.4]
+
+
+
+
+
+
+
+
In [34]:
+
+
+
type(x[2])
+
+
+
+
+
+
+
+
Out[34]:
+
+
float
+
+
+
+
+
+
+
+
In [35]:
+
+
+
type(x[1])
+
+
+
+
+
+
+
+
Out[35]:
+
+
int
+
+
+
+
+
+
+
+
+

A NumPy array always contains just one datatype:

+
+
+
+
+
+
In [36]:
+
+
+
np.array(x)
+
+
+
+
+
+
+
+
Out[36]:
+
+
array(['hello', '2', '3.4'], dtype='<U32')
+
+
+
+
+
+
+
+
+

NumPy will choose the least-generic-possible datatype that can contain the data:

+
+
+
+
+
+
In [37]:
+
+
+
y = np.array([2, 3.4])
+
+
+
+
+
+
+
+
In [38]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[38]:
+
+
array([2. , 3.4])
+
+
+
+
+
+
+
+
+

You can access the array's dtype, or check the type of individual elements:

+
+
+
+
+
+
In [39]:
+
+
+
y.dtype
+
+
+
+
+
+
+
+
Out[39]:
+
+
dtype('float64')
+
+
+
+
+
+
+
+
In [40]:
+
+
+
type(y[0])
+
+
+
+
+
+
+
+
Out[40]:
+
+
numpy.float64
+
+
+
+
+
+
+
+
In [41]:
+
+
+
z = np.array([3, 4, 5])
+z
+
+
+
+
+
+
+
+
Out[41]:
+
+
array([3, 4, 5])
+
+
+
+
+
+
+
+
In [42]:
+
+
+
type(z[0])
+
+
+
+
+
+
+
+
Out[42]:
+
+
numpy.int64
+
+
+
+
+
+
+
+
+

The results are, when you get to know them, fairly obvious string codes for datatypes: +NumPy supports all kinds of datatypes beyond the python basics.

+
+
+
+
+
+
+

NumPy will convert python type names to dtypes:

+
+
+
+
+
+
In [43]:
+
+
+
x = [2, 3.4, 7.2, 0]
+
+
+
+
+
+
+
+
In [44]:
+
+
+
int_array = np.array(x, dtype=int)
+
+
+
+
+
+
+
+
In [45]:
+
+
+
float_array = np.array(x, dtype=float)
+
+
+
+
+
+
+
+
In [46]:
+
+
+
int_array
+
+
+
+
+
+
+
+
Out[46]:
+
+
array([2, 3, 7, 0])
+
+
+
+
+
+
+
+
In [47]:
+
+
+
float_array
+
+
+
+
+
+
+
+
Out[47]:
+
+
array([2. , 3.4, 7.2, 0. ])
+
+
+
+
+
+
+
+
In [48]:
+
+
+
int_array.dtype
+
+
+
+
+
+
+
+
Out[48]:
+
+
dtype('int64')
+
+
+
+
+
+
+
+
In [49]:
+
+
+
float_array.dtype
+
+
+
+
+
+
+
+
Out[49]:
+
+
dtype('float64')
+
+
+
+
+
+
+
+
+

Broadcasting

+
+
+
+
+
+
+

This is another really powerful feature of NumPy.

+
+
+
+
+
+
+

By default, array operations are element-by-element:

+
+
+
+
+
+
In [50]:
+
+
+
np.arange(5) * np.arange(5)
+
+
+
+
+
+
+
+
Out[50]:
+
+
array([ 0,  1,  4,  9, 16])
+
+
+
+
+
+
+
+
+

If we multiply arrays with non-matching shapes we get an error:

+
+
+
+
+
+
In [51]:
+
+
+
np.arange(5) * np.arange(6)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[51], line 1
+----> 1 np.arange(5) * np.arange(6)
+
+ValueError: operands could not be broadcast together with shapes (5,) (6,) 
+
+
+
+
+
+
+
+
In [52]:
+
+
+
np.zeros([2,3]) * np.zeros([2,4])
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[52], line 1
+----> 1 np.zeros([2,3]) * np.zeros([2,4])
+
+ValueError: operands could not be broadcast together with shapes (2,3) (2,4) 
+
+
+
+
+
+
+
+
In [53]:
+
+
+
m1 = np.arange(100).reshape([10, 10])
+
+
+
+
+
+
+
+
In [54]:
+
+
+
m2 = np.arange(100).reshape([10, 5, 2])
+
+
+
+
+
+
+
+
In [55]:
+
+
+
m1 + m2
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[55], line 1
+----> 1 m1 + m2
+
+ValueError: operands could not be broadcast together with shapes (10,10) (10,5,2) 
+
+
+
+
+
+
+
+
+

Arrays must match in all dimensions in order to be compatible:

+
+
+
+
+
+
In [56]:
+
+
+
np.ones([3, 3]) * np.ones([3, 3]) # Note elementwise multiply, *not* matrix multiply.
+
+
+
+
+
+
+
+
Out[56]:
+
+
array([[1., 1., 1.],
+       [1., 1., 1.],
+       [1., 1., 1.]])
+
+
+
+
+
+
+
+
+

Except, that if one array has any Dimension 1, then the data is REPEATED to match the other.

+
+
+
+
+
+
In [57]:
+
+
+
col = np.arange(10).reshape([10, 1])
+col
+
+
+
+
+
+
+
+
Out[57]:
+
+
array([[0],
+       [1],
+       [2],
+       [3],
+       [4],
+       [5],
+       [6],
+       [7],
+       [8],
+       [9]])
+
+
+
+
+
+
+
+
In [58]:
+
+
+
row = col.transpose()
+row
+
+
+
+
+
+
+
+
Out[58]:
+
+
array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
+
+
+
+
+
+
+
+
In [59]:
+
+
+
col.shape # "Column Vector"
+
+
+
+
+
+
+
+
Out[59]:
+
+
(10, 1)
+
+
+
+
+
+
+
+
In [60]:
+
+
+
row.shape # "Row Vector"
+
+
+
+
+
+
+
+
Out[60]:
+
+
(1, 10)
+
+
+
+
+
+
+
+
In [61]:
+
+
+
row + col
+
+
+
+
+
+
+
+
Out[61]:
+
+
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
+       [ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10],
+       [ 2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
+       [ 3,  4,  5,  6,  7,  8,  9, 10, 11, 12],
+       [ 4,  5,  6,  7,  8,  9, 10, 11, 12, 13],
+       [ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14],
+       [ 6,  7,  8,  9, 10, 11, 12, 13, 14, 15],
+       [ 7,  8,  9, 10, 11, 12, 13, 14, 15, 16],
+       [ 8,  9, 10, 11, 12, 13, 14, 15, 16, 17],
+       [ 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]])
+
+
+
+
+
+
+
+
In [62]:
+
+
+
10 * row + col
+
+
+
+
+
+
+
+
Out[62]:
+
+
array([[ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
+       [ 1, 11, 21, 31, 41, 51, 61, 71, 81, 91],
+       [ 2, 12, 22, 32, 42, 52, 62, 72, 82, 92],
+       [ 3, 13, 23, 33, 43, 53, 63, 73, 83, 93],
+       [ 4, 14, 24, 34, 44, 54, 64, 74, 84, 94],
+       [ 5, 15, 25, 35, 45, 55, 65, 75, 85, 95],
+       [ 6, 16, 26, 36, 46, 56, 66, 76, 86, 96],
+       [ 7, 17, 27, 37, 47, 57, 67, 77, 87, 97],
+       [ 8, 18, 28, 38, 48, 58, 68, 78, 88, 98],
+       [ 9, 19, 29, 39, 49, 59, 69, 79, 89, 99]])
+
+
+
+
+
+
+
+
+

This works for arrays with more than one unit dimension.

+
+
+
+
+
+
+

Newaxis

+
+
+
+
+
+
+

Broadcasting is very powerful, and numpy allows indexing with np.newaxis to temporarily create new one-long dimensions on the fly.

+
+
+
+
+
+
In [63]:
+
+
+
import numpy as np
+x = np.arange(10).reshape(2, 5)
+y = np.arange(8).reshape(2, 2, 2)
+
+
+
+
+
+
+
+
In [64]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[64]:
+
+
array([[0, 1, 2, 3, 4],
+       [5, 6, 7, 8, 9]])
+
+
+
+
+
+
+
+
In [65]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[65]:
+
+
array([[[0, 1],
+        [2, 3]],
+
+       [[4, 5],
+        [6, 7]]])
+
+
+
+
+
+
+
+
In [66]:
+
+
+
x[:, :, np.newaxis, np.newaxis].shape
+
+
+
+
+
+
+
+
Out[66]:
+
+
(2, 5, 1, 1)
+
+
+
+
+
+
+
+
In [67]:
+
+
+
y[:, np.newaxis, :, :].shape
+
+
+
+
+
+
+
+
Out[67]:
+
+
(2, 1, 2, 2)
+
+
+
+
+
+
+
+
In [68]:
+
+
+
res = x[:, :, np.newaxis, np.newaxis] * y[:, np.newaxis, :, :]
+
+
+
+
+
+
+
+
In [69]:
+
+
+
res.shape
+
+
+
+
+
+
+
+
Out[69]:
+
+
(2, 5, 2, 2)
+
+
+
+
+
+
+
+
In [70]:
+
+
+
np.sum(res)
+
+
+
+
+
+
+
+
Out[70]:
+
+
830
+
+
+
+
+
+
+
+
+

Note that newaxis works because a $3 \times 1 \times 3$ array and a $3 \times 3$ array contain the same data, +differently shaped:

+
+
+
+
+
+
In [71]:
+
+
+
threebythree = np.arange(9).reshape(3, 3)
+threebythree
+
+
+
+
+
+
+
+
Out[71]:
+
+
array([[0, 1, 2],
+       [3, 4, 5],
+       [6, 7, 8]])
+
+
+
+
+
+
+
+
In [72]:
+
+
+
threebythree[:, np.newaxis, :]
+
+
+
+
+
+
+
+
Out[72]:
+
+
array([[[0, 1, 2]],
+
+       [[3, 4, 5]],
+
+       [[6, 7, 8]]])
+
+
+
+
+
+
+
+
+

Dot Products

+
+
+
+
+
+
+

NumPy multiply is element-by-element, not a dot-product:

+
+
+
+
+
+
In [73]:
+
+
+
a = np.arange(9).reshape(3, 3)
+a
+
+
+
+
+
+
+
+
Out[73]:
+
+
array([[0, 1, 2],
+       [3, 4, 5],
+       [6, 7, 8]])
+
+
+
+
+
+
+
+
In [74]:
+
+
+
b = np.arange(3, 12).reshape(3, 3)
+b
+
+
+
+
+
+
+
+
Out[74]:
+
+
array([[ 3,  4,  5],
+       [ 6,  7,  8],
+       [ 9, 10, 11]])
+
+
+
+
+
+
+
+
In [75]:
+
+
+
a * b
+
+
+
+
+
+
+
+
Out[75]:
+
+
array([[ 0,  4, 10],
+       [18, 28, 40],
+       [54, 70, 88]])
+
+
+
+
+
+
+
+
+

To get a dot-product, (matrix inner product) we can use a built in function:

+
+
+
+
+
+
In [76]:
+
+
+
np.dot(a, b)
+
+
+
+
+
+
+
+
Out[76]:
+
+
array([[ 24,  27,  30],
+       [ 78,  90, 102],
+       [132, 153, 174]])
+
+
+
+
+
+
+
+
+

Though it is possible to represent this in the algebra of broadcasting and newaxis:

+
+
+
+
+
+
In [77]:
+
+
+
a[:, :, np.newaxis].shape
+
+
+
+
+
+
+
+
Out[77]:
+
+
(3, 3, 1)
+
+
+
+
+
+
+
+
In [78]:
+
+
+
b[np.newaxis, :, :].shape
+
+
+
+
+
+
+
+
Out[78]:
+
+
(1, 3, 3)
+
+
+
+
+
+
+
+
In [79]:
+
+
+
a[:, :, np.newaxis] * b[np.newaxis, :, :]
+
+
+
+
+
+
+
+
Out[79]:
+
+
array([[[ 0,  0,  0],
+        [ 6,  7,  8],
+        [18, 20, 22]],
+
+       [[ 9, 12, 15],
+        [24, 28, 32],
+        [45, 50, 55]],
+
+       [[18, 24, 30],
+        [42, 49, 56],
+        [72, 80, 88]]])
+
+
+
+
+
+
+
+
In [80]:
+
+
+
(a[:, :, np.newaxis] * b[np.newaxis, :, :]).sum(1)
+
+
+
+
+
+
+
+
Out[80]:
+
+
array([[ 24,  27,  30],
+       [ 78,  90, 102],
+       [132, 153, 174]])
+
+
+
+
+
+
+
+
+

Or if you prefer:

+
+
+
+
+
+
In [81]:
+
+
+
(a.reshape(3, 3, 1) * b.reshape(1, 3, 3)).sum(1)
+
+
+
+
+
+
+
+
Out[81]:
+
+
array([[ 24,  27,  30],
+       [ 78,  90, 102],
+       [132, 153, 174]])
+
+
+
+
+
+
+
+
+

We use broadcasting to generate $A_{ij}B_{jk}$ as a 3-d matrix:

+
+
+
+
+
+
In [82]:
+
+
+
a.reshape(3, 3, 1) * b.reshape(1, 3, 3)
+
+
+
+
+
+
+
+
Out[82]:
+
+
array([[[ 0,  0,  0],
+        [ 6,  7,  8],
+        [18, 20, 22]],
+
+       [[ 9, 12, 15],
+        [24, 28, 32],
+        [45, 50, 55]],
+
+       [[18, 24, 30],
+        [42, 49, 56],
+        [72, 80, 88]]])
+
+
+
+
+
+
+
+
+

Then we sum over the middle, $j$ axis, [which is the 1-axis of three axes numbered (0,1,2)] of this 3-d matrix. Thus we generate $\Sigma_j A_{ij}B_{jk}$.

+

We can see that the broadcasting concept gives us a powerful and efficient way to express many linear algebra operations computationally.

+
+
+
+
+
+
+

Record Arrays

+
+
+
+
+
+
+

These are a special array structure designed to match the CSV "Record and Field" model. It's a very different structure +from the normal NumPy array, and different fields can contain different datatypes. We saw this when we looked at CSV files:

+
+
+
+
+
+
In [83]:
+
+
+
x = np.arange(50).reshape([10, 5])
+
+
+
+
+
+
+
+
In [84]:
+
+
+
record_x = x.view(dtype={'names': ["col1", "col2", "another", "more", "last"], 
+                         'formats': [int]*5 })
+
+
+
+
+
+
+
+
In [85]:
+
+
+
record_x
+
+
+
+
+
+
+
+
Out[85]:
+
+
array([[( 0,  1,  2,  3,  4)],
+       [( 5,  6,  7,  8,  9)],
+       [(10, 11, 12, 13, 14)],
+       [(15, 16, 17, 18, 19)],
+       [(20, 21, 22, 23, 24)],
+       [(25, 26, 27, 28, 29)],
+       [(30, 31, 32, 33, 34)],
+       [(35, 36, 37, 38, 39)],
+       [(40, 41, 42, 43, 44)],
+       [(45, 46, 47, 48, 49)]],
+      dtype=[('col1', '<i8'), ('col2', '<i8'), ('another', '<i8'), ('more', '<i8'), ('last', '<i8')])
+
+
+
+
+
+
+
+
+

Record arrays can be addressed with field names like they were a dictionary:

+
+
+
+
+
+
In [86]:
+
+
+
record_x['col1']
+
+
+
+
+
+
+
+
Out[86]:
+
+
array([[ 0],
+       [ 5],
+       [10],
+       [15],
+       [20],
+       [25],
+       [30],
+       [35],
+       [40],
+       [45]])
+
+
+
+
+
+
+
+
+

We've seen these already when we used NumPy's CSV parser.

+
+
+
+
+
+
+

Logical arrays, masking, and selection

+
+
+
+
+
+
+

Numpy defines operators like == and < to apply to arrays element by element:

+
+
+
+
+
+
In [87]:
+
+
+
x = np.zeros([3, 4])
+x
+
+
+
+
+
+
+
+
Out[87]:
+
+
array([[0., 0., 0., 0.],
+       [0., 0., 0., 0.],
+       [0., 0., 0., 0.]])
+
+
+
+
+
+
+
+
In [88]:
+
+
+
y = np.arange(-1, 2)[:, np.newaxis] * np.arange(-2, 2)[np.newaxis, :]
+y
+
+
+
+
+
+
+
+
Out[88]:
+
+
array([[ 2,  1,  0, -1],
+       [ 0,  0,  0,  0],
+       [-2, -1,  0,  1]])
+
+
+
+
+
+
+
+
In [89]:
+
+
+
iszero = x == y
+iszero
+
+
+
+
+
+
+
+
Out[89]:
+
+
array([[False, False,  True, False],
+       [ True,  True,  True,  True],
+       [False, False,  True, False]])
+
+
+
+
+
+
+
+
+

A logical array can be used to select elements from an array:

+
+
+
+
+
+
In [90]:
+
+
+
y[np.logical_not(iszero)]
+
+
+
+
+
+
+
+
Out[90]:
+
+
array([ 2,  1, -1, -2, -1,  1])
+
+
+
+
+
+
+
+
+

Although when printed, this comes out as a flat list, if assigned to, the selected elements of the array are changed!

+
+
+
+
+
+
In [91]:
+
+
+
y[iszero] = 5
+
+
+
+
+
+
+
+
In [92]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[92]:
+
+
array([[ 2,  1,  5, -1],
+       [ 5,  5,  5,  5],
+       [-2, -1,  5,  1]])
+
+
+
+
+
+
+
+
+

Numpy memory

+
+
+
+
+
+
+

Numpy memory management can be tricksy:

+
+
+
+
+
+
In [93]:
+
+
+
x = np.arange(5)
+y = x[:]
+
+
+
+
+
+
+
+
In [94]:
+
+
+
y[2] = 0
+x
+
+
+
+
+
+
+
+
Out[94]:
+
+
array([0, 1, 0, 3, 4])
+
+
+
+
+
+
+
+
+

It does not behave like lists!

+
+
+
+
+
+
In [95]:
+
+
+
x = list(range(5))
+y = x[:]
+
+
+
+
+
+
+
+
In [96]:
+
+
+
y[2] = 0
+x
+
+
+
+
+
+
+
+
Out[96]:
+
+
[0, 1, 2, 3, 4]
+
+
+
+
+
+
+
+
+

We must use np.copy to force separate memory. Otherwise NumPy tries its hardest to make slices be views on data.

+
+
+
+
+
+
+

Now, this has all been very theoretical, but let's go through a practical example, and see how powerful NumPy can be.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/082NumPy.ipynb b/ch02data/082NumPy.ipynb new file mode 100644 index 000000000..a7f449656 --- /dev/null +++ b/ch02data/082NumPy.ipynb @@ -0,0 +1,1601 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "20759e7c", + "metadata": {}, + "source": [ + "## NumPy" + ] + }, + { + "cell_type": "markdown", + "id": "de763f56", + "metadata": {}, + "source": [ + "### The Scientific Python Trilogy" + ] + }, + { + "cell_type": "markdown", + "id": "ad8a48cd", + "metadata": {}, + "source": [ + "Why is Python so popular for research work?" + ] + }, + { + "cell_type": "markdown", + "id": "bc4cfcd6", + "metadata": {}, + "source": [ + "MATLAB has typically been the most popular \"language of technical computing\", with strong built-in support for efficient numerical analysis with matrices (the *mat* in MATLAB is for Matrix, not Maths), and plotting." + ] + }, + { + "cell_type": "markdown", + "id": "6b342a63", + "metadata": {}, + "source": [ + "Other dynamic languages have cleaner, more logical syntax (Ruby, Haskell)" + ] + }, + { + "cell_type": "markdown", + "id": "0d947174", + "metadata": {}, + "source": [ + "But Python users developed three critical libraries, matching the power of MATLAB for scientific work:" + ] + }, + { + "cell_type": "markdown", + "id": "d4603adf", + "metadata": {}, + "source": [ + "* Matplotlib, the plotting library created by [John D. Hunter](https://en.wikipedia.org/wiki/John_D._Hunter)\n", + "* NumPy, a fast matrix maths library created by [Travis Oliphant](https://en.wikipedia.org/wiki/Travis_Oliphant)\n", + "* IPython, the precursor of the notebook, created by [Fernando Perez](https://en.wikipedia.org/wiki/Fernando_P%nC3%A9rez_(software_developer))" + ] + }, + { + "cell_type": "markdown", + "id": "7b50f4dc", + "metadata": {}, + "source": [ + "By combining a plotting library, a matrix maths library, and an easy-to-use interface allowing live plotting commands\n", + "in a persistent environment, the powerful capabilities of MATLAB were matched by a free and open toolchain." + ] + }, + { + "cell_type": "markdown", + "id": "7ece3baf", + "metadata": {}, + "source": [ + "We've learned about Matplotlib and IPython in this course already. NumPy is the last part of the trilogy." + ] + }, + { + "cell_type": "markdown", + "id": "9f75c73e", + "metadata": {}, + "source": [ + "### Limitations of Python Lists" + ] + }, + { + "cell_type": "markdown", + "id": "a91a6f54", + "metadata": {}, + "source": [ + "The normal Python list is just one dimensional. To make a matrix, we have to nest Python lists:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4370f2e9", + "metadata": {}, + "outputs": [], + "source": [ + "x = [list(range(5)) for N in range(5)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4bb2b3c", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d50a628", + "metadata": {}, + "outputs": [], + "source": [ + "x[2][2]" + ] + }, + { + "cell_type": "markdown", + "id": "ba696181", + "metadata": {}, + "source": [ + "Applying an operation to every element is a pain:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "866e9dc9", + "metadata": {}, + "outputs": [], + "source": [ + "x + 5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e59d14f", + "metadata": {}, + "outputs": [], + "source": [ + "[[elem + 5 for elem in row] for row in x]" + ] + }, + { + "cell_type": "markdown", + "id": "95450a63", + "metadata": {}, + "source": [ + "Common useful operations like transposing a matrix or reshaping a 10 by 10 matrix into a 20 by 5 matrix are not easy to code in raw Python lists." + ] + }, + { + "cell_type": "markdown", + "id": "6acb0895", + "metadata": {}, + "source": [ + "### The NumPy array" + ] + }, + { + "cell_type": "markdown", + "id": "6a39c433", + "metadata": {}, + "source": [ + "NumPy's array type represents a multidimensional matrix $M_{i,j,k...n}$" + ] + }, + { + "cell_type": "markdown", + "id": "77634550", + "metadata": {}, + "source": [ + "The NumPy array seems at first to be just like a list. For example, we can index it and iterate over it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26aabb44", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "my_array = np.array(range(5))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f00e5dab", + "metadata": {}, + "outputs": [], + "source": [ + "my_array" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d660a591", + "metadata": {}, + "outputs": [], + "source": [ + "my_array[2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a1742d1", + "metadata": {}, + "outputs": [], + "source": [ + "for element in my_array:\n", + " print(\"Hello\" * element)" + ] + }, + { + "cell_type": "markdown", + "id": "0c80ca50", + "metadata": {}, + "source": [ + "We can also see our first weakness of NumPy arrays versus Python lists:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41a51e11", + "metadata": {}, + "outputs": [], + "source": [ + "my_array.append(4)" + ] + }, + { + "cell_type": "markdown", + "id": "3956cc52", + "metadata": {}, + "source": [ + "For NumPy arrays, you typically don't change the data size once you've defined your array,\n", + "whereas for Python lists, you can do this efficiently. However, you get back lots of goodies in return..." + ] + }, + { + "cell_type": "markdown", + "id": "f4fab615", + "metadata": {}, + "source": [ + "### Elementwise Operations" + ] + }, + { + "cell_type": "markdown", + "id": "ff20517d", + "metadata": {}, + "source": [ + "Most operations can be applied element-wise automatically!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e531b5fa", + "metadata": {}, + "outputs": [], + "source": [ + "my_array + 2" + ] + }, + { + "cell_type": "markdown", + "id": "596e5ac1", + "metadata": {}, + "source": [ + "These \"vectorized\" operations are very fast: (the `%%timeit` magic reports how long it takes to run a cell; there is [more information](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit) available if interested)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e9af00e", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "big_list = range(10000)\n", + "big_array = np.arange(10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a72ca93c", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "[x**2 for x in big_list]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8daabc03", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "big_array**2" + ] + }, + { + "cell_type": "markdown", + "id": "ddc109b1", + "metadata": {}, + "source": [ + "### arange and linspace" + ] + }, + { + "cell_type": "markdown", + "id": "9401a7ac", + "metadata": {}, + "source": [ + "NumPy has two methods for quickly defining evenly-spaced arrays of (floating-point) numbers. These can be useful, for example, in plotting.\n", + "\n", + "The first method is `arange`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "408e2fa0", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(0, 10, 0.1) # Start, stop, step size" + ] + }, + { + "cell_type": "markdown", + "id": "cba36111", + "metadata": {}, + "source": [ + "This is similar to Python's `range`, although note that we can't use non-integer steps with the latter!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fe1ed87", + "metadata": {}, + "outputs": [], + "source": [ + "y = list(range(0, 10, 0.1))" + ] + }, + { + "cell_type": "markdown", + "id": "f33a237b", + "metadata": {}, + "source": [ + "The second method is `linspace`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09502625", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "values = np.linspace(0, math.pi, 100) # Start, stop, number of steps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1250d4fb", + "metadata": {}, + "outputs": [], + "source": [ + "values" + ] + }, + { + "cell_type": "markdown", + "id": "2b1e0a11", + "metadata": {}, + "source": [ + "Regardless of the method used, the array of values that we get can be used in the same way.\n", + "\n", + "In fact, NumPy comes with \"vectorised\" versions of common functions which work element-by-element when applied to arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18a0bebc", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "from matplotlib import pyplot as plt\n", + "plt.plot(values, np.sin(values))" + ] + }, + { + "cell_type": "markdown", + "id": "34a0973a", + "metadata": {}, + "source": [ + "So we don't have to use awkward list comprehensions when using these." + ] + }, + { + "cell_type": "markdown", + "id": "07818529", + "metadata": {}, + "source": [ + "### Multi-Dimensional Arrays" + ] + }, + { + "cell_type": "markdown", + "id": "fe5e05b6", + "metadata": {}, + "source": [ + "NumPy's true power comes from multi-dimensional arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d098852a", + "metadata": {}, + "outputs": [], + "source": [ + "np.zeros([3, 4, 2]) # 3 arrays with 4 rows and 2 columns each" + ] + }, + { + "cell_type": "markdown", + "id": "5563fa4c", + "metadata": {}, + "source": [ + "Unlike a list-of-lists in Python, we can reshape arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9c9b326", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.array(range(40))\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfff725c", + "metadata": {}, + "outputs": [], + "source": [ + "y = x.reshape([4, 5, 2])\n", + "y" + ] + }, + { + "cell_type": "markdown", + "id": "45c21f51", + "metadata": {}, + "source": [ + "And index multiple columns at once:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "449524d3", + "metadata": {}, + "outputs": [], + "source": [ + "y[3, 2, 1]" + ] + }, + { + "cell_type": "markdown", + "id": "5289856e", + "metadata": {}, + "source": [ + "Including selecting on inner axes while taking all from the outermost:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bc438fd", + "metadata": {}, + "outputs": [], + "source": [ + "y[:, 2, 1]" + ] + }, + { + "cell_type": "markdown", + "id": "aff283d5", + "metadata": {}, + "source": [ + "And subselecting ranges:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43bd2caf", + "metadata": {}, + "outputs": [], + "source": [ + "y[2:, :1, :] # Last 2 axes, 1st row, all columns" + ] + }, + { + "cell_type": "markdown", + "id": "55eb97d4", + "metadata": {}, + "source": [ + "And [transpose](https://en.wikipedia.org/wiki/Transpose) arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f15dada3", + "metadata": {}, + "outputs": [], + "source": [ + "y.transpose()" + ] + }, + { + "cell_type": "markdown", + "id": "5e1176bd", + "metadata": {}, + "source": [ + "You can get the dimensions of an array with `shape`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56dae61a", + "metadata": {}, + "outputs": [], + "source": [ + "y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdacb72c", + "metadata": {}, + "outputs": [], + "source": [ + "y.transpose().shape" + ] + }, + { + "cell_type": "markdown", + "id": "31417728", + "metadata": {}, + "source": [ + "Some numpy functions apply by default to the whole array, but can be chosen to act only on certain axes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7dc6093", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(12).reshape(4,3)\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22904b95", + "metadata": {}, + "outputs": [], + "source": [ + "x.mean(1) # Mean along the second axis, leaving the first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8693cf9f", + "metadata": {}, + "outputs": [], + "source": [ + "x.mean(0) # Mean along the first axis, leaving the second." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e41a64d1", + "metadata": {}, + "outputs": [], + "source": [ + "x.mean() # mean of all axes" + ] + }, + { + "cell_type": "markdown", + "id": "3c83a14e", + "metadata": {}, + "source": [ + "### Array Datatypes" + ] + }, + { + "cell_type": "markdown", + "id": "158b73d6", + "metadata": {}, + "source": [ + "A Python `list` can contain data of mixed type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbe02438", + "metadata": {}, + "outputs": [], + "source": [ + "x = ['hello', 2, 3.4]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05c0ae6e", + "metadata": {}, + "outputs": [], + "source": [ + "type(x[2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c47d1258", + "metadata": {}, + "outputs": [], + "source": [ + "type(x[1])" + ] + }, + { + "cell_type": "markdown", + "id": "c4e82033", + "metadata": {}, + "source": [ + "A NumPy array always contains just one datatype:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b798bcbb", + "metadata": {}, + "outputs": [], + "source": [ + "np.array(x)" + ] + }, + { + "cell_type": "markdown", + "id": "2f382710", + "metadata": {}, + "source": [ + "NumPy will choose the least-generic-possible datatype that can contain the data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "638ef767", + "metadata": {}, + "outputs": [], + "source": [ + "y = np.array([2, 3.4])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d03d9ef", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "markdown", + "id": "3cf5336c", + "metadata": {}, + "source": [ + "You can access the array's `dtype`, or check the type of individual elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d029fab1", + "metadata": {}, + "outputs": [], + "source": [ + "y.dtype" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e95edf9", + "metadata": {}, + "outputs": [], + "source": [ + "type(y[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a2926fe", + "metadata": {}, + "outputs": [], + "source": [ + "z = np.array([3, 4, 5])\n", + "z" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af438058", + "metadata": {}, + "outputs": [], + "source": [ + "type(z[0])" + ] + }, + { + "cell_type": "markdown", + "id": "1a7ed9fd", + "metadata": {}, + "source": [ + "The results are, when you get to know them, fairly obvious string codes for datatypes: \n", + " NumPy supports all kinds of datatypes beyond the python basics." + ] + }, + { + "cell_type": "markdown", + "id": "c30b1989", + "metadata": {}, + "source": [ + "NumPy will convert python type names to dtypes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4be01234", + "metadata": {}, + "outputs": [], + "source": [ + "x = [2, 3.4, 7.2, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ca93a8f", + "metadata": {}, + "outputs": [], + "source": [ + "int_array = np.array(x, dtype=int)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14011859", + "metadata": {}, + "outputs": [], + "source": [ + "float_array = np.array(x, dtype=float)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "682bb5e8", + "metadata": {}, + "outputs": [], + "source": [ + "int_array" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b90feac6", + "metadata": {}, + "outputs": [], + "source": [ + "float_array" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a05b8bf", + "metadata": {}, + "outputs": [], + "source": [ + "int_array.dtype" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e923a97", + "metadata": {}, + "outputs": [], + "source": [ + "float_array.dtype" + ] + }, + { + "cell_type": "markdown", + "id": "9046ba86", + "metadata": {}, + "source": [ + "### Broadcasting" + ] + }, + { + "cell_type": "markdown", + "id": "8a61064f", + "metadata": {}, + "source": [ + "This is another really powerful feature of NumPy." + ] + }, + { + "cell_type": "markdown", + "id": "1c794540", + "metadata": {}, + "source": [ + "By default, array operations are element-by-element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f43fc35", + "metadata": {}, + "outputs": [], + "source": [ + "np.arange(5) * np.arange(5)" + ] + }, + { + "cell_type": "markdown", + "id": "a16e71ca", + "metadata": {}, + "source": [ + "If we multiply arrays with non-matching shapes we get an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a4ce300", + "metadata": {}, + "outputs": [], + "source": [ + "np.arange(5) * np.arange(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d789071d", + "metadata": {}, + "outputs": [], + "source": [ + "np.zeros([2,3]) * np.zeros([2,4])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "811781d8", + "metadata": {}, + "outputs": [], + "source": [ + "m1 = np.arange(100).reshape([10, 10])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b7866dc", + "metadata": {}, + "outputs": [], + "source": [ + "m2 = np.arange(100).reshape([10, 5, 2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38a44950", + "metadata": {}, + "outputs": [], + "source": [ + "m1 + m2" + ] + }, + { + "cell_type": "markdown", + "id": "c02d3dcf", + "metadata": {}, + "source": [ + "Arrays must match in all dimensions in order to be compatible:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aabc0d07", + "metadata": {}, + "outputs": [], + "source": [ + "np.ones([3, 3]) * np.ones([3, 3]) # Note elementwise multiply, *not* matrix multiply." + ] + }, + { + "cell_type": "markdown", + "id": "ba707de4", + "metadata": {}, + "source": [ + "**Except**, that if one array has any Dimension 1, then the data is **REPEATED** to match the other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8708c0d", + "metadata": {}, + "outputs": [], + "source": [ + "col = np.arange(10).reshape([10, 1])\n", + "col" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1e55717", + "metadata": {}, + "outputs": [], + "source": [ + "row = col.transpose()\n", + "row" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d878351", + "metadata": {}, + "outputs": [], + "source": [ + "col.shape # \"Column Vector\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f225b0f0", + "metadata": {}, + "outputs": [], + "source": [ + "row.shape # \"Row Vector\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edb7e93a", + "metadata": {}, + "outputs": [], + "source": [ + "row + col" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1820592e", + "metadata": {}, + "outputs": [], + "source": [ + "10 * row + col" + ] + }, + { + "cell_type": "markdown", + "id": "38aa8eee", + "metadata": {}, + "source": [ + "This works for arrays with more than one unit dimension. " + ] + }, + { + "cell_type": "markdown", + "id": "5482f376", + "metadata": {}, + "source": [ + "### Newaxis" + ] + }, + { + "cell_type": "markdown", + "id": "41184124", + "metadata": {}, + "source": [ + "Broadcasting is very powerful, and numpy allows indexing with `np.newaxis` to temporarily create new one-long dimensions on the fly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8a3daab", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "x = np.arange(10).reshape(2, 5)\n", + "y = np.arange(8).reshape(2, 2, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8afe2b0", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f3b5477", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ce3ffc1", + "metadata": {}, + "outputs": [], + "source": [ + "x[:, :, np.newaxis, np.newaxis].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d1c4868", + "metadata": {}, + "outputs": [], + "source": [ + "y[:, np.newaxis, :, :].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e816bdb4", + "metadata": {}, + "outputs": [], + "source": [ + "res = x[:, :, np.newaxis, np.newaxis] * y[:, np.newaxis, :, :]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa116a2b", + "metadata": {}, + "outputs": [], + "source": [ + "res.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62d2580c", + "metadata": {}, + "outputs": [], + "source": [ + "np.sum(res)" + ] + }, + { + "cell_type": "markdown", + "id": "9e7dc718", + "metadata": {}, + "source": [ + "Note that `newaxis` works because a $3 \\times 1 \\times 3$ array and a $3 \\times 3$ array contain the same data,\n", + "differently shaped:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4169947d", + "metadata": {}, + "outputs": [], + "source": [ + "threebythree = np.arange(9).reshape(3, 3)\n", + "threebythree" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d91d5c1c", + "metadata": {}, + "outputs": [], + "source": [ + "threebythree[:, np.newaxis, :]" + ] + }, + { + "cell_type": "markdown", + "id": "abb13441", + "metadata": {}, + "source": [ + "### Dot Products" + ] + }, + { + "cell_type": "markdown", + "id": "791ae3d2", + "metadata": {}, + "source": [ + "NumPy multiply is element-by-element, not a dot-product:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b4d6aac", + "metadata": {}, + "outputs": [], + "source": [ + "a = np.arange(9).reshape(3, 3)\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8adc5677", + "metadata": {}, + "outputs": [], + "source": [ + "b = np.arange(3, 12).reshape(3, 3)\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c0896b7", + "metadata": {}, + "outputs": [], + "source": [ + "a * b" + ] + }, + { + "cell_type": "markdown", + "id": "e309bb31", + "metadata": {}, + "source": [ + "To get a dot-product, (matrix inner product) we can use a built in function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b98c8c53", + "metadata": {}, + "outputs": [], + "source": [ + "np.dot(a, b)" + ] + }, + { + "cell_type": "markdown", + "id": "337070d6", + "metadata": {}, + "source": [ + "Though it is possible to represent this in the algebra of broadcasting and newaxis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ea14029", + "metadata": {}, + "outputs": [], + "source": [ + "a[:, :, np.newaxis].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b496f82e", + "metadata": {}, + "outputs": [], + "source": [ + "b[np.newaxis, :, :].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77a97ae2", + "metadata": {}, + "outputs": [], + "source": [ + "a[:, :, np.newaxis] * b[np.newaxis, :, :]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e8f620", + "metadata": {}, + "outputs": [], + "source": [ + "(a[:, :, np.newaxis] * b[np.newaxis, :, :]).sum(1)" + ] + }, + { + "cell_type": "markdown", + "id": "6cb0cc95", + "metadata": {}, + "source": [ + "Or if you prefer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9790b32b", + "metadata": {}, + "outputs": [], + "source": [ + "(a.reshape(3, 3, 1) * b.reshape(1, 3, 3)).sum(1)" + ] + }, + { + "cell_type": "markdown", + "id": "ea813dcc", + "metadata": {}, + "source": [ + "We use broadcasting to generate $A_{ij}B_{jk}$ as a 3-d matrix:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35e84fff", + "metadata": {}, + "outputs": [], + "source": [ + "a.reshape(3, 3, 1) * b.reshape(1, 3, 3)" + ] + }, + { + "cell_type": "markdown", + "id": "7e316b3f", + "metadata": {}, + "source": [ + "Then we sum over the middle, $j$ axis, [which is the 1-axis of three axes numbered (0,1,2)] of this 3-d matrix. Thus we generate $\\Sigma_j A_{ij}B_{jk}$.\n", + "\n", + "We can see that the broadcasting concept gives us a powerful and efficient way to express many linear algebra operations computationally." + ] + }, + { + "cell_type": "markdown", + "id": "a21a8495", + "metadata": {}, + "source": [ + "### Record Arrays" + ] + }, + { + "cell_type": "markdown", + "id": "04e461ea", + "metadata": {}, + "source": [ + "These are a special array structure designed to match the CSV \"Record and Field\" model. It's a very different structure\n", + "from the normal NumPy array, and different fields *can* contain different datatypes. We saw this when we looked at CSV files:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c919f0f3", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(50).reshape([10, 5])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30df776d", + "metadata": {}, + "outputs": [], + "source": [ + "record_x = x.view(dtype={'names': [\"col1\", \"col2\", \"another\", \"more\", \"last\"], \n", + " 'formats': [int]*5 })" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32877cf2", + "metadata": {}, + "outputs": [], + "source": [ + "record_x" + ] + }, + { + "cell_type": "markdown", + "id": "97fa5d02", + "metadata": {}, + "source": [ + "Record arrays can be addressed with field names like they were a dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c8a259", + "metadata": {}, + "outputs": [], + "source": [ + "record_x['col1']" + ] + }, + { + "cell_type": "markdown", + "id": "0f51bee0", + "metadata": {}, + "source": [ + "We've seen these already when we used NumPy's CSV parser." + ] + }, + { + "cell_type": "markdown", + "id": "b20ccdff", + "metadata": {}, + "source": [ + "### Logical arrays, masking, and selection" + ] + }, + { + "cell_type": "markdown", + "id": "c30de304", + "metadata": {}, + "source": [ + "Numpy defines operators like == and < to apply to arrays *element by element*:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cfb3ba8a", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.zeros([3, 4])\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "376cffb4", + "metadata": {}, + "outputs": [], + "source": [ + "y = np.arange(-1, 2)[:, np.newaxis] * np.arange(-2, 2)[np.newaxis, :]\n", + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c7742f4", + "metadata": {}, + "outputs": [], + "source": [ + "iszero = x == y\n", + "iszero" + ] + }, + { + "cell_type": "markdown", + "id": "a8074ea2", + "metadata": {}, + "source": [ + "A logical array can be used to select elements from an array:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "544d557c", + "metadata": {}, + "outputs": [], + "source": [ + "y[np.logical_not(iszero)]" + ] + }, + { + "cell_type": "markdown", + "id": "3d0e1c1f", + "metadata": {}, + "source": [ + "Although when printed, this comes out as a flat list, if assigned to, the *selected elements of the array are changed!*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f24b2eef", + "metadata": {}, + "outputs": [], + "source": [ + "y[iszero] = 5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd27f7a8", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "markdown", + "id": "ed0b341f", + "metadata": {}, + "source": [ + "### Numpy memory" + ] + }, + { + "cell_type": "markdown", + "id": "7c5d4293", + "metadata": {}, + "source": [ + "Numpy memory management can be tricksy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5525ee1", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(5)\n", + "y = x[:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f4da2d9", + "metadata": {}, + "outputs": [], + "source": [ + "y[2] = 0\n", + "x" + ] + }, + { + "cell_type": "markdown", + "id": "2dcfa12c", + "metadata": {}, + "source": [ + "It does **not** behave like lists!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4829b83b", + "metadata": {}, + "outputs": [], + "source": [ + "x = list(range(5))\n", + "y = x[:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c65b9bd4", + "metadata": {}, + "outputs": [], + "source": [ + "y[2] = 0\n", + "x" + ] + }, + { + "cell_type": "markdown", + "id": "5fc11441", + "metadata": {}, + "source": [ + "We must use `np.copy` to force separate memory. Otherwise NumPy tries its hardest to make slices be *views* on data." + ] + }, + { + "cell_type": "markdown", + "id": "d83e6f9f", + "metadata": {}, + "source": [ + "Now, this has all been very theoretical, but let's go through a practical example, and see how powerful NumPy can be." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Numerical Python" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/082NumPy.ipynb.py b/ch02data/082NumPy.ipynb.py new file mode 100644 index 000000000..790a582dd --- /dev/null +++ b/ch02data/082NumPy.ipynb.py @@ -0,0 +1,561 @@ +# --- +# jupyter: +# jekyll: +# display_name: Numerical Python +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## NumPy + +# %% [markdown] +# ### The Scientific Python Trilogy + +# %% [markdown] +# Why is Python so popular for research work? + +# %% [markdown] +# MATLAB has typically been the most popular "language of technical computing", with strong built-in support for efficient numerical analysis with matrices (the *mat* in MATLAB is for Matrix, not Maths), and plotting. + +# %% [markdown] +# Other dynamic languages have cleaner, more logical syntax (Ruby, Haskell) + +# %% [markdown] +# But Python users developed three critical libraries, matching the power of MATLAB for scientific work: + +# %% [markdown] +# * Matplotlib, the plotting library created by [John D. Hunter](https://en.wikipedia.org/wiki/John_D._Hunter) +# * NumPy, a fast matrix maths library created by [Travis Oliphant](https://en.wikipedia.org/wiki/Travis_Oliphant) +# * IPython, the precursor of the notebook, created by [Fernando Perez](https://en.wikipedia.org/wiki/Fernando_P%nC3%A9rez_(software_developer)) + +# %% [markdown] +# By combining a plotting library, a matrix maths library, and an easy-to-use interface allowing live plotting commands +# in a persistent environment, the powerful capabilities of MATLAB were matched by a free and open toolchain. + +# %% [markdown] +# We've learned about Matplotlib and IPython in this course already. NumPy is the last part of the trilogy. + +# %% [markdown] +# ### Limitations of Python Lists + +# %% [markdown] +# The normal Python list is just one dimensional. To make a matrix, we have to nest Python lists: + +# %% +x = [list(range(5)) for N in range(5)] + +# %% +x + +# %% +x[2][2] + +# %% [markdown] +# Applying an operation to every element is a pain: + +# %% +x + 5 + +# %% +[[elem + 5 for elem in row] for row in x] + +# %% [markdown] +# Common useful operations like transposing a matrix or reshaping a 10 by 10 matrix into a 20 by 5 matrix are not easy to code in raw Python lists. + +# %% [markdown] +# ### The NumPy array + +# %% [markdown] +# NumPy's array type represents a multidimensional matrix $M_{i,j,k...n}$ + +# %% [markdown] +# The NumPy array seems at first to be just like a list. For example, we can index it and iterate over it: + +# %% +import numpy as np +my_array = np.array(range(5)) + +# %% +my_array + +# %% +my_array[2] + +# %% +for element in my_array: + print("Hello" * element) + +# %% [markdown] +# We can also see our first weakness of NumPy arrays versus Python lists: + +# %% +my_array.append(4) + +# %% [markdown] +# For NumPy arrays, you typically don't change the data size once you've defined your array, +# whereas for Python lists, you can do this efficiently. However, you get back lots of goodies in return... + +# %% [markdown] +# ### Elementwise Operations + +# %% [markdown] +# Most operations can be applied element-wise automatically! + +# %% +my_array + 2 + +# %% [markdown] +# These "vectorized" operations are very fast: (the `%%timeit` magic reports how long it takes to run a cell; there is [more information](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit) available if interested) + +# %% +import numpy as np +big_list = range(10000) +big_array = np.arange(10000) + +# %% +# %%timeit +[x**2 for x in big_list] + +# %% +# %%timeit +big_array**2 + +# %% [markdown] +# ### arange and linspace + +# %% [markdown] +# NumPy has two methods for quickly defining evenly-spaced arrays of (floating-point) numbers. These can be useful, for example, in plotting. +# +# The first method is `arange`: + +# %% +x = np.arange(0, 10, 0.1) # Start, stop, step size + +# %% [markdown] +# This is similar to Python's `range`, although note that we can't use non-integer steps with the latter! + +# %% +y = list(range(0, 10, 0.1)) + +# %% [markdown] +# The second method is `linspace`: + +# %% +import math +values = np.linspace(0, math.pi, 100) # Start, stop, number of steps + +# %% +values + +# %% [markdown] +# Regardless of the method used, the array of values that we get can be used in the same way. +# +# In fact, NumPy comes with "vectorised" versions of common functions which work element-by-element when applied to arrays: + +# %% +# %matplotlib inline + +from matplotlib import pyplot as plt +plt.plot(values, np.sin(values)) + +# %% [markdown] +# So we don't have to use awkward list comprehensions when using these. + +# %% [markdown] +# ### Multi-Dimensional Arrays + +# %% [markdown] +# NumPy's true power comes from multi-dimensional arrays: + +# %% +np.zeros([3, 4, 2]) # 3 arrays with 4 rows and 2 columns each + +# %% [markdown] +# Unlike a list-of-lists in Python, we can reshape arrays: + +# %% +x = np.array(range(40)) +x + +# %% +y = x.reshape([4, 5, 2]) +y + +# %% [markdown] +# And index multiple columns at once: + +# %% +y[3, 2, 1] + +# %% [markdown] +# Including selecting on inner axes while taking all from the outermost: + +# %% +y[:, 2, 1] + +# %% [markdown] +# And subselecting ranges: + +# %% +y[2:, :1, :] # Last 2 axes, 1st row, all columns + +# %% [markdown] +# And [transpose](https://en.wikipedia.org/wiki/Transpose) arrays: + +# %% +y.transpose() + +# %% [markdown] +# You can get the dimensions of an array with `shape`: + +# %% +y.shape + +# %% +y.transpose().shape + +# %% [markdown] +# Some numpy functions apply by default to the whole array, but can be chosen to act only on certain axes: + +# %% +x = np.arange(12).reshape(4,3) +x + +# %% +x.mean(1) # Mean along the second axis, leaving the first. + +# %% +x.mean(0) # Mean along the first axis, leaving the second. + +# %% +x.mean() # mean of all axes + +# %% [markdown] +# ### Array Datatypes + +# %% [markdown] +# A Python `list` can contain data of mixed type: + +# %% +x = ['hello', 2, 3.4] + +# %% +type(x[2]) + +# %% +type(x[1]) + +# %% [markdown] +# A NumPy array always contains just one datatype: + +# %% +np.array(x) + +# %% [markdown] +# NumPy will choose the least-generic-possible datatype that can contain the data: + +# %% +y = np.array([2, 3.4]) + +# %% +y + +# %% [markdown] +# You can access the array's `dtype`, or check the type of individual elements: + +# %% +y.dtype + +# %% +type(y[0]) + +# %% +z = np.array([3, 4, 5]) +z + +# %% +type(z[0]) + +# %% [markdown] +# The results are, when you get to know them, fairly obvious string codes for datatypes: +# NumPy supports all kinds of datatypes beyond the python basics. + +# %% [markdown] +# NumPy will convert python type names to dtypes: + +# %% +x = [2, 3.4, 7.2, 0] + +# %% +int_array = np.array(x, dtype=int) + +# %% +float_array = np.array(x, dtype=float) + +# %% +int_array + +# %% +float_array + +# %% +int_array.dtype + +# %% +float_array.dtype + +# %% [markdown] +# ### Broadcasting + +# %% [markdown] +# This is another really powerful feature of NumPy. + +# %% [markdown] +# By default, array operations are element-by-element: + +# %% +np.arange(5) * np.arange(5) + +# %% [markdown] +# If we multiply arrays with non-matching shapes we get an error: + +# %% +np.arange(5) * np.arange(6) + +# %% +np.zeros([2,3]) * np.zeros([2,4]) + +# %% +m1 = np.arange(100).reshape([10, 10]) + +# %% +m2 = np.arange(100).reshape([10, 5, 2]) + +# %% +m1 + m2 + +# %% [markdown] +# Arrays must match in all dimensions in order to be compatible: + +# %% +np.ones([3, 3]) * np.ones([3, 3]) # Note elementwise multiply, *not* matrix multiply. + +# %% [markdown] +# **Except**, that if one array has any Dimension 1, then the data is **REPEATED** to match the other. + +# %% +col = np.arange(10).reshape([10, 1]) +col + +# %% +row = col.transpose() +row + +# %% +col.shape # "Column Vector" + +# %% +row.shape # "Row Vector" + +# %% +row + col + +# %% +10 * row + col + +# %% [markdown] +# This works for arrays with more than one unit dimension. + +# %% [markdown] +# ### Newaxis + +# %% [markdown] +# Broadcasting is very powerful, and numpy allows indexing with `np.newaxis` to temporarily create new one-long dimensions on the fly. + +# %% +import numpy as np +x = np.arange(10).reshape(2, 5) +y = np.arange(8).reshape(2, 2, 2) + +# %% +x + +# %% +y + +# %% +x[:, :, np.newaxis, np.newaxis].shape + +# %% +y[:, np.newaxis, :, :].shape + +# %% +res = x[:, :, np.newaxis, np.newaxis] * y[:, np.newaxis, :, :] + +# %% +res.shape + +# %% +np.sum(res) + +# %% [markdown] +# Note that `newaxis` works because a $3 \times 1 \times 3$ array and a $3 \times 3$ array contain the same data, +# differently shaped: + +# %% +threebythree = np.arange(9).reshape(3, 3) +threebythree + +# %% +threebythree[:, np.newaxis, :] + +# %% [markdown] +# ### Dot Products + +# %% [markdown] +# NumPy multiply is element-by-element, not a dot-product: + +# %% +a = np.arange(9).reshape(3, 3) +a + +# %% +b = np.arange(3, 12).reshape(3, 3) +b + +# %% +a * b + +# %% [markdown] +# To get a dot-product, (matrix inner product) we can use a built in function: + +# %% +np.dot(a, b) + +# %% [markdown] +# Though it is possible to represent this in the algebra of broadcasting and newaxis: + +# %% +a[:, :, np.newaxis].shape + +# %% +b[np.newaxis, :, :].shape + +# %% +a[:, :, np.newaxis] * b[np.newaxis, :, :] + +# %% +(a[:, :, np.newaxis] * b[np.newaxis, :, :]).sum(1) + +# %% [markdown] +# Or if you prefer: + +# %% +(a.reshape(3, 3, 1) * b.reshape(1, 3, 3)).sum(1) + +# %% [markdown] +# We use broadcasting to generate $A_{ij}B_{jk}$ as a 3-d matrix: + +# %% +a.reshape(3, 3, 1) * b.reshape(1, 3, 3) + +# %% [markdown] +# Then we sum over the middle, $j$ axis, [which is the 1-axis of three axes numbered (0,1,2)] of this 3-d matrix. Thus we generate $\Sigma_j A_{ij}B_{jk}$. +# +# We can see that the broadcasting concept gives us a powerful and efficient way to express many linear algebra operations computationally. + +# %% [markdown] +# ### Record Arrays + +# %% [markdown] +# These are a special array structure designed to match the CSV "Record and Field" model. It's a very different structure +# from the normal NumPy array, and different fields *can* contain different datatypes. We saw this when we looked at CSV files: + +# %% +x = np.arange(50).reshape([10, 5]) + +# %% +record_x = x.view(dtype={'names': ["col1", "col2", "another", "more", "last"], + 'formats': [int]*5 }) + +# %% +record_x + +# %% [markdown] +# Record arrays can be addressed with field names like they were a dictionary: + +# %% +record_x['col1'] + +# %% [markdown] +# We've seen these already when we used NumPy's CSV parser. + +# %% [markdown] +# ### Logical arrays, masking, and selection + +# %% [markdown] +# Numpy defines operators like == and < to apply to arrays *element by element*: + +# %% +x = np.zeros([3, 4]) +x + +# %% +y = np.arange(-1, 2)[:, np.newaxis] * np.arange(-2, 2)[np.newaxis, :] +y + +# %% +iszero = x == y +iszero + +# %% [markdown] +# A logical array can be used to select elements from an array: + +# %% +y[np.logical_not(iszero)] + +# %% [markdown] +# Although when printed, this comes out as a flat list, if assigned to, the *selected elements of the array are changed!* + +# %% +y[iszero] = 5 + +# %% +y + +# %% [markdown] +# ### Numpy memory + +# %% [markdown] +# Numpy memory management can be tricksy: + +# %% +x = np.arange(5) +y = x[:] + +# %% +y[2] = 0 +x + +# %% [markdown] +# It does **not** behave like lists! + +# %% +x = list(range(5)) +y = x[:] + +# %% +y[2] = 0 +x + +# %% [markdown] +# We must use `np.copy` to force separate memory. Otherwise NumPy tries its hardest to make slices be *views* on data. + +# %% [markdown] +# Now, this has all been very theoretical, but let's go through a practical example, and see how powerful NumPy can be. diff --git a/ch02data/084Boids.html b/ch02data/084Boids.html new file mode 100644 index 000000000..591b2a850 --- /dev/null +++ b/ch02data/084Boids.html @@ -0,0 +1,128017 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The Boids + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

The Boids!

This section shows an example of using NumPy to encode a model of how a group of birds or other animals moves. It is based on a paper by Craig W. Reynolds. Reynolds calls the simulated creatures "bird-oids" or "boids", so that's what we'll be calling them here too.

+
+
+
+
+
+
+

Flocking

+
+
+
+
+
+
+
+

The aggregate motion of a flock of birds, a herd of land animals, or a school of fish is a beautiful and familiar +part of the natural world... The aggregate motion of the simulated flock is created by a distributed behavioral model much +like that at work in a natural flock; the birds choose their own course. Each simulated bird is implemented as an independent +actor that navigates according to its local perception of the dynamic environment, the laws of simulated physics that rule its +motion, and a set of behaviors programmed into it... The aggregate motion of the simulated flock is the result of the +dense interaction of the relatively simple behaviors of the individual simulated birds.

+
+

-- Craig W. Reynolds, "Flocks, Herds, and Schools: A Distributed Behavioral Model", Computer Graphics 21 4 1987, pp 25-34

+
+
+
+
+
+
+

The model includes three main behaviours which, together, give rise to "flocking". In the words of the paper:

+
    +
  • Collision Avoidance: avoid collisions with nearby flockmates
  • +
  • Velocity Matching: attempt to match velocity with nearby flockmates
  • +
  • Flock Centering: attempt to stay close to nearby flockmates
  • +
+
+
+
+
+
+
+

Setting up the Boids

+
+
+
+
+
+
+

Our boids will each have an x velocity and a y velocity, and an x position and a y position.

+
+
+
+
+
+
+

We'll build this up in NumPy notation, and eventually, have an animated simulation of our flying boids.

+
+
+
+
+
+
In [1]:
+
+
+
import numpy as np
+
+
+
+
+
+
+
+
+

Let's start with simple flying in a straight line.

+
+
+
+
+
+
+

Our positions, for each of our N boids, will be an array, shape $2 \times N$, with the x positions in the first row, +and y positions in the second row.

+
+
+
+
+
+
In [2]:
+
+
+
boid_count = 10
+
+
+
+
+
+
+
+
+

We'll want to be able to seed our Boids in a random position.

+
+
+
+
+
+
+

We'd better define the edges of our simulation area:

+
+
+
+
+
+
In [3]:
+
+
+
limits = np.array([2000, 2000])
+
+
+
+
+
+
+
+
In [4]:
+
+
+
positions = np.random.rand(2, boid_count) * limits[:, np.newaxis]
+positions
+
+
+
+
+
+
+
+
Out[4]:
+
+
array([[1649.55926147, 1601.71950648,  388.02851125, 1193.14175018,
+        1971.77917384, 1657.78603676,  143.22765487, 1603.10528672,
+         113.93190424,  332.26664446],
+       [1664.55116361,  657.64707223,  424.02934765, 1919.13893061,
+         332.73286647, 1304.50103822, 1136.49850859, 1991.36831221,
+        1453.11278046,  537.48225435]])
+
+
+
+
+
+
+
+
In [5]:
+
+
+
positions.shape
+
+
+
+
+
+
+
+
Out[5]:
+
+
(2, 10)
+
+
+
+
+
+
+
+
+

We used broadcasting with np.newaxis to apply our upper limit to each boid. +rand gives us a random number between 0 and 1. We multiply by our limits to get a number up to that limit.

+
+
+
+
+
+
In [6]:
+
+
+
limits[:, np.newaxis]
+
+
+
+
+
+
+
+
Out[6]:
+
+
array([[2000],
+       [2000]])
+
+
+
+
+
+
+
+
In [7]:
+
+
+
limits[:, np.newaxis].shape
+
+
+
+
+
+
+
+
Out[7]:
+
+
(2, 1)
+
+
+
+
+
+
+
+
In [8]:
+
+
+
np.random.rand(2, boid_count).shape
+
+
+
+
+
+
+
+
Out[8]:
+
+
(2, 10)
+
+
+
+
+
+
+
+
+

So we multiply a $2\times1$ array by a $2 \times 10$ array -- and get a $2\times 10$ array.

+
+
+
+
+
+
+

Let's put that in a function:

+
+
+
+
+
+
In [9]:
+
+
+
def new_flock(count, lower_limits, upper_limits):
+    width = upper_limits - lower_limits
+    return (lower_limits[:, np.newaxis] + np.random.rand(2, count) * width[:, np.newaxis])
+
+
+
+
+
+
+
+
+

For example, let's assume that we want our initial positions to vary between 100 and 200 in the x axis, and 900 and 1100 in the y axis. We can generate random positions within these constraints with:

+
positions = new_flock(boid_count, np.array([100, 900]), np.array([200, 1100]))
+
+
+
+
+
+
+
+

But each bird will also need a starting velocity. Let's make these random too:

+

We can reuse the new_flock function defined above, since we're again essentially just generating random numbers from given limits. This saves us some code, but keep in mind that using a function for something other than what its name indicates can become confusing!

+

Here, we will let the initial x velocities range over $[0, 10]$ and the y velocities over $[-20, 20]$.

+
+
+
+
+
+
In [10]:
+
+
+
velocities = new_flock(boid_count, np.array([0, -20]), np.array([10, 20]))
+velocities
+
+
+
+
+
+
+
+
Out[10]:
+
+
array([[  3.11050372,   7.54448206,   7.88934674,   5.83277467,
+          4.4568626 ,   7.87371654,   1.3552303 ,   3.69226272,
+          5.64429447,   9.196302  ],
+       [-14.5292522 ,  -9.33854171,  13.91975592,   1.58196781,
+         -0.33627022,  -1.69345768, -15.18744   ,  -5.82284056,
+         -9.07483705,   8.58586815]])
+
+
+
+
+
+
+
+
+

Flying in a Straight Line

+
+
+
+
+
+
+

Now we see the real amazingness of NumPy: if we want to move our whole flock according to

+

$\delta_x = \delta_t \cdot \frac{dv}{dt}$

+
+
+
+
+
+
+

we just do:

+
+
+
+
+
+
In [11]:
+
+
+
positions += velocities
+
+
+
+
+
+
+
+
+

Matplotlib Animations

+
+
+
+
+
+
+

So now we can animate our Boids using the matplotlib animation tools. All we have to do is import the relevant libraries:

+
+
+
+
+
+
In [12]:
+
+
+
from matplotlib import animation
+from matplotlib import pyplot as plt
+%matplotlib inline
+
+
+
+
+
+
+
+
+

Then, we make a static plot, showing our first frame:

+
+
+
+
+
+
In [13]:
+
+
+
# create a simple plot
+# initial x position in [100, 200], initial y position in [900, 1100]
+# initial x velocity in [0, 10], initial y velocity in [-20, 20]
+positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))
+
+figure = plt.figure()
+axes = plt.axes(xlim=(0, limits[0]), ylim=(0, limits[1]))
+scatter = axes.scatter(positions[0, :], positions[1, :],
+                       marker='o', edgecolor='k', lw=0.5)
+scatter
+
+
+
+
+
+
+
+
Out[13]:
+
+
<matplotlib.collections.PathCollection at 0x7ff828169730>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Then, we define a function which updates the figure for each timestep

+
+
+
+
+
+
In [14]:
+
+
+
def update_boids(positions, velocities):
+    positions += velocities
+
+
+def animate(frame):
+    update_boids(positions, velocities)
+    scatter.set_offsets(positions.transpose())
+
+
+
+
+
+
+
+
+

Call FuncAnimation, and specify how many frames we want:

+
+
+
+
+
+
In [15]:
+
+
+
anim = animation.FuncAnimation(figure, animate,
+                               frames=50, interval=50)
+
+
+
+
+
+
+
+
+

Save out the figure:

+
+
+
+
+
+
In [16]:
+
+
+
positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))
+anim.save('boids_1.mp4')
+
+
+
+
+
+
+
+
+

And download the saved animation.

+
+
+
+
+
+
+

You can even view the results directly in the notebook.

+
+
+
+
+
+
In [17]:
+
+
+
from IPython.display import HTML
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[17]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+

Fly towards the middle

+
+
+
+
+
+
+

Boids try to fly towards the middle:

+
+
+
+
+
+
In [18]:
+
+
+
positions = new_flock(4, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(4, np.array([0, -20]), np.array([10, 20]))
+
+
+
+
+
+
+
+
In [19]:
+
+
+
positions
+
+
+
+
+
+
+
+
Out[19]:
+
+
array([[ 115.24692057,  179.98687569,  159.59348836,  131.00690041],
+       [1050.05685844,  961.76844806, 1037.36716748,  937.32030979]])
+
+
+
+
+
+
+
+
In [20]:
+
+
+
velocities
+
+
+
+
+
+
+
+
Out[20]:
+
+
array([[ 3.27146507,  0.02486096,  5.46779034,  0.15641596],
+       [-7.77095679,  3.13863574, -9.80502453,  5.81561024]])
+
+
+
+
+
+
+
+
In [21]:
+
+
+
middle = np.mean(positions, 1)
+middle
+
+
+
+
+
+
+
+
Out[21]:
+
+
array([146.45854626, 996.62819594])
+
+
+
+
+
+
+
+
In [22]:
+
+
+
direction_to_middle = positions - middle[:, np.newaxis]
+direction_to_middle
+
+
+
+
+
+
+
+
Out[22]:
+
+
array([[-31.21162569,  33.52832944,  13.1349421 , -15.45164584],
+       [ 53.42866249, -34.85974788,  40.73897154, -59.30788615]])
+
+
+
+
+
+
+
+
+

This is easier and faster than:

+
for bird in birds:
+    for dimension in [0, 1]:
+        direction_to_middle[dimension][bird] = positions[dimension][bird] - middle[dimension]
+
+
+
+
+
+
+
In [23]:
+
+
+
move_to_middle_strength = 0.01
+velocities = velocities - direction_to_middle * move_to_middle_strength
+
+
+
+
+
+
+
+
+

Let's update our function, and animate that:

+
+
+
+
+
+
In [24]:
+
+
+
def update_boids(positions, velocities):
+    move_to_middle_strength = 0.01
+    middle = np.mean(positions, 1)
+    direction_to_middle = positions - middle[:, np.newaxis]
+    velocities -= direction_to_middle * move_to_middle_strength
+    positions += velocities
+
+
+
+
+
+
+
+
In [25]:
+
+
+
def animate(frame):
+    update_boids(positions, velocities)
+    scatter.set_offsets(positions.transpose())
+
+
+
+
+
+
+
+
In [26]:
+
+
+
anim = animation.FuncAnimation(figure, animate,
+                               frames=50, interval=50)
+
+
+
+
+
+
+
+
In [27]:
+
+
+
positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[27]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+

Avoiding collisions

+
+
+
+
+
+
+

We'll want to add our other flocking rules to the behaviour of the Boids.

+
+
+
+
+
+
+

We'll need a matrix giving the distances between each bird. This should be $N \times N$.

+
+
+
+
+
+
In [28]:
+
+
+
positions = new_flock(4, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(4, np.array([0, -20]), np.array([10, 20]))
+
+
+
+
+
+
+
+
+

We might think that we need to do the X-distances and Y-distances separately:

+
+
+
+
+
+
In [29]:
+
+
+
xpos = positions[0, :]
+
+
+
+
+
+
+
+
In [30]:
+
+
+
xsep_matrix = xpos[:, np.newaxis] - xpos[np.newaxis, :]
+
+
+
+
+
+
+
+
In [31]:
+
+
+
xsep_matrix.shape
+
+
+
+
+
+
+
+
Out[31]:
+
+
(4, 4)
+
+
+
+
+
+
+
+
In [32]:
+
+
+
xsep_matrix
+
+
+
+
+
+
+
+
Out[32]:
+
+
array([[  0.        , -23.23247568, -16.35642945, -50.53173464],
+       [ 23.23247568,   0.        ,   6.87604623, -27.29925895],
+       [ 16.35642945,  -6.87604623,   0.        , -34.17530518],
+       [ 50.53173464,  27.29925895,  34.17530518,   0.        ]])
+
+
+
+
+
+
+
+
+

But in NumPy we can be cleverer than that, and make a $2 \times N \times N$ matrix of separations:

+
+
+
+
+
+
In [33]:
+
+
+
separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]
+
+
+
+
+
+
+
+
In [34]:
+
+
+
separations.shape
+
+
+
+
+
+
+
+
Out[34]:
+
+
(2, 4, 4)
+
+
+
+
+
+
+
+
+

And then we can get the sum-of-squares $\delta_x^2 + \delta_y^2$ like this:

+
+
+
+
+
+
In [35]:
+
+
+
squared_displacements = separations * separations
+
+
+
+
+
+
+
+
In [36]:
+
+
+
square_distances = np.sum(squared_displacements, 0)
+
+
+
+
+
+
+
+
In [37]:
+
+
+
square_distances
+
+
+
+
+
+
+
+
Out[37]:
+
+
array([[   0.        , 3958.31019715, 7765.05729499, 8342.87692309],
+       [3958.31019715,    0.        ,  838.00172985, 1055.70585242],
+       [7765.05729499,  838.00172985,    0.        , 1278.20156818],
+       [8342.87692309, 1055.70585242, 1278.20156818,    0.        ]])
+
+
+
+
+
+
+
+
+

Now we need to find birds that are too close:

+
+
+
+
+
+
In [38]:
+
+
+
alert_distance = 2000
+close_birds = square_distances < alert_distance
+close_birds
+
+
+
+
+
+
+
+
Out[38]:
+
+
array([[ True, False, False, False],
+       [False,  True,  True,  True],
+       [False,  True,  True,  True],
+       [False,  True,  True,  True]])
+
+
+
+
+
+
+
+
+

Find the direction distances only to those birds which are too close:

+
+
+
+
+
+
In [39]:
+
+
+
separations_if_close = np.copy(separations)
+far_away = np.logical_not(close_birds)
+
+
+
+
+
+
+
+
+

Set x and y values in separations_if_close to zero if they are far away:

+
+
+
+
+
+
In [40]:
+
+
+
separations_if_close[0, :, :][far_away] = 0
+separations_if_close[1, :, :][far_away] = 0
+separations_if_close
+
+
+
+
+
+
+
+
Out[40]:
+
+
array([[[  0.        ,   0.        ,   0.        ,   0.        ],
+        [  0.        ,   0.        ,  -6.87604623,  27.29925895],
+        [  0.        ,   6.87604623,   0.        ,  34.17530518],
+        [  0.        , -27.29925895, -34.17530518,   0.        ]],
+
+       [[  0.        ,   0.        ,   0.        ,   0.        ],
+        [  0.        ,   0.        ,  28.1197745 ,  17.61977052],
+        [  0.        , -28.1197745 ,   0.        , -10.50000399],
+        [  0.        , -17.61977052,  10.50000399,   0.        ]]])
+
+
+
+
+
+
+
+
+

And fly away from them:

+
+
+
+
+
+
In [41]:
+
+
+
np.sum(separations_if_close, 2)
+
+
+
+
+
+
+
+
Out[41]:
+
+
array([[  0.        ,  20.42321272,  41.05135142, -61.47456414],
+       [  0.        ,  45.73954502, -38.61977849,  -7.11976653]])
+
+
+
+
+
+
+
+
In [42]:
+
+
+
velocities = velocities + np.sum(separations_if_close, 2)
+
+
+
+
+
+
+
+
+

Now we can update our animation:

+
+
+
+
+
+
In [43]:
+
+
+
def update_boids(positions, velocities):
+    move_to_middle_strength = 0.01
+    middle = np.mean(positions, 1)
+    direction_to_middle = positions - middle[:, np.newaxis]
+    velocities -= direction_to_middle * move_to_middle_strength
+
+    separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]
+    squared_displacements = separations * separations
+    square_distances = np.sum(squared_displacements, 0)
+    alert_distance = 100
+    far_away = square_distances > alert_distance
+    separations_if_close = np.copy(separations)
+    separations_if_close[0, :, :][far_away] = 0
+    separations_if_close[1, :, :][far_away] = 0
+    velocities += np.sum(separations_if_close, 1)
+
+    positions += velocities
+
+
+
+
+
+
+
+
In [44]:
+
+
+
def animate(frame):
+    update_boids(positions, velocities)
+    scatter.set_offsets(positions.transpose())
+
+
+anim = animation.FuncAnimation(figure, animate,
+                               frames=50, interval=50)
+
+positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[44]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+

Match speed with nearby birds

+
+
+
+
+
+
+

This is pretty similar:

+
+
+
+
+
+
In [45]:
+
+
+
def update_boids(positions, velocities):
+    move_to_middle_strength = 0.01
+    middle = np.mean(positions, 1)
+    direction_to_middle = positions - middle[:, np.newaxis]
+    velocities -= direction_to_middle * move_to_middle_strength
+
+    separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]
+    squared_displacements = separations * separations
+    square_distances = np.sum(squared_displacements, 0)
+    alert_distance = 100
+    far_away = square_distances > alert_distance
+    separations_if_close = np.copy(separations)
+    separations_if_close[0, :, :][far_away] = 0
+    separations_if_close[1, :, :][far_away] = 0
+    velocities += np.sum(separations_if_close, 1)
+
+    velocity_differences = velocities[:, np.newaxis, :] - velocities[:, :, np.newaxis]
+    formation_flying_distance = 10000
+    formation_flying_strength = 0.125
+    very_far = square_distances > formation_flying_distance
+    velocity_differences_if_close = np.copy(velocity_differences)
+    velocity_differences_if_close[0, :, :][very_far] = 0
+    velocity_differences_if_close[1, :, :][very_far] = 0
+    velocities -= np.mean(velocity_differences_if_close, 1) * formation_flying_strength
+
+    positions += velocities
+
+
+
+
+
+
+
+
In [46]:
+
+
+
def animate(frame):
+    update_boids(positions, velocities)
+    scatter.set_offsets(positions.transpose())
+
+
+anim = animation.FuncAnimation(figure, animate,
+                               frames=200, interval=50)
+
+
+positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
+velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[46]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+

Hopefully the power of NumPy should be pretty clear now. This would be enormously slower and harder to understand using traditional lists.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/084Boids.ipynb b/ch02data/084Boids.ipynb new file mode 100644 index 000000000..9c1978842 --- /dev/null +++ b/ch02data/084Boids.ipynb @@ -0,0 +1,974 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9a0eba35", + "metadata": {}, + "source": [ + "## The Boids!\n", + "\n", + "This section shows an example of using NumPy to encode a model of how a group of birds or other animals moves. It is based on [a paper by Craig W. Reynolds](http://www.cs.toronto.edu/~dt/siggraph97-course/cwr87/). Reynolds calls the simulated creatures \"bird-oids\" or \"boids\", so that's what we'll be calling them here too." + ] + }, + { + "cell_type": "markdown", + "id": "a1d4d913", + "metadata": {}, + "source": [ + "### Flocking" + ] + }, + { + "cell_type": "markdown", + "id": "68acb38e", + "metadata": {}, + "source": [ + "\n", + "> The aggregate motion of a flock of birds, a herd of land animals, or a school of fish is a beautiful and familiar\n", + "part of the natural world... The aggregate motion of the simulated flock is created by a distributed behavioral model much\n", + "like that at work in a natural flock; the birds choose their own course. Each simulated bird is implemented as an independent\n", + "actor that navigates according to its local perception of the dynamic environment, the laws of simulated physics that rule its\n", + "motion, and a set of behaviors programmed into it... The aggregate motion of the simulated flock is the result of the\n", + "dense interaction of the relatively simple behaviors of the individual simulated birds. \n", + "\n", + "-- Craig W. Reynolds, \"Flocks, Herds, and Schools: A Distributed Behavioral Model\", *Computer Graphics* **21** _4_ 1987, pp 25-34" + ] + }, + { + "cell_type": "markdown", + "id": "b632adac", + "metadata": {}, + "source": [ + "The model includes three main behaviours which, together, give rise to \"flocking\". In the words of the paper:\n", + "\n", + "* Collision Avoidance: avoid collisions with nearby flockmates\n", + "* Velocity Matching: attempt to match velocity with nearby flockmates\n", + "* Flock Centering: attempt to stay close to nearby flockmates" + ] + }, + { + "cell_type": "markdown", + "id": "b119faf8", + "metadata": {}, + "source": [ + "### Setting up the Boids" + ] + }, + { + "cell_type": "markdown", + "id": "696f2a4d", + "metadata": {}, + "source": [ + "Our boids will each have an x velocity and a y velocity, and an x position and a y position." + ] + }, + { + "cell_type": "markdown", + "id": "3dd82448", + "metadata": {}, + "source": [ + "We'll build this up in NumPy notation, and eventually, have an animated simulation of our flying boids." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1737511a", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "33ac68b2", + "metadata": {}, + "source": [ + "Let's start with simple flying in a straight line." + ] + }, + { + "cell_type": "markdown", + "id": "78e8cde4", + "metadata": {}, + "source": [ + "Our positions, for each of our N boids, will be an array, shape $2 \\times N$, with the x positions in the first row,\n", + "and y positions in the second row." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4447f62", + "metadata": {}, + "outputs": [], + "source": [ + "boid_count = 10" + ] + }, + { + "cell_type": "markdown", + "id": "ff3ed91c", + "metadata": {}, + "source": [ + "We'll want to be able to seed our Boids in a random position." + ] + }, + { + "cell_type": "markdown", + "id": "31a44c54", + "metadata": {}, + "source": [ + "We'd better define the edges of our simulation area:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0479f23d", + "metadata": {}, + "outputs": [], + "source": [ + "limits = np.array([2000, 2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51db33ab", + "metadata": {}, + "outputs": [], + "source": [ + "positions = np.random.rand(2, boid_count) * limits[:, np.newaxis]\n", + "positions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86925d98", + "metadata": {}, + "outputs": [], + "source": [ + "positions.shape" + ] + }, + { + "cell_type": "markdown", + "id": "d949edd1", + "metadata": {}, + "source": [ + "We used **broadcasting** with np.newaxis to apply our upper limit to each boid.\n", + "`rand` gives us a random number between 0 and 1. We multiply by our limits to get a number up to that limit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "320fdfd7", + "metadata": {}, + "outputs": [], + "source": [ + "limits[:, np.newaxis]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "653dd53c", + "metadata": {}, + "outputs": [], + "source": [ + "limits[:, np.newaxis].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53cba297", + "metadata": {}, + "outputs": [], + "source": [ + "np.random.rand(2, boid_count).shape" + ] + }, + { + "cell_type": "markdown", + "id": "8cadce29", + "metadata": {}, + "source": [ + "So we multiply a $2\\times1$ array by a $2 \\times 10$ array -- and get a $2\\times 10$ array." + ] + }, + { + "cell_type": "markdown", + "id": "b3a1fd91", + "metadata": {}, + "source": [ + "Let's put that in a function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a65f310", + "metadata": {}, + "outputs": [], + "source": [ + "def new_flock(count, lower_limits, upper_limits):\n", + " width = upper_limits - lower_limits\n", + " return (lower_limits[:, np.newaxis] + np.random.rand(2, count) * width[:, np.newaxis])" + ] + }, + { + "cell_type": "markdown", + "id": "fe5af93e", + "metadata": {}, + "source": [ + "For example, let's assume that we want our initial positions to vary between 100 and 200 in the x axis, and 900 and 1100 in the y axis. We can generate random positions within these constraints with:\n", + "```python\n", + "positions = new_flock(boid_count, np.array([100, 900]), np.array([200, 1100]))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7981cc47", + "metadata": {}, + "source": [ + "But each bird will also need a starting velocity. Let's make these random too:\n", + "\n", + "We can reuse the `new_flock` function defined above, since we're again essentially just generating random numbers from given limits. This saves us some code, but keep in mind that using a function for something other than what its name indicates can become confusing!\n", + "\n", + "Here, we will let the initial x velocities range over $[0, 10]$ and the y velocities over $[-20, 20]$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "853447c9", + "metadata": {}, + "outputs": [], + "source": [ + "velocities = new_flock(boid_count, np.array([0, -20]), np.array([10, 20]))\n", + "velocities" + ] + }, + { + "cell_type": "markdown", + "id": "77cf037f", + "metadata": {}, + "source": [ + "### Flying in a Straight Line" + ] + }, + { + "cell_type": "markdown", + "id": "579d414b", + "metadata": {}, + "source": [ + "Now we see the real amazingness of NumPy: if we want to move our *whole flock* according to\n", + "\n", + "$\\delta_x = \\delta_t \\cdot \\frac{dv}{dt}$" + ] + }, + { + "cell_type": "markdown", + "id": "6370ef76", + "metadata": {}, + "source": [ + "we just do:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "793dc2b3", + "metadata": {}, + "outputs": [], + "source": [ + "positions += velocities" + ] + }, + { + "cell_type": "markdown", + "id": "dc8b1c84", + "metadata": {}, + "source": [ + "### Matplotlib Animations" + ] + }, + { + "cell_type": "markdown", + "id": "247164a2", + "metadata": {}, + "source": [ + "So now we can animate our Boids using the matplotlib animation tools. All we have to do is import the relevant libraries:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0800fc77", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import animation\n", + "from matplotlib import pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "22fb9100", + "metadata": {}, + "source": [ + "Then, we make a static plot, showing our first frame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1565ed5d", + "metadata": {}, + "outputs": [], + "source": [ + "# create a simple plot\n", + "# initial x position in [100, 200], initial y position in [900, 1100]\n", + "# initial x velocity in [0, 10], initial y velocity in [-20, 20]\n", + "positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))\n", + "\n", + "figure = plt.figure()\n", + "axes = plt.axes(xlim=(0, limits[0]), ylim=(0, limits[1]))\n", + "scatter = axes.scatter(positions[0, :], positions[1, :],\n", + " marker='o', edgecolor='k', lw=0.5)\n", + "scatter" + ] + }, + { + "cell_type": "markdown", + "id": "199520ef", + "metadata": {}, + "source": [ + "Then, we define a function which **updates** the figure for each timestep" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41b56693", + "metadata": {}, + "outputs": [], + "source": [ + "def update_boids(positions, velocities):\n", + " positions += velocities\n", + "\n", + "\n", + "def animate(frame):\n", + " update_boids(positions, velocities)\n", + " scatter.set_offsets(positions.transpose())" + ] + }, + { + "cell_type": "markdown", + "id": "61b05659", + "metadata": {}, + "source": [ + "Call `FuncAnimation`, and specify how many frames we want:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd2ffbd9", + "metadata": {}, + "outputs": [], + "source": [ + "anim = animation.FuncAnimation(figure, animate,\n", + " frames=50, interval=50)" + ] + }, + { + "cell_type": "markdown", + "id": "1aecb100", + "metadata": {}, + "source": [ + "Save out the figure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1fd32d2", + "metadata": {}, + "outputs": [], + "source": [ + "positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))\n", + "anim.save('boids_1.mp4')" + ] + }, + { + "cell_type": "markdown", + "id": "7cd1eac7", + "metadata": {}, + "source": [ + "And download the [saved animation](http://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch02data/boids_1.mp4)." + ] + }, + { + "cell_type": "markdown", + "id": "6828d0f5", + "metadata": {}, + "source": [ + "You can even view the results directly in the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4152c275", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import HTML\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "id": "8ce67ef8", + "metadata": {}, + "source": [ + "### Fly towards the middle" + ] + }, + { + "cell_type": "markdown", + "id": "ae8d3cdc", + "metadata": {}, + "source": [ + "Boids try to fly towards the middle:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef7124e2", + "metadata": {}, + "outputs": [], + "source": [ + "positions = new_flock(4, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(4, np.array([0, -20]), np.array([10, 20]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b6aec19", + "metadata": {}, + "outputs": [], + "source": [ + "positions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f40d9bb", + "metadata": {}, + "outputs": [], + "source": [ + "velocities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee607259", + "metadata": {}, + "outputs": [], + "source": [ + "middle = np.mean(positions, 1)\n", + "middle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81f84c79", + "metadata": {}, + "outputs": [], + "source": [ + "direction_to_middle = positions - middle[:, np.newaxis]\n", + "direction_to_middle" + ] + }, + { + "cell_type": "markdown", + "id": "534aa08f", + "metadata": {}, + "source": [ + "This is easier and faster than:\n", + "\n", + "``` python\n", + "for bird in birds:\n", + " for dimension in [0, 1]:\n", + " direction_to_middle[dimension][bird] = positions[dimension][bird] - middle[dimension]\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a56fced9", + "metadata": {}, + "outputs": [], + "source": [ + "move_to_middle_strength = 0.01\n", + "velocities = velocities - direction_to_middle * move_to_middle_strength" + ] + }, + { + "cell_type": "markdown", + "id": "afcb2205", + "metadata": {}, + "source": [ + "Let's update our function, and animate that:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f53f7785", + "metadata": {}, + "outputs": [], + "source": [ + "def update_boids(positions, velocities):\n", + " move_to_middle_strength = 0.01\n", + " middle = np.mean(positions, 1)\n", + " direction_to_middle = positions - middle[:, np.newaxis]\n", + " velocities -= direction_to_middle * move_to_middle_strength\n", + " positions += velocities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c858a77", + "metadata": {}, + "outputs": [], + "source": [ + "def animate(frame):\n", + " update_boids(positions, velocities)\n", + " scatter.set_offsets(positions.transpose())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55aef70d", + "metadata": {}, + "outputs": [], + "source": [ + "anim = animation.FuncAnimation(figure, animate,\n", + " frames=50, interval=50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bad2a9cf", + "metadata": {}, + "outputs": [], + "source": [ + "positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "id": "70db3c79", + "metadata": {}, + "source": [ + "### Avoiding collisions" + ] + }, + { + "cell_type": "markdown", + "id": "d788b82d", + "metadata": {}, + "source": [ + "We'll want to add our other flocking rules to the behaviour of the Boids." + ] + }, + { + "cell_type": "markdown", + "id": "ba5106b8", + "metadata": {}, + "source": [ + "We'll need a matrix giving the distances between each bird. This should be $N \\times N$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17419757", + "metadata": {}, + "outputs": [], + "source": [ + "positions = new_flock(4, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(4, np.array([0, -20]), np.array([10, 20]))" + ] + }, + { + "cell_type": "markdown", + "id": "897fa277", + "metadata": {}, + "source": [ + "We might think that we need to do the X-distances and Y-distances separately:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1c52858", + "metadata": {}, + "outputs": [], + "source": [ + "xpos = positions[0, :]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75af52ae", + "metadata": {}, + "outputs": [], + "source": [ + "xsep_matrix = xpos[:, np.newaxis] - xpos[np.newaxis, :]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce3da15e", + "metadata": {}, + "outputs": [], + "source": [ + "xsep_matrix.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be59c071", + "metadata": {}, + "outputs": [], + "source": [ + "xsep_matrix" + ] + }, + { + "cell_type": "markdown", + "id": "b84d378d", + "metadata": {}, + "source": [ + "But in NumPy we can be cleverer than that, and make a $2 \\times N \\times N$ matrix of separations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a038aa9", + "metadata": {}, + "outputs": [], + "source": [ + "separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16a3e681", + "metadata": {}, + "outputs": [], + "source": [ + "separations.shape" + ] + }, + { + "cell_type": "markdown", + "id": "0fc36b77", + "metadata": {}, + "source": [ + "And then we can get the sum-of-squares $\\delta_x^2 + \\delta_y^2$ like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a39937cb", + "metadata": {}, + "outputs": [], + "source": [ + "squared_displacements = separations * separations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd84b69c", + "metadata": {}, + "outputs": [], + "source": [ + "square_distances = np.sum(squared_displacements, 0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea61c489", + "metadata": {}, + "outputs": [], + "source": [ + "square_distances" + ] + }, + { + "cell_type": "markdown", + "id": "c7bf5afd", + "metadata": {}, + "source": [ + "Now we need to find birds that are too close:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a373112", + "metadata": {}, + "outputs": [], + "source": [ + "alert_distance = 2000\n", + "close_birds = square_distances < alert_distance\n", + "close_birds" + ] + }, + { + "cell_type": "markdown", + "id": "59934685", + "metadata": {}, + "source": [ + "Find the direction distances **only** to those birds which are too close:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37b226e4", + "metadata": {}, + "outputs": [], + "source": [ + "separations_if_close = np.copy(separations)\n", + "far_away = np.logical_not(close_birds)" + ] + }, + { + "cell_type": "markdown", + "id": "64fc147d", + "metadata": {}, + "source": [ + "Set `x` and `y` values in `separations_if_close` to zero if they are far away:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c3dcefd", + "metadata": {}, + "outputs": [], + "source": [ + "separations_if_close[0, :, :][far_away] = 0\n", + "separations_if_close[1, :, :][far_away] = 0\n", + "separations_if_close" + ] + }, + { + "cell_type": "markdown", + "id": "cc6e8b90", + "metadata": {}, + "source": [ + "And fly away from them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82884fcf", + "metadata": {}, + "outputs": [], + "source": [ + "np.sum(separations_if_close, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd5a242a", + "metadata": {}, + "outputs": [], + "source": [ + "velocities = velocities + np.sum(separations_if_close, 2)" + ] + }, + { + "cell_type": "markdown", + "id": "5e420436", + "metadata": {}, + "source": [ + "Now we can update our animation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b1df43b", + "metadata": {}, + "outputs": [], + "source": [ + "def update_boids(positions, velocities):\n", + " move_to_middle_strength = 0.01\n", + " middle = np.mean(positions, 1)\n", + " direction_to_middle = positions - middle[:, np.newaxis]\n", + " velocities -= direction_to_middle * move_to_middle_strength\n", + "\n", + " separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]\n", + " squared_displacements = separations * separations\n", + " square_distances = np.sum(squared_displacements, 0)\n", + " alert_distance = 100\n", + " far_away = square_distances > alert_distance\n", + " separations_if_close = np.copy(separations)\n", + " separations_if_close[0, :, :][far_away] = 0\n", + " separations_if_close[1, :, :][far_away] = 0\n", + " velocities += np.sum(separations_if_close, 1)\n", + "\n", + " positions += velocities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87e96b6f", + "metadata": {}, + "outputs": [], + "source": [ + "def animate(frame):\n", + " update_boids(positions, velocities)\n", + " scatter.set_offsets(positions.transpose())\n", + "\n", + "\n", + "anim = animation.FuncAnimation(figure, animate,\n", + " frames=50, interval=50)\n", + "\n", + "positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "id": "5549bc52", + "metadata": {}, + "source": [ + "### Match speed with nearby birds" + ] + }, + { + "cell_type": "markdown", + "id": "d974c033", + "metadata": {}, + "source": [ + "This is pretty similar:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c7eddbf", + "metadata": {}, + "outputs": [], + "source": [ + "def update_boids(positions, velocities):\n", + " move_to_middle_strength = 0.01\n", + " middle = np.mean(positions, 1)\n", + " direction_to_middle = positions - middle[:, np.newaxis]\n", + " velocities -= direction_to_middle * move_to_middle_strength\n", + "\n", + " separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis]\n", + " squared_displacements = separations * separations\n", + " square_distances = np.sum(squared_displacements, 0)\n", + " alert_distance = 100\n", + " far_away = square_distances > alert_distance\n", + " separations_if_close = np.copy(separations)\n", + " separations_if_close[0, :, :][far_away] = 0\n", + " separations_if_close[1, :, :][far_away] = 0\n", + " velocities += np.sum(separations_if_close, 1)\n", + "\n", + " velocity_differences = velocities[:, np.newaxis, :] - velocities[:, :, np.newaxis]\n", + " formation_flying_distance = 10000\n", + " formation_flying_strength = 0.125\n", + " very_far = square_distances > formation_flying_distance\n", + " velocity_differences_if_close = np.copy(velocity_differences)\n", + " velocity_differences_if_close[0, :, :][very_far] = 0\n", + " velocity_differences_if_close[1, :, :][very_far] = 0\n", + " velocities -= np.mean(velocity_differences_if_close, 1) * formation_flying_strength\n", + "\n", + " positions += velocities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08c1778a", + "metadata": {}, + "outputs": [], + "source": [ + "def animate(frame):\n", + " update_boids(positions, velocities)\n", + " scatter.set_offsets(positions.transpose())\n", + "\n", + "\n", + "anim = animation.FuncAnimation(figure, animate,\n", + " frames=200, interval=50)\n", + "\n", + "\n", + "positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))\n", + "velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "id": "1f34c3ea", + "metadata": {}, + "source": [ + "Hopefully the power of NumPy should be pretty clear now. This would be **enormously slower** and harder to understand using traditional lists." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "The Boids" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/084Boids.ipynb.py b/ch02data/084Boids.ipynb.py new file mode 100644 index 000000000..d21c976d0 --- /dev/null +++ b/ch02data/084Boids.ipynb.py @@ -0,0 +1,435 @@ +# --- +# jupyter: +# jekyll: +# display_name: The Boids +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## The Boids! +# +# This section shows an example of using NumPy to encode a model of how a group of birds or other animals moves. It is based on [a paper by Craig W. Reynolds](http://www.cs.toronto.edu/~dt/siggraph97-course/cwr87/). Reynolds calls the simulated creatures "bird-oids" or "boids", so that's what we'll be calling them here too. + +# %% [markdown] +# ### Flocking + +# %% [markdown] +# +# > The aggregate motion of a flock of birds, a herd of land animals, or a school of fish is a beautiful and familiar +# part of the natural world... The aggregate motion of the simulated flock is created by a distributed behavioral model much +# like that at work in a natural flock; the birds choose their own course. Each simulated bird is implemented as an independent +# actor that navigates according to its local perception of the dynamic environment, the laws of simulated physics that rule its +# motion, and a set of behaviors programmed into it... The aggregate motion of the simulated flock is the result of the +# dense interaction of the relatively simple behaviors of the individual simulated birds. +# +# -- Craig W. Reynolds, "Flocks, Herds, and Schools: A Distributed Behavioral Model", *Computer Graphics* **21** _4_ 1987, pp 25-34 + +# %% [markdown] +# The model includes three main behaviours which, together, give rise to "flocking". In the words of the paper: +# +# * Collision Avoidance: avoid collisions with nearby flockmates +# * Velocity Matching: attempt to match velocity with nearby flockmates +# * Flock Centering: attempt to stay close to nearby flockmates + +# %% [markdown] +# ### Setting up the Boids + +# %% [markdown] +# Our boids will each have an x velocity and a y velocity, and an x position and a y position. + +# %% [markdown] +# We'll build this up in NumPy notation, and eventually, have an animated simulation of our flying boids. + +# %% +import numpy as np + +# %% [markdown] +# Let's start with simple flying in a straight line. + +# %% [markdown] +# Our positions, for each of our N boids, will be an array, shape $2 \times N$, with the x positions in the first row, +# and y positions in the second row. + +# %% +boid_count = 10 + +# %% [markdown] +# We'll want to be able to seed our Boids in a random position. + +# %% [markdown] +# We'd better define the edges of our simulation area: + +# %% +limits = np.array([2000, 2000]) + +# %% +positions = np.random.rand(2, boid_count) * limits[:, np.newaxis] +positions + +# %% +positions.shape + +# %% [markdown] +# We used **broadcasting** with np.newaxis to apply our upper limit to each boid. +# `rand` gives us a random number between 0 and 1. We multiply by our limits to get a number up to that limit. + +# %% +limits[:, np.newaxis] + +# %% +limits[:, np.newaxis].shape + +# %% +np.random.rand(2, boid_count).shape + + +# %% [markdown] +# So we multiply a $2\times1$ array by a $2 \times 10$ array -- and get a $2\times 10$ array. + +# %% [markdown] +# Let's put that in a function: + +# %% +def new_flock(count, lower_limits, upper_limits): + width = upper_limits - lower_limits + return (lower_limits[:, np.newaxis] + np.random.rand(2, count) * width[:, np.newaxis]) + + +# %% [markdown] +# For example, let's assume that we want our initial positions to vary between 100 and 200 in the x axis, and 900 and 1100 in the y axis. We can generate random positions within these constraints with: +# ```python +# positions = new_flock(boid_count, np.array([100, 900]), np.array([200, 1100])) +# ``` + +# %% [markdown] +# But each bird will also need a starting velocity. Let's make these random too: +# +# We can reuse the `new_flock` function defined above, since we're again essentially just generating random numbers from given limits. This saves us some code, but keep in mind that using a function for something other than what its name indicates can become confusing! +# +# Here, we will let the initial x velocities range over $[0, 10]$ and the y velocities over $[-20, 20]$. + +# %% +velocities = new_flock(boid_count, np.array([0, -20]), np.array([10, 20])) +velocities + +# %% [markdown] +# ### Flying in a Straight Line + +# %% [markdown] +# Now we see the real amazingness of NumPy: if we want to move our *whole flock* according to +# +# $\delta_x = \delta_t \cdot \frac{dv}{dt}$ + +# %% [markdown] +# we just do: + +# %% +positions += velocities + +# %% [markdown] +# ### Matplotlib Animations + +# %% [markdown] +# So now we can animate our Boids using the matplotlib animation tools. All we have to do is import the relevant libraries: + +# %% +from matplotlib import animation +from matplotlib import pyplot as plt +# %matplotlib inline + +# %% [markdown] +# Then, we make a static plot, showing our first frame: + +# %% +# create a simple plot +# initial x position in [100, 200], initial y position in [900, 1100] +# initial x velocity in [0, 10], initial y velocity in [-20, 20] +positions = new_flock(100, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(100, np.array([0, -20]), np.array([10, 20])) + +figure = plt.figure() +axes = plt.axes(xlim=(0, limits[0]), ylim=(0, limits[1])) +scatter = axes.scatter(positions[0, :], positions[1, :], + marker='o', edgecolor='k', lw=0.5) +scatter + + +# %% [markdown] +# Then, we define a function which **updates** the figure for each timestep + +# %% +def update_boids(positions, velocities): + positions += velocities + + +def animate(frame): + update_boids(positions, velocities) + scatter.set_offsets(positions.transpose()) + + +# %% [markdown] +# Call `FuncAnimation`, and specify how many frames we want: + +# %% +anim = animation.FuncAnimation(figure, animate, + frames=50, interval=50) + +# %% [markdown] +# Save out the figure: + +# %% +positions = new_flock(100, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(100, np.array([0, -20]), np.array([10, 20])) +anim.save('boids_1.mp4') + +# %% [markdown] +# And download the [saved animation](http://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch02data/boids_1.mp4). + +# %% [markdown] +# You can even view the results directly in the notebook. + +# %% +from IPython.display import HTML +HTML(anim.to_jshtml()) + +# %% [markdown] +# ### Fly towards the middle + +# %% [markdown] +# Boids try to fly towards the middle: + +# %% +positions = new_flock(4, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(4, np.array([0, -20]), np.array([10, 20])) + +# %% +positions + +# %% +velocities + +# %% +middle = np.mean(positions, 1) +middle + +# %% +direction_to_middle = positions - middle[:, np.newaxis] +direction_to_middle + +# %% [markdown] +# This is easier and faster than: +# +# ``` python +# for bird in birds: +# for dimension in [0, 1]: +# direction_to_middle[dimension][bird] = positions[dimension][bird] - middle[dimension] +# ``` + +# %% +move_to_middle_strength = 0.01 +velocities = velocities - direction_to_middle * move_to_middle_strength + + +# %% [markdown] +# Let's update our function, and animate that: + +# %% +def update_boids(positions, velocities): + move_to_middle_strength = 0.01 + middle = np.mean(positions, 1) + direction_to_middle = positions - middle[:, np.newaxis] + velocities -= direction_to_middle * move_to_middle_strength + positions += velocities + + +# %% +def animate(frame): + update_boids(positions, velocities) + scatter.set_offsets(positions.transpose()) + + +# %% +anim = animation.FuncAnimation(figure, animate, + frames=50, interval=50) + +# %% +positions = new_flock(100, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(100, np.array([0, -20]), np.array([10, 20])) +HTML(anim.to_jshtml()) + +# %% [markdown] +# ### Avoiding collisions + +# %% [markdown] +# We'll want to add our other flocking rules to the behaviour of the Boids. + +# %% [markdown] +# We'll need a matrix giving the distances between each bird. This should be $N \times N$. + +# %% +positions = new_flock(4, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(4, np.array([0, -20]), np.array([10, 20])) + +# %% [markdown] +# We might think that we need to do the X-distances and Y-distances separately: + +# %% +xpos = positions[0, :] + +# %% +xsep_matrix = xpos[:, np.newaxis] - xpos[np.newaxis, :] + +# %% +xsep_matrix.shape + +# %% +xsep_matrix + +# %% [markdown] +# But in NumPy we can be cleverer than that, and make a $2 \times N \times N$ matrix of separations: + +# %% +separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis] + +# %% +separations.shape + +# %% [markdown] +# And then we can get the sum-of-squares $\delta_x^2 + \delta_y^2$ like this: + +# %% +squared_displacements = separations * separations + +# %% +square_distances = np.sum(squared_displacements, 0) + +# %% +square_distances + +# %% [markdown] +# Now we need to find birds that are too close: + +# %% +alert_distance = 2000 +close_birds = square_distances < alert_distance +close_birds + +# %% [markdown] +# Find the direction distances **only** to those birds which are too close: + +# %% +separations_if_close = np.copy(separations) +far_away = np.logical_not(close_birds) + +# %% [markdown] +# Set `x` and `y` values in `separations_if_close` to zero if they are far away: + +# %% +separations_if_close[0, :, :][far_away] = 0 +separations_if_close[1, :, :][far_away] = 0 +separations_if_close + +# %% [markdown] +# And fly away from them: + +# %% +np.sum(separations_if_close, 2) + +# %% +velocities = velocities + np.sum(separations_if_close, 2) + + +# %% [markdown] +# Now we can update our animation: + +# %% +def update_boids(positions, velocities): + move_to_middle_strength = 0.01 + middle = np.mean(positions, 1) + direction_to_middle = positions - middle[:, np.newaxis] + velocities -= direction_to_middle * move_to_middle_strength + + separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis] + squared_displacements = separations * separations + square_distances = np.sum(squared_displacements, 0) + alert_distance = 100 + far_away = square_distances > alert_distance + separations_if_close = np.copy(separations) + separations_if_close[0, :, :][far_away] = 0 + separations_if_close[1, :, :][far_away] = 0 + velocities += np.sum(separations_if_close, 1) + + positions += velocities + + +# %% +def animate(frame): + update_boids(positions, velocities) + scatter.set_offsets(positions.transpose()) + + +anim = animation.FuncAnimation(figure, animate, + frames=50, interval=50) + +positions = new_flock(100, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(100, np.array([0, -20]), np.array([10, 20])) +HTML(anim.to_jshtml()) + + +# %% [markdown] +# ### Match speed with nearby birds + +# %% [markdown] +# This is pretty similar: + +# %% +def update_boids(positions, velocities): + move_to_middle_strength = 0.01 + middle = np.mean(positions, 1) + direction_to_middle = positions - middle[:, np.newaxis] + velocities -= direction_to_middle * move_to_middle_strength + + separations = positions[:, np.newaxis, :] - positions[:, :, np.newaxis] + squared_displacements = separations * separations + square_distances = np.sum(squared_displacements, 0) + alert_distance = 100 + far_away = square_distances > alert_distance + separations_if_close = np.copy(separations) + separations_if_close[0, :, :][far_away] = 0 + separations_if_close[1, :, :][far_away] = 0 + velocities += np.sum(separations_if_close, 1) + + velocity_differences = velocities[:, np.newaxis, :] - velocities[:, :, np.newaxis] + formation_flying_distance = 10000 + formation_flying_strength = 0.125 + very_far = square_distances > formation_flying_distance + velocity_differences_if_close = np.copy(velocity_differences) + velocity_differences_if_close[0, :, :][very_far] = 0 + velocity_differences_if_close[1, :, :][very_far] = 0 + velocities -= np.mean(velocity_differences_if_close, 1) * formation_flying_strength + + positions += velocities + + +# %% +def animate(frame): + update_boids(positions, velocities) + scatter.set_offsets(positions.transpose()) + + +anim = animation.FuncAnimation(figure, animate, + frames=200, interval=50) + + +positions = new_flock(100, np.array([100, 900]), np.array([200, 1100])) +velocities = new_flock(100, np.array([0, -20]), np.array([10, 20])) +HTML(anim.to_jshtml()) + +# %% [markdown] +# Hopefully the power of NumPy should be pretty clear now. This would be **enormously slower** and harder to understand using traditional lists. diff --git a/ch02data/110Capstone.html b/ch02data/110Capstone.html new file mode 100644 index 000000000..37ba3f5af --- /dev/null +++ b/ch02data/110Capstone.html @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Understanding the Exemplar + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Recap: Understanding the "Greengraph" Example

+
+
+
+
+
+
+

We now know enough to understand everything we did in the initial example chapter on the "Greengraph" (notebook). Go back to that part of the notes, and re-read the code.

+
+
+
+
+
+
+

Now, we can even write it up into a class, and save it as a module. Remember that it is generally a better idea to create files in an editor or integrated development environment (IDE) rather than through the notebook!

+
+
+
+
+
+
+

Classes for Greengraph

The original example was written as a collection of functions. Alternatively, we can rewrite it in an object-oriented style, using classes to group related functionality.

+
+
+
+
+
+
In [1]:
+
+
+
%%bash
+mkdir -p greengraph  # Create the folder for the module (on mac or linux)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%writefile greengraph/graph.py
+import numpy as np
+import geopy
+from matplotlib import pyplot as plt
+
+from .map import Map
+
+
+class Greengraph(object):
+    def __init__(self, start, end):
+        self.start = start
+        self.end = end
+        self.geocoder = geopy.geocoders.Nominatim(user_agent="comp0023")
+
+    def geolocate(self, place):
+        return self.geocoder.geocode(place, exactly_one=False)[0][1]
+
+    def location_sequence(self, start, end, steps):
+        lats = np.linspace(start[0], end[0], steps)
+        longs = np.linspace(start[1], end[1], steps)
+        return np.vstack([lats, longs]).transpose()
+
+    def green_between(self, steps):
+        """Count the amount of green space along a linear path between two locations."""
+        self.steps = steps
+
+        sequence = self.location_sequence(
+            start=self.geolocate(self.start),
+            end=self.geolocate(self.end),
+            steps=steps,
+        )
+        maps = [Map(*location) for location in sequence]
+        self.green_at_each_location = [current_map.count_green() for current_map in maps]
+
+        return self.green_at_each_location
+
+    def plot_green_between(self, steps):
+        """ount the amount of green space along a linear path between two locations"""
+        if not hasattr(self, 'green_at_each_location') or steps != self.steps:
+            green_between_locations = self.green_between(steps)
+        else:
+            green_between_locations = self.green_at_each_location
+        plt.plot(green_between_locations)
+        xticks_steps = 5 if steps > 10 else 1
+        plt.xticks(range(0, steps, xticks_steps))
+        plt.xlabel("Sequence step")
+        plt.ylabel(r"$N_{green}$")
+        plt.title(f"{self.start} -- {self.end}")
+
+
+
+
+
+
+
+
+
+
Writing greengraph/graph.py
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%writefile greengraph/map.py
+import math
+from io import BytesIO
+
+import numpy as np
+import imageio.v3 as iio
+import requests
+
+class Map(object):
+    def __init__(self, latitude, longitude, satellite=True, zoom=10,
+                 sensor=False):
+        base = "https://mt0.google.com/vt?"
+        x_coord, y_coord = self.deg2num(latitude, longitude, zoom)
+
+        params = dict(
+            x=x_coord,
+            y=y_coord,
+            z=zoom,
+        )
+        if satellite:
+            params['lyrs'] = 's'
+
+        self.image = requests.get(
+            base, params=params).content  # Fetch our PNG image data
+        content = BytesIO(self.image)
+        self.pixels = iio.imread(content) # Parse our PNG image as a numpy array
+
+    def deg2num(self, latitude, longitude, zoom):
+        """Convert latitude and longitude to XY tiles coordinates."""
+
+        lat_rad = math.radians(latitude)
+        n = 2.0 ** zoom
+        x_tiles_coord = int((longitude + 180.0) / 360.0 * n)
+        y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
+
+        return (x_tiles_coord, y_tiles_coord)
+
+    def green(self, threshold):
+        """Determine if each pixel in an image array is green."""
+
+        # RGB indices
+        red, green, blue = range(3)
+
+        # Use NumPy to build an element-by-element logical array
+        greener_than_red = self.pixels[:, :, green] > threshold * self.pixels[:, :, red]
+        greener_than_blue = self.pixels[:, :, green] > threshold * self.pixels[:, :, blue]
+        green = np.logical_and(greener_than_red, greener_than_blue)
+        return green
+
+    def count_green(self, threshold=1.1):
+        return np.sum(self.green(threshold))
+
+    def show_green(data, threshold=1.1):
+        green = self.green(threshold)
+        out = green[:, :, np.newaxis] * array([0, 1, 0])[np.newaxis, np.newaxis, :]
+        buffer = BytesIO()
+        result = iio.imwrite(buffer, out, extension='.png')
+        return buffer.getvalue()
+
+
+
+
+
+
+
+
+
+
Writing greengraph/map.py
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%writefile greengraph/__init__.py
+from .graph import Greengraph
+
+
+
+
+
+
+
+
+
+
Writing greengraph/__init__.py
+
+
+
+
+
+
+
+
+
+

Invoking our code and making a plot

+
+
+
+
+
+
In [5]:
+
+
+
from matplotlib import pyplot as plt
+from greengraph import Greengraph
+%matplotlib inline
+
+mygraph = Greengraph('New York', 'Chicago')
+data = mygraph.green_between(20)
+mygraph.plot_green_between(20)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/110Capstone.ipynb b/ch02data/110Capstone.ipynb new file mode 100644 index 000000000..48cd94ca1 --- /dev/null +++ b/ch02data/110Capstone.ipynb @@ -0,0 +1,220 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dc768877", + "metadata": {}, + "source": [ + "## Recap: Understanding the \"Greengraph\" Example" + ] + }, + { + "cell_type": "markdown", + "id": "973f2954", + "metadata": {}, + "source": [ + "We now know enough to understand everything we did in [the initial example chapter on the \"Greengraph\"](../ch01python/010exemplar.html) ([notebook](../ch01python/010exemplar.ipynb)). Go back to that part of the notes, and re-read the code. " + ] + }, + { + "cell_type": "markdown", + "id": "7c68d23e", + "metadata": {}, + "source": [ + "Now, we can even write it up into a class, and save it as a module. Remember that it is generally a better idea to create files in an editor or integrated development environment (IDE) rather than through the notebook!" + ] + }, + { + "cell_type": "markdown", + "id": "eaf8e9c3", + "metadata": {}, + "source": [ + "### Classes for Greengraph\n", + "\n", + "The original example was written as a collection of functions. Alternatively, we can rewrite it in an object-oriented style, using classes to group related functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8db2aa4c", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p greengraph # Create the folder for the module (on mac or linux)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dd56015", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greengraph/graph.py\n", + "import numpy as np\n", + "import geopy\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from .map import Map\n", + "\n", + "\n", + "class Greengraph(object):\n", + " def __init__(self, start, end):\n", + " self.start = start\n", + " self.end = end\n", + " self.geocoder = geopy.geocoders.Nominatim(user_agent=\"comp0023\")\n", + "\n", + " def geolocate(self, place):\n", + " return self.geocoder.geocode(place, exactly_one=False)[0][1]\n", + "\n", + " def location_sequence(self, start, end, steps):\n", + " lats = np.linspace(start[0], end[0], steps)\n", + " longs = np.linspace(start[1], end[1], steps)\n", + " return np.vstack([lats, longs]).transpose()\n", + "\n", + " def green_between(self, steps):\n", + " \"\"\"Count the amount of green space along a linear path between two locations.\"\"\"\n", + " self.steps = steps\n", + "\n", + " sequence = self.location_sequence(\n", + " start=self.geolocate(self.start),\n", + " end=self.geolocate(self.end),\n", + " steps=steps,\n", + " )\n", + " maps = [Map(*location) for location in sequence]\n", + " self.green_at_each_location = [current_map.count_green() for current_map in maps]\n", + "\n", + " return self.green_at_each_location\n", + "\n", + " def plot_green_between(self, steps):\n", + " \"\"\"ount the amount of green space along a linear path between two locations\"\"\"\n", + " if not hasattr(self, 'green_at_each_location') or steps != self.steps:\n", + " green_between_locations = self.green_between(steps)\n", + " else:\n", + " green_between_locations = self.green_at_each_location\n", + " plt.plot(green_between_locations)\n", + " xticks_steps = 5 if steps > 10 else 1\n", + " plt.xticks(range(0, steps, xticks_steps))\n", + " plt.xlabel(\"Sequence step\")\n", + " plt.ylabel(r\"$N_{green}$\")\n", + " plt.title(f\"{self.start} -- {self.end}\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae5c6608", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greengraph/map.py\n", + "import math\n", + "from io import BytesIO\n", + "\n", + "import numpy as np\n", + "import imageio.v3 as iio\n", + "import requests\n", + "\n", + "class Map(object):\n", + " def __init__(self, latitude, longitude, satellite=True, zoom=10,\n", + " sensor=False):\n", + " base = \"https://mt0.google.com/vt?\"\n", + " x_coord, y_coord = self.deg2num(latitude, longitude, zoom)\n", + "\n", + " params = dict(\n", + " x=x_coord,\n", + " y=y_coord,\n", + " z=zoom,\n", + " )\n", + " if satellite:\n", + " params['lyrs'] = 's'\n", + "\n", + " self.image = requests.get(\n", + " base, params=params).content # Fetch our PNG image data\n", + " content = BytesIO(self.image)\n", + " self.pixels = iio.imread(content) # Parse our PNG image as a numpy array\n", + "\n", + " def deg2num(self, latitude, longitude, zoom):\n", + " \"\"\"Convert latitude and longitude to XY tiles coordinates.\"\"\"\n", + "\n", + " lat_rad = math.radians(latitude)\n", + " n = 2.0 ** zoom\n", + " x_tiles_coord = int((longitude + 180.0) / 360.0 * n)\n", + " y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)\n", + "\n", + " return (x_tiles_coord, y_tiles_coord)\n", + "\n", + " def green(self, threshold):\n", + " \"\"\"Determine if each pixel in an image array is green.\"\"\"\n", + "\n", + " # RGB indices\n", + " red, green, blue = range(3)\n", + "\n", + " # Use NumPy to build an element-by-element logical array\n", + " greener_than_red = self.pixels[:, :, green] > threshold * self.pixels[:, :, red]\n", + " greener_than_blue = self.pixels[:, :, green] > threshold * self.pixels[:, :, blue]\n", + " green = np.logical_and(greener_than_red, greener_than_blue)\n", + " return green\n", + "\n", + " def count_green(self, threshold=1.1):\n", + " return np.sum(self.green(threshold))\n", + "\n", + " def show_green(data, threshold=1.1):\n", + " green = self.green(threshold)\n", + " out = green[:, :, np.newaxis] * array([0, 1, 0])[np.newaxis, np.newaxis, :]\n", + " buffer = BytesIO()\n", + " result = iio.imwrite(buffer, out, extension='.png')\n", + " return buffer.getvalue()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08db5e93", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greengraph/__init__.py\n", + "from .graph import Greengraph" + ] + }, + { + "cell_type": "markdown", + "id": "460940bf", + "metadata": {}, + "source": [ + "### Invoking our code and making a plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9100ce8c", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "from greengraph import Greengraph\n", + "%matplotlib inline\n", + "\n", + "mygraph = Greengraph('New York', 'Chicago')\n", + "data = mygraph.green_between(20)\n", + "mygraph.plot_green_between(20)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Understanding the Exemplar" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch02data/110Capstone.ipynb.py b/ch02data/110Capstone.ipynb.py new file mode 100644 index 000000000..b520c6a33 --- /dev/null +++ b/ch02data/110Capstone.ipynb.py @@ -0,0 +1,158 @@ +# --- +# jupyter: +# jekyll: +# display_name: Understanding the Exemplar +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Recap: Understanding the "Greengraph" Example + +# %% [markdown] +# We now know enough to understand everything we did in [the initial example chapter on the "Greengraph"](../ch01python/010exemplar.html) ([notebook](../ch01python/010exemplar.ipynb)). Go back to that part of the notes, and re-read the code. + +# %% [markdown] +# Now, we can even write it up into a class, and save it as a module. Remember that it is generally a better idea to create files in an editor or integrated development environment (IDE) rather than through the notebook! + +# %% [markdown] +# ### Classes for Greengraph +# +# The original example was written as a collection of functions. Alternatively, we can rewrite it in an object-oriented style, using classes to group related functionality. + +# %% language="bash" +# mkdir -p greengraph # Create the folder for the module (on mac or linux) + +# %% +# %%writefile greengraph/graph.py +import numpy as np +import geopy +from matplotlib import pyplot as plt + +from .map import Map + + +class Greengraph(object): + def __init__(self, start, end): + self.start = start + self.end = end + self.geocoder = geopy.geocoders.Nominatim(user_agent="comp0023") + + def geolocate(self, place): + return self.geocoder.geocode(place, exactly_one=False)[0][1] + + def location_sequence(self, start, end, steps): + lats = np.linspace(start[0], end[0], steps) + longs = np.linspace(start[1], end[1], steps) + return np.vstack([lats, longs]).transpose() + + def green_between(self, steps): + """Count the amount of green space along a linear path between two locations.""" + self.steps = steps + + sequence = self.location_sequence( + start=self.geolocate(self.start), + end=self.geolocate(self.end), + steps=steps, + ) + maps = [Map(*location) for location in sequence] + self.green_at_each_location = [current_map.count_green() for current_map in maps] + + return self.green_at_each_location + + def plot_green_between(self, steps): + """ount the amount of green space along a linear path between two locations""" + if not hasattr(self, 'green_at_each_location') or steps != self.steps: + green_between_locations = self.green_between(steps) + else: + green_between_locations = self.green_at_each_location + plt.plot(green_between_locations) + xticks_steps = 5 if steps > 10 else 1 + plt.xticks(range(0, steps, xticks_steps)) + plt.xlabel("Sequence step") + plt.ylabel(r"$N_{green}$") + plt.title(f"{self.start} -- {self.end}") + + + +# %% +# %%writefile greengraph/map.py +import math +from io import BytesIO + +import numpy as np +import imageio.v3 as iio +import requests + +class Map(object): + def __init__(self, latitude, longitude, satellite=True, zoom=10, + sensor=False): + base = "https://mt0.google.com/vt?" + x_coord, y_coord = self.deg2num(latitude, longitude, zoom) + + params = dict( + x=x_coord, + y=y_coord, + z=zoom, + ) + if satellite: + params['lyrs'] = 's' + + self.image = requests.get( + base, params=params).content # Fetch our PNG image data + content = BytesIO(self.image) + self.pixels = iio.imread(content) # Parse our PNG image as a numpy array + + def deg2num(self, latitude, longitude, zoom): + """Convert latitude and longitude to XY tiles coordinates.""" + + lat_rad = math.radians(latitude) + n = 2.0 ** zoom + x_tiles_coord = int((longitude + 180.0) / 360.0 * n) + y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + + return (x_tiles_coord, y_tiles_coord) + + def green(self, threshold): + """Determine if each pixel in an image array is green.""" + + # RGB indices + red, green, blue = range(3) + + # Use NumPy to build an element-by-element logical array + greener_than_red = self.pixels[:, :, green] > threshold * self.pixels[:, :, red] + greener_than_blue = self.pixels[:, :, green] > threshold * self.pixels[:, :, blue] + green = np.logical_and(greener_than_red, greener_than_blue) + return green + + def count_green(self, threshold=1.1): + return np.sum(self.green(threshold)) + + def show_green(data, threshold=1.1): + green = self.green(threshold) + out = green[:, :, np.newaxis] * array([0, 1, 0])[np.newaxis, np.newaxis, :] + buffer = BytesIO() + result = iio.imwrite(buffer, out, extension='.png') + return buffer.getvalue() + + +# %% +# %%writefile greengraph/__init__.py +from .graph import Greengraph + +# %% [markdown] +# ### Invoking our code and making a plot + +# %% +from matplotlib import pyplot as plt +from greengraph import Greengraph +# %matplotlib inline + +mygraph = Greengraph('New York', 'Chicago') +data = mygraph.green_between(20) +mygraph.plot_green_between(20) diff --git a/ch02data/boids_1.mp4 b/ch02data/boids_1.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..b301d2e90b91642cb08a63f37af54828b0889b9c GIT binary patch literal 69263 zcmY(p19W9guqb@uWMbR4?POwG6Whs&ZQB#u#>BR5Yhq38ynOe*_pkM@bD z#Mf8Frr2rM(VApSD%~p48tK)wI|~OJ5j~NyJu{XBn zXXatzVInfLGqmw=GUaD-XXRmXXJ%$4vNh$mFm)$#axwabIEfsbJibN0pZY)(er5)y zZ=vr8k*%e>sfqr78kxT(^nr$U=BE7297M(zKzmz5{cllbB4?nfjg6(#H^k-6Wn%37 z4H!Gx@-ux8!O+Ch-p-Vtg_(|-naIr0$ywjQ$=cH4KaT%Z;OL-lZ)WCX>da5iLgZ`# z{FZRy=OD7Nx3@O5_-^R`KO8fWlZ~bE_gMZPf{Dlu_&XP}{_-M5NwMxfz;JZ3;cTT`d+$uiP+@c4!;P54>9 zVM7x`hyScF(l@d+bo#FoOQ7lh*yUzwX>Q?c^i8vOFtyV+w|Dra{x8zuTh!Xr<6AdB z3p>;Q1@&z$?f6-kh@6Z~?M#hbocY<9{u?IH@V_YqnmSp0ivx}I|37p8qXUikje%xF zwnpFc`kz_f4Sp6@1|}lM|BT^hV)%|HM~D9u|Igddou7mI8{y<^>cG!NWa;qjr0;a$5obfc@WJ ze6Zt{b;=mO*|!v<0ZgWGNhK!yk#Cw zNxP}dZGz6L5YQtN;|1T*zOC3~CovK8Z#lVqt9;NWjRXQwl;Bv%-%k?dDE7< zIlt{2W6peabl69zLM7=f6}vr|{s(s+t+YuXpvOCb*WMpP?82OyYXnz`wdzTocv`3> z=dT!M2Y^<|Fv|7WHF=kEz>EbTmT!0Q7qO|#Q>o7WL{W1cIv+saB^qT z?B7YlfXK4wV%uMYrVS^tuhfxp;sx8#fX*R~Y29Edw#MPbSF*qHwp_iGu!$p)5sRrH zQy`2Ia3mL6D|j=aTRINmK5y2m15Up)%{JkOtv^+S&qg56S1I}{%Bvwx71%9I$n1~H z+8{LEC4}=gXk$Tr1V!J>523Hkt!)KfU)5JL8z1{(Shos7@D|CWuKGb;LzyXSp_VR+ zOdo~7AA}{svRuaVjkbuc{>KN3V{?3gYsi`tcH`5GvNrG}UF4fK{(KIT81{qlSWW3F zICG88xN+QfnO5iX5Y-B=eKUknFb`6<$~}vBg0=_0$UbnM(=Yu-ePA@ z&s+=tUM6<2@fGn%+T(_Q{dXT&1_Qt?{l^=fOwLw+6fshR+zPGspj@$e!B$Z6=O3xs zg+KZG*XdjCYf7>s3>`BzWRotQZ18+Ld6Gt5j{%nr;%Kd`so}Oq!0)rqx(~?MHA4wZ z>&oOvK%jK~fCfg|aL5G`-?=~h&;_A({C3j1v3vTAh@M*OK)wwIVp=pocCgQ)T%>6{ zii;!_GKrDwmo0HG+Jpqn({QoalY|&Utvi9|Y=I-o2N4NRas^>-7Rw(?8XOzV1nU^a zEOW+VZPx1(ag_M%<*${5&*tTx4zKT!1p`HCuW7+3qc7JvK0g3t1yb!x%=Cau@x5&j zd5+a#Q&sBmOBFl4C7;>ahglDSN`=l7yQQcK*l{sE1Yzv8$sm%+w?oOQLZ&wCo4 zd4~CQA>B6_c{PH9MG=}gpR2XwdVyM4AdKH}W_~lGa3w45*%wQFnaDJ6a%3|D2*vhT z9!u_%TKCz>Km`*50J>~vN(VG;irv9$vcP^@;WZgq$m-P)6iIlACeY`1lx8b|J_bsI zb%m|+H2v{ERbM-Kd;VA`7)-@pK$RG5cOE@{J{5I~BdwtbEUi*} zE`x`wcZ(8xCM$N0e~ayS-s!Qb(#9xT%+75kmhwrgaCnH1j;-2;SF#E-=v)q8NI5{| zUqi=)e<+xCo$X(+w}3y^ zp>JzejXeDOHQu<2cRf{+M;i!%HxL{5lR}almIwf-yyF?Wh=1U3TjHfPp5Adan(uDN z8o1MhQaazC>0Sq)A71B;d*2WYMsC!Ler0D|4cl3MB^k{9fdzVJ~q6{0->G zG&7_mv7(?A`byZs+3{_PIS2);c>P`Ycky<6ifVvstw$}nQX7^1cjfwqLH>Z~A*oT9w<$Fe(mAsIi_aVblG`HIO!biYi{r73$Vqv6gsv06Qxp|~Lka|s{v^4Tv3 zK{AHpH@LqfJASs*IS4{8BzWI{LF@=k^LkH1l-rsA!`{^zd(2c0YFDZ6|Ac#`Z+Gx4 z?$B&jQNIA$illDx)7+oH8Np4~d*^S;>jrI&t4WR&QX=O)K(hHUM^9hjrt3u}QlRYn zQQ|TUsx%ZZdcLlK)FCWoD+FRsVz5ej22 zl#9VD%4>cI6jZ{*EzWFeE)-b%@hKTsOP?ceCy z*sRYSKOQAhflYO_s9)r7t}a28UsZf?dtD2TauN)5ZV}_!aW4v_vt%5@=~*rtPQq)O z2s0F*iB?b=w;>YZz*R8o_E04qnH}8=gbB8IQ@F=U0(!mObrS}z3u>OeWUU^sTUQwL8eVo>I((DM!g|}Y9B)~TV z0q$@o&@U%mAq*7E>tAa==x-gjQyndg{!hI)bje@QK5|V_Z`cWoA!+NC$hxF+JEq@6<-7l>~QOj963tTL5ILyKA?sr*n#U*N$hS2nzZL21#40 zngq`k6<-P`PB$7fVd5Ltj}U4xGwR14s5b%fBmTjiE#S-KC8CW)6s-asY%_wCjB`hD z$lH~dm#+jSD<*E$(c)!XRCGmXA!^r^XUcu^bZEaGJ1R#nkpc*A__-(}3Vk@zdT3rm z(gU8{QHA&&OXqx!dw>fXeAiO9dmW1S(Q#!Zt2Ub12)f1|w)bDD(|ee33_uln|1|5q z$g~D@xm#a<9CE9=ht0_Bb2;n8wvz}l=&THP@)D1gnRcP6hFcWy^R}VZtkC>O?HFU zVJa~4p3CpFBxK&fcbI-e$$OKbI#smqw-S;<*DInBMR6 z+hU~3XJkL3*czqPun)VF2z1hZ&7v+!>JyTPq2T_b;r2`xpdCcuWNz&o4glSSj|0m0 z0wCX|g)CrGm_}09;pD90xG)A`9A_J!d00kvQUCSDs45Y_867_IM@<| zD&oKsnh&%h9uw~$1c19Y?9y0|s(JZ~d+&?OEpS`H7R*8&+~zg}H+rf3^{D6+A<1VK zR3#HUx4=fCaijcMxlxX|URM&qlBVaTIGEW|mMs9Sun=bfwz6^rj z8F0L2`cL}o*08W!-|K@PTKye{5>JG2o;ll%v znCn1`8(4<()L$M5gff}xhD3!TAxMts!dC>72|YFxXN!LsK_7Mhy!RoNkPQ!Kune(= zx$zfE_GGW7QIzqw{P_sV|GLlafOX|`GV3aai^=Vdqv}8jRX=YWBjH+7wRq?4YfHX+)-E zZvQ@j+Zl^(H(w~v4PHBPZ)%ilIJ*`!WL+!J-}6Bt2PHB<-`7YSiwt#fFQyzQRs@kRT4mU}lV2mVs;@yzXq$la_g-s?A6CVKg^|LiuCya_6>7AcoV^z#N6if93aRkLBnnxE&mnCX^tDvq|-B0 ze6xXIhioQ|p6HpP&kNI*`+VkVwi|uLEhom5Pj%C7wOGStoFlv-0$FOV_^+6|r2yP5 ze9uYbV|)V~_(9j}7u(tJyx`o_sLzYLK~LX)D_^^5m2_SfD7=a9X7%=ohi_4b;F{mx zfbQnnWtGG?qh2YHW@!l&!BW+y%?*ArlkIpJSPNa&N=CUWTS8Z_vC6-chli+#kwe?; zqMQU?mj+ykD=;{0w2R-F~X$IfrKNpB_4d3uv8o3C9DW zh(!bM!A>mF|8bqba_~7=#pjlqU~=D(=2XR&gGIYY?t({4o<)+YOa$uUnmtL7YfUC5 zF*_pu9>G37M-gTVhvQ5edvnxC2z?K&uHP_fGa!z6!zBf~s7p}5GrmJwQnxN0^S|Hq zA#53SMU=MpT}PXlAi-Z%_ZwT0CuKn&h-T-B<3{ainDF;z+c$p#_0!iIqB!Z>d2Ht! zM?ydNde!nv8`Micbg7C*#T`}XNXgv$)nx$xwt`FRc&|NDI!l{fcpb-B!Yc6-4UiWY z>JmpXAJg48V~w-6<5PTqqN@nUWHBk%LFj$^Ai~CB)nB&0yyl>#B$__9$8I+<@DHxolF<)h{~i{Cfz$| zcYhI-9vZ$4Y1?{-dYnjA1PL)pp6x{a*#O5g&?{7k_!HW!2mzihzre0Z{HgM7-fpG) z-tCHysTGnn*CV&358|lMvsTW0#bBNP3i0x!aO&(Dl!0knVtWDg3%}lx9Td+-Mkez5 z#;3tmu&>@Cz{Hi$`|7Bny4nUPB|&sF6b|{WNastP+}#JW31u5 z?0`!XE;^QAG*IuG^W3`OZZJmZU&kHmq@A<#Mwp34lX@h1&7LQR%6QU^HGtcAr^GGlY%&M29`Qfs9B@x~+nQ?$1$8YwY9Q3fdRF;DPLTL3aq3 zmMK->tcP(^EF~t?5@Yb1LMe06$^t0p#N}o8H3GQt`Xclqyfh*VjPpSgOgZVZXf}mV zI?!bHhvia!JYd{?yK`Npj8lw0agrVr2r5eT=|TIb<;`dPi^YV4s+5k(pD-qeh%c! z7)<(7u2c<4n{fkrOU;Q?70Q0`3NGR0BLbI}9JiobgYl_^8GLY<)TI)psV6~^ zt?{hwsut{hu6nWX9tOJwR4~ZB=>%X%t+qPef(}CS8@naPJ9F`jvE&d%3ZZdKd$iGO z=JC`xO{6b`v#w;@-Ibch`Q*O3!EJ^x--jhdDtZcUIIz;IzK&VLz8EGCPpXOGw^R*~ z1&4|bUYOVfK>Yf4T%MqAJB03NMpw(Bo>B!@4O)JC{8{yH5P+$a?H=12WXmp``j_=| z5y2k>6952L7tSEH5%v2l4F-UX@23FL3Sa5vhiv=VvhE@%J(J?%DdpJulA8rtn(cOc z!wJ(ZwjO6c5+)d^;stkS&9_}L`vi&(6o>b%$>X6n>ysR7&4Q%nZ3Q`}1LAlAFq|BJ zB3jbyd2#zqu#0V1la%;N2Yw)|22+r^dj$LIMOEX;j9VL0mzIUL(jLYe7Oy<4{}xt{~!6aB1Hhco$-0{L1)0xs$a|+xa;gjI2(okvLCgFEi$N8 zL6^B6Rhiejm{a3ipQr+CEFRa!^4sYRExRR(3J`AoZ)1lMa*92La(hHft>P7FJwdyT z=IlVTc;e+TLzRb>-68kNN&8Ple{cDShtp8I^ur@XwIPgs9R(bl!j&r_()hwZR97s~ zxD&c$rjwwT!xV$Yr`xVI!KNLf%0*ot!nQ-54so479$rR*jhG}0* zE`k2g?qlZSeI)NlRyS1a%UnSws#91f2~ zN+_AJcPF{H>F>-j8QK{mDB`cJ52D7Tc6_J=c$r4EiH=p4BWQOquX_p;P&c9{_A0P#xbETz)qkSvwEdGi z_1R=>ten~!zQ3^?Y7M?0P3-b#p#jK;{I`{wrvtTgRegaIou{(KzlcqS%?nEx9!hdW zFKA(gXaCR(mJA^BtEVVHduGlI9J=WT&x4MJVn=3}&--@xeJNnSk5kXM7PIK@rC{Q? z4zH~m+b9&?e8QsPz)cE_n)*uZ2A{it*~`~3dk7q`#c`X&7t&{OiR#?heWkapk-Q&b zkmvm7VF5PbM>6G{b1rr0YgTC5Pu%@whJ!v z;6bH856;Lmqrz6-ZCY>%_!@~>-<8Mec8)^q9L!LxqzTZ3IRTElKmg;L@GM6LvTTi% z(@na;E3<}{x9Teb55a@7zy*aZz8tFg8cU$rr1PWr4>2|f4txR7499mCvkS94Yc%mN z5*0;C)~PS*=(Vqhjp4W=htzi+_3F;EI&|ux%DDY!WrI`kM$eX2!3p=8PV^anu5*7d zPd}Y9YIwU3ur{t-9_RF-+rB^58|Dd*ClN?`f6aliB(i@sH~Eh_PJHAW?=%V4V&QA+ zY;Q}Olgl4>xhh4}>$wHIAQP|C=xv9_nkQ~MuE;>r)F~H}1sxs1Xc*j`m?lwN;^!={ z`TZ)2TPxeZDmse$lecX@b=LkX+|Cf`<*h(3hBf;R0F`(NKibZoU_+4i;bv7w!JW6K zMWk_fBz_~4%{X$fY`+T{IO*)Cg3V~;qH_V0uysS?r?9fwPJx7@<*{L$JZsBCjh^~%b2+=T~?l1 zhkKtS=|zVqrFw6z|CXsgYo2W9@bzc<-7r08q07v=mpHRpR?BLbP+u5x=JWjSfZlax zI@64SjpO8V@8@+1!;5>3_%C>Z2@@?vczH$9PR&wze@z4m`_sC(mP~mmZY}DO$M*pM z*5??~L|pvtFPmX;(rPD8VU`oZx+~z6_-L;i_59E4nVKrQbeGy;cZaj$ zi@l22j0v35I@1Xmv-|F^M2ri9oJ9D6%Yl0C%+DpAr7Og5mT^h6#;E z*79e#J~^f1>1uV8-4;y^tI^6i!d~9Cwn_u#EVXb| zxSB0{R&%n$fRA8de-gIcP~5-=*-XrvIDI~(?67NT_}|{JR#Zd2dk4$QO&tjJ%?I*_ z+A_2P)Gw)wNtZZ+)crBH-2JV~|Ip%Y_);eto_#E?aW^+L@)Zyf`~o=H|3ro7M`jFj z=_eFlGV!c8DqUvK1iA6je#Z+2NRE+}NY8iW1H{*v<)eS)Ee(J$rlx**N>cg<30>uI z9+4MMoyjQg(BCKzTEz{N2!{=&7V@687rz__0Zs4gQ`}CBDEXzw`K{#azn zQWuqC?40P6SUsAAJJu0sW~Qrn1h~9Dgx8xDTt+MEsg2offZzh?&U55vj{i{Ln?tCa z2e}ntaMT6r^tvyf^>O@iu*f65o}pY64QM+=pS*#gkqc2Xl};!$>xWt6X#oWn;r%3o zd{3l1j~om{FLuLJzND z$GljZAEyRT2ev<8MHU0NwVzwDcUSCQQ#R@1FC}0Gz!|gWlXM|nZ<&JX1pOha=Z+PM z$M$DL21$blg}E+xL`B!#UYURuK4{m?AcrgT^m{_I=orqrSwtK$UD75?;3`k|nEyKUV1MwG@d+IHoYImY*qE4iJ|nk02Hrn(p9-8oz0ua>K0et@c|*xO2(=jB@enVZVIox~Y>_St?e_Iq zp8AafoFZMoQTQXCLg{OzI}hI1w6HY3uUZFl-y-7MUiIyuc3x98VywhKhSNxnn5isO zDY43U_(HcCH%lIJyks{CC`=ei??;~fX#If(duAHtn?v8Wp|{xBU1U|sxvj0j4EIom z3ftX>a8E$c{N7*AUpfflzvrE02ehwY>^hea_O0z+vDgJP{^s3?;(A=QgL&?*@}`6*eKopVd0%pLgtn|CC2d=A7UfR>c8TkIqhr5%Dp+ zA`sqxEm@PzmR-`&7lp#KboPcrF*0va@^2It#!upfPR;bq%6?vbWpOoN{3`|dpe}a- z)cP6G*;l_4fx*OIukvOBmI+`mMq7a-Tl0h2M?r@@kYRH ze#k0$@}`BoT{w@7Nswpt*%ieRx1_eYYW;(M%U@d9eGj zBRS{9riw{sfeG>JIOW9aBy}ktE}`_HIA{By6shFltdA2CBr~ifxUt3Y2$cdhQ5T_w zym&o)Jg)f4NS&X7;YyFa(MI6$Wl-j=gWB+XG@!~8B~YbZ{%92YHQ{77bSTWn zj;*^^rg}j-0v#m$mvOJ)l;Z11dn|pvA~UIpdU^53=P-(@83)CQGGVN+UL1SUIHC7^ zYEl0HI-a&Je85|Hf$Yf*Nsa0w04(4IR+q^0gLW@IHGhT-spHfYJ%GjHSBS2Pm!lI{ z#lD%>yX>0P7=>SxuHq8_03j-z5hk_wL)nyxxaYsa!}qGaxzYos7!JO&ZBDc9#}ink z(Euz?((>J3G0C=fFC9N36rv>D{4+@c0gq4f56co3VdRZxOSr8U9M3=5!QTD^K@zR| zjAQiCxK@a*yV>40BtS^;kHLw@m0Solfs)GTr)pE0*zIfDjzR+%Wvi+yt&{|;QS5L5 zm&i+Pvr(L0q(MVht9+nlvJH9;vMZFTCdE4%an0zf0p?`COdWptn7luFa_t5y8;6Roc{f#5iN^gK$n9_( zi`B9WF=x$r&-rosfMJjWnOqvh=nJ18nr~^>MZj_Ors8rbeq(LN`l0DfG*J+q6OrjY21^8UAG= z5e~r=&R|n*hW51}z;?*qeq(Id$EWJ4FJN8ss?%A$bTE<=ypP2@l;b6Tk%Q_ByJsR= zrs+GTpT7~t)hIAdQ!9FzA z!#YQ4P{EClwi2#FOYFKd65!23zz^4{ecT)z`^#maOq_>|k8{F-!-_kN4o&%RZz>iJ zx#u*H7`5X1^841vyR!7GAv9OBh-^cJ_!NoVwyLnkryz1i*PWy|DY!pPWmjzK1eOlm zkb7Uy*(Oo?vtxd7oMU*DTO!QIq?qb!Td?5iZyt3oV z)nQiko#ugDBVlp^h0{z+;6z-`&Sp@BWUfDhVzDzz_pp>F_z>)&bDn;-jQ|Pnbc`~2 z_j4O4vm-C}j0o4SC`QBN26y-@D<8nil{dg}w&dMcOUe*!xz_3;d zka=Nl;)TnQ`PgsD|~tQiQ+~N+{^|q z&VNu+Zv9<87N_=sJ=mwV&@5>k5WqqRm3ZfQ2{C^Ka$W84u$eqf3A#Re4Fw@>mI$!u z7Jh0JZ>;Ol2LpW4Ti#NDh2jp4Tiu*iEN9FZySyRHcSjyDc5=qoq-I4D%1>9Pz0xI% zz*iZHcQ9a-kdye8IKhNg41hl4ptKo|JyJ)_=q!Rwkh9fFon|UsQ^AV3R^trNzpE;r zif?*qgCvI}9Jhgm6p1z-f{LhhyF-G1*zi&b#2>a$?_Wu0<&M|ddEVNk zYyczkeJG$1EpR$>#%4e|QN`P8i^XU2`f`V;~`F-F4L(5Y2KM1sUxxR271;V2Q!7*Z$$($@aRJM^G=YI3+ra4Mxt z<4Pl3yLiQgD)XN_(l1HK7Sm*>nYVUkiw+&nrl$@9HKvegTPeAP{_PfX@wk*m2uF4S zC^9cQQb~1f2NA<=oj~XwW~i+Phz#D$8&Qrdj>;KTMYUp**r>Lrf*e9}bhP>UM--cg zs68U{x+=n43@W)-iSxbhMN>pLr&x?lmhtPq1r&c;@celeD!%VN&=;%M<$=Yg2fI-{1#`U?}eux%3D-cqd^ zRdZX%bgW>hwGao}iuCGU>$ArKZzD&2W5Aqf-z#q$45Ld3*U-hAxnc72-AOPk!4zt+ zQcvB$SmolI){J{;{*)kW!urW}zHa4m` zdyj!i_-QxOZidQ7l5D3}r)FtDwAeSnqAvrQ63098#;X5f)mKH|qGN$txvni9)$=eJ z{KqbkfJr9Fuho5(RFgk%wmEY#$ts(^ucw|f4SmG|tB7L_(~ERGDShD$d=g8yDLB}n zq3D5HHC_w($Ba|)h?&lSe(s`UcDMWA88Y?{JMv>r_~1B~xqAqeqQ(1y#ggdI(?G2# zgM`{4H?4fKyl12ae8D~WYno??z`%2B@T^S}q3)@QFas|WBb?kxH*JdvQGjh9r)TrM zbhy{R$wU|GD zwkgnik@|{24WhwvjN3Sq&_5{ge!q(0Dmx~+N{ezi;4CXn@ux#@j4XkZXEH>vxIp}Y zTfq~>Z*0?pX!W6>?Vl-@9XcYMD+&#LukBSUSOv&IY`l7m%*_KhDKe8tstk+5QDIJ`GrstM1%qa?>^bc+gAZsylfw zVCmLv+-Gp4hP{j|G)~7SVkN!BbZn9V5^9Sf7oHr0_oz3cI?{DLP~=pVf*< zO+yKSY1E3DJ1ykK7~G&h;5BKw$7Z&<`Qksra51gan+XSQRFCHeN1M+@KJ66W3`Jph zo*Ot=s_*AQANwZ`;8@zcqknQRFfxO)js8x2Y5wF25o1qFZb=)cbGngAa0(?gd1%SG zH(f~#&a5UL%>|1Kk9-DC7g~EiF{ep11qfh9Q7NR)iw!qkw+%C3c>~^z_AOyc5Fxuw z)t>n^1eX=i>|A?Kgwr>SWos)p>4w31PB;s|)7JK)roGln%%-3I_3)K0Nw7zHK-s+d zk628Z2P^ASL?Df+h+p;54Fa{iPqNnfr`5=EZn4YlyoCa zcia_FUDyr{Tq% zFRi946SdHprN7MYod_(e6vaEQrH_My+fb3HgN+fU^ayQ>T3yt+U;daqD$k7$O1+h> zu0qa-Gt&~rItNnY(LP1~mMi`}%O%B|a#+89Ch9(?eAhMe zm7;YH5pdY97ZzNcHFFHEC}5=F66!Tv!lvQcSNa`#_G{B^_U>I3z8{n{(@!%bkpBn6 zy9``~>?g`VcrQQnQ7FTgDi5@CpOR?J zv|kd?-ZR!m^#tI;C823_755;QLGjET6R6h|e&GGy-gG311h#hYfs5q{!_WM6w+M5~kWDcIKRuI! z3SwhZ;i4E-x0Gl18(E--b`;;tvE_d+cvH;soFCK05Wm6q6t-fHD!dlHUc^hRtC6HW zC0SA9CrYb|2G};pz~&II2On>oGwSq|ddUm4o#Afj@4NZuBM@r(JS+C(^e!>6?6CYc zT;IeQWjPjGpfEe6`-JZpJ83#XI^XByoEisX<^am4b!HILdZwhg@zVs|;tpK%1|e~W zB={3@)P(8hZfWwVt5g<^?jNdBUNW!q&d_awyf@n5bqn0cp_|mRSLliiJ_e(T>@LL< z$yb#=zf3X(Azo(z%0Z)Q361sCC7kDMbS42lxBjgkCuWh!2Ge{@V8eX;J-3Sm3( z4{{-4MWYD6Uwe{K?jvvVa~hidK4%;ns!&{|N8wJvRkT%K&V)Pk518uC<~N6FpN~_D zmZ5puHcpaUrzJvZ+|gt&-b2SZ(VIK+yCGx#k2%>&EgYcMvVgi06)x0oyc+L$Q7>X@%G|Dp6tfRX70*Wr3DXvQH`}4vb-m42kK2(!MT5;NLd5)jC zEc>4h)^5W~=M%dW>@0F3SSOzVmS`ZkwfJnj;5A;n_xNBKzEe7kZd~`cKacq1+)*3- zl(doqSns?T$qdsk_wgRan!V;=HK8I&lzLAkR>LR93eTmE<6BM0YTbeoqVa}3wuz8N zA9@>4V|G}i->#cz(TdA#%H=zC=nd|)z8ci@=4d%?^rvV5@Di8`1=b_Mbk-Y9NxiQ^ zwN#|QIhCy2cz>`pwKeasd6u0VpXA5den|92H#)CA#Ky|qvDxH-9R3&j0oT@{ z#K4#FVZ4$@L3_cX+WM-xn5WFeLgJM52u^~it9Hvg*4c8)8SHrThCwwBs0 z57MFvBMNw8loz1lm8Sq{aZ;9IKVg6Rj;5?-*p3L!ca7IV^3lb2p`3D|f?ZG@+c{{5 zg3sU4aUnvp&^#xiSbRRhbnZ{mt*%u24`*m(`}>e@9BUKpg#P`7HW_Yn!x!=yGL(L2 z01+3qwop`?+odn1ijlC-^u4Y41Cn#{Kidj$PTh9S=wd5wd}jI8Nb(hnJW!%NVtdPc zg!ry`;%2rhn&o`(^0jcITPH0&>IJ}}iL~HoayobsrEJxmoW`q34xLB$>%%1b6##y7y`dK zj8Bp%d&JVDdFgUPo~i!9C@1EF2i^heVhx@qnQ$4}%R6x-EIfR4xW_J|U>^{WMbl_P;sYB6xw2|s$TrC5f#=g9V>u`bI%RB#)-68YERX}-lz*MTnmafjbl?IqjD;=wt;Zmj(4{1^c>QWSij2J zVP$(J%d)BN?F?DQfR`n2csx47k0MO{l`>Q`$O4l?Vz%qM^aj2toN@O5OU^+MBWeUK z0?J^W76e-yX*w|H@cTRW{CE;CK*#2k(Yv$v8ThK$dwWN)enwdr+sHf(O;63T#`B`W zO#iE5!Q8=!|0K*o;O2J~pRId-|SjJRfbMa?H~ zeodSt%^eloX9o?n4o??M{01BoE5H)WOO9Bz=aU!ya zu;<$dcaH3yhg$?$vbWsWI0$!34MVFz`+coM39X7a%OKL6EEapMz2msk5$M$PXZesE z5t#Rihi!XjMQEm93wjd#QIz5oAr1_0cbfxy5F!S8h5JEgQ5(e&?9guaS>%LytrtuvuwR z2kmXG8wx{7QEp=pghbveYb#J{pOsm4Bu;2IFl8IKZ5Vo`IqTzwHN|U%k_7E`>?x)1 zT!g3|+K@*efjzG4CSM3UBd|n6Iy>9DH3@G6x5l>-c39lHDt%FqsLivwsE-db&17At z3A@NPvluyhfzZDyKe)(o3j4dmB%sZh%@OWbks2l2|$XM!o4*@zkY*xuzZM|G=KzT2AqXK~!jmIzH#tq*~ zZDuZF)z*(pVzGqlTc`7;*av}tSNalP_@wAuc1mNK)}@i|nVTu_NeW(yu-%R=HjH;B z5hogG!Sud4{AE6@fae1~e)hg;$R1j5DUOj6%uXK_le3xIp`(i6=$@`Q4mNn~eY{al zMaZu@(~k*3yTNIcrBz8X&nUyzGsy;?YkTun=;Or=F8{_q?(GMzMpf85qgilF z8qIOAJY^l`N3FBm;-8XFyJw*ZRmp5hqBeL8ZA#yJb=NHTVn-pVa#Vm& zJ8CH#2Y)h`?SK~+aoPz#xB4>UKB0ZQ>g3oLD>QoEA~lDG5xP-u`;JGZM6}CG2P{}X zAV97&ld|o({1Ry*wwU1f=Ze=&*|wO1iGqldA9HlNEQYyE!46d!EXrdYDMY>M5TBi% z2$cvXhlW$h(c1S_+GxJbMaLI^mCv4rL#$@eE2k@^&t+4>@KSgPhv^X5c-P8o?cdP~ z5Kxj+jNE!OW8WntxrI%yA4A=d3Vh#*`x>pMh9UYan)~S3u$1EljIw;O9frztwyDp_ zA@0{l1;kOr)c8b}cYDl7N}%KSx;7x32`jaS5Ru#)`t|<~RhSEzT*E$c(137mT=ah} zFk3%=$7SSZMP*y&Hs}@N^ZDk9b3^>C3E?!>+Qp?fg)<&n8w$m|Y`ooSKLQVBe=VB2 z6+t_o9DRlAnOvc$WNlB6wv6uQGyBoDY!LKT6759+;L+0hc}V)KSu5)$fdbyKZB31~ z(JN^hH#JW~(M>vB7)w{qjYu7P%Vl05kudt3-mi3(_Qd`qshWG8VwypG*}#__nLmOA(gNF9`p_s$C&nsvPzuC#vWKrm#5C#lOz$D7@pRr zw1~nBe>JjWc^b{%vt8`p3fhLe5DO1N%Jc`x1GJoEP^6(>7aZ5_=Q{romTzEeq5){( zeGS&I#lPeFRKe(gVb`k6T16oA5lq#CbYq^O2K5T>1C53TD%Kl2!$%n=o2aNP=^~Os z@>lZtVv!reaQ#B&2a&n-L?>hdj>rPrzu64++lkJ-%vF{;p#m6XQwYgQ+lbet(vHOQ zb=LKbjS^;bpQq%**WYEa*>44I-N%Go82DqUzX+!XD#75G<}mb7BaFqCGMLtu*z7#^ z)wrzFS>%yA_h#wisg+9>c%gVPXmvT%wrv<=9`vXzT~ZN^9T=M z#L6~krq%%7xDQC#9{g($o7nEY{;fwDJj#_bY3HI0NfG<*EJIs7q8mm)c#Q^p5| z*)cF#gw*&1H^gX<5c=4#`h%eM{mltBG9nWvf{(DE<(lCrtKgKJzb84uUhW4w5aTOX zpqrPFg2FkT>s5WT9EOVpRhSKlTbZY2#{9xfZc~x`G%`{lo$%=CWfzt{Zb5k`U_|>s zY(dR+5kEwuse4r6_@QawLXkL(sQD*E9ax31S%vf7t<%C7vX-av42zv_lTTNBwCOFn zYlAeN$a1}iTsMmnTxsX8E8o1n)g>WUmI|?f7zp3VJjJT{D*ze^?pMfVNVCcqnl6w3 zOnqybSs51nyIYLhOzM#Y(`U^#V07q!4_Jn6aXzUvFCkaOMJiu;zsBMfow{mgMu^r5 z-w5vex!9zbu~)yN0LxZYPN7g#Pn@@*=_~ zvZT*GglSRJBJ!L?MoYKI%(Sl^lc2$>c+qP}nwr$&Xmu=g&ZQGc>Gf(IK!d`1fWJYF0 zQov{F-Hu<(AO$E;Qfw~qw0p?zVAqPGl0CV?$?q-wMbt`!O~=;}OBW^kl%z@cguLyF zC$|1zhic@TE{Hite%ku@P62Rdf65)9mA0ifUf{jsHw|z#%=nG@`COx?{p0IyM%3v= zGg;}~N)ixDYsOx!HPhc$zpWvyf!ij$#$J1UlttJ?^!`sA`Vh#Xme_|URRY5LKNy3Z zF7QWAbF>y{#Dm#+3N9P7R|fSJ5$8`c33;kBkQ5?Cf9*)q$b?HU>vF(;DX>P^VVt3f zr)P~!1!?2iyBG$Qe;3zc>{1k1&=B6eBkzd9*>+=;E3>Yy@o~YYYFS5&o_hot&qmqm zM2FI~9P{~V$L;H| zAeBZ5yoeNpfqp|;;rj4UD|tS;=7EZ4LRlF~N2)-PRG2Z4Hrj%i<#$!3&?HTl%c(rTyz zV~2cyPpl{*FcBn+%p=s9C%{SEjR!bed({|Xoj+Kyp2nRTKiwG)h*=JJNRleTU&8TVl`bXP zR?9ynBN0VF28LVFSHw0ZNJNMRARylvyb&BIa(_v!TK49fPb|Q zT?-)_CxbJ?0SSj3;89VFLDbe6iUf-PvZ~9y{@2GkZIC*f-ddF=Z?y^zkV31%CKP)N zjOAp~%KWV6a@rT_EEw!tQcd160C14l6u4?~Ly*+hG8p2@W?hUiu)HOdoBQ8l<4k#D zs>9Ua8049#;s!m22?0=s<2fp&Srh;qTY3{N;I7!en1p`fnA8T3W*QJA-~Bp5Fv-3En?(ETlJF zv+S2b0L5&F0P0vCdE^X&jA1i>wY@sm@v0iPdIA0Wp-_kLarNV0OR#=9rrK|-=phY_ z>Ol_@SUJFmE)VDqzzwHrQ#yQ0aGI#Cn99J?MJU%T74SZT!)`Sb93R3qutlleX75NS zV|cfYE4s)d+1}p%c=w?IMZZ=ho(Jl04OZ1w0gDl+5T2aPh&K*`1zT>YGA_HxthiMl zc(DsRkDOKmv|{!#ah_!`*YP5$29*uW#f8T zrR#ICJh~+-Hn3N+5c8B~ySL<{13-|ryR>dsfz~R&P)Rr%gi~F}v*ZPlUfqyh-i}Qh z{Kko0V>Tf60(V0|%8=9T2qzXe4~oYf+rTC*k=Q*vz(CA|IOds3BNX3FcE2KS=DQqb z4R(qY5H^)>j_O=+8##R0?;m&WtHm*bsnaZ#4|OtUCX7}X)x5!4Iwy9*Kz6wQ+9;z- zV$yf{QNkq!Fzdo{CewVe+mk+~P0W&P(uByEgh&)znI_iV~Q^ zpIop(AmS4<@KTJT(&|q^EB|HSo2EAc+`XF?G#iD&FvYTJtb2gj+H;o5D7hKFodE<_ z5sFc86(!v{b)3z?NUlEE-+~e+W`%X#T$%Q|#R+!AstO!SRgE&juZNTmDZG~oI2uiw zNcI0wz~XMt6F!f=^5iDctDfO(VC#hepT=CN20t%anX=$96OB)cyPj}^Fg#8QYGl(%t_E^O&4)KW~>>u>jS zUv(lLp75Nl0x}@URl)$(`q<(4JK`IRqgi=^gM{G!+(Nb@m%%NhT{6pByY6$5?QIyM zow!jM_6IPqagrkAZSW0{k@GCYT>tC5=pIS|F*kJ5<>0TybmA_0Z4y4vvd`6AX54Mr z%eHn0*fDd!0uC6OxW+hD=3yJ3Fu$)+Q>V38t7Vr(LhWyKu zRKbS`*3m!oHx|f}`49TT5%#9xdC&#FS5+lV zTma2xc5BuLr&biee5(zYs{o#6eANHgBKFRF5NDChu2K-ZiVyHpiw$MZ8RS{XB19So z2%MNAU4}KvAQ8VAz7I!JEfN~aKdk0rBBu0q*J$tQT=!*rWAU-Lua>Y5X1_MYfN~>! zJ~mCzOzOR`Q?2I50P|^UqWMDm*0+Ong|J-3N0{aOv6^d>q&c5E7^XK62g_?P!g+GF zCQLuFoUZ9R;m=fY6n!I+AjFzlPkzIWLJ$(LUcR+{Qt2^g& z0moU+Cv4D`#mbq*N3^IC*L~%5aBX!89?EX8n z=X)WhzF7D&pT*ye0BN5SbsQn8HZ|qMKC)2yqc!}(f$p4_>}-C_PjpjU<0$x(UuHhI ztgF0q(X|XItH9jHClc;iqeQ_(B^2eUe8dV}Dj^-3q)oqN&sP7OZ|o57stQd;|Hb{V z&Jva(%EiJ1)bGL;{?gM*d@xuUPb|aPPwk?Vs!{Xp27QquP~Kb6 z%5P|)RfRKAx(V$l(O6P&ojc{ESXo&Fun)cKWHu1iK8px{F9f(wt2XF^heEGj=BAs0 z0KOCU5wSP`fWr{e@Cc#2c;}|=%SL)Wg^&CsAD{Y7WC0-GD8rD9-I=`?Q`;O%G@hK^ zm$Eb070riCsf0g71jyeaj^ye)ljoxgW~>knW#Q>DjliyFDf5X~)R4H=F3|nJ8J45} z6ke*dc9M@V3zsizuoImn*E_~?IVzvo_s3-oIbdcQ^ZwH`EM`VGk^@*{8-YBG*|afu z-pxtzsI9E_rtoojRA%BBz*Dt&n*m+S%&Qfw+#_I$q6%rGDgRi%=Z?yRO4zO z*k%mrHsvCps>-on@PY3`>8nLb*Boy3OXG?uU3wGvPJ1dPF}LIM`UHCM0b@C*bBj7O zLMZe%dXCYZ!L9xGQlTPJz))`_)Yyi_M7}d0{AG$W;Isy3dT!p~6W4b-k9gR0(Tttt zv;3Rv;%ihPad--q8kL@#Y>4s{N6`GGKyNBA^Xw%0l)}`<5JZ$ARIu8H?^o(H%k7`| zqKhs=TJx(Ee~!2bqDbrM>#6GxMQA-snUSYh^2Z$mV}uipJO$}Yw;Yxib)Fge@;*}~ zQwXPQGFr!PV4K(rH7yQEu{Z2Un!B2r{u;Kb8=W8_0EvanOnVA=yZ=L!TVMse4OzL#p#_ z9|~w#z2$gqntHJbKRc_6Oiz&k_N3?{fps1mq^2a_51z8Bx&e9+||Xzp<9AbVx=RqEqJd;>Qzlswl0>t=EY>P3A@fRx3*uq zCnu0a96q=cw;08QkzfEMBs$+=k3k`<7mGch&{@2oI|S0227WKLF5?1!31GeX2)yVh z#$|6kZs6YidCDxvhBV~X7`kH$`%D(tnH(~qsxX}vOq=X^vI?+&2^brPp#kj$&0+7{q}cUoZwHe@>g{f+l8iha72EE%-QIVc z6Ig>vro@+Q;$~eIHdFCvt0k<63cI}r;QQPo@3b2ES26~rNQ$F3gM*af6su*K9+m6} zT{yZ_fXSxI9;y9Jd!T;vWvW)&Gv?QV(Z@WCN~2Xk+%gtJ7S`#AkX9f`yREVXJ)_?P z%h(mN?4^{JB!c(?4|}0J&-LKJxkfPmvy~A@djMonG!w_AxvV`f3H46MadGvny@Ras zUSnN8_!D-S2wx5gsZ!5T#W`UJ*AQlWJTph&*6UGOetW_gw4dZKFj+Cd(2Vi}fUYwtRIRr=FYhBN$M_MKgMnBoOIb;SF-r`)d zqx4w8H3D4|+L&`D2h*Edc(TQHJ?gfFEezC%#*N5OxKXom0m@hgvznoidp`Pu69Ak4 zM5L0XbqLb1U|B!<3;=>)1pzo(oulCHZ0oiDzL!Pa(_KgZ#`{p2?_90Zxs2~i`a8ec z*G9$Hq_WDL(FnEoLpH5M2Ekwpc43hdarKPf&cAZvl<8u7+om(0)+~e>8j$8wdsK0k zDOs|9Ik1Vp8X%6XDyb7dF-?`&?M*1VewtUq`I!xGn@;VbeXvuANBf zkJH?&pRwQP*nw$iAeop0znvSPS+mkR_ldfJSo=d5K2R(m)iEH^y4sOSXgdi@0E7c? zA;R~fuP;ZR0N{Of7UcDcZ8|yc1=0d<3^GsEEK4b!yAA%q?H$e3R-~bvHAjNtgQ<@B z1EW5|r&T`a-w(X-jh8K&^m?|(>`hDmY9_QpL4Grr;>>Hexw3hwuiLKTBPP+IhUhR0 zOB(g|)(n;}SF2mKoW=~F*r~Fo!;*D7OxQRAqx7nC-*jaYL9+^Y8nuAbu>2}mcl<*- zdSg}P9giI_L+zW>x2Az!RXg&_o-a|UPSXa4IutD25oz+P>Yqb7ObA5rCoBs$qVHQ3{@&4QBO&u(jr zbbszrT<5h5a5Cu%fG8`IO*ne0m41NDvhBmL9#GBz&)#azhOly?t|{3yz3!M{RUqM4 zQK2kk4F@4jAIa$tL?W!(i@{JTd0YP!MtW(~6jJ zZ#!P7&QrcN)^gBn`%;}=6pscQ+IZQ&7t0{D=n_NklmGDBNFXcjzxYkiyT=b;JpqCr zoF3NA4Xz&WUAt}>l-AwGJqMTBR*+F8ifViAk(LK4lg;N3vBz>1X{Gt&#*HR@Xzoy# zha{dKI{J}zfL3$?=0nvoJ<#|T)~zNhbm^A*HH8}ZUObFq+%AjnbMdJJsX9S7)`~X~@uagD5YH4u8Vc^0% zY|dr&ZSej1js7m-4=e<3g2MMP!A0$rm2HW> zz!zk8wy^hjQJ}M-=p_wuwA_KjegHwhtL}!jiC)$kxR!`eWPxhupg1zJim+QoI9UrA z8bW!T8TRH5mQIr_DCS+w8qJ36wp0*!H~VXS+&bc<)p zWvg8e2SMw-;podv0u+JhzORL z9^PA@ShR_1qF6;2;ij)%wx`xKaWl7IB+;e_BjbcSV z5EGRy$?Nh@2Uwf!9;t%7l(SlLA9jbN(urv`$~T+{#<7u;sVrNb7GI|L=!6(;lraqv zCB~p}Pnzm`hL=}^(XS`y>;vp`pk=%EhCW4a)#f&;;cd}&_lt=!c&}*}^zW;Hto-7G z7mQW>tJTzNkGyUVXA3Q>KiY3RyUznSD5IcO#6Yr_km%ZqJumOoo$O4*?^)71YDEjp zjWJ8?5_=*VczE*y`Ifcgz}A77SY4zwH{FN+f{-~Le(3o86++aK&idLJRuHbpUfRDn zPU)Nv??b%G;)!fI1v?xazA54u#*Smqy-)C>A$Op82DfH}i81}lUcvRpfJEjFogBwI zY+yCUQI*$$CJ8b^y;Y*vWb*W&SpV$=HT(wxW1oi^r!a7D*h29WrrVpMQ3u5auFXxQ zVOxCML-P)dgz`1ZjQZpz4MxjqS}1%ZFM+bm!4M)NEdN#YWr%JlmkSDionFm-U<=R%A;%Q*|?+z{%Bl&cVeiJn~qYj~w50(awq z%A0c}ameM`qxc5e!OQP~gFW3n7#J*n-M& z$Fk$5$c0^Oe?3?I!t|4OS2tbQACIEV_%AN3X}Wfe#G2b~4cCMx@vV>6-TwKz*C|WK z*pm+Brmt3;d=&3UB$8yfD+_E3ErEO`7v~~3Sz}u(9eks-**w^7UWHusT?0|d@_5l5 zSO5U9C4sEE|G+HCHd`}1k4w^=du}IEAGk6@I)IKE$*AaOBw}BFbUNqL&1K?QnY85u zrFKt?O?n<1;5!m+n~L4Jd*nxnXge28gL|vJr|f$pPeix3H0ChBl<(C;gaqqDt?<{y zprK9Dp&Iw<>Kv;+I|J%g#?Ywp&oN}n_REme;lvr-a1x~rF=)KlR>VdJa#mp8ZkftNztFfdZ~F507%nJSm>L2~q3~#_=C4dG?emrg(NKm*7N# zg_)F;ru=C0Nq}cyNLkk6(2VvX;83wO&pGIlf7(Bv8%IwNzx`<&TVXSPrGrKIv5e>p zzWYqBU<AA&Bau8+(79Y%Gd4f&J zmMipurL(8T+7LCHrRdiE^pt#!raS0883~XLT2$Gv8Iq;v^rH7ZY$<21M32$Tcbe=7 zSiFvHI~4b(3A`98#+SCB_}rFOInQbPWD1m4(_RD!NSge*^;3xp^Ld3A|6dc85f1uX zla_3yS)ypKJxb<+G%U|Z3q3~^CV&)23{1cG@OJiV4>(gte0B}X1jn)J3x_Rzd0zq_ zUG^MuO}}@`s+A1DB*c~i7eB3}&PZ#9l^TiP%m zfVVZgTTjl*%I#4h#-d!z*~)jLOM%5D83TWkPrv^pLmiV8cxBr71}t~Ds!V*p-AMn;{>VeAXQySeB;(lYtZ?lI8a_FZhiQq>7 zC-JsjgtO-g4tO%w)uS?wP(NwB=hjq!rTU{oj3OJXNd7tJwnPu^4_nfje^J|Pgy%TL zlC!|bF_E1s7egUuR^S@H=r$q4SYelGGAuU(usxg=7dr|!qE0 zV@_Xe6d_(EF#=v8_@i~29L=|4#SF7=*UkS{SHIy@I066r=b3m*IHLsdM^38ofTC83 z8g^|zTi)2K@Jn1Fo+&p*{I1+2#049*3!IWY?C1#Ab!UJ|AITwy!?8&WCwWM_(Q6yS6DR<01fLh^S*1RRUqggVN@fQ*+YqXdz539>2h|yxTM}hMZ8g&f_0dL;ae2@AAsD9um?TcfsE} z#khNQfa-c)isjgXrUG}<6D6&I+lnrD97}1lyjbAEnph!s*hVd}$_tYH**ZPZb`PYr z_JogZ4USuZte>4mDS%$X6m^up`>aLThjB=l>K0X@3$prCcI3x1DQvr#He&VRtMXQ3OedXhg~Nb?&JLi^MB?Ac@%Se~-X<+@3;& zwj_%3w`mCNWu4ua^i^$zxrO@xQvgpXW%$-Se&V9nJ(=fRLX0|Or^Z7&ce-d%gz#wp z;4Hf$O|blXjqQ0qdsxy9`;qL=xzTXhm;i;^t6&>pxIR1EtCkg@?UT)Z@`ptD0JpA}Eot zBWy;*Z>_r`x%SRtl0hBSHKlns_Y#*&F&>EF;X)O91HO>%5OvN#98tA@RLq=KVDR^e z;(JLGjPp&Jgz9^G1vhtfQO&z?wtFjH{CrFZKsMRkYUn#;G<90o4GNJ1&q8Xl1Si{Ln zT4F%{7Sh2ZbSDn$3#8|Jirr7;g$a0_?$m>G+d9XXX3OHWa?8*#Y47914&rrx#L$lm z;%K6~Dy9Idl^wCw$g>|~;IJs|`J?}n(|?$r%~E*>vS26Yjx#D>N1#vc?0Y~ zq50KXTyu8>ET0!&twH|zUg!4EA4>{URUSj(;4Z2YKaV%&5ZPL-(WXR!f?G_aZSK$R zec8Z@V)1L=-#HbmU#w3fZ`!Do+^=lHjpztwr;UFFTDO*HxKxk7IJbH{&|>iLeH6_? z&IhIR6cL8x+^Y7Cum;jwE0~+t4H}nzi{RYomY!8(IAa$nFw?szn%U_Ot|a{FlRv@9 z!p=CtoHq~w;IPik1iLftx{pAh1asj`b%K@)e8+f21zW{#zMJV%CB-ypb*Afwb}F$` zX$;5anSQWY?^s2I9wdutAw~344`dcf3XFHM<&T3H5$!>8PdEgL{?n)mr>W0K zR_}f!tDS=GkTu-$x3_EiJ=gu9LC2c5TC)v=O9+oi#RH;s&ViUd%(w}6<08k;N{*~r zdxeF!5EK~udFM=W)ODxAA`p;uA{tNX+D09tcz(cbmApO@kJrcQ&*;sQc)y3NCI#Gx zwvbh20!8et0ed$bTXoY4KwKN~8&hMR3KFj-Psa*$l4>o@*i9hAO1h9u%NDv*K)20~ zHkPW-7hlbkRFqZWs(m~Uj5{f*)r94mfU!7@-ufmW5>cshq-HdPLnTZN5bp`Zur zbRNrQJyJ~PABPh}1Vf?3f&9!+CE;`b_sNsjam_xMrh4lWdMQ=fGqz*))jyLox=_C7?CWRBj@#$mi}Jdbp){KH93A^(1|cs^K;7I8M?+?(qGfPX8#d z;p(}N72pyLh>{t?#eT}*N)s-C^1j5!x9tOMwu=uT>;1DEJiur1#&^Fy+2;7s@GqJm zlreOMGbdRBwEO6Q|1qBRpZO3XWfi8lB0aY9QTHRVvsr)x9uM0;rhml<9O?<9-)yeX z3(r|Cui0yaj9g3MbhdmUI3`Aq)G~^cxzd~hVkPvYaSDqDY%V+N4b^yBla0X|m6*7MF|wk$afu)Y6nwo|ky(gg%eBBTY0LR!##He58XWGC z37E$oQl}v*$@wb$Q=}R}94BY^$*kHdttB2?Iufmptn@Ou?h)el`cIP z{KUdPP>7`Qs4KeN+Y7d3Wgq_R+`&MapmQ`UWLLR_v!f)y-TlJya=(s31Uqmwwl$*C z3R3t1o21kFedx_@YPLtE!YKklNqb`-6Vn6lnK_A{d zZ~QK<=YcbnD-L!)?9TRfEi{BsU;nkJ%RQTu?4~YbByNs|%O)1n9}B(QDIzo%@Fn2j zHxzc`O6FaEzBgz9Tki;kQho2zXBFB6X|;An7oTRC{>By66fgc1D9>Nz-NTqhy^}7Kq3pPd+IbvQP2KC(*TdFpf0xcAOcQvFEVIInC z7H(vyoNuEgE|twUJ2jJqa+7089oQZaxa|fgQ#fJ~at4BOvA_`ISS|PJXL>loj z;b*htmrB{e=WMDIXgj)5Fy*4bJe_ACUT~Ao1gBJ7At8^G_Kg#+MEx%!}&scO-yZfRg+j3c`9)L-zq)q+$*MpJ{xNN8;eh-b8 z%_E=Xud3AM?&&JDOYUeW8Yy6Al%ft&MjEzyH0XSlrh7e*FGSr(CY+)SJdZhNtyxS1 z`I>F*MOc}?HL%RSa5z5LjHdM!s+$-+J_A6{Mdz$X0yNb1!SjyABzhG`?m?pK&5@N3 z^BQ`Pzfhodo0x*!OZ@5M?u9i!)zmc5!sSb^1TA2;(=x=eWe_O^TV5-slSjfz=G|MG zeg8z3@8+hsyw8wY8<*o$N^FYCZ+ELWGWRMpp3Fy4CIOhyMAaC!nI9zrQTAvd##wi9OgfM2 zhd9L)4Gd4nB=MO9({`H%6iYK5c?SonIJ)v!8mOTCCPQQ%=Nm|;5j;+?&MfOpUAAPI zx$!7(MJwebiR0uTxsgoxm~fQd-KLT(x<4_fZ(M*^uAUqLU~upUTjbWx)|1{%FwE5o zhWKV_0YQFY%R9u>iETEkn;n-hoiKHQCW4vFx4wCZ3|Y#$aSf-x;`R(I$F{6phx(RE z2FU`?RMJglJC*gRF8BgHMrg@?6PNdh7j6+!0XOglB{f|(KWIZ=WA};tw>}xTMTJ|> z9_wY@2X7YKxn2~C%Mi`Z#7=7^3Sd+M>MMmqtL8W=y;(m_SzV`1$OJL)XGjBcdNAqy zxkj3XugoT2!vD2j&MS4md|uyCK8%8<1th3rjoLCdVzIvN&s%f64SBrz4^+Fu_$Onn z_yB1lT-qU-w1e*JJya@NwNH?mD+-o;A0R0rS*l#|k-~(?sm4F)RHKZ_bojb(+r`1B zfQ|O#$?;?eGS=Q}$zFfKAWOEjuWyXWqNL%5c!4<`OCOQg!n+kIXA z$L%EV4fjYS?VD>#1>bF^oRqX@Ev{e<)R6H=1YZhhcIjW@$yvCi{x(+6T*#y^R~}N0 zOU)ze_02cEVPFr`v$L)OWX3j(h-GxB_yhB12XBBVp|Y_OaE;30gF@rTY@c>`hMnjRyMSbfwiF^cA=i5ZYIq@S{Na zR?)1FO;)m3nxmdWwg`q=lB!;-qAMt73rOKQ5=)hs+$>y265uid{<-N`?wN+wY_?!5 zRs7n6c0yg&k(s>CHuc7MTBsOlyYI*ZhnzU!aOkg=Aq<=V1@QJAgK|OmUQYo5?r$|Q ze0i<3R^0Y}l7C0L#04@@{+E3~5xLp^Z)KFMCs0Z{us(OmeJBt>3hzW-jt;H#lar3L z1_Qk|PQJt2HEx4ejv9B>8OK$XR_&8{b>6{MBI1cYtB5pKaTA~Ec+wOs9kE*`D`GW< zCRo$ofrK{)WJ6Iv6H&__xkNZ=8|+;uT=^KP;M2t6q#E;Cq&8HL>Z#qUQSD!=DJl2$ zJ7T{r^j8T4k5)N(l-IM@D<*cpwZWLMlnld0z$CFmj8ymnG~W!RdyR0(Pzp%3V@-5<{hu?#amBy8}OX*tM| zA}9gCkDGehkBEgv8kGsBWw8+*5jnp;5>kRFyhV{7w0td9rMz)-P64t>%7p4&ClE-q zjq5J=woeXhRk-o;5}L1g(5!;=8Q#KVd|W#`clJ-#WsG6vxR{l-q~Gd1R#d&lvIIIe zo6(nkn!TWAku&#-ApLE+F!iY#XWiSH zoC+iba5$8e3huzLRg3j7)f~*lK3WwM@rG+i6~1TT@teTep0}h!3SwNt+|i!Q!9N4L zEbDARS>n7LC}WBsvxAiInZ+l&eKn8mjO1eJ+%0uFa-Ncy22p_6_+MX=uLzi)2`%QF zWW;=?vC6q=2`a5oLytkFtUt9EnM6T$$qEkI(dp>&CAlfp4rWD~*`!AEc?bn}8MRCp zlcqD{uB%{J_yfTw&C27V!w#9kV2V^)d8WJS*qK!dfgqfBfx0iX!*e6?jW_ZHPMQJ$~(*GI9{m@z)ejI3aTa6x6) zBKp=F+Pg0BT25LLa%O(&#{k7iGUqw$WyUE$Dd2}q;6Q)zT=t!J^Jpn!+Qa7bGn4o{e>ijEiTqf zy(?`_vYh+MwwfTL`JflXeoPC-Mx1p~EAwa6N_vLe*Xu}T0(ls6X+yKxe({o(&h>cZ-c*mLM1v9Z=5wjUB$qgHB#r-b;J*~UK`U<#d}zlEK4Tp`|q9J$aYrz z^73~QVE82m(!6G(u0OZ`J+@1btk9Mv4#S%0xJ13JEl63PNap@Wm%OBf1dpnBYnGIh z_NSj65nw_HxSM#OI$!MHf|5&KPcgN%gkw(;3>6>xq|Dnd1G)3zh$E0|^+RZ-Yd9w; zinMHF@W9zkf%h(Z<#sriNyg8%#}KI!iWT_t3+ttrKLf}|^hXJAfassnvm1_z$lg$g$&r*$Vh$!xVT`R=&svJ`arG~q|Bmf)r zBJbvkF8ac|qQMC(If+*=Rry_mA$w_^slXJ$7&|7;5XW^ojXtyZ%eW!$0jf6v?kTD{ zp0AU21OSlYRq;1e$0a^$u-(X>&Sex&rrR=l42-GKH z1wISPOy3FF%z3UA15*Y=$OUb+vA5(5khuNnG-$A}6XEq81H)Y9?MKgU1%pMKK$y&} zDi<+bV@T29syjg#eH(iKu4ua*%JeNvYSAVup4ZBHx*$+ym8iNuzhbco#RrE1<|7nX zCRFvtU8(bny)*a1&NeI0dNJaGw^*eFnF*Msd@78e^!-aHF{XQT!4ZA&!Cc&$Z@Ug}qBB_bsuVo6A81iR zur4m1z)k<135o2%+aeGGEu}k(rp-uk3a^_jGiJFXQpr_XmR7vjE?c;vnqp77wLTBwa8NX} zz)ttr?&LtFh-bw?C9EBJZQ)%W1edkH0XsDK@q29^nvd@vMD^IX|+G=a@?U|<)bE0=|(9Vt^L;IA)wcA(3W_5XK~W*PXq zzm#k*KSlxA;8sNVY=Ta2NRQ!#od*qE-gn%q7D^`gSv7=7V(h_7F!y7I+%G+4KE{Tb zygr0!cS~A-nSvl%XWhaU2m;$w9P^+l!`_*dv$hl#zZg?7|eHrP; zwad1PHQ_#uYJZi^19|v0NMP0IqZZ+=rC%_{I^u|e_{iWn`HK$d@P0T!1Sh1O-E@Gh zhPLoMoMPqLpFf*TbS5sSF76k`Q1@p4rB4b20s^-+6IFMFGJ=czXI3hb5=6YCiFN+{ z`EiVm)~e_o8@;=?PFif{K&R+=poGxqtks4OE8g)oR3Y0Vw%8@)hOa+`v-+2S2yX5x z_PY)_WE{*;%NZBCdV}_30Us>rX9K9$xOJ@auL4lS> z1ulQKV}=ra;pekM>!Qy-2Qb91%z{P$+#)Gp%tdbNKU(qfmBpZ=nH2_1H+uky^=l@( zs>|l#72p*voN7aST1N$ZLeb980qKj6M9vx}ooB7o#Li*nDf$M)c>hhb-JR1>l0&}nf-Qfc5 z(`5{fx3=>;u?Z?B9Yf5vZ!2)Lm#m6C9u)%smBQPfAu4TL&{>b@SI0M|1vTNs@KQB< zOT2#8v)u$ra;!Gm|J10M@_=1*q>!z{X~qakktiLt{;RjQ}TOTUT2FHNRw3*Km})25NwL2SR81p2hBv{>%un`2A>)cL{y}K+>L5(T>Js zU20}hB=W>Hx6Sp0r1Bm*9^u9eRJEZ3Mf;_Gcr)|OrL$xKV&-rA2CMo!gLXuO=-T@t z2UndmJm8@)ZQer7P2Qq<{ukoK5&+fb;%GZD?(e3_FA7fcdt9&6{U=_CKxKk4Ymv5@ z-%k?~_#yXxyFCaW`8eX1JjV20(~1Lka6-mq&gYf;Kj8;nAd}@k!jHvo(V0PB!xc$s z%#5u*mZK6qJIf#Oi4g%M;bU%?V=cv_xnSDp$TeDPVSeE1#8;72bh9+=alhf($0)`E zrAfJ&&Q|OJ^Gj|7mn{>dDCkt-OM-I+49e6LYCy}&*wPGWT_?v%&VU;T1Dt{YC!HHm zwbaqrd{h0NBw!Mbg(4bxg^_~GXhsIVLNTR^@@WFnwFko%^L-iAs(!J}>X5P@bff;T z4&bE5i&jM>rdj%KA}B@ODs8Ua{IfpSwZHZq*#6{G9tDEzp<~K>aD*)7jCi;}DaMgr z#TTrK*N4$fR4fTg?I9EO(J>}K$3cwMf?vXm2q?5P#0&P3S+~bI;;oVt)N^cr&4 zMTWLGOiE}8H3Wm+IdX7kS@qwvt#1J;=G#nrdfqm>sqOg}T&^1%;>hI<)eerUXSTD> zLV(}G?KaJS9pFL|FC;*b4VM!viWx1EtC$Cie%*<*JL<)}quKrq4Iv`HE~Id5!NQ?b zliHCQ!(0t=gqU`!tifcM53V!{y_apN<7JLJ;?!YXPPsQF!>m&IAxSD8dI2lVpzHM3 zu|wHON!U{A{bRtyl=Ks!o~_meQRzi449B`O`!KVuqbTj3h`-}qiT5eqJ{w)0XYldR z`_Jo>Y`yMnBgq6_t+wF|MjZc!im<57r+6B|J+~yEl2uE09Vug?SA&Tqw}#V>03MX} zyjhnE(8#TO7o#J<_GW6#vUk4bdQv`In{@xPHqlo=0EjFwb6DPK-`^ue$CONg^-Ydi z)_yB#PW&hkc(fhghQp*k#?0mo3kDhPNK|TmatIt&lmicg9+0b~Z*^lzE>*)8q|c_G)Eb zB!Lhq3g|z@(7xjIKd?qAQptcRj;?B@Q)unr0&9~Il?!cowJ9--_vQHN`il=?6H5(f zHP&dzIjm{dDBa)l4&X)%cF@QXV*-e!g$Tg?zK$o*2TV;z>-F?oDIfKGc(ZphYywCFSyh%$q-J6;E^no)I#Y5EbbAGugtV!)EeNaGN-3sMt#Sir- zrcIgCFHkKi>?h?x#QxUMOZCw`e zY>#WqZ*FW}NYAoSuMMTx|8duCfz?{v>FtH3G?70rhr23)MH^l>l=3$U4tHS&UGCkV z%OeI5>D@8~ZmoAXKeyP}rVP7lsW2?ldkGm#6qxD~w)Cj9@tti|(*ef*$~f+nvG#)l z?wJg4An_M{qO8XKhOmLSH<5GlJp?aEr9CgmLgzC)Uy!Adr8 z+uF)z)Q7qvIT99HZS(5IaXmj@IJSP?mg$R4kY(vsJET0m2JA-NYVtda%psU{J+68~ zP2Y=~(GN^El0jO8Ai|9gWfF4Beho!Y!*|auSDV7DTu}4Cr6w zOz)IK?VR4ptb$h2p_Fhj(`Q3I?5~f@kTh#aJ8x0wSK#c5HaIMSfl<==iQa453RdQ* zRINmS0wWY1^GpsIwS7DcgCt$E*p2Pwxr}kKJIk2D>GWafE#~4kgfU??9mY8*^H7Z=)87S)%rl;AA7GYlV z$a{laTw|Kqr)R;rD&vQ)M%=!NGfD{Ke3OZ`+z~m|+_JLlORIa@Vl0UludTG`Z>mlu z1_iPmSKCIiRfel_d!W5RzF=bc>wa`PnF7zfo~Hu9RdGXeNCFG7CMZ{G|8+f%2%rCo z%Df#wpfh}(rH)NDpk*d-Rq#)Z`Iqgc_W!9dwib|f%wv8pam(b(=zv8XjlI7Uw!yGZ zTs@#QcA&?4UW9AqA^hw3stFu^nf+AH9={L=$$RlLCC8V-V zEugFf6p={3uJUW4V7SER61qeZfyZ?5AGt0iW5Eg;FiG8Wz1G8fz-i|Zavr}S6MZTM zR9o~nzDp%0+pTJ9*a$$iMgGy*yj`|&JkJm?FpaYq?Ya$1FD$6fEo=eF5DE;$Q)i8xr*9t7(H&)-o}0ao4>(CZAzL}R4MDqds1{%Q82S>DHJ$3URDws{V@QB`Wb8;vq?P!8&A58?!kY7l2bN=}G>IinDZq@BL zL>>oJLhSPk4F1U6s7i^dt{PJ`(|D;3W0UPkv+~Vpn?@}T*yW$6qRu8MpP3a62{Z5E z=JMaU`Q??!BGEwbyaVl4+n)kyrNREHnjuRmB?||*Z>>rKCC~V;XnokuEvk-xN0LIt zn#4bW25MkW<3K_Lu0-EM6i~S&GVblttr|?Xn0k5fU&iWys8;1+tsqr9sEQ5aFV zjUsOL5BmehLDOX~!%$4xm+R`NMjP3$c}%1pzJ(e;oZk(n$o8cieX~ zv2EL_*tXsE?(Gk+f5lpRuDQnmzL_#kIDoeqHGB`v674z7bxpaNg$!JszkLTQpTm%s z|9+xQFAZ|9AY6WIq`DBEF--u8N4=$Oc#se%>&5IP{=wvH4P& zqy_e~j1@+@j5_`;pBH}nDN(ba_)$TMgoe}n`zbA0zzQ;GEnkdc5%?U-D>zu@q~c9P zo@TJrlV9pgEEE|gQr~f#@%dNqJ0-jvRmsb_OqqG_sgo@$g>e&Wh*2@f6Gf=445RDG zBEG$AEeLPO3plk9hE6>up(V1nG3^Nn3K`f0-9eOw^xa|rCN%^C!X%m&midfhn&9GP zjCZFFS(Z`_`qS-&F<9+)nuS&#Yf7LN4sVF{3fWg<+NnfIqg87{Mpsv*wG^ildV=I` zP)uR-AE(jHCS#Z{r~#bwa8ijl^&;GCPol~>kC$P6hOx(a(XQDL5@1-TSBta?tEiQ; z{II;~D=ooYiL?ospBx0?h3JsV06No!St-izTOUIEJ*l;m+j3eL%sdu0G6_ZD9Rf_n zIgbm`QmfGh{K(zhVL9=Z^E)ufMkdTUmMzs_4_>?d(RpP%?k{zTHoJ+?qF}AGp{Nzs zw*Wu_sDs%(60VKW21em+wzWY(;+B+(5o4{n$->V!$Nen=r1-&^zTQqdwO4PusS>n! zc*s}_ef%QL)4%pqSiIMihRveodqG8G!lVTf=vuer9AWxs^E6QAw?srxnFBwE#4bJ* zE~7IV?i9?{qTquN$`JQ2K!{aGnn=-*f!9To1T0a40g{7^dE$lcpw6d~mzp1ERuzy( zo`tC^&J3P$TWZm9$?V^zU7pi3!rj>38Np%0%b|>PUXn)>FA#Bp+sjN<2gC#jS4b== zKM&`{pDB)X>r4BI_D{be%XzKaXK>Y`am`+#oACA*dm&`_ES4~;gD8Feg`+~vv~5+3 z1V8{X1U(DH4jB}J@-{M&YnofUe&lV|iUZB(ns%`HyWa)F*fN$6eY8}8TGFzo-#_3X zpqegakT=03juV=|W~lUDblxswKYz9vn4vH{5Th5ycL$sQ^aVBjEMx!GfSv|P2`qe^ z>?0^JM#qIvw7PqdbZAwhibkM^G2s69#-d3RgkIq-l!lj9Zym@8d{hWkxk*8X*Mkx@ zkUC1_K?P(_icMHyAKbwMDoK12l3^~qhY4v5rp55}{n(1UJ;f=NFOkx@><*636drZ= z4K2MdH?o9x>)fS0IY`|;L_p^_RLN@F}9P`m_%b|fEPv7tm zdmAGcKJ)mdSc{?j%HZ`W(Lws;R&LCwp!}H3OVTw(08|`HQc{uYtyyjm$GQQo@=Xl3 zDozi~^~Sbjni}eoCF!h*6uLoKDQdTDqQ|%q9JcXDiFFzIl1Opa(3wbq*KtHK>)c6Y z5Q|ZKn@4x_8#uwOny1!9DG2J};)A`M@(>BMDANvry(M|g7BZH#@Rc@vGyBopX{JmC z-mXI=aSK+28GiljYb1**1^zdP*1Xlypg4vCKy^B_<*E?fNqkHF!+C~)&JDBFi#3IF z;*(p7nLLer(2Q=dBaelP3|7h%wC>0J&bWlk*>d?-SF3fE4NHKoyCJjmy>o#K7?SXc z#F>LTOC{S`6T}kASm&rg61Sm$jOql~;dCKGyp=N5bEaEn?Kvm9j#wsn`2x0ATemjq zS=q==gUy%&NTH+$mSh5;{Q`s(r;7kja;V>uLl#o@#aI6G;c6=gLZgQz2i} zn*$ByGT$wi2|45j`$j{;z(&2_axQ{AN2@|=e>=n(#51Bz9g zgIG@?lwrt7kZrFz8&{ejfwoqaD(2|O6rXmX;V{y-K_(*eXen#-u0osvuxs0*qobvt zKp$8p;?G?9^+0`=Q`IiUL)Skr8TdaNiu?abiR>)4j{z`JXI~WW+SHyqtx#gRO-a5jZOn!yt$8*yp0>D73vC)YDq{Qd37i zclctpcIf1NBG6%z?3crMr7e7g54d2lX7%ojyq1zR&r>a;o_!OBB6+u-$afo__CFXK zc!mdtCLR0mrB(Kj9!X7OhF~C4iQTxfOv?56Vnj2XLIYOto}rPrWjf>df05(u zJLSK6JDs=MEXHXaHO>v`0Zz z@e*~JmG(VM<|^|ZhM2k8RBH*+%+bDd0iQP4;OTBiXgcm9R`+T%k1M%cWQ5RH-Y$mA zF-Fi7^@xCgf_chl#FkIR2=qQN^@4R${II**V?0iJ)5pF8(qnI8t9_z>0DPkp*GX=H zuwo#*5SadODVn0mqGmh}WC&)vJc9@27xj{2Vvo9?EMS#iX?h39UP_p%_;N% zS`a~ZW}y(@`GBHNtjXXAU8UR;NsXqJ&AoYO^D*;bRW#=tw29#$V!PJRYBJN%tT)9< z18W*E<<6cl-RN;+vsN-EvK9?}$3i<))Y?REe{QmIzk#ThmQYxn=J<2m{LI+u`-H8D z^ZE$CuuI@(%JwM`*#6fOcgV#PAjkAUZnIIxL0wj|hi;@iV%OQ@b z7=LKIb09H-G8|v5(_L)Z)!FZSH-_bYqg(H6B&`cSl$hyFV^mQ1v6Y8%0=RDbPX7eT zPc3z}jnP9@w$)7(H@~D~Cf5tpb~~-#gB^!M6S?-Kbv&)9=n*xk$-+k8UA6D~+%`)o zvnT6g>#(m8tiPbBBL~po84h_!3r3b;HT+dO0JB}A?q!0L2Wo+KCqBEzK^#AY+mSLp zEn&56sebbK>I?Y(h?EP43JlogejCz%he>MyPk=CK*Ddv)&{)V0b=<9?oCf!L%vsQi zJ*2*^)5j@<P1-D;HSJo6}#u^jsG0TV6 zx9Swd_DcPcDv=Df4zWk__<|Dc>au=f6vN^75^JB;(XQ-p zX2~(2uuQ>uB*r3T_Txz*C~Gpe9qLm@)@n=p{K@&AS7)qcI-oV|ks?{M@0{1svF;nH zdP2Tcf1GPa^!1K5$o5z#AIOp4hKMM%xN6A(nzRbvWG0D3mdaRsIa{*)U=Qo}Sk6M! z2Jn`#1yRyJDT+sye!R|7&Q)yVvYz#8kv=iZVaWGWRa1~sI}3iS_O#&rnVhz(=eo?N z=v`x1D*ylyBAA)+f5(Vkz{IJO4{~F5A0){zm&e3`p5#UfynH%@!_f;)#VUw_C4W{E zy;c1cOIxhzHQ8h=_)l;Qi6+sNQ)}Rj!hfdbqxYlkx`Ori4v9|?)Caz2`mn7X)#+V~$6bqiqb5~=s9O1yA=^t6d^E{U zY{Q)E1R&h*F%&8-#U3+*R_)&ksjA_3onLo&~yvu)RJSm}%mwR&3orD~6FB1_YTCCOel zc*CRIHGXI%bF|dh;k#(mEkuzZUQW+0>q@lyLFEKkd+Tk;Ie+km7(REYlLM)XQ9=qu z!JU1>@=pbsgEfMwA|y$S5WWuTkb^!d zJyE9qL)u#Qz(m%6V=8bT05ubxKIa$n{;Y?<(+?cvCbRo1QWk#WvN}sGbX1K!(lNYo zo_2PBQx7I%=$LADW>Y*@X8U59tlODtt1;9 zyV|lBlvfgc$LdjOD>MWUc^hvlq0|KEt>!qyrbu+UU5{ECWiZ?Q7rupVVU}=lV6o2& z_wq7`u^*)#D9G(8I+NoSp|ZTn#;7OC&OMoO|B4@XtjNDGMk`v#BDS`|EqI?G}gdVUP0=#B36eO$-Ry$FCNck$W6pf8?YOWfT@MIuYA=N$xg*ApRw1yTK8Q#@E9%$QCBv`KW0L|o;2hPI(K zps6ua^eUlbcCr0N7m!$@O8Ld#6o6oqz6tb=?z5w)k4Nwg!am038-wdi$Q@j`^bSv!dM zGgO6;+?|2O>c;_Xz_HicC0waezm!DZ^FSo9Vn>QDAdTZ&pzjZQrLGmdv~k*hBwJ5s1Wjx2uR7-K5zq0pO4KfBq)jOVp@` zNmDT80qw!~9(SV`KHQ6iX>n%z^Wb42Nb0dqE=Np`HU*fcG(3S9(niD)5v%8@WsXxw zv@no*Q)Cv>F8@XBp%nj*SeY{^gcK}u?lOppC#=c&_f*RzId?A$kMBY8P_eO0W6hK* zg<5eg#`VTsDznnORX`C3Ur-79Y)LQ$37*z(J!jU8_L=ITN&4sq8Oz-~Pq_ahP!dC= z1t@6dz|ewlr>kGmvK;SVOE84mTevCj=Q*QAOjaJa929W}*kkF}ff7@lIeqowkCh%c zx&2k-n|$=R-?vRXF-W*49M$P9AjmQjh_!}|sYwIc~~1 zaXx4fnydQ;=_sTVJ)I`kKA9NzSayxUI3~m;wGqx`OK(_a>fd*ztwHEYc}^R_kk5$m zdvV(%UZ#wcz2riY$z4w=Wm9jHSbNt6+Q~2AjQ5<0vBy7Ql{T1U0&}tCo@@l|OMDsZ z?@-4zQY2CldEpEl=($>OFOReUi+Et3?JG>ofZ^W-sFz#)mbBdgisOfDM_!+qbfj^V zp)!3;-8Hxm^mp~Eo$Xh9a^D0?;8jfxL;nhy^8*R0%n}G+we>||fcnZvwxxj<=m!m+`0zfV>Z{IfTEdo>{b2=LbclH33+lhE2(a;1ht7i-gZ@HXgrCpgOp6b5$VhI+= z`u}^+Z3$*}{C5!l`mb;{`^P2R-Yo<#*2Y)4uP$3ih+*V?{2NI+cQ&jDQ}4YisoDz1 z{Nap~YyYdz*y)KsCY!L&fotWBJ|0>flBBWXVMj+hr?#b;LG?+QBxh==j=azpA_!Lz z6iMU)6Zs3�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 literal 0 HcmV?d00001 diff --git a/ch02data/greengraph/graph.py b/ch02data/greengraph/graph.py new file mode 100644 index 000000000..18df4cb12 --- /dev/null +++ b/ch02data/greengraph/graph.py @@ -0,0 +1,48 @@ +import numpy as np +import geopy +from matplotlib import pyplot as plt + +from .map import Map + + +class Greengraph(object): + def __init__(self, start, end): + self.start = start + self.end = end + self.geocoder = geopy.geocoders.Nominatim(user_agent="comp0023") + + def geolocate(self, place): + return self.geocoder.geocode(place, exactly_one=False)[0][1] + + def location_sequence(self, start, end, steps): + lats = np.linspace(start[0], end[0], steps) + longs = np.linspace(start[1], end[1], steps) + return np.vstack([lats, longs]).transpose() + + def green_between(self, steps): + """Count the amount of green space along a linear path between two locations.""" + self.steps = steps + + sequence = self.location_sequence( + start=self.geolocate(self.start), + end=self.geolocate(self.end), + steps=steps, + ) + maps = [Map(*location) for location in sequence] + self.green_at_each_location = [current_map.count_green() for current_map in maps] + + return self.green_at_each_location + + def plot_green_between(self, steps): + """ount the amount of green space along a linear path between two locations""" + if not hasattr(self, 'green_at_each_location') or steps != self.steps: + green_between_locations = self.green_between(steps) + else: + green_between_locations = self.green_at_each_location + plt.plot(green_between_locations) + xticks_steps = 5 if steps > 10 else 1 + plt.xticks(range(0, steps, xticks_steps)) + plt.xlabel("Sequence step") + plt.ylabel(r"$N_{green}$") + plt.title(f"{self.start} -- {self.end}") + diff --git a/ch02data/greengraph/map.py b/ch02data/greengraph/map.py new file mode 100644 index 000000000..9a4b51ab1 --- /dev/null +++ b/ch02data/greengraph/map.py @@ -0,0 +1,57 @@ +import math +from io import BytesIO + +import numpy as np +import imageio.v3 as iio +import requests + +class Map(object): + def __init__(self, latitude, longitude, satellite=True, zoom=10, + sensor=False): + base = "https://mt0.google.com/vt?" + x_coord, y_coord = self.deg2num(latitude, longitude, zoom) + + params = dict( + x=x_coord, + y=y_coord, + z=zoom, + ) + if satellite: + params['lyrs'] = 's' + + self.image = requests.get( + base, params=params).content # Fetch our PNG image data + content = BytesIO(self.image) + self.pixels = iio.imread(content) # Parse our PNG image as a numpy array + + def deg2num(self, latitude, longitude, zoom): + """Convert latitude and longitude to XY tiles coordinates.""" + + lat_rad = math.radians(latitude) + n = 2.0 ** zoom + x_tiles_coord = int((longitude + 180.0) / 360.0 * n) + y_tiles_coord = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + + return (x_tiles_coord, y_tiles_coord) + + def green(self, threshold): + """Determine if each pixel in an image array is green.""" + + # RGB indices + red, green, blue = range(3) + + # Use NumPy to build an element-by-element logical array + greener_than_red = self.pixels[:, :, green] > threshold * self.pixels[:, :, red] + greener_than_blue = self.pixels[:, :, green] > threshold * self.pixels[:, :, blue] + green = np.logical_and(greener_than_red, greener_than_blue) + return green + + def count_green(self, threshold=1.1): + return np.sum(self.green(threshold)) + + def show_green(data, threshold=1.1): + green = self.green(threshold) + out = green[:, :, np.newaxis] * array([0, 1, 0])[np.newaxis, np.newaxis, :] + buffer = BytesIO() + result = iio.imwrite(buffer, out, extension='.png') + return buffer.getvalue() diff --git a/ch02data/hdf5_example.svg b/ch02data/hdf5_example.svg new file mode 100644 index 000000000..02b3ca624 --- /dev/null +++ b/ch02data/hdf5_example.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + / + + + + + sea_level_20191012 + + + + + ... + measurements + + diff --git a/ch02data/index.html b/ch02data/index.html new file mode 100644 index 000000000..3d102672a --- /dev/null +++ b/ch02data/index.html @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Research Data in Python + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Working with files on the disk
  • +
  • Interacting with the internet
  • +
  • JSON and YAML
  • +
  • Plotting with Matplotlib
  • +
  • Animations with Matplotlib
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch02data/maze.json b/ch02data/maze.json new file mode 100644 index 000000000..887639af5 --- /dev/null +++ b/ch02data/maze.json @@ -0,0 +1 @@ +{"living": {"exits": {"north": "kitchen", "outside": "garden", "upstairs": "bedroom"}, "people": ["James"], "capacity": 2}, "kitchen": {"exits": {"south": "living"}, "people": [], "capacity": 1}, "garden": {"exits": {"inside": "living"}, "people": ["Sue"], "capacity": 3}, "bedroom": {"exits": {"downstairs": "living", "jump": "garden"}, "people": [], "capacity": 1}} \ No newline at end of file diff --git a/ch02data/maze.yaml b/ch02data/maze.yaml new file mode 100644 index 000000000..ee6c44daa --- /dev/null +++ b/ch02data/maze.yaml @@ -0,0 +1,25 @@ +bedroom: + capacity: 1 + exits: + downstairs: living + jump: garden + people: [] +garden: + capacity: 3 + exits: + inside: living + people: + - Sue +kitchen: + capacity: 1 + exits: + south: living + people: [] +living: + capacity: 2 + exits: + north: kitchen + outside: garden + upstairs: bedroom + people: + - James diff --git a/ch02data/my_file.hdf5 b/ch02data/my_file.hdf5 new file mode 100644 index 0000000000000000000000000000000000000000..0cdad28510994e17d6e51f5a62215bee4f0dac8e GIT binary patch literal 4816 zcmeD5aB<`1lHy_j0S*oZ76t(@6Gr@pf(t?r5f~pPp8#brLg@}Dy@CnCU}OM61_lYJ zxFFPgbaf#?uC5F~l`!*RG*lad0Skl$GY2LRk;eeFB=!pj?KW z{N%)v%>2A!s6cLNVsU9vYHn&?30Mf0P7Poh2+)iStl;zuAwVe%q+bQ^^f6SRh5x|0Y-rgG0BiDQkpKVy literal 0 HcmV?d00001 diff --git a/ch02data/my_graph.png b/ch02data/my_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..024e6bbb44cbf9552e4234733af2f3ce5a8a9408 GIT binary patch literal 32983 zcmdSBbyU?+_da-OX=#yCB&9(*C8VUJJEWw$Q>2mZZjkOy0qK&K?r!OtbA7+-H}A~+ zX8s@7a-neVx#yhEj%Ppn*#yhWild{vM1epc=#mnmpCAxe9|#0yjjL~ zb|y^teq1A95o8+)b$bW|Qy=;lrckip3k2dLDk=J2$tCq*(b)xebLrRVaXxh=T(i9-L(6_Up_(0$6^x^;iZ{A?T5B1Ly z2~5@w2*Nc0=Q3zguQt}NRI5;Mt2OB56BDb6Q_rjf}R^9|AUE`@D<)rgZii+wjE-d_RfBSwiU@!2lA zMaRUPUS4iBZl^bIUY2P$|B_~E{Z?Mil%AgMuruacZ8B<5sphg@TF*M)dNHl3Mvsp{ z%q0{-%1@(G@`6F52BJ5b&i{9RCISWT?aJ2H2O%NIDBn36_~4PM()MZFow3(V4!d6y z5(p@zVpld3bg8(xZvrkJPP#BBi;KbdE9(xwA(cOQ$u^c zJvy3N?{Q_goKmS0+EdAv^3(e@8&Y zA1l-5MeAy?BGGPgc+Jh7RQb90H~7}C2(r=4_kLyO_4-|iIxedSO^*A&8f@1kWM%Ue z3uI>yq_Lu!4T6FaEw+Y|5cMn;o2C-EY_nH)cfU|jQW6H}zmOArs5u4uAp-w48jNTE zQ*WKoxUkw4h~AON=V}*!;kYx_Nuyeps$Z*N-86hSUr*%20t|%y{O)4k1oA79P?=gHjUGdp+FT}#5HU$T7Wtf=uZCv&`}?(; z9D?tTnlXJyc%8Iny!s+vb1p0~wcke@d6}Bh;L)iJdFU3%lC-q6fNQ^&*~h`^X9Urp zU-ka}{uw&D(Nc@6$J4`=p`qbeqy2VD-SI>LCK0FA#^eu02)#y4P)*G{x2q%1!NXJ@ zM-(I^q=5vc3?r{RsW_{qp(kt9x{jN(L8kTN+K8 zO5@~}-rd=8>ALxaenf|h)u;#@Kl+&OW+R4+i>m^-Yl_a}gY@ZI&zI#kesI^D;E)yz z4X1S`PmqClhR`Xq&F)|v^(``fw|Dc7I*oSz6&1`M!d`UFTelC#$O-i4L)_CG6ue6GWwD7JqU&)g7Jmb*qt7YLJ1y_-$z)UD>Ha*0k?qUxTq3J-%lW$htZT>G z48e*b+MTbruA8s3UQWlRl&UZtCkK}$-5*V9-TbjD0DX7z2Tq3fBRBB;t+uU>xe5u`D>tKQ*uU_)@v; zx5jeBG54lRQw|^Suo_i);AqGZ(nCX0wVRy~`ldIR+daTB&kT)0(4TnvR=2SAt-F;DSY@M8pZ_YM3E!%BYg|)P`30Vx_1)uKOL;^94ZvXDi zm8pwTP^+OWD$tYrchHr${QSE+5gZcov#V>pY^K_LhH-DEd=zYHXRg{LeCldi)B5ys z-rD|jP0Yj7)83+$oZMN&1R|E(AuTIwJd(=ous69I{&Tq&gzc28o9&GCm6Z-SOuon% z%8;N9x+WxfQIrx?0>%^YC@Wn~4H$LXh6b71J;Vnih&aQ6-=BjhvA(g<4Lnc1&4b$y z5uF?uO+pbq7E1<547h4Y7{r+-qv^5q>d~j`y^7^Jt-Km-AgPpS)P^`OdnSDQ20su> z+ofsU+SC5@aJ)dPCQ6Yp#{WMl_3D40i2pwC3 zaO&>u#UUn6z2UWrz=a%l>Lz%$wzT|9_$V*j+dKyam^?zEn1JQ~h6x-n0T_@V9q8+O z#lSFpz>2~yE-s#$ntE_c+CKjeA@sjNMn)F>@WJ3kz+4`#8!0I%JR%}+cLOKdn`kJM zkRa^??*>T|6B84J7YC=e2`>WPo0+{OVA9zDo~2Z#HD|@^-i8PL=qSF(_;?VOuWxQv zkB+RIxc!qs2pJg}0f8n2*ww3EK2GTgD;JwYf(R7Kr2zYchj*fZ2&WDoO_nSCksARH%mo_vuVqs(djAzia zYjXi`C(0vV2)?7cTNK#)V6J3L)zl-{VhwPxKr*jHVDTUiFsFN7s(S&OAL0PEV|abC z8b-(#03xk?y_L2`z16TkDuJCjC)g26oY%wcILKSPI*j!6VS7`>F;P)brI%2v0TGLW zifZY!ZsQmI2N-w!MT5!;!~SSZuhk`wa~ZG48dwb;bOLm>9|1!29ZQkM4c;9FW+>h9_JLa&E~)tI-jVJMr*^&U7I>aYm1 zt*z~g0ACPANf>4TI0?WclLZ)|y7eQlpdXs`j5!~^52)N}l2|`@5&SOj>FVl2Mn@lJ zEqC0XMkC@t^8q&xg3Ji;U^YQ#8=F_}-o>7roTQX-Qd0WI#>Ng^ot~a@YhLSO+`}Vz z!y-}7Z|qpy&Ge&d@mI@|w#nDzTH0N}E^5)+NvjmUU#NY@NR?}|Ylfm4{aZppa^1|1 z2bp%4f}$WMS)X#zEb?)?Jpd*g13#6Ro{L+<#?Gc=u_r3KOxC4|`{AaXjWVQZMRgKS zP)>Crm1KBHIZA8$@X)h&5M*fI&%NQ}KNJgiHOjo79?^)oB0)%A+fbremrj2hE6)7( zEsD4I6aD%9!$WXH#P*+Bb6^AhVPRoJb=&}3$;|LN9jGmN-5qNE;11n+sBj)nW7^(9 zNrzccMD!RCuB@u_mR5poe_PiHH6A_32dh*DgwA4M?vMbmg3ZS3vWi_6=7_VoC7 z`XQZzFlba}-uV@Q?{+6A$z&6)Yh_%VtY>^Y3S@e{O0}Ti;4IlR9=`J-wwCh|UI_GM z-QC?4PKO#AbrwmVdw=TS4L}k#KZFzapR9JVE%*K>TAULV;i6dS=*a|zy_;{Jc93XB zq*b+4G=rt+eZNL2(E$0O`uYumf!rL`v~Igj`R}?{JE|nG`@-`4np_TJNhq|5pC(H0 zu{`5iP1a|<8N=UuZCGGsD`*V%#N%*xKfoD7UPu3VsPHY}83&OwOE}ASqP4zn)tV9uB590|fB${FeW^{Kk?Ta(oKLHY0A?gZQ10qz zk&x4hSWe)ELbY7S=;8K)lVsTN@?Z{)OdvHV=ov^nyEElv!zrBG;6@SjzzhEFjK8UF z?J>{?ubM2=P61*69Dp4*fIOt)-uCQ_=djH;HZ;hW>+nyP>y|?j;ZTeqXwRO#;^E;L zY}wf{1)$5u*|~Ro97|lhm#7Z;*|UaSH*IZgTSv#?xa8_*Ru(BoqJP*mpSQS?mZjeE zKGWewNO<)5W*QVxBh#z$)S7CK^{fOg@1<~3zWkN|&00$J-J56;@^XsG!wtN;^e}xn zL*l^fHOaw-(+K%pgQM6h24Nf9h#@wcyinJb<4vhx^0G!J#1Uq+viw~8Q!|w9&oNJY za7o=siRP&xl54a6@Zhewomiqi6X2*<3cj^hU(PO@WZV?0jD&xBN>S^OVPG)LQ z2Fih$^z;!$IexLZDkGRe?dD6F`I#Szfh?bYLF60ljHqN&CDhc^j3IoEd${l4zgLf^ zRW8C+%>S4rmmwf3CUy$aZ3O^b`-~Te2ne=&lRr`y_?wk!;PfW)Wo|%iQZ=MmYlbJ2 z!hvAnE%wf0HnO6kVkl6uM1@XYUmuNx=Uw^WFyY8iopf8zz#rj-C+9;Ht$R2|k~(bz zfqK;JOCb&NRE&C=)jA{t8il#Ka1_irDg&ih3Uc#=%hK>C{F3r4( z&9R*9(yj^&9Weux{6AJyDdtT$?UH*8I>Dzfahkln#cMsX?_v%Lms{=gFD4F_U0Jzt zxlI4Q-sca`NSS+bw;FoybxjT#3CA~!JIj}BTzgl(Asutp<7ln_dhO_-&(~QarS9;c z2n1aQ&AMEt!#_1vi^(db8sm-jCZOhW>bi8?o9sUmU}k2P#w3~nWx=WlGpJbJNypQ# z`e6#_oo)1klzDU8w74iKB{kr5XlIW=4xyr;umPqybVV(jst4YM%cPUaYBa!Xw;?f- z&ObP#>y6&A(&?APWlL_|a*7Bo2mzYd0>Ec;JHvZKs~0#vK$A@0U$SRQ#!z`Y-fV;P z^t;{riEp``Z&e3v;M;6|yBgcdZ!E5QoDtEE-C>*4S6TuFDNbIW%?Z>9-tt*AzthgA z<-!jt|Bfp(^NDTN<}ddCV?$p+P6C%Lt$6~bEC#+={8_&3oWUfMVi-?eE3NiWtB^KF ztr|VNOEcEW;jf3d2i9%s0wbFh4?g#i?yAGsLwXYj4-{Ux-l}s4dc|py-kyG6s?Csz z>?$TqRR&4Cr)&hXWlFlx9)mw*Z=HXFn5SH{P>c*X4pLH5R(jq!5tEQ`+imor&ye8Yi1dWu9qU_fFx@<*AX&`U zeFv$Z>-<6YYV`tSWB~4LAMagdGX&CZTtRxh0kG0_Gfwq-`RP%7_jlgSUU9kU;_Y#J zO_S5%Ku;(EA*dS^6ci?lRU<$+*1FP@$l6+YMp-CgLw+X+HoT-I;0O3($Y=y7^L5?$>j#16HDNoiz_F=of>oacPf=`{za> z?$0$AGsa$0e$gu98VPr+5VZ^JF~|aN{hjqv>UfU0Vx5J0JiWS|>VA=Ky8u8e4OWjJ z=Y)XLZ7_kU06-xqDiSAkrI1bmMLjUz?dCsahRUxVrEkYXFc<$CE3O3Oc<}ul6FFFD zJdfWe;c+nSY~4uFm*;;2KmxWXgV{oHJQ(kCU+gy!B}D}ym2e~MLjfvaR$z)Ume zv}(pWc=e&uVwAfst?8%W)3J6_A}@I8{SR@TjpYW&EeomEZP~&@29>i%y7Wh#is(|G zUm*4}QdK+#9ir|2`kcT`64M>_ojS4d6n^Pwq~-@<$7e^lFB)0!vM7EaVv>_TLq*L2 z_5y|PAlftPv`Ad+O@Z2Cl%yPreTLHcc?VmdWep*l@pDpAS%4y;H5=4X+%FIMTP8iu zHY9<6xodM;+c~3%LU3{L7?2&CW~s~(MXmh1+B>pzEfRtw?#)t&wX@E0D42@uR;zyGm!{9`bg6D03KrQb19l>EEP zEqszcc1r~9f|oHwah!HwS%QDiFE|DG&yUDw2ccS;A&gae(Y}mh$X}K+^n1^Yh* zwuwVVCI@0OD0k&d=j#{%Vr^N!xHy=L;aqa@>k7nFtTj^x2k-NNfrD~Nrem2f5bO2_ z4m#B`!;_WHr1?ZaZ-G*c+5wN}Vn;H*M936^m3QggOr!yx%UzGhmU1tjdWMP290T0= zvy|W*Wi5MIOAw)!c6uq{wMZtmC&+j_je5Cy+VsArm_#VEzByPK--A%akAd1it!|rm z@JbBpJi^LmApRuN-tRZxRK>eI4~NuYr5EC`jhf}{k30Zd0-AtQ^H5Py@rPC;>-pAj z4zSZXjSp{p7m{ID_{Ru#RoT2xOHt5og<47L1_uoYpqWu`kPzQe zP{iSWIcHnx;wty^!zVr!>d2KfQYsZSmpl`-IfnyQ!9#)Gv(|0NkF}2PEGmVnLnwEa za?wi7;0xcF;m|-H88|rNncA+uV!ipWvSM?0yawC^85MOMFm;2M9Jml`8BludpgO=! zqj6PYI1N{$n@8}Hf{-2&!Tskw3N8SK5FCnEe554_&i@zb)S zF)skD_46YYdi#WoI<;wWjuk?Kdyi#Ku#KWh-70VJRMnj+Lx~)r*g#cLNI*IEE~` z8Ub-}cR5MbH%jrHB&u^^U4UL~dA9c#0 zM<9J|M7viMPHq?d-=`SkmyoMXLr2RnC5p!A=tMSi_Qmh&C@=c4*?&=&8l?aGSX}+C zw`a)pO7+r6DD;d^ehw1}Nn3!v5WxK;0^5+e)dE%~O!sEqiE?%BTXs6}O7bStJ2A+P!lnC9e31#n!O;I!mV>pI`nObrurb?zoFgcYfbt7$; zS%t^4E7gNnnBVp~jOO!$`4VEkqw-9)ug;P%%+|D!rT=&oO@b(ykK?*ULJ&;F?1vYw zrWCPbuT~l@?|#sEH0?1HMf@RsZe#0^DK0?>-95zm!E_F%i~0KD1VP~G@R>XLI)r9# zubXXn@bmx<@4zXstvT<%A9d_t_GM>srFRBa4VQ42*U8wk`R9(&IpI1IdJ7>UC4A4( zeX4W(%f9;&p4m`-pVZ$5MNPdQ!L`E_S&|97CtoVztqr9cCquE7SCu5p$rh>03Jl;t zKLQ%OYN`zs&F`!h5><6QFY+z_m#@T%j~sqoUJiUiXL+0!DpXC7H79R#@vgS!=Zdfg zYsQ1qH|zSe*Ii~SdcFlji01}_Qn8Hr<2)PID#e}UcRAOFQ)Hlifd~Oi-7;PDmShic z_kHHPJfx*E!|OH}K&&)BBury-rRP2%A)W%F$!4t^xuvz$diDd%$@Ju0%!c>-i_C5E zovUg~+^8B+wL6%wa?E+-KOe}Vc%2y7{YF(DI{nB>^&jIW4Asy{OIJq+R7qF5MjUY; zH+V2G@c&F>81@Waop&nMo?h8eo(-@EFf8<}<8W5okUgM;7|vXuuJ10ld)IZ534xGZ zs8&f1sM7wP0TfOIAzH41S|5|Hm0flD7FY_MZ$@2u1AaCJt#ea&F9P&Wx<;z`&iY?I z5;%P`|3dhVX1y!BQzmJ;04q*vucT-pIVJ~gF*#O$*c|iRZNJE+iKp^Oi*GUCYRkWI zo*O5|&?y+fPhZF^n|p#p=e}B_Gz>6kpdeb^+mlLSHF5!LGR$nVGgAcL*|VdgqgWb+ zukZa3qaq_C8xNA%%}syjB8!FKcJ}rLZVji{?@iJ|ph*cZCa!?8tgdmkwbg&PZGUPO zuv*}fil>)!aB$da0c2__XcR0=Wf90TWqbl{9Qkv}!pj#H4+BmSdVh5i9}-LV6UCSP z*EWaCSeO4eQpG#RADQ&-s&zQs9O)hwP1LFQ1i-}bZXK=lMF!H1w@MNUXgemlVinvN zS>v{ov~*N646tpiO5rM){N_xEhkrt6HJN|nL1LWqPWImDi~5+t@qJX_E)1VR!v>&2 z3=$qu0|W9>&H8-6MuXNv9F$!=s2*i?*pW9LP8M@>;{imG;Yx?^P=;Uzlvc5ri@sS- zK_}u^SzUb(&<9)n1%OXbnh7xM!=S&w4H_eWpD`HE7Q4Q?GpRENS9RR}BtT0`+t5@L zn$4T%4mtKcdfkVF&D53r^s|moPpxwt?}!XzIMBEHsPbkou3Je!%Qgr>a}-j?$REI` z6%4~Lu)bA7bWLeO91^mfmWMR!Fyutb)iL;8Az~_1+w#E|hO>d1fxQxtVW3P{+urU4 zG??qKrBt0Gb4br%ny`sI7PUWd|^CMG6Qu{53Ffx4V@B9&`iL2CIwv*$9FMb zOC^v^pMove#Ijm#zs91YmDftD$r`(QfT~`{= zo&a05QDO$8GvvwfEe`aDHMsFtq$_Vjdli$lbD<={cg(c7>Oh8|q}oFZ9+#6> z;38`*=FS2#rb;zA+3sNg046S;8UXzu*A-u+9Z>&+=eY&ye@Z}mF={tGfAQj32cVR# zu7$z?4GyZ;JixBRGHAvFOE_raI6A6ZLXM7h80=gGOz$VI>lq&bLE&w}6~bq#vKCV#(TRw3iv7ITHyfFHRqH3}Sv4;;-}rm%FJk=l2)vX9s{E3 zE-p4fYmrmS3KY~3!1_GL!LeW5_8vYzKW{F$J&1$N#3jW?o!o;LsTZEes`G-wWb~JO z{hVdWDjmNqKDl#t6KR*(_6?}OK6Up*J=K(t{3g&GYtXW-9>n2Y-8$%_i%`+utUz+9 z2qF7Hs6tgDAeAyYQ@J}!Sv?rT3*`V778e0ZYCft4-dyc|WrdA{!?ta?(0~ORM@1?R z?jNsiFZS6$p$aTZ6ojorPHUa5`@+J)ec=_row|VHDv`tTHTU#4O_y#s22yUWA%t7Y%)9s=E5=i?>druHE6 z13FF8#f7UtHq8J$0%C4Eiu0@paMbnv{lQ<~iP-8BqN3h|j)5uY{6IUOpj~+|R~-l? zqolR>XUYpeW#Z~akAkxZN8_L1Ewj@OTPG8^ycEz^%Dm}idWyn#!o!7^UhvBx=98=Q z-21k9{pJOdox@+va+BmW0Z*3kxc#2Mj5umxzo_&l*wSIJn4EuUTg30*6CrRZ7LfrE zY*K{$W}#3a$Hvai0MG}(hr>HMgg_z<3k*bRf4rpwO+(i2M4Z36y8M8VpY9f9z=A;4 z2|ApK{O%l}vqZ(r90>@@lLh-xn3bI!iKoYhBwlC62>#0#5NMc!9)FH?o<`o+*Y{%@ zPaKG76Gh5&fM1ukvtt#!KY;@cLFR2ATv#`hYtkLsY*>mT`uj5;;>WqmHbZn51MFBc zJu7W)y(wSQ$67-sg7SR5N}=WaO_x$f<&dH~^!|dnv@redD|a?=oG+SmKF++;KkH3b zZ*mSkyu!)uVD_=#m#x&VjY&WS^`a; zY^RT#w^LZEEFJv?X(ItAwblE2*!70Y1a97=EgY1R%U=3ejpism2JL2iFd}tA7Z`x= zojUFus(Ws~a4=wHI^X}|&x18TYJFO#f$w`vrIqc3qXMra1LFiLJ2|aXVwcVFxA$B% z+{zF9jewP{*0uh;apf-Yv47b+HW*h4th^i&TakwBVTaP-_I?9DaRIh&v}wKbx4pDk zQ(S}>zi0yOYHGM-c5DRiInq*Z`=8yC3|vNbzm^+YQ~PvFeWFqK$9T3HqW1K|0;>fD zoA-IVj@Q2gW@ZjN&dBy6*WC6gr&D*O>dv;)|az` z<*Hd#QbN+?FXr8~*S0z&Ikx&?2jK>Hh>SnVUH=8*@-f$7A=y<_Y_e}eARC!ek;r~B zjB$dKDiEG<+pcvxb|p)WoS&Y)e)Vc4AT4bFs@Vsj15kgbAk$&t;fXCaIRcY~l}Ucr zs6QV_>J|i2o(^X_Ab#%et4rBdKx>Nyq;C3*daFgAdH&FSRHeQ=ijFf*y&0|fJ64&j zbF>qbfm)d(UZUPHS6(hxZe%1>kjqd+$bNjw@%te8{LX3Z6FJlHa{p4bE#$AIM!a`z zd3dp6XEWxsS?wOH4I{Yvk(LdwH@6RrLNGR~Km9>FI1nmhh>Uy=`r`Hgb#;R@O{-qD z;duBN$WQLc4nV7RvQ#snyzMpw>=337@`8k9cqoYtu<}h!pGwp&(~Bkv`eYBZ(WC?4 z?x}L zDER=G%cR*>7$Yf{M6D#;cmH;-Bj~KnVAVWl>6_Uwop2(tc;8i zNJ!rT9zCafIy-&G#^ga8qq;U6nzI0s89KR2U^kzZuQHS@QYjrc9~B(k989oo3XhG& z1TK?Q~S zCzz!wBUEVGoHNN7W^D)jK2${T&ENlYHIS?dG^U_Jh7TWpq0g`Z=m=^kwt|(FmF8ty zT9?$tl}fp+2?rZmlmd>9SfmxD%g1LoPw@v~x$C*#iQ8i(z_UZmFdyhZdb@BEV1(6Q z1R@Lc8<|LNA}c3DTCF4L9< zv@t`1_fg~aGfYg-T zU5wi#wWz1j0K*Qp4GcHj&i;OAPR^V0arNGSu<&rIS1Z$3o`9II((8mpM0;T~f2dR_ z52LHA8x9mRU8A6lbb59+z#5!(RtgW-m8oJf&umBcp<~U&LJnhDSV@a@F+acrpkxgd zW8?N;DBk2(E8{O8@BbR079qIU(Z2R4)fGFLx?O*XEWCja_)G^|+bsRA0PtXpMnF*q z{mT8uprx(V=5Z5{_8~;8H}9FlEy^DcQ(n}DC->{Olb0QR=ob8h zU7vBLtJfXq0W>uxM=m&`fJQ6{Tw{UV+4jw18;sKHu zr5~UCVc-zQKt~?53L}b(&7m74H#+irZ=UJ!St8u8F-?En<i~UbfrDqGql@qDlJRB+F!c!k@?INTi5Ie5TJTqUQ>B9?p|D1F60H?6vI{Q14JPS8eUU~&A$9YL}}egl$;LD}gf zLCW>R7((?RHj6)OW*>i3YNT+)mz0~`(Nm9J0p%2yh?(yG)6pmQM%VXO2xAU~hYx=+ zSMGcx^IsQ>o@p3)%Xtz1PQ81JOZ}QM4W_!D)qO$|EY@!iL}*tlJO0HE_XeMhw?}j_ zO@*(hjLNKcR-TnE2V&q@rhPcphF^C0);;K|QytVwr_$|To`IZYo}*WS7RnamNAVgiwlKQwe4k4gM^5S*zWmNnb96obM_~{y; zvjrHy2rgTTyLg_a4@L2B2@nYfXFGGbee5?ov3L<{2H#&0Rgu(kg*(o1l=1$=RsxN76VoJ|U^U_buo@~g6XowOXH)?27l>jJ zY+<%Uh?`$;-v`j+n_iMTq{DqTMDS$m6{+54W?Xh#tspWZpOu4d$P{7$aua0=0^cfHw&{Jd~-@$Ui2Z{WUl}m!aL( zq_F1DDMR?Hiy;QL^3yAE35AiQKgBXsf(j_F>@ig~VhZ-xcJ%c7N2<{hOWR=cY~&%h zH{Q^G*OR)Y#K+-G^&V_`k0Svox28=?ESOpgPlWhFc-sQY*UWaBOVh8&AyD?Z{FdYA z;#?GHndJ7?3~R|suz=fg&xX*PVJ=$;^hl#RMaS!pwaLC>yW{*dF+7)Q5EAEa;^qGd zeN_W(=m^s5#aV!l#d~El6+J}sLYM{9#7X?uxRDgsw!uYaJgye-#91Df^oHAKIbO4Y z`a;Kd1FTt^o?2-~RT1r}pSlTd@8R)C18PJQXAm&%O|4|eor*yDL+{l__IPIFTcpkz1up2b#^lR(Eh8Yh)~Z--8sL9+4PB4&zC`Qr6B92pjtP))kyfqqp8T=eZ;7E{gbtO z68b+*zrdYa0mbma0N^;?yzn~XV^{zvX4#&jswq4$2kR7^LR{=n)QI(GwAAj=ato5R zu*w_TX33mKUkBql)QU-}p0=mTh)^SL5H z=D(To8zLw^pLWD_B(n!&ZNhaw);KORH_*mC@DDT6nJA<9>x6bTJ|otJv*#5VP_8b< zuH*%54}eyy-#L_Fu61Xzem%IUy_P>C0p$OrthQ|Ke(W%`(EezPi7((xW+aWv(p<8j z09;rHwoJ#y>~F8}^;hk$6q*tP=ZrSXSYMBAi`$(0_A#7X;6&nnhF;GX&_8RLuf&J2air@_M%-0s z;K5RQ%R^#?ccGatttqMS+VE`x{lM4lqc>yoiw9L_AATT?In%nt`q=ZX?qi_%l2h$^ z>Z<>8Eb{Atwdcs#(v`k^3oq3MOvisDYNyyC}ZGngGI;)L)22+ZV;>{~-gl`r`!TqH6T< z^f4)#)6ex@AcD#ba9q!wot=e6L;&EFusROd$nH{5-Q{p(OThkAh_%zx;(Mi#U06J4 zl$2xWxYLXl68mkZmHEzAqQww;5xf1jc{e5I{Gi)+SSO*Q+Y*QdK3~BX+2{XFK*~wx zJ`Q^LKwo7d3mCM4vl}qs@IugwPuJ^)66jPx9r+E6U&N%Acuk^YgI3-fA;RR7;y_y>Mn? zeg}L9&=|!6O*)YLc$-ZYb|&b01p^L{&3LF`mlfERkB`q#|MIfnIUC67_vfQGqu!5Q zP;3qKh}XCK<=oCk#DL8cAw7uN$c1i*#gYw36o71OxW(kStG8>UYgzZcXjVAWS@6tfTMj?BqM`#yMDS7d)>9YS`y^cyUH)K>iB43kwcufX@eVXOpCE51@kEy-|h- zGSfoEKPlX!0V{{A7EbCnq1JhVB6`oY_zat=nOC#tl!IsUE*)W$CUXP;kLNY(C~E@S zg9}tuI$(|hs6**ZP9RXpXh=xYDl1@Rjedm@-UGl7Xa%I=DXD7X;h?ZE_&%*8*=g?o zDp%bX*&D?JbIN{y)_{jz1?}N5C!Fw!lj?PI7B*hia#i~%yw~FUEBKJaT$xfKDu<`L zk1PA3z)7s}f*>xO$sCqapvoy!EhohkxPGP#lwS5 zLXu|W<>2rR1_owMBe1%f?P7msdq-h@Wx<=lnR?&B(XnT6kZzD-i|fqfx*24Y(oZ@= zgqeeFb6cAIPE;r5NFtMRnL%IvOkoVQP@e#M3cHro?v_}hc@8rM#Hj)3M4$qT=N<)W zWqkY(ppXMJL|7Qw<`(`Y=$&x&jrOKno0kQb#}xY+UEPo#w38Xh+!_L1xk$PV#qeDin+joq{GE zY#sHtKy;#C?#HdYz#F=kTHOI>f(h6;hlAPiFzr|iE?3$p(g}-)du7ZTe?k63_XMa# z>tP87{mH5H6Z`nrWifNe3wGO@yGv90%KSjDUs=|eAW|EUW}86U8%D~XTv$j0`XIeP z>c>j9#^rhY4gsva_?HLtLOzGz6D5S*Y-)dYH(%7AcXX=vbD#7rC< z>CwpnnTNbwD<@kQ3m?raB%>fL{3MEK>CE*F2O_nQ#BEVEo+V=5O8sT~ElrGz$ddp;?=s8zH&O z3XoWj-G7+HY7;0wLB-8_HTV2$sC%MzyeG{(ZqqQFxGGl-T09z}dBDGG0pg2|&G2AqH&#p5P}q&CWy(eE>|kS98jy`)G=GJgyCjPqa0F`mVN*veRUZ z-vUG5RNr!DsnA+i4^~;T^9uL&*Uf0=|J-wyVG!@uQK!dWa%-W|ei`@6tOH>0s;v`H z9Y@1{gpof(0Km6atXbXh)k{zfw(U&>1sH`UX~#n*4-pRqD_eD*kV~E>7u-Y-V*UL5 zE9IuLJP(C_v!Wr6&dGM44jLy=BE-UGCIVsdj*+7`ScT}zBP-uY)8PeEd1u+p*7O^L0?@>`&NhKNDwaj#`lY#l~Lp;gGUVkk9r%>ja#*1?vS0C z65{f2DjbdEjh{iSx=UAh{VbRA=Ys6>*5!Y-grpqj7TUAU<-xc&1lT*A_NZV+1MKAn zz}t-LSN?MGvMN4h{zVQxym4*{kyspM_hV*bI)^7m6p2L1mIP(MKdlYYkWNECO8;0wd zuis3s^WSuve}lzhQ@Xq|RC%JpwY8B1T%#p|P6m!w3r`RQf-p z{YZnDN!rm+#2#G?lw!O5{9o;D`;|`UewI9bKXtI@0BfN43_vUIEp!v|m3e{4vH~oz zp|K`o2$@3>Tt}>9?~PsQ(P&%=E`)N1B0qP+ds-!2`y<8J{C8_`dH>+0q}5qD3_pS2 zzewffLR!*O)|ZEzcL=n>_v#VoF}XK-a*9BP;AU$5e)20SNnS|B(im=)EwEyj|*)8T0+THV_{m zV-koPUs?<2MW+KZaQ$IN!^hoo={AU~_EZUV#!5~NRj5j_1QE(=02vxbGetiVIE*+L zbD5XEs=(C5f1(KtG@p+ zVn^_Wu_}c@0+=g;Dxaoc`2Za(d`yU?)sPHuqX9O8P(K#krPyf(*(EIV-&Lu<<~mCo zAr#9MYMBoO@`o&a68;GMuguD!k)P7q*`&=YAGxzryNj-7k>psE`l#m8=x zJY07x4}rJxeEzyEYwE$Z{4)bR{`X@#&Xohi#jW@K$#u4V+k6PY z2}5O4-HdVP3_pi}DcaZ?u8$7_ux(YM;y7a{hYQ&(gO-?}#@CXOEl`*<_;`?Y>bc3W z;F$@SLlU%vh@UNtFIi=~DIBHlPK?R}xVY7TaF_4+(L)IA1M5L!P`fcYQEHh`6O2V< zg?KBp?afQ9-$`A%AOLvK*~*o=Xhp=zD01~GIC0hYt(V8|JjQ>-<6v#C40r&5Gg+tv zlmZ{yJV&=CXx>z3eEAv?7&>Pp`F>3m!Hx0Oy{{odXVUaFIhAX+4(;Q_5{KbJARWf( z;-v(z(-!$m*cC9V2PU?O!Js}U0xG~%cS>Cv5RCv;h9Hph=crX0fGR>~rcJlq3tHSM zf@Y7MJ10ubUo<+Vk>$>jiAreW_AZGE-pxFSvk^6qsB@yabM0C4w#Y0m)F|rHTiap% zNDCnyDa(iY!>g!NsuYc-pl$-weykqXHlQTv0$qqgFxUZ=3;pyb@|K(dgNIIVwGyJa&n1N zi~C4n!hiDeof{(aO0%zxUCtaHa5MlmsIJU}z6&vd5uD$Ey9QqX9^3c|iv?lQZUV!p zh>wqt>RWercQ~L_EOl(T*-C*&Krlj%0z=NLK-NPArZd5;Xe4NH|E{ytWM#Elcny{M zfZxL~lKR!p@7SvVC9=YA{NBBPImhE~TX_iprdgjWg_CO;()3G zflMDN83fJQ1snc1;^7pGjEwd?_FErG4}D#o(t?e4_~vQI(um@xX~?48iHWl&RG$;W z_S-TSuyq4Q8l--u-xJ@hvwA9tUFj51;VvAfV1Tu`G*@2+OvWAvH&ZFPC`B1=x6YRFpBSm4l*Q1EW3Aj7}>`d8^6PDUTZ{|rt3YI>d1Y+;zqr^wi}E+|7xw_v;}3B&_1 z+OY-3p!mR0{eQ-rydl6Hx}bxt^WLB%ANGS}Z6eg#7hu7#}djt>gXR z$YH%S)E`3)mHY=&r)Pp$Vlcb)C)RU_Df-g{A+tFh5S?IYEu;}Xyj1L->Ib1d9hc60 zpXN^ih5+Y6gTq&OL-uHK7bp_ZxIvM)J(@9UA?SK$2*ew^3yo|*OqDbNagcJOeqrJ^tV`P$oC%VAe zGUWvFlTc7gEDQXjqlX#y%W&(KkDC?rMj%p0cLu{Opcwkz0}DhJs@F01Svr_5*MfC3 zgUdc5f+?C2L1|TL^d2WlBpOO*l(g)AULbo?QGqMnV5EhhK$ee#Hga?^;ZCO4*}rHj zlJH9loOPIw{bR%VAmsH)+EGE;x5YU-W-#emzvIC_ zpJWvlU%NK3R_lIl;ZFr?P~6=Y30>W+?7#-bca}eKG?QaBPk})r9cyhopZbI&lg)ov z@jxC@!v<93<;Pc5kA0MUTEt1?^(>t|`~c@|jKo7cy3qCMpe-xll4?mRX5vNi-mtQW})849T2i z2n|AHo{AzvX8!Yj+yCC{|M$1Q<2$}}9BXwfdEe)G?&rR*@jTBf{f4o%#XG6p_B&|m zYyaX>p(0;w@KKGnbWCSXq#{0P0MZRMK(CRJ9NI`SFZ6ay zOo#`?Jk%IJ&T8N0C`Nl*mGNi_HJY@#}yE`BWtE+cCx@I`-eo6!X-sf^N4c?)OZNSI z+O~NUGXwL3eo8sj)YqStL}xl~qY3sXBKz%&(I+nJ>7u2ZpG+|bA$;Hw2Y0%9@O$FU z97KGtwa+S}_+6~te3cXkn;?9^mL0pFz= zCWQ;MW^0z+h}u&WeIhsF=FZp;ZF6ZS)C3zEdukT&3Pi>wD&7DPjCJ7d!~Z7qh}|Hi}k^eQ6D@W zcD_XYwb+CAIMPz$BnmV>K#}zSJby0SZ!w*N)J~F}%RXSs9-^qtNFDYQlsjZ@u9xfb ziMlUhveueT*l9VQb4SlymTK`W5zi(@{-Ah_bZjFr{@O6r?QqOXSy}G`5Di3nB)Ev- z5%*rl@v>`|kCnEr0(B?uiRqPN+c(A-N9dIFbWrqkNjLh-@~9U6fB1yaP2a0(eD{wy zIa`2@@(-Wzp=y5)n?EEdh`t7Z;#m;5g60R`7F)jI!da<;^An{gjm;1Har5xJ1}XW` z>Zq&tx^H+|^$~G|rEnk_u{I|&9_90f=l6BdeC74mneSq;3QbCbj|t_DOsh`1X^XfGjz7y0^HQ&3X6k-R_@EGKarfGEV(4Q$SFSTb#R>TG?$~T5N4R_&#ButTPLj zM9p{CV8-_61t!0l1y4}#rwXX15`9;`#ys|6^@G(zPRtKfnEjDAK%XiYNheA1JPBj> z{XGUKJ5}GJUCadW1KKXN5GX_P!UzWN1N>{0>)3heha$)oMD9YyjH2TH^FD-9(9{SA zQOz=&NMLkVg!SX5MMXpcMYONfL$WF2##qJv4lKo3Z6+NBz>rY47j$r^uh~@8%z&QiFM0@l5#6wwyUI6^{q`OW zr65DE&pme%+7L`lrPhCg%BK1NI){c0$%LZEe&!FWlU51oM4Iy)`k&rcsr4KDg4b)$Rw^yxlT5LD3fMe1JE zDc9NAY50W>0sq#lc_i3K4XQr8L9YZ1wwP3TfFY-*rX=<4LtaI^me0y@+O+8cW6IPo z-QYU_S$bAhE`M1Yz;$blIv4!MCoP;t&PrK#9!i=~-gfkNAv-5WL^1Y8Nu|KDC?{+IX`Plx7hQYQNoQ9dTlpz`3?gQ)w5b=8lZ z0L;1Hy{wDItYLs&&4fC3Ua!=(8sJ&hlf|8`<+iJ|Rq{%Nt2>X)4s;*ZVlKi4 zB@{?NEpJMjsymzDbv74xDWadNY|?59jV`F2i4q}{IS8T9(%O5Q>S}5hZ)#bb7><4V zv7uqPv~(I4apaeyxkcC#`sv*Msu}Gbc6!AC@uKGi*QU6(_?^L76;bZEM%zJXE4kZ- zrylH$B#Mg4M?Wq0cXe%t@EJ0+>buscfPmf)M6aR|VdLTLL6vJmmJdW5AiDDQgP6K{ z95kzKXeNhHoV?;{}sK8uR1a(0_XX0Tk?Mq7G%$%*ZKGe23%0 z5+546LIFy@+a1JNZ;kWqD7Mghs#BX3#5ONDL;7;b`(t*3BQ=G?;q<(?$mKsIjnvK} zHEC>rYA6pPSOtzCQF^+{E8Pdzk#O``k;+Z)?odUKU*){~3a6KWc{OG<-4(=W&z1JJ*awR-8IbOouR$l^f*^1>N6MDy@b*o_0F80yN=v%%`nHk;e6QDKJlxHlHwd(%5?elH* z=7B%tGD3R1X69?(sQIg_s|#xQ3i<;tZ1Li_bHRG3fJr|R0@`i`W79O7?G%X(v@+j) zb7R};`u#ODSae|00g2cdy0Vif3ugNLq9=|mgI;xg7%`wc4sCwKA%>2Ihu-0f! z%Mwk0u;D1_kR~zlowkHDTDYIhMBKBzQ9G*5wz+$3Vcm0B0TrL{n3qZ6nvl8!A4z0% z5m+w$kE`dNcoJtW9CvGVrpw9r{jOxyo4>(ha7*@my{R*6>2d8(R zi1#O%LaCcx!g<|g;oeA;A;mX+Bq-InxP;n)*3Q^c60Z{@R>;d#%Xt zU09IUtee^c@8|_=UbW))r2#tBKtODzH;k=5KBoj-L;^S zOmp&cCr~tT><&Dkafv%KxYiNjwn{CQ@cuI3qd6<3pJWI8j&Cwan@-!rgeRpXi!^ut zy}aXMN_L}00sSsNd8Et|H>uARvuo$J*i^GqO+w7dMJ?o8t`il%N(adm12UXd8cdIu z%1oXKQ6q=*Pzd#1@6wg3pAh(ywdjiLT#?1LlMo&Cq5K6|G0-ABJUu(RyUU;-qNAge zYdOnn>KzU}*RG#GbqVqN+k?oEt5>T()LLX1DQ+@IU!CK=OG|W?Brzf*8;G*i^w%{% z?qFGT)?6Q^BigmiMSdD7N1s_B3V3=V=Nfq8U!Tu|q@e_e5%kbx!5cXVkQOnufX^E> zx`8=)7b+#r-x2jl{k#m*IU>bSP)K*oK`D#BP?+g6D-J?9ZLJ00bWYBDc&I~3JK3`B zzwSY~k%HC$iJ#WW$wee2B-lQ)2GN>;LclYC&>^LTzj)o=+pYQo|H4KG0=L%w@o)A^+j_ZTeO!OT;Y zl4H23^(L>BASu**yaK+@WAdweTE6XEt;+Do+qGeYoe9FPXJ-&E-bcrSNRyY}UcR}u zsv1&HKK&{pA`#p+u7Oiy>7(rN5ybU}Vb!f?*NOeGD3++KmUF+JrdHpN3wSMYk zB@#6JM3{7OMgy?~3=tC66zDM=L1N=xy=D1^!$AH}@o&-badEo)_OSwmlw-7jh!0ia zC3<>#P#+4+${O3$5`6LE2>e|{L_}ap${<@y?#q{L}Y+|2xY)1{z-#P+&n zhwE_Q?^9&gQDz_?+28Np;s#>TI-q7C<@pgT3(CCA#SB=zq;rvx`9~qxSQ`asOKfUcCa{L5C(hO_=tvNx@dM z4$Q&hw{6Z0g6|{*e+UuZo~9pxe$eBi&sLtuwxzM{OaojDm1a3GK|!tpf=pz?hAV%u zo@iW(Sg#j?N)`yWh+{xuc?=R#kvKl@pZh>)b**k{y{ptzV>UyHa_s6~5#)qWFHRE$ zel6mQ2k3G^jkNx;zBn|IK)7ZO_$Pyf}&2!q_K<}GfQdLr0&xi&GY`s zVdneL_o9}lX|4->a!%k`*#i!fL}hUjdWI-1SK-#7Iag(N=Qo2dWZ zmwDVscYML~w?l7{`Q&!$&v%obr=R)740#*sruySvrmqp(XpTlq%4DJ&Z+dcBj!#Do zfy{(^6T^I8#>vrNT&($J(C)o~o_b~FWyI@E6HBXO-rHm0C6{}6ZLS+bp(mF`9f6kh zpq>iV@94dpD0vkgS+nqT6TQt_CC+?c6+{j-dGZ9ewO=@PCfA$3&+XF96!L9oEc0+? zFt$BNoa7>5wp(eS5FrmA5h49zPEM~*(!*R5dCw8Kt0LtoKlMYz17wIlSgD!giP|b- zWa)><5*gykMu{s6DR?}x+T{*AH2`_K@&TZdRC)HHgQ4srQ)mb7=Qf)cC&$Wj5Ai=H zn+NO!Wa-T&^3x|ak_A&GqV5)Tci4g$+tLdbrZ+cby@f3i)gvaoDL;6TVSgMQtHG$C zpou-Q=!k0K9fo4dY3+diN$4Blok3<4m#U(UxQS^jm!4uU^S8Ge0K;=5J zRu}N@oCt_prMMMte-pu1T@5`e5)6r$Qknxfu$IZx zNOf7U3#sbB@RN8Nf6$tt`1Sm3<2Cf?0s{l>b6bffDN0iW6YMaB+^X7hD5HY7YV=$w z-q3I)-BzPl9-O?qk+u!I!op!WIf@XZMcnXpnw=Pjibfy|uE((+`LZ2GLc(it)pFSj zzC~lx{Kcq3d3$@yLC*%d<$wmkjkY>9BNpfy9NddM2n;~c-MjtT?+Zem4PIOxzlmf4 z;;z=nK@^Rx_v~Rtd)rV9@%gdNZ3BgryR98zz3Gpn|4;#1w->Ve!<9k-POe&2TwMGL zue9#r!=Ls?4!%!fgm_LT5CqonZr)?>6cH6{tUkZUzu;dpnR9&j@BLw`9H{-qU*Xt6 zS|!ZEegWF^Jha-Qvih_g(C1C{(&=8*L#OxU-~`7?g%G?W{d_mO^N^5(YS6ogb$cO@ z(xld)_BT*(D^WvdzJ=^-($nu!alH9CzaY(U@2bkbLJMcUr$FC-__p1K!;dr}?++E? zRUWEpYOzsKd>A~CSx^9iGzFBL zUJfnSZQr|l084E$=mux6v} zJpGfb)8XE0O&K-Q)!vM~^fMTTfPH@tc|1|o^(^#*<2YKrE1@EOo?ggsDT~+kFPm{l zIY&J;{HJ4)0&`Yg;vTe{r%ztDZJNPEb!*62>hlLNDBsdj4WC7meM z@EKDr=_~HT!ZVxbI82p_1gzAS97`gtr=P^`>9jFFVw?B9bdLsURE;f5#!AE8qSC}y z>$Gh&WSd!}ZL?h67qyArq%fy*tG4JYF|n@&*KH`3-IH;` z57qlAPc>e8%_5Vcre&{~zaf0P9=X4oBcC{CHU)hMB?a(#CBfORFp12Yi5-=V_mVxK z5zs4q@wryZ+ck{7Fr?wz|2!2V$WqRnhleTrk8xUurrwZGt)p6Bjk`vjP_Zgf7f(ERz!$Z}ea zLiX6?3rQOVk1qi#ykULTM(#>7kwoi$N6J_JnT2s{MOhW|<9kEe$Wl(^_5j)^N5;KpQ1ELOPep9xk`hAbg@Xqc1jSrc9quXa_Vy!LEtlOIPtx_ zKTT0+fN)$ed=)OJNWI|5IhRWn>KELEFO+k-)#bRZ^x6D{e3a@}C0^ZOy$C1QARv-c z{3*C&$PF7zHkNuj_REC>0#ZtHHW;Zf%6}~e=}C+Vex zY52bpy!#bzVN$5VtN?L;v0*o$RU4YRaO?Sk_Q^7;v8RW{9PYXZsBGbQT-=s4-O6n_2LHq0tI#Ma}aii zChCd7CLp$887+$aX&QaDph}LyE#^-w1sABp%~0tdweB0(BL{qs*l`4Z8esaYV6d>& zTZ8Ek_d8HV8+!*Se2@@943kyk?BsOM<5!oKwxvVDF!&Vu1?OEM5O&*gL1p+=Sx&oe zHL3*z7*!$tYJ`>yM(HK#>eZYa_dXs$L2qjnyq4*!DdMVXEXNBR2BjT`b7UsWa~(KT}AZ}h@vkecVyB|Uagv21Pe!UU!9)G_p z$EY26pUWsp+=YgjbKPc4L#hP9b_>spR1gf}H<^82TE^v&={@yk=YNs0W~#^D`d7yK zwH6>Y2r&~skYgu+VFLNXg?kZpz|Sh$c7OkV{$rgMvF(k5OC0=%8B2e)+ZmVNZDsnQ zPRq-YmBL+lFqvLcLqh|z5$s!1u~UfLKk9y{7c&(j8_t020(ebs(^$KY@R7E5ctT^I zjYs#<=xM!)*&me_K#{H)e-YU_^ID_Fx7)oS@8fbL^zGzXOUH>lfUP*)^WQ7D+QgMmO*76G$yIic%} z?#UUcIS6dGpJvj@w{mk8an~k}N>qh*u+;f%;`?~xtJ?c6NpO_r87JRf=R`GJTA9@k zm{fmDj2qc{Y4&_DpL(+H=`54nhj*#EqAkR-Y5hFhyQ_2B@zH{k!AJ86uk6))KE>Fg znbhy@vFDMXF)j=cf9M~hc($wk3@xbZp^mBbaF}RBE6X61@6Hb z+ztydis5&AmNOamH;L70hA1z|5)Kv))+F^{ZW`*%I_;XMFxPqAKELGWT{>=o((Gun z7k5s?G^tTP+uGGfxgeXGNiCDk|pN|EQ+-)%YL=4w&#=hG*mm+?Jc@x_}0=8c=m zJ|Di@>=$0|wK^v5M$HyQrIofezv1ycC-BL4r3@5Z`PwO4_Fn`>ZzGQiHIa*mM7H+v zSp6_B(<3^Gb0|j1F*7DLhY)mSkjDv~eQOA?mtXIgbVIYQMyrv!fu+AM9<) zjXO<3JeRL3VZGeLHOL8BC&C! zQ(gz|+d2c`4N9_~t9g0R>V;R$%lFWb%793O>8-Quh^HX*_e~6`OpJKR;`idrW2^fv zlHakEWmR+V%|QhSh*B{rDRyaT>8xtJrP3Oij?;Wk1sJVGY{w+89mvo6LBqAUGWqtg z>jRS~Xhz}&V8lf{Yh^k( zmI|m}xUjb*{Y&5U(HEgghaR%z^2p{CdhaP}XZD}eGF^>ehd1@^#gND8-DhnLkQ-C5|>rh)!XIpaV<5iHFVGJ58&;aY5XJ?XGhah6hED>`rPdDZKs=8TEgFcZ@66hOD*%yJB;V7 z7vhFbFxa?eGi-0K>T9s3o%h{HY2!wZM(ciY`ti=5^Cy<6j;y_JIy;-?{{_m)kETU= zq#aB`J+=Pz5E9oTx;yLTnkLl*7r$=lX*J#R+5d@MqKyiZ4TU|(p@!-2U%K~7zy*!w z(vC_tvr}{Ph6nu~HF0fFXI(B7cyHbQjq>8({Q7;Cj!W`~Y6qUzPAv7I%mi;VGsQoxHzRzPm%8s?GuWWUC>W{GjpJnQtD}G-L z+faIO=ScT~CUq7_){YUs2~Z&69*5UtafKb?0G1qB7?IFu0pc)u7N3{@*d zNL(S$uVnS30Beq_=rT^u5)9|^EOP~t{5l-YH0tXMmwu3`JZfQG2VR8Q(aNxfs9CZE zzMt*rGH>B=uGyk${K=%nMc%%x!$i5V|L@Y@arC&TvGm<*&@1aKQ8v4;6FgAIg?CZ40-Vps9?2a5(+CS$;X9` z+$3;tGHED!81Wi_{aZ3b$grZ?N`^g0v-T@m@MS*??OHNaoTBZZId|PMZ+zSZQ~cy2 zi#Ra_(8ze~rw{`!vmIfjvF`8n_HOIk%5PEz`ul_9<4sea=6g?%LgjE7D8Bqtq`Dmv z(gDSJ#)SV6#0*HYfx0fyI_jh0|4=X93SkT>c!3a1z|NR-kvZOc44F9!|Fr&?u+o%n z{%W$#YYx)_hrZ@Vq8Y!8her#XU7AP#vHt!;+@DObWPakkxa=*4u|%tRx%JH=My4B? z9WEM!v@VS!B9N}Gg~D^=5cOOwD;tgm6Q_W{UL*tP1!-Fe`%|FlYKa+WkcH&}2848d zp{zV+^$_2zwePUm3{6egr?BgoZB>%gK1}Pl6z>|<25R2)znz@9uTYp-N@MoyvRhMA z9yd0USyV$agYGSHj5~;^IL6a3U^Zn_&mLT+)9i-L=Qza z|30k)>>H|>d}s<0q6l_?G8i|vw_wsCnLCHscLxC8!E4fTae2ObIj@kg_XrId^iVFd z1hc1B870yOSV5>x(?Z9=rZM#$QaL#dMb-;I+jH~rg;^PiU%$S>l8haR-o7@9 znPg-oXfv-cOwsU7L_FriVg(Da{s}D_mD>=8o4d6MyobT z8o#`$eRV|!B=KrvRVpxP$I_yKp&b=OG*_9)s2DlZimldUj1@X_-9tk)a01GX9h7+Q z(WR*VRolvgUO{GN0!Fo4ON$;V+lc^$iHeKI76^s@u3f+WEuV_p)5}MetMf`3$xz7nK}>erX>0qmb7;sGV0scxJ5 z>f+;h*R5EIDxqSWXJ54BSls#V5=nULm&~z^d5Xcf+1)pb*MxE-pMM6Pzh(;%kBT6_ z6FQb^N2ucSr^xAz*|wf#tdA7gq7vj6J2IVfnOiEF9+xs9q+F<{W1 z2(e~PNYtUJO;ZeE+~#z1Iyh_wm#5Bq-L9R5crZ4T0z7)w{(qLgdpzGU1l?$6c-sys z>c3N)oH5t+#*+=+7>xu76q%4grn&ksdlFsxYWNAP1->mP zCMKr2m&XPc3`g!4I-tD%u+~`)?doLs=gjXPXPsPws!5rkHP1O((Lo6Gs8jG9m`nE; zKu4?2N?dnYswDhtVgdute!YCSCMhWiC2yI>PfCdimpqaE7lbonkUU5@X zljINAq2X4%G}NtW*;XnPS5_`X^&hH_)v)yEoSaJA+i|Xf;Us|-2$dXraxASD2{?^h z*u#Ywr(r?|!VJ)D&^{&XtpcyfzFWsk%yGQ25cXIIHjMT9xvlb#Lv6{X*|)$#_NEre&CRDp|1r`G#*` zH`$7(5JZH=s<^KN+ER{ntyjehKm&GSo?9pe|D42up;{iC!I&3&83Jj|)D_H2>64(f za_Q>TtMBjc=01s_*(&mI2aV!gz^$U2`*0&~FoD+$m6#d~LzId09^KsyiPyg6#vyZy zlt(9Hoo`DS+u#sZ6B~tch}FK>t(&X+kES&6ZMM1DIXvuuM$Af#0aIWXR<}L$g5j9A z_baN=v#h!)i&EiVAmi~Z0)`z*hAeK``q{l7k6F~|7mpWqmT%|pYlDzb3B_Ftz&LJ3 zp7Gbh{Hy>x-DYzKX7;P*b4dcu#Ph-_lR=*U+t`GR1n1fCZ=TQ5SfEUX52Bao0_EoC z!)#VTVI}i9hFZ&om$aN*Nt`_qq@Ws(Q8$c(nPqj>j ziJ7?z!EW!1MiaW#NV5I>{K(7&oKYpn{G(4~hDU%oW%38gkxZ9H9<`L2`KA4dv!9

Y@W^6?3k zhm@kGj)=b)W}doT7fVI>cv!^*Ruff8^)KMD5VyKQtN?c4#EbHAn zbc2_st7!{zbKlUnyc*K8o_b$@lzIHgusu)Md%T%?m?{cUB(k-Y#MI$B+bo=J_!^{O zipG=wrB1(>%32ZB-+0ood7#S*lnW2B?PI9yjUv#{f8}uw!*S_~fk%BysIgGLmoQye z{F1lhh_=5BYniavg?FdFSa4r`Z=n#QyS3?=1y{*lnUJV`Vi_KER!M<5t5;-PO*c2M zj!T!Uz7*SkxvaRZy-lVjXWzq*ckB9%(i#t^G>GmlYOkC3g#=O6;6#<968^5zEq~tW z=j^<0PRstJ_iycUs(-lc#l(kGa+Myc3yRfrb7C}1bH}b#oIGF9Cfy$&AKz@dWw)<4 zgM7@pOp~-RZ}pEq+_H?MKH6^aYx7eT;t$=aEXe-&@CJU?CD2EC++br`8(Y_J`0Qw& zk<{bH!QBz_zVTsUVRv;OK5zL^R3up}%;is^1pQjFjFKQj`@j27JI%?RE6Pi_%N;5B OXScSlR_b=M!2bjLhDtC1 literal 0 HcmV?d00001 diff --git a/ch02data/mydata.txt b/ch02data/mydata.txt new file mode 100644 index 000000000..9ccadb377 --- /dev/null +++ b/ch02data/mydata.txt @@ -0,0 +1,23 @@ +A poet once said, 'The whole universe is in a glass of wine.' +We will probably never know in what sense he meant it, +for poets do not write to be understood. +But it is true that if we look at a glass of wine closely enough we see the entire universe. +There are the things of physics: the twisting liquid which evaporates depending +on the wind and weather, the reflection in the glass; +and our imagination adds atoms. +The glass is a distillation of the earth's rocks, +and in its composition we see the secrets of the universe's age, and the evolution of stars. +What strange array of chemicals are in the wine? How did they come to be? +There are the ferments, the enzymes, the substrates, and the products. +There in wine is found the great generalization; all life is fermentation. +Nobody can discover the chemistry of wine without discovering, +as did Louis Pasteur, the cause of much disease. +How vivid is the claret, pressing its existence into the consciousness that watches it! +If our small minds, for some convenience, divide this glass of wine, this universe, +into parts -- +physics, biology, geology, astronomy, psychology, and so on -- +remember that nature does not know it! + +So let us put it all back together, not forgetting ultimately what it is for. +Let it give us one more final pleasure; drink it and forget it all! + - Richard Feynman diff --git a/ch02data/myfile.json b/ch02data/myfile.json new file mode 100644 index 000000000..0841597ca --- /dev/null +++ b/ch02data/myfile.json @@ -0,0 +1,3 @@ +{ + "somekey": ["a list", "with values"] +} diff --git a/ch02data/myfile.yaml b/ch02data/myfile.yaml new file mode 100644 index 000000000..cc9d8c19f --- /dev/null +++ b/ch02data/myfile.yaml @@ -0,0 +1,3 @@ +somekey: + - a list # Look, this is a list + - with values diff --git a/ch02data/mywrittenfile b/ch02data/mywrittenfile new file mode 100644 index 000000000..5138e776f --- /dev/null +++ b/ch02data/mywrittenfile @@ -0,0 +1 @@ +HelloWorldHelloJames \ No newline at end of file diff --git a/ch02data/planets_data.csv b/ch02data/planets_data.csv new file mode 100644 index 000000000..ae0e2bb25 --- /dev/null +++ b/ch02data/planets_data.csv @@ -0,0 +1,9 @@ +Name,Mean distance from sun / AU,Orbit period / years,Mass / M🜨,Radius / R🜨,Number of satellites +Mercury,0.39,0.24,0.06,0.38,0 +Venus,0.72,0.62,0.82,0.95,0 +Earth,1.0,1.0,1.0,1.0,1 +Mars,1.5,1.9,0.11,0.53,2 +Jupiter,5.2,12.0,320.0,11.0,63 +Saturn,9.5,29.0,95.0,9.4,61 +Uranus,19.0,84.0,15.0,4.1,27 +Neptune,30.0,170.0,17.0,3.9,14 diff --git a/ch03tests/01testingbasics.html b/ch03tests/01testingbasics.html new file mode 100644 index 000000000..a532b157b --- /dev/null +++ b/ch03tests/01testingbasics.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Testing Basics + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Testing

+
+
+
+
+
+
+

Introduction

+
+
+
+
+
+
+

When programming, it is very important to know that the code we have written does what it was intended. Unfortunately, this step is often skipped in scientific programming, especially when developing code for our own personal work.

+

Researchers sometimes check that their code behaves correctly by manually running it on some sample data and inspecting the results. However, it is much better and safer to automate this process, so the tests can be run often -- perhaps even after each new commit! This not only reassures us that the code behaves as it should at any given moment, it also gives us more flexibility to change it, because we have a way of knowing when we have broken something by accident.

+

In this chapter, we will mostly look at how to write unit tests, which check the behaviour of small parts of our code. We will work with a particular framework for Python code, but the principles we discuss are general. We will also look at how to use a debugger to locate problems in our code, and services that simplify the automated running of tests.

+
+
+
+
+
+
+

A few reasons not to do testing

+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SensibilitySense
It's boringMaybe
Code is just a one off throwawayAs with most research codes
No time for itA bit more code, a lot less debugging
Tests can be buggy tooSee above
Not a professional programmerSee above
Will do it laterSee above
+
+
+
+
+
+
+

A few reasons to do testing

    +
  • laziness: testing saves time
  • +
  • peace of mind: tests (should) ensure code is correct
  • +
  • runnable specification: best way to let others know what a function should do and +not do
  • +
  • reproducible debugging: debugging that happened and is saved for later reuse
  • +
  • code structure / modularity: since we may have to call parts of the code independently during the tests
  • +
  • ease of modification: since results can be tested
  • +
+
+
+
+
+
+
+

Not a panacea

+

Trying to improve the quality of software by doing more testing is like trying to lose weight by +weighing yourself more often. +- Steve McConnell

+
+
+
+
+
+
+
+
    +
  • Testing won't correct a buggy code
  • +
  • Testing will tell you were the bugs are...
  • +
  • ... if the test cases cover the bugs
  • +
+
+
+
+
+
+
+

If the test cases do not cover the bugs, things can go horribly wrong - an example for this is Therac-25.

+
+
+
+
+
+
+

Tests at different scales

+ + + + + + + + + + + + + + + + + + + + +
Level of testArea covered by test
Unit testingsmallest logical block of work (often < 10 lines of code)
Component testingseveral logical blocks of work together
Integration testingall components together / whole program
+
+
+Always start at the smallest scale! + +
+If a unit test is too complicated, go smaller. +
+
+
+
+
+
+
+
+

Legacy code hardening

    +
  • Very difficult to create unit-tests for existing code
  • +
  • Instead we make a regression test
  • +
  • Run program as a black box:
  • +
+
setup input
+run program
+read output
+check output against expected result
+
+
    +
  • Does not test correctness of code
  • +
  • Checks code is a similarly wrong on day N as day 0
  • +
+
+
+
+
+
+
+

Testing vocabulary

    +
  • fixture: input data
  • +
  • action: function that is being tested
  • +
  • expected result: the output that should be obtained
  • +
  • actual result: the output that is obtained
  • +
  • coverage: proportion of all possible paths in the code that the tests take
  • +
+
+
+
+
+
+
+

Branch coverage:

+
+
+
+
+
+
+
if energy > 0:
+    ! Do this 
+else:
+    ! Do that
+
+
+
+
+
+
+
+

Is there a test for both energy > 0 and energy <= 0?

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/01testingbasics.ipynb b/ch03tests/01testingbasics.ipynb new file mode 100644 index 000000000..e51227b81 --- /dev/null +++ b/ch03tests/01testingbasics.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b9b6f567", + "metadata": {}, + "source": [ + "# Testing" + ] + }, + { + "cell_type": "markdown", + "id": "b77ee91e", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "id": "2e241970", + "metadata": {}, + "source": [ + "When programming, it is very important to know that the code we have written does what it was intended. Unfortunately, this step is often skipped in scientific programming, especially when developing code for our own personal work.\n", + "\n", + "Researchers sometimes check that their code behaves correctly by manually running it on some sample data and inspecting the results. However, it is much better and safer to automate this process, so the tests can be run often -- perhaps even after each new commit! This not only reassures us that the code behaves as it should at any given moment, it also gives us more flexibility to change it, because we have a way of knowing when we have broken something by accident.\n", + "\n", + "In this chapter, we will mostly look at how to write **unit tests**, which check the behaviour of small parts of our code. We will work with a particular framework for Python code, but the principles we discuss are general. We will also look at how to use a debugger to locate problems in our code, and services that simplify the automated running of tests." + ] + }, + { + "cell_type": "markdown", + "id": "e35ac458", + "metadata": {}, + "source": [ + "### A few reasons not to do testing" + ] + }, + { + "cell_type": "markdown", + "id": "bd858afa", + "metadata": {}, + "source": [ + "Sensibility | Sense\n", + " ------------------------------------ | -------------------------------------\n", + " **It's boring** | *Maybe*\n", + " **Code is just a one off throwaway** | *As with most research codes*\n", + " **No time for it** | *A bit more code, a lot less debugging*\n", + " **Tests can be buggy too** | *See above*\n", + " **Not a professional programmer** | *See above*\n", + " **Will do it later** | *See above*" + ] + }, + { + "cell_type": "markdown", + "id": "18e2aaf9", + "metadata": {}, + "source": [ + "### A few reasons to do testing\n", + "\n", + "* **laziness**: testing saves time\n", + "* **peace of mind**: tests (should) ensure code is correct\n", + "* **runnable specification**: best way to let others know what a function should do and\n", + " not do\n", + "* **reproducible debugging**: debugging that happened and is saved for later reuse\n", + "* **code structure / modularity**: since we may have to call parts of the code independently during the tests\n", + "* **ease of modification**: since results can be tested" + ] + }, + { + "cell_type": "markdown", + "id": "c46cfc78", + "metadata": {}, + "source": [ + "### Not a panacea\n", + "\n", + "> Trying to improve the quality of software by doing more testing is like trying to lose weight by\n", + "> weighing yourself more often.\n", + " - Steve McConnell" + ] + }, + { + "cell_type": "markdown", + "id": "580fe57b", + "metadata": {}, + "source": [ + " * Testing won't correct a buggy code\n", + " * Testing will tell you were the bugs are...\n", + " * ... if the test cases *cover* the bugs" + ] + }, + { + "cell_type": "markdown", + "id": "d19d40e9", + "metadata": {}, + "source": [ + "If the test cases do not cover the bugs, things can go horribly wrong - an example for this is [Therac-25](https://en.wikipedia.org/wiki/Therac-25)." + ] + }, + { + "cell_type": "markdown", + "id": "c79ac05b", + "metadata": {}, + "source": [ + "### Tests at different scales\n", + "\n", + "Level of test |Area covered by test\n", + "-------------------------- |----------------------\n", + "**Unit testing** |smallest logical block of work (often < 10 lines of code)\n", + "**Component testing** |several logical blocks of work together\n", + "**Integration testing** |all components together / whole program\n", + "\n", + "\n", + "
\n", + "
\n", + "Always start at the smallest scale! \n", + "\n", + "
\n", + "If a unit test is too complicated, go smaller.\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "3ba71888", + "metadata": {}, + "source": [ + "### Legacy code hardening\n", + "\n", + "* Very difficult to create unit-tests for existing code\n", + "* Instead we make a **regression test**\n", + "* Run program as a black box:\n", + "\n", + "```\n", + "setup input\n", + "run program\n", + "read output\n", + "check output against expected result\n", + "```\n", + "\n", + "* Does not test correctness of code\n", + "* Checks code is a similarly wrong on day N as day 0" + ] + }, + { + "cell_type": "markdown", + "id": "17b95c89", + "metadata": {}, + "source": [ + "### Testing vocabulary\n", + "\n", + "* **fixture**: input data\n", + "* **action**: function that is being tested\n", + "* **expected result**: the output that should be obtained\n", + "* **actual result**: the output that is obtained\n", + "* **coverage**: proportion of all possible paths in the code that the tests take" + ] + }, + { + "cell_type": "markdown", + "id": "817fa2cc", + "metadata": {}, + "source": [ + "### Branch coverage:" + ] + }, + { + "cell_type": "markdown", + "id": "6948233a", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "source": [ + "```python\n", + "if energy > 0:\n", + " ! Do this \n", + "else:\n", + " ! Do that\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "dcda5175", + "metadata": {}, + "source": [ + "Is there a test for both `energy > 0` and `energy <= 0`?" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Testing Basics" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/01testingbasics.ipynb.py b/ch03tests/01testingbasics.ipynb.py new file mode 100644 index 000000000..60a554f2c --- /dev/null +++ b/ch03tests/01testingbasics.ipynb.py @@ -0,0 +1,123 @@ +# --- +# jupyter: +# jekyll: +# display_name: Testing Basics +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Testing + +# %% [markdown] +# ## Introduction + +# %% [markdown] +# When programming, it is very important to know that the code we have written does what it was intended. Unfortunately, this step is often skipped in scientific programming, especially when developing code for our own personal work. +# +# Researchers sometimes check that their code behaves correctly by manually running it on some sample data and inspecting the results. However, it is much better and safer to automate this process, so the tests can be run often -- perhaps even after each new commit! This not only reassures us that the code behaves as it should at any given moment, it also gives us more flexibility to change it, because we have a way of knowing when we have broken something by accident. +# +# In this chapter, we will mostly look at how to write **unit tests**, which check the behaviour of small parts of our code. We will work with a particular framework for Python code, but the principles we discuss are general. We will also look at how to use a debugger to locate problems in our code, and services that simplify the automated running of tests. + +# %% [markdown] +# ### A few reasons not to do testing + +# %% [markdown] +# Sensibility | Sense +# ------------------------------------ | ------------------------------------- +# **It's boring** | *Maybe* +# **Code is just a one off throwaway** | *As with most research codes* +# **No time for it** | *A bit more code, a lot less debugging* +# **Tests can be buggy too** | *See above* +# **Not a professional programmer** | *See above* +# **Will do it later** | *See above* + +# %% [markdown] +# ### A few reasons to do testing +# +# * **laziness**: testing saves time +# * **peace of mind**: tests (should) ensure code is correct +# * **runnable specification**: best way to let others know what a function should do and +# not do +# * **reproducible debugging**: debugging that happened and is saved for later reuse +# * **code structure / modularity**: since we may have to call parts of the code independently during the tests +# * **ease of modification**: since results can be tested + +# %% [markdown] +# ### Not a panacea +# +# > Trying to improve the quality of software by doing more testing is like trying to lose weight by +# > weighing yourself more often. +# - Steve McConnell + +# %% [markdown] +# * Testing won't correct a buggy code +# * Testing will tell you were the bugs are... +# * ... if the test cases *cover* the bugs + +# %% [markdown] +# If the test cases do not cover the bugs, things can go horribly wrong - an example for this is [Therac-25](https://en.wikipedia.org/wiki/Therac-25). + +# %% [markdown] +# ### Tests at different scales +# +# Level of test |Area covered by test +# -------------------------- |---------------------- +# **Unit testing** |smallest logical block of work (often < 10 lines of code) +# **Component testing** |several logical blocks of work together +# **Integration testing** |all components together / whole program +# +# +#
+#
+# Always start at the smallest scale! +# +#
+# If a unit test is too complicated, go smaller. +#
+#
+ +# %% [markdown] +# ### Legacy code hardening +# +# * Very difficult to create unit-tests for existing code +# * Instead we make a **regression test** +# * Run program as a black box: +# +# ``` +# setup input +# run program +# read output +# check output against expected result +# ``` +# +# * Does not test correctness of code +# * Checks code is a similarly wrong on day N as day 0 + +# %% [markdown] +# ### Testing vocabulary +# +# * **fixture**: input data +# * **action**: function that is being tested +# * **expected result**: the output that should be obtained +# * **actual result**: the output that is obtained +# * **coverage**: proportion of all possible paths in the code that the tests take + +# %% [markdown] +# ### Branch coverage: + +# %% [markdown] attributes={"classes": [" python"], "id": ""} +# ```python +# if energy > 0: +# ! Do this +# else: +# ! Do that +# ``` + +# %% [markdown] +# Is there a test for both `energy > 0` and `energy <= 0`? diff --git a/ch03tests/02SaskatchewanFields.html b/ch03tests/02SaskatchewanFields.html new file mode 100644 index 000000000..17eb77947 --- /dev/null +++ b/ch03tests/02SaskatchewanFields.html @@ -0,0 +1,1155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The Fields of Saskatchewan + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

How to Test

+
+
+
+
+
+
+

Equivalence partitioning

+
+
+
+
+
+
+

Think hard about the different cases the code will run under: this is science, not coding!

+
+
+
+
+
+
+

We can't write a test for every possible input: this is an infinite amount of work.

+
+
+
+
+
+
+

We need to write tests to rule out different bugs. There's no need to separately test equivalent inputs.

+
+
+
+
+
+
+

Let's look at an example of this question outside of coding:

+
+
+
+
+
+
+
    +
  • Research Project : Evolution of agricultural fields in Saskatchewan from aerial photography
  • +
  • In silico translation : Compute overlap of two rectangles
  • +
+
+
+
+
+
+
In [1]:
+
+
+
import matplotlib.pyplot as plt
+from matplotlib.path import Path
+import matplotlib.patches as patches
+%matplotlib inline
+
+
+
+
+
+
+
+
+

Let's make a little fragment of matplotlib code to visualise a pair of fields.

+
+
+
+
+
+
In [2]:
+
+
+
def show_fields(field1, field2):
+    def vertices(left, bottom, right, top):
+        verts = [(left, bottom),
+                 (left, top),
+                 (right, top),
+                 (right, bottom),
+                 (left, bottom)]
+        return verts
+    
+    codes = [Path.MOVETO,
+             Path.LINETO,
+             Path.LINETO,
+             Path.LINETO,
+             Path.CLOSEPOLY]
+    path1 = Path(vertices(*field1), codes)
+    path2 = Path(vertices(*field2), codes)         
+    fig = plt.figure()
+    ax = fig.add_subplot(111)
+    patch1 = patches.PathPatch(path1, facecolor='orange', lw=2)
+    patch2 = patches.PathPatch(path2, facecolor='blue', lw=2)         
+    ax.add_patch(patch1)
+    ax.add_patch(patch2)       
+    ax.set_xlim(0,5)
+    ax.set_ylim(0,5)
+
+show_fields((1.,1.,4.,4.), (2.,2.,3.,3.))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Here, we can see that the area of overlap, is the same as the smaller field, with area 1.

+
+
+
+
+
+
+

We could now go ahead and write a subroutine to calculate that, and also write some test cases for our answer.

+
+
+
+
+
+
+

But first, let's just consider that question abstractly, what other cases, not equivalent to this might there be?

+
+
+
+
+
+
+

For example, this case, is still just a full overlap, and is sufficiently equivalent that it's not worth another test:

+
+
+
+
+
+
In [3]:
+
+
+
show_fields((1.,1.,4.,4.),(2.5,1.7,3.2,3.4))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

But this case is no longer a full overlap, and should be tested separately:

+
+
+
+
+
+
In [4]:
+
+
+
show_fields((1.,1.,4.,4.),(2.,2.,3.,4.5))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

On a piece of paper, sketch now the other cases you think should be treated as non-equivalent. Some answers are below:

+
+
+
+
+
+
In [5]:
+
+
+
for _ in range(10):
+    print("\n\n\nSpoiler space\n\n\n")
+
+
+
+
+
+
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+Spoiler space
+
+
+
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
show_fields((1.,1.,4.,4.),(2,2,4.5,4.5)) # Overlap corner
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [7]:
+
+
+
show_fields((1.,1.,4.,4.),(2.,2.,3.,4.)) # Just touching
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [8]:
+
+
+
show_fields((1.,1.,4.,4.),(4.5,4.5,5,5)) # No overlap
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [9]:
+
+
+
show_fields((1.,1.,4.,4.),(2.5,4,3.5,4.5)) # Just touching from outside
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [10]:
+
+
+
show_fields((1.,1.,4.,4.),(4,4,4.5,4.5)) # Touching corner
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Using our tests

+
+
+
+
+
+
+

OK, so how might our tests be useful?

+
+
+
+
+
+
+

Here's some code that might correctly calculate the area of overlap:

+
+
+
+
+
+
In [11]:
+
+
+
def overlap(field1, field2):
+    left1, bottom1, top1, right1 = field1
+    left2, bottom2, top2, right2 = field2
+    overlap_left = max(left1, left2)
+    overlap_bottom = max(bottom1, bottom2)
+    overlap_right = min(right1, right2)
+    overlap_top = min(top1, top2)
+    overlap_height = (overlap_top-overlap_bottom)
+    overlap_width = (overlap_right-overlap_left)
+    return overlap_height * overlap_width
+
+
+
+
+
+
+
+
+

So how do we check our code?

+
+
+
+
+
+
+

The manual approach would be to look at some cases, and, once, run it and check:

+
+
+
+
+
+
In [12]:
+
+
+
overlap((1.,1.,4.,4.),(2.,2.,3.,3.))
+
+
+
+
+
+
+
+
Out[12]:
+
+
1.0
+
+
+
+
+
+
+
+
+

That looks OK.

+
+
+
+
+
+
+

But we can do better, we can write code which raises an error if it gets an unexpected answer:

+
+
+
+
+
+
In [13]:
+
+
+
assert overlap((1.,1.,4.,4.),(2.,2.,3.,3.)) == 1.0
+
+
+
+
+
+
+
+
In [14]:
+
+
+
assert overlap((1.,1.,4.,4.),(2.,2.,3.,4.5)) == 2.0 
+
+
+
+
+
+
+
+
In [15]:
+
+
+
assert overlap((1.,1.,4.,4.),(2.,2.,4.5,4.5)) == 4.0 
+
+
+
+
+
+
+
+
In [16]:
+
+
+
assert overlap((1.,1.,4.,4.),(4.5,4.5,5,5)) == 0.0 
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AssertionError                            Traceback (most recent call last)
+Cell In[16], line 1
+----> 1 assert overlap((1.,1.,4.,4.),(4.5,4.5,5,5)) == 0.0 
+
+AssertionError: 
+
+
+
+
+
+
+
+
In [17]:
+
+
+
print(overlap((1.,1.,4.,4.),(4.5,4.5,5,5)))
+
+
+
+
+
+
+
+
+
+
0.25
+
+
+
+
+
+
+
+
+
In [18]:
+
+
+
show_fields((1.,1.,4.,4.),(4.5,4.5,5,5))
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

What? Why is this wrong?

+
+
+
+
+
+
+

In our calculation, we are actually getting:

+
+
+
+
+
+
In [19]:
+
+
+
overlap_left = 4.5
+overlap_right = 4
+overlap_width = -0.5
+overlap_height = -0.5
+
+
+
+
+
+
+
+
+

Both width and height are negative, resulting in a positive area. +The above code didn't take into account the non-overlap correctly.

+
+
+
+
+
+
+

It should be:

+
+
+
+
+
+
In [20]:
+
+
+
def overlap(field1, field2):
+    left1, bottom1, top1, right1 = field1
+    left2, bottom2, top2, right2 = field2
+    
+    overlap_left = max(left1, left2)
+    overlap_bottom = max(bottom1, bottom2)
+    overlap_right = min(right1, right2)
+    overlap_top = min(top1, top2)
+    
+    overlap_height = max(0, (overlap_top-overlap_bottom))
+    overlap_width = max(0, (overlap_right-overlap_left))
+    
+    return overlap_height*overlap_width
+
+
+
+
+
+
+
+
In [21]:
+
+
+
assert overlap((1,1,4,4), (2,2,3,3)) == 1.0
+assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0 
+assert overlap((1,1,4,4), (2,2,4.5,4.5)) == 4.0 
+assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0 
+assert overlap((1,1,4,4), (2.5,4,3.5,4.5)) == 0.0 
+assert overlap((1,1,4,4), (4,4,4.5,4.5)) == 0.0 
+
+
+
+
+
+
+
+
+

Note, we reran our other tests, to check our fix didn't break something else. (We call that "fallout")

+
+
+
+
+
+
+

Boundary cases

+
+
+
+
+
+
+

"Boundary cases" are an important area to test:

+
    +
  • Limit between two equivalence classes: edge and corner sharing fields
  • +
  • Wherever indices appear, check values at 0, N, N+1
  • +
  • Empty arrays:
  • +
+
+
+
+
+
+
+
atoms = [read_input_atom(input_atom) for input_atom in input_file]
+    energy = force_field(atoms)
+
+
+
+
+
+
+
+
    +
  • What happens if atoms is an empty list?
  • +
  • What happens when a matrix/data-frame reaches one row, or one column?
  • +
+
+
+
+
+
+
+

Positive and negative tests

    +
  • Positive tests: code should give correct answer with various inputs
  • +
  • Negative tests: code should crash as expected given invalid inputs, rather than lying
  • +
+
+Bad input should be expected and should fail early and explicitly. + +
+Testing should ensure that explicit failures do indeed happen. + + +
+
+
+
+
+
+

Raising exceptions

+
+
+
+
+
+
+

In Python, we can signal an error state by raising an error:

+
+
+
+
+
+
In [22]:
+
+
+
def I_only_accept_positive_numbers(number):
+    # Check input
+    if number < 0: 
+        raise ValueError("Input {} is negative".format(number))
+
+    # Do something
+
+
+
+
+
+
+
+
In [23]:
+
+
+
I_only_accept_positive_numbers(5)
+
+
+
+
+
+
+
+
In [24]:
+
+
+
I_only_accept_positive_numbers(-5)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[24], line 1
+----> 1 I_only_accept_positive_numbers(-5)
+
+Cell In[22], line 4, in I_only_accept_positive_numbers(number)
+      1 def I_only_accept_positive_numbers(number):
+      2     # Check input
+      3     if number < 0: 
+----> 4         raise ValueError("Input {} is negative".format(number))
+
+ValueError: Input -5 is negative
+
+
+
+
+
+
+
+
+

There are standard "Exception" types, like ValueError we can raise

+
+
+
+
+
+
+

We would like to be able to write tests like this:

+
+
+
+
+
+
In [25]:
+
+
+
assert I_only_accept_positive_numbers(-5) == # Gives a value error
+
+
+
+
+
+
+
+
+
+
+  Cell In[25], line 1
+    assert I_only_accept_positive_numbers(-5) == # Gives a value error
+                                                 ^
+SyntaxError: invalid syntax
+
+
+
+
+
+
+
+
+
+

But to do that, we need to learn about more sophisticated testing tools, called "test frameworks".

+
+
+
+
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/02SaskatchewanFields.ipynb b/ch03tests/02SaskatchewanFields.ipynb new file mode 100644 index 000000000..7ef857fef --- /dev/null +++ b/ch03tests/02SaskatchewanFields.ipynb @@ -0,0 +1,650 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "89640afe", + "metadata": {}, + "source": [ + "## How to Test" + ] + }, + { + "cell_type": "markdown", + "id": "f42a508e", + "metadata": {}, + "source": [ + "### Equivalence partitioning" + ] + }, + { + "cell_type": "markdown", + "id": "9bab2e33", + "metadata": {}, + "source": [ + "Think hard about the different cases the code will run under: this is science, not coding!" + ] + }, + { + "cell_type": "markdown", + "id": "8899582e", + "metadata": {}, + "source": [ + "We can't write a test for every possible input: this is an infinite amount of work." + ] + }, + { + "cell_type": "markdown", + "id": "11d7e570", + "metadata": {}, + "source": [ + "We need to write tests to rule out different bugs. There's no need to separately test *equivalent* inputs. " + ] + }, + { + "cell_type": "markdown", + "id": "879591be", + "metadata": {}, + "source": [ + "Let's look at an example of this question outside of coding:" + ] + }, + { + "cell_type": "markdown", + "id": "4f84976f", + "metadata": {}, + "source": [ + "* Research Project : Evolution of agricultural fields in Saskatchewan from aerial photography\n", + "* In silico translation : Compute overlap of two rectangles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7626ef51", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.path import Path\n", + "import matplotlib.patches as patches\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "83642739", + "metadata": {}, + "source": [ + "Let's make a little fragment of matplotlib code to visualise a pair of fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c29e853d", + "metadata": {}, + "outputs": [], + "source": [ + "def show_fields(field1, field2):\n", + " def vertices(left, bottom, right, top):\n", + " verts = [(left, bottom),\n", + " (left, top),\n", + " (right, top),\n", + " (right, bottom),\n", + " (left, bottom)]\n", + " return verts\n", + " \n", + " codes = [Path.MOVETO,\n", + " Path.LINETO,\n", + " Path.LINETO,\n", + " Path.LINETO,\n", + " Path.CLOSEPOLY]\n", + " path1 = Path(vertices(*field1), codes)\n", + " path2 = Path(vertices(*field2), codes) \n", + " fig = plt.figure()\n", + " ax = fig.add_subplot(111)\n", + " patch1 = patches.PathPatch(path1, facecolor='orange', lw=2)\n", + " patch2 = patches.PathPatch(path2, facecolor='blue', lw=2) \n", + " ax.add_patch(patch1)\n", + " ax.add_patch(patch2) \n", + " ax.set_xlim(0,5)\n", + " ax.set_ylim(0,5)\n", + "\n", + "show_fields((1.,1.,4.,4.), (2.,2.,3.,3.))" + ] + }, + { + "cell_type": "markdown", + "id": "511e7920", + "metadata": {}, + "source": [ + "Here, we can see that the area of overlap, is the same as the smaller field, with area 1." + ] + }, + { + "cell_type": "markdown", + "id": "0e1ae816", + "metadata": {}, + "source": [ + "We could now go ahead and write a subroutine to calculate that, and also write some test cases for our answer." + ] + }, + { + "cell_type": "markdown", + "id": "a808e5d5", + "metadata": {}, + "source": [ + "But first, let's just consider that question abstractly, what other cases, *not equivalent to this* might there be?" + ] + }, + { + "cell_type": "markdown", + "id": "42fc6bc0", + "metadata": {}, + "source": [ + "For example, this case, is still just a full overlap, and is sufficiently equivalent that it's not worth another test:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a7ee9c7", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(2.5,1.7,3.2,3.4))" + ] + }, + { + "cell_type": "markdown", + "id": "369a8e8d", + "metadata": {}, + "source": [ + "But this case is no longer a full overlap, and should be tested separately:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25c870e5", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(2.,2.,3.,4.5))" + ] + }, + { + "cell_type": "markdown", + "id": "40274282", + "metadata": {}, + "source": [ + "On a piece of paper, sketch now the other cases you think should be treated as non-equivalent. Some answers are below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06b7127a", + "metadata": {}, + "outputs": [], + "source": [ + "for _ in range(10):\n", + " print(\"\\n\\n\\nSpoiler space\\n\\n\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a233a963", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(2,2,4.5,4.5)) # Overlap corner" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60a3164e", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(2.,2.,3.,4.)) # Just touching" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7f371f6", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(4.5,4.5,5,5)) # No overlap" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5d319e6", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(2.5,4,3.5,4.5)) # Just touching from outside" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c0106c8", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(4,4,4.5,4.5)) # Touching corner" + ] + }, + { + "cell_type": "markdown", + "id": "fb637d6e", + "metadata": {}, + "source": [ + "### Using our tests" + ] + }, + { + "cell_type": "markdown", + "id": "248000d4", + "metadata": {}, + "source": [ + "OK, so how might our tests be useful?" + ] + }, + { + "cell_type": "markdown", + "id": "16d2b023", + "metadata": {}, + "source": [ + "Here's some code that **might** correctly calculate the area of overlap:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13753018", + "metadata": {}, + "outputs": [], + "source": [ + "def overlap(field1, field2):\n", + " left1, bottom1, top1, right1 = field1\n", + " left2, bottom2, top2, right2 = field2\n", + " overlap_left = max(left1, left2)\n", + " overlap_bottom = max(bottom1, bottom2)\n", + " overlap_right = min(right1, right2)\n", + " overlap_top = min(top1, top2)\n", + " overlap_height = (overlap_top-overlap_bottom)\n", + " overlap_width = (overlap_right-overlap_left)\n", + " return overlap_height * overlap_width" + ] + }, + { + "cell_type": "markdown", + "id": "b4f13a75", + "metadata": {}, + "source": [ + "So how do we check our code?" + ] + }, + { + "cell_type": "markdown", + "id": "6b4404cd", + "metadata": {}, + "source": [ + "The manual approach would be to look at some cases, and, once, run it and check:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85675274", + "metadata": {}, + "outputs": [], + "source": [ + "overlap((1.,1.,4.,4.),(2.,2.,3.,3.))" + ] + }, + { + "cell_type": "markdown", + "id": "2e59815e", + "metadata": {}, + "source": [ + "That looks OK." + ] + }, + { + "cell_type": "markdown", + "id": "5923a231", + "metadata": {}, + "source": [ + "But we can do better, we can write code which **raises an error** if it gets an unexpected answer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f37abaae", + "metadata": {}, + "outputs": [], + "source": [ + "assert overlap((1.,1.,4.,4.),(2.,2.,3.,3.)) == 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3037360", + "metadata": {}, + "outputs": [], + "source": [ + "assert overlap((1.,1.,4.,4.),(2.,2.,3.,4.5)) == 2.0 " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5feec9af", + "metadata": {}, + "outputs": [], + "source": [ + "assert overlap((1.,1.,4.,4.),(2.,2.,4.5,4.5)) == 4.0 " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a77cbbb", + "metadata": {}, + "outputs": [], + "source": [ + "assert overlap((1.,1.,4.,4.),(4.5,4.5,5,5)) == 0.0 " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5d24e38", + "metadata": {}, + "outputs": [], + "source": [ + "print(overlap((1.,1.,4.,4.),(4.5,4.5,5,5)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a52e8671", + "metadata": {}, + "outputs": [], + "source": [ + "show_fields((1.,1.,4.,4.),(4.5,4.5,5,5))" + ] + }, + { + "cell_type": "markdown", + "id": "45433e52", + "metadata": {}, + "source": [ + "What? Why is this wrong?" + ] + }, + { + "cell_type": "markdown", + "id": "88aaba6d", + "metadata": {}, + "source": [ + "In our calculation, we are actually getting:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fad1258e", + "metadata": {}, + "outputs": [], + "source": [ + "overlap_left = 4.5\n", + "overlap_right = 4\n", + "overlap_width = -0.5\n", + "overlap_height = -0.5" + ] + }, + { + "cell_type": "markdown", + "id": "cd669b08", + "metadata": {}, + "source": [ + "Both width and height are negative, resulting in a positive area.\n", + "The above code didn't take into account the non-overlap correctly." + ] + }, + { + "cell_type": "markdown", + "id": "c5be60d0", + "metadata": {}, + "source": [ + "It should be:\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ee280a3", + "metadata": {}, + "outputs": [], + "source": [ + "def overlap(field1, field2):\n", + " left1, bottom1, top1, right1 = field1\n", + " left2, bottom2, top2, right2 = field2\n", + " \n", + " overlap_left = max(left1, left2)\n", + " overlap_bottom = max(bottom1, bottom2)\n", + " overlap_right = min(right1, right2)\n", + " overlap_top = min(top1, top2)\n", + " \n", + " overlap_height = max(0, (overlap_top-overlap_bottom))\n", + " overlap_width = max(0, (overlap_right-overlap_left))\n", + " \n", + " return overlap_height*overlap_width" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b9c1bdf", + "metadata": {}, + "outputs": [], + "source": [ + "assert overlap((1,1,4,4), (2,2,3,3)) == 1.0\n", + "assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0 \n", + "assert overlap((1,1,4,4), (2,2,4.5,4.5)) == 4.0 \n", + "assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0 \n", + "assert overlap((1,1,4,4), (2.5,4,3.5,4.5)) == 0.0 \n", + "assert overlap((1,1,4,4), (4,4,4.5,4.5)) == 0.0 " + ] + }, + { + "cell_type": "markdown", + "id": "d146f1f8", + "metadata": {}, + "source": [ + "Note, we reran our other tests, to check our fix didn't break something else. (We call that \"fallout\")" + ] + }, + { + "cell_type": "markdown", + "id": "1e394c5c", + "metadata": {}, + "source": [ + "### Boundary cases" + ] + }, + { + "cell_type": "markdown", + "id": "da8dc950", + "metadata": {}, + "source": [ + "\"Boundary cases\" are an important area to test:\n", + "\n", + "* Limit between two equivalence classes: edge and corner sharing fields\n", + "* Wherever indices appear, check values at ``0``, ``N``, ``N+1``\n", + "* Empty arrays:" + ] + }, + { + "cell_type": "markdown", + "id": "aecd3bc8", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "source": [ + "``` python\n", + " atoms = [read_input_atom(input_atom) for input_atom in input_file]\n", + " energy = force_field(atoms)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7ffcdbb5", + "metadata": {}, + "source": [ + "* What happens if ``atoms`` is an empty list?\n", + "* What happens when a matrix/data-frame reaches one row, or one column?" + ] + }, + { + "cell_type": "markdown", + "id": "746c9417", + "metadata": {}, + "source": [ + "### Positive *and* negative tests\n", + "\n", + "* **Positive tests**: code should give correct answer with various inputs\n", + "* **Negative tests**: code should crash as expected given invalid inputs, rather than lying\n", + "\n", + "
\n", + "Bad input should be expected and should fail early and explicitly.\n", + "\n", + "
\n", + "Testing should ensure that explicit failures do indeed happen." + ] + }, + { + "cell_type": "markdown", + "id": "67262cf2", + "metadata": {}, + "source": [ + "### Raising exceptions" + ] + }, + { + "cell_type": "markdown", + "id": "c28e30a4", + "metadata": {}, + "source": [ + "In Python, we can signal an error state by raising an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c614faa3", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "def I_only_accept_positive_numbers(number):\n", + " # Check input\n", + " if number < 0: \n", + " raise ValueError(\"Input {} is negative\".format(number))\n", + "\n", + " # Do something" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c981ee78", + "metadata": {}, + "outputs": [], + "source": [ + "I_only_accept_positive_numbers(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f1d6520", + "metadata": {}, + "outputs": [], + "source": [ + "I_only_accept_positive_numbers(-5)" + ] + }, + { + "cell_type": "markdown", + "id": "778f4f0a", + "metadata": {}, + "source": [ + "There are standard \"Exception\" types, like `ValueError` we can `raise`" + ] + }, + { + "cell_type": "markdown", + "id": "eee72f6d", + "metadata": {}, + "source": [ + "We would like to be able to write tests like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f32c575a", + "metadata": {}, + "outputs": [], + "source": [ + "assert I_only_accept_positive_numbers(-5) == # Gives a value error" + ] + }, + { + "cell_type": "markdown", + "id": "d21a0924", + "metadata": {}, + "source": [ + "But to do that, we need to learn about more sophisticated testing tools, called \"test frameworks\"." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "The Fields of Saskatchewan" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/02SaskatchewanFields.ipynb.py b/ch03tests/02SaskatchewanFields.ipynb.py new file mode 100644 index 000000000..7a2786699 --- /dev/null +++ b/ch03tests/02SaskatchewanFields.ipynb.py @@ -0,0 +1,284 @@ +# --- +# jupyter: +# jekyll: +# display_name: The Fields of Saskatchewan +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## How to Test + +# %% [markdown] +# ### Equivalence partitioning + +# %% [markdown] +# Think hard about the different cases the code will run under: this is science, not coding! + +# %% [markdown] +# We can't write a test for every possible input: this is an infinite amount of work. + +# %% [markdown] +# We need to write tests to rule out different bugs. There's no need to separately test *equivalent* inputs. + +# %% [markdown] +# Let's look at an example of this question outside of coding: + +# %% [markdown] +# * Research Project : Evolution of agricultural fields in Saskatchewan from aerial photography +# * In silico translation : Compute overlap of two rectangles + +# %% +import matplotlib.pyplot as plt +from matplotlib.path import Path +import matplotlib.patches as patches +# %matplotlib inline + +# %% [markdown] +# Let's make a little fragment of matplotlib code to visualise a pair of fields. + +# %% +def show_fields(field1, field2): + def vertices(left, bottom, right, top): + verts = [(left, bottom), + (left, top), + (right, top), + (right, bottom), + (left, bottom)] + return verts + + codes = [Path.MOVETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.CLOSEPOLY] + path1 = Path(vertices(*field1), codes) + path2 = Path(vertices(*field2), codes) + fig = plt.figure() + ax = fig.add_subplot(111) + patch1 = patches.PathPatch(path1, facecolor='orange', lw=2) + patch2 = patches.PathPatch(path2, facecolor='blue', lw=2) + ax.add_patch(patch1) + ax.add_patch(patch2) + ax.set_xlim(0,5) + ax.set_ylim(0,5) + +show_fields((1.,1.,4.,4.), (2.,2.,3.,3.)) + +# %% [markdown] +# Here, we can see that the area of overlap, is the same as the smaller field, with area 1. + +# %% [markdown] +# We could now go ahead and write a subroutine to calculate that, and also write some test cases for our answer. + +# %% [markdown] +# But first, let's just consider that question abstractly, what other cases, *not equivalent to this* might there be? + +# %% [markdown] +# For example, this case, is still just a full overlap, and is sufficiently equivalent that it's not worth another test: + +# %% +show_fields((1.,1.,4.,4.),(2.5,1.7,3.2,3.4)) + +# %% [markdown] +# But this case is no longer a full overlap, and should be tested separately: + +# %% +show_fields((1.,1.,4.,4.),(2.,2.,3.,4.5)) + +# %% [markdown] +# On a piece of paper, sketch now the other cases you think should be treated as non-equivalent. Some answers are below: + +# %% +for _ in range(10): + print("\n\n\nSpoiler space\n\n\n") + +# %% +show_fields((1.,1.,4.,4.),(2,2,4.5,4.5)) # Overlap corner + +# %% +show_fields((1.,1.,4.,4.),(2.,2.,3.,4.)) # Just touching + +# %% +show_fields((1.,1.,4.,4.),(4.5,4.5,5,5)) # No overlap + +# %% +show_fields((1.,1.,4.,4.),(2.5,4,3.5,4.5)) # Just touching from outside + +# %% +show_fields((1.,1.,4.,4.),(4,4,4.5,4.5)) # Touching corner + + +# %% [markdown] +# ### Using our tests + +# %% [markdown] +# OK, so how might our tests be useful? + +# %% [markdown] +# Here's some code that **might** correctly calculate the area of overlap: + +# %% +def overlap(field1, field2): + left1, bottom1, top1, right1 = field1 + left2, bottom2, top2, right2 = field2 + overlap_left = max(left1, left2) + overlap_bottom = max(bottom1, bottom2) + overlap_right = min(right1, right2) + overlap_top = min(top1, top2) + overlap_height = (overlap_top-overlap_bottom) + overlap_width = (overlap_right-overlap_left) + return overlap_height * overlap_width + + +# %% [markdown] +# So how do we check our code? + +# %% [markdown] +# The manual approach would be to look at some cases, and, once, run it and check: + +# %% +overlap((1.,1.,4.,4.),(2.,2.,3.,3.)) + +# %% [markdown] +# That looks OK. + +# %% [markdown] +# But we can do better, we can write code which **raises an error** if it gets an unexpected answer: + +# %% +assert overlap((1.,1.,4.,4.),(2.,2.,3.,3.)) == 1.0 + +# %% +assert overlap((1.,1.,4.,4.),(2.,2.,3.,4.5)) == 2.0 + +# %% +assert overlap((1.,1.,4.,4.),(2.,2.,4.5,4.5)) == 4.0 + +# %% +assert overlap((1.,1.,4.,4.),(4.5,4.5,5,5)) == 0.0 + +# %% +print(overlap((1.,1.,4.,4.),(4.5,4.5,5,5))) + +# %% +show_fields((1.,1.,4.,4.),(4.5,4.5,5,5)) + +# %% [markdown] +# What? Why is this wrong? + +# %% [markdown] +# In our calculation, we are actually getting: + +# %% +overlap_left = 4.5 +overlap_right = 4 +overlap_width = -0.5 +overlap_height = -0.5 + + +# %% [markdown] +# Both width and height are negative, resulting in a positive area. +# The above code didn't take into account the non-overlap correctly. + +# %% [markdown] +# It should be: +# + +# %% +def overlap(field1, field2): + left1, bottom1, top1, right1 = field1 + left2, bottom2, top2, right2 = field2 + + overlap_left = max(left1, left2) + overlap_bottom = max(bottom1, bottom2) + overlap_right = min(right1, right2) + overlap_top = min(top1, top2) + + overlap_height = max(0, (overlap_top-overlap_bottom)) + overlap_width = max(0, (overlap_right-overlap_left)) + + return overlap_height*overlap_width + + +# %% +assert overlap((1,1,4,4), (2,2,3,3)) == 1.0 +assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0 +assert overlap((1,1,4,4), (2,2,4.5,4.5)) == 4.0 +assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0 +assert overlap((1,1,4,4), (2.5,4,3.5,4.5)) == 0.0 +assert overlap((1,1,4,4), (4,4,4.5,4.5)) == 0.0 + + +# %% [markdown] +# Note, we reran our other tests, to check our fix didn't break something else. (We call that "fallout") + +# %% [markdown] +# ### Boundary cases + +# %% [markdown] +# "Boundary cases" are an important area to test: +# +# * Limit between two equivalence classes: edge and corner sharing fields +# * Wherever indices appear, check values at ``0``, ``N``, ``N+1`` +# * Empty arrays: + +# %% [markdown] attributes={"classes": [" python"], "id": ""} +# ``` python +# atoms = [read_input_atom(input_atom) for input_atom in input_file] +# energy = force_field(atoms) +# ``` + +# %% [markdown] +# * What happens if ``atoms`` is an empty list? +# * What happens when a matrix/data-frame reaches one row, or one column? + +# %% [markdown] +# ### Positive *and* negative tests +# +# * **Positive tests**: code should give correct answer with various inputs +# * **Negative tests**: code should crash as expected given invalid inputs, rather than lying +# +#
+# Bad input should be expected and should fail early and explicitly. +# +#
+# Testing should ensure that explicit failures do indeed happen. + +# %% [markdown] +# ### Raising exceptions + +# %% [markdown] +# In Python, we can signal an error state by raising an error: + +# %% attributes={"classes": [" python"], "id": ""} +def I_only_accept_positive_numbers(number): + # Check input + if number < 0: + raise ValueError("Input {} is negative".format(number)) + + # Do something + + +# %% +I_only_accept_positive_numbers(5) + +# %% +I_only_accept_positive_numbers(-5) + +# %% [markdown] +# There are standard "Exception" types, like `ValueError` we can `raise` + +# %% [markdown] +# We would like to be able to write tests like this: + +# %% +assert I_only_accept_positive_numbers(-5) == # Gives a value error + +# %% [markdown] +# But to do that, we need to learn about more sophisticated testing tools, called "test frameworks". diff --git a/ch03tests/03pytest.html b/ch03tests/03pytest.html new file mode 100644 index 000000000..3b4aa1f0a --- /dev/null +++ b/ch03tests/03pytest.html @@ -0,0 +1,792 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Frameworks + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Testing frameworks

Why use testing frameworks?

+
+
+
+
+
+
+

Frameworks should simplify our lives:

+
    +
  • Should be easy to add simple test
  • +
  • Should be possible to create complex test:
      +
    • Fixtures
    • +
    • Setup/Tear down
    • +
    • Parameterized tests (same test, mostly same input)
    • +
    +
  • +
  • Find all our tests in a complicated code-base
  • +
  • Run all our tests with a quick command
  • +
  • Run only some tests, e.g. test --only "tests about fields"
  • +
  • Report failing tests
  • +
  • Additional goodies, such as code coverage
  • +
+
+
+
+
+
+
+

Common testing frameworks

+
+
+
+
+
+
+
    +
  • Language agnostic: CTest

    +
      +
    • Test runner for executables, bash scripts, etc...
    • +
    • Great for legacy code hardening
    • +
    +
  • +
  • C unit-tests:

    + +
  • +
  • C++ unit-tests:

    + +
  • +
  • Python unit-tests:

    +
      +
    • nose includes test discovery, coverage, etc
    • +
    • unittest comes with standard python library
    • +
    • pytest, branched off of nose
    • +
    +
  • +
  • R unit-tests:

    + +
  • +
  • Fortran unit-tests:

    + +
  • +
+
+
+
+
+
+
+

pytest framework: usage

pytest is a recommended python testing framework.

+
+
+
+
+
+
+

We can use its tools in the notebook for on-the-fly tests in the notebook. This, happily, includes the negative-tests example we were looking for a moment ago.

+
+
+
+
+
+
In [1]:
+
+
+
def I_only_accept_positive_numbers(number):
+    # Check input
+    if number < 0: 
+        raise ValueError("Input {} is negative".format(number))
+
+    # Do something
+
+
+
+
+
+
+
+
In [2]:
+
+
+
from pytest import raises
+
+
+
+
+
+
+
+
In [3]:
+
+
+
with raises(ValueError):
+    I_only_accept_positive_numbers(-5)
+
+
+
+
+
+
+
+
+

but the real power comes when we write a test file alongside our code files in our homemade packages:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+mkdir -p saskatchewan
+touch saskatchewan/__init__.py
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%writefile saskatchewan/overlap.py
+def overlap(field1, field2):
+    left1, bottom1, top1, right1 = field1
+    left2, bottom2, top2, right2 = field2
+    
+    overlap_left = max(left1, left2)
+    overlap_bottom = max(bottom1, bottom2)
+    overlap_right = min(right1, right2)
+    overlap_top = min(top1, top2)
+    # Here's our wrong code again
+    overlap_height = (overlap_top - overlap_bottom)
+    overlap_width = (overlap_right - overlap_left)
+    
+    return overlap_height * overlap_width
+
+
+
+
+
+
+
+
+
+
Writing saskatchewan/overlap.py
+
+
+
+
+
+
+
+
+
In [6]:
+
+
+
%%writefile saskatchewan/test_overlap.py
+from .overlap import overlap
+
+def test_full_overlap():
+    assert overlap((1.,1.,4.,4.), (2.,2.,3.,3.)) == 1.0
+
+def test_partial_overlap():
+    assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0
+                 
+def test_no_overlap():
+    assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0
+
+
+
+
+
+
+
+
+
+
Writing saskatchewan/test_overlap.py
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
%%bash --no-raise-error
+cd saskatchewan
+pytest
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/saskatchewan
+plugins: cov-4.1.0, anyio-3.7.1
+collected 3 items
+
+test_overlap.py ..F                                                      [100%]
+
+=================================== FAILURES ===================================
+_______________________________ test_no_overlap ________________________________
+
+    def test_no_overlap():
+>       assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0
+E       assert 0.25 == 0.0
+E        +  where 0.25 = overlap((1, 1, 4, 4), (4.5, 4.5, 5, 5))
+
+test_overlap.py:10: AssertionError
+=========================== short test summary info ============================
+FAILED test_overlap.py::test_no_overlap - assert 0.25 == 0.0
+ +  where 0.25 = overlap((1, 1, 4, 4), (4.5, 4.5, 5, 5))
+========================= 1 failed, 2 passed in 0.09s ==========================
+
+
+
+
+
+
+
+
+
+

Note that it reported which test had failed, how many tests ran, and how many failed.

+
+
+
+
+
+
+

The symbol ..F means there were three tests, of which the third one failed.

+
+
+
+
+
+
+

Pytest will:

+
    +
  • automagically finds files test_*.py
  • +
  • collects all subroutines called test_*
  • +
  • runs tests and reports results
  • +
+
+
+
+
+
+
+

Some options:

+
    +
  • help: pytest --help
  • +
  • run only tests for a given feature: pytest -k foo # tests with 'foo' in the test name
  • +
+
+
+
+
+
+
+

Testing with floating points

Floating points are not reals

Floating points are inaccurate representations of real numbers:

+

1.0 == 0.99999999999999999 is true to the last bit.

+
+
+
+
+
+
+

This can lead to numerical errors during calculations: $1000 (a - b) \neq 1000a - 1000b$

+
+
+
+
+
+
In [8]:
+
+
+
1000.0 * 1.0 - 1000.0 * 0.9999999999999998
+
+
+
+
+
+
+
+
Out[8]:
+
+
2.2737367544323206e-13
+
+
+
+
+
+
+
+
In [9]:
+
+
+
1000.0 * (1.0 - 0.9999999999999998)
+
+
+
+
+
+
+
+
Out[9]:
+
+
2.220446049250313e-13
+
+
+
+
+
+
+
+
+

Both results are wrong: 2e-13 is the correct answer.

+

The size of the error will depend on the magnitude of the floating points:

+
+
+
+
+
+
In [10]:
+
+
+
1000.0 * 1e5 - 1000.0 * 0.9999999999999998e5
+
+
+
+
+
+
+
+
Out[10]:
+
+
1.4901161193847656e-08
+
+
+
+
+
+
+
+
+

The result should be 2e-8.

+
+
+
+
+
+
+

Comparing floating points

Use the "approx", for a default of a relative tolerance of $10^{-6}$

+
+
+
+
+
+
In [11]:
+
+
+
from pytest import approx
+assert  0.7 == approx(0.7 + 1e-7) 
+
+
+
+
+
+
+
+
+

Or be more explicit:

+
+
+
+
+
+
In [12]:
+
+
+
magnitude = 0.7
+assert 0.7 == approx(0.701 , rel=0.1, abs=0.1)
+
+
+
+
+
+
+
+
+

Choosing tolerances is a big area of debate.

+
+
+
+
+
+
+

Comparing vectors of floating points

Numerical vectors are best represented using numpy.

+
+
+
+
+
+
In [13]:
+
+
+
from numpy import array, pi
+
+vector_of_reals = array([0.1, 0.2, 0.3, 0.4]) * pi
+
+
+
+
+
+
+
+
+

Numpy ships with a number of assertions (in numpy.testing) to make +comparison easy:

+
+
+
+
+
+
In [14]:
+
+
+
from numpy import array, pi
+from numpy.testing import assert_allclose
+expected = array([0.1, 0.2, 0.3, 0.4, 1e-12]) * pi
+actual = array([0.1, 0.2, 0.3, 0.4, 2e-12]) * pi
+actual[:-1] += 1e-6
+
+assert_allclose(actual, expected, rtol=1e-5, atol=1e-8)
+
+
+
+
+
+
+
+
+

It compares the difference between actual and expected to atol + rtol * abs(expected).

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/03pytest.ipynb b/ch03tests/03pytest.ipynb new file mode 100644 index 000000000..a098ea3b8 --- /dev/null +++ b/ch03tests/03pytest.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5083463d", + "metadata": {}, + "source": [ + "## Testing frameworks\n", + "\n", + "### Why use testing frameworks?" + ] + }, + { + "cell_type": "markdown", + "id": "e90ca316", + "metadata": {}, + "source": [ + "Frameworks should simplify our lives:\n", + "\n", + "* Should be easy to add simple test\n", + "* Should be possible to create complex test:\n", + " * Fixtures\n", + " * Setup/Tear down\n", + " * Parameterized tests (same test, mostly same input)\n", + "* Find all our tests in a complicated code-base \n", + "* Run all our tests with a quick command\n", + "* Run only some tests, e.g. ``test --only \"tests about fields\"``\n", + "* **Report failing tests**\n", + "* Additional goodies, such as code coverage" + ] + }, + { + "cell_type": "markdown", + "id": "914587f9", + "metadata": {}, + "source": [ + "### Common testing frameworks" + ] + }, + { + "cell_type": "markdown", + "id": "b37640ed", + "metadata": {}, + "source": [ + "* Language agnostic: [CTest](http://www.cmake.org/cmake/help/v2.8.12/ctest.html)\n", + " * Test runner for executables, bash scripts, etc...\n", + " * Great for legacy code hardening\n", + " \n", + "\n", + "* C unit-tests:\n", + " * all c++ frameworks,\n", + " * [Check](https://libcheck.github.io/check/),\n", + " * [CUnit](http://cunit.sourceforge.net)\n", + "\n", + "\n", + "* C++ unit-tests:\n", + " * [CppTest](http://cpptest.sourceforge.net/),\n", + " * [Boost::Test](http://www.boost.org/doc/libs/1_55_0/libs/test/doc/html/index.html),\n", + " * [google-test](https://code.google.com/p/googletest/),\n", + " * [Catch](https://github.com/philsquared/Catch) (best)\n", + "\n", + "\n", + "* Python unit-tests:\n", + " * [nose](https://nose.readthedocs.org/en/latest/) includes test discovery, coverage, etc\n", + " * [unittest](https://docs.python.org/3/library/unittest.html) comes with standard python library\n", + " * [pytest](https://docs.pytest.org/en/latest/index.html), branched off of nose\n", + "\n", + "\n", + "* R unit-tests:\n", + " * [RUnit](http://cran.r-project.org/web/packages/RUnit/index.html),\n", + " * [svUnit](http://cran.r-project.org/web/packages/svUnit/index.html)\n", + " * (works with [SciViews](http://www.sciviews.org/) GUI)\n", + " \n", + "\n", + "* Fortran unit-tests:\n", + " * [funit](https://rubygems.org/gems/funit),\n", + " * [pfunit](http://sourceforge.net/projects/pfunit/)(works with MPI)" + ] + }, + { + "cell_type": "markdown", + "id": "d1aa26b2", + "metadata": {}, + "source": [ + "### pytest framework: usage\n", + "\n", + "[pytest](https://docs.pytest.org/en/latest/) is a recommended python testing framework." + ] + }, + { + "cell_type": "markdown", + "id": "26605eab", + "metadata": {}, + "source": [ + "We can use its tools in the notebook for on-the-fly tests in the notebook. This, happily, includes the negative-tests example we were looking for a moment ago." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f586c0d8", + "metadata": {}, + "outputs": [], + "source": [ + "def I_only_accept_positive_numbers(number):\n", + " # Check input\n", + " if number < 0: \n", + " raise ValueError(\"Input {} is negative\".format(number))\n", + "\n", + " # Do something" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44d817e0", + "metadata": {}, + "outputs": [], + "source": [ + "from pytest import raises" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "305306f9", + "metadata": {}, + "outputs": [], + "source": [ + "with raises(ValueError):\n", + " I_only_accept_positive_numbers(-5)" + ] + }, + { + "cell_type": "markdown", + "id": "a21b4fed", + "metadata": {}, + "source": [ + "but the real power comes when we write a test file alongside our code files in our homemade packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c536054a", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p saskatchewan\n", + "touch saskatchewan/__init__.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e198844", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile saskatchewan/overlap.py\n", + "def overlap(field1, field2):\n", + " left1, bottom1, top1, right1 = field1\n", + " left2, bottom2, top2, right2 = field2\n", + " \n", + " overlap_left = max(left1, left2)\n", + " overlap_bottom = max(bottom1, bottom2)\n", + " overlap_right = min(right1, right2)\n", + " overlap_top = min(top1, top2)\n", + " # Here's our wrong code again\n", + " overlap_height = (overlap_top - overlap_bottom)\n", + " overlap_width = (overlap_right - overlap_left)\n", + " \n", + " return overlap_height * overlap_width" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "759218f1", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile saskatchewan/test_overlap.py\n", + "from .overlap import overlap\n", + "\n", + "def test_full_overlap():\n", + " assert overlap((1.,1.,4.,4.), (2.,2.,3.,3.)) == 1.0\n", + "\n", + "def test_partial_overlap():\n", + " assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0\n", + " \n", + "def test_no_overlap():\n", + " assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0de92c74", + "metadata": { + "attributes": { + "classes": [ + " bash" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "cd saskatchewan\n", + "pytest" + ] + }, + { + "cell_type": "markdown", + "id": "465561ef", + "metadata": {}, + "source": [ + "Note that it reported **which** test had failed, how many tests ran, and how many failed." + ] + }, + { + "cell_type": "markdown", + "id": "e535739a", + "metadata": {}, + "source": [ + "The symbol `..F` means there were three tests, of which the third one failed." + ] + }, + { + "cell_type": "markdown", + "id": "8ca83640", + "metadata": {}, + "source": [ + "Pytest will:\n", + "\n", + "* automagically finds files ``test_*.py``\n", + "* collects all subroutines called ``test_*``\n", + "* runs tests and reports results" + ] + }, + { + "cell_type": "markdown", + "id": "025d0173", + "metadata": {}, + "source": [ + "Some options:\n", + "\n", + "* help: `pytest --help`\n", + "* run only tests for a given feature: `pytest -k foo` # tests with 'foo' in the test name" + ] + }, + { + "cell_type": "markdown", + "id": "61bffd2b", + "metadata": {}, + "source": [ + "## Testing with floating points\n", + "\n", + "### Floating points are not reals\n", + "\n", + "\n", + "Floating points are inaccurate representations of real numbers:\n", + "\n", + "`1.0 == 0.99999999999999999` is true to the last bit." + ] + }, + { + "cell_type": "markdown", + "id": "d18cb85a", + "metadata": {}, + "source": [ + "This can lead to numerical errors during calculations: $1000 (a - b) \\neq 1000a - 1000b$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ff78892", + "metadata": {}, + "outputs": [], + "source": [ + "1000.0 * 1.0 - 1000.0 * 0.9999999999999998" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a7e03c4", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "1000.0 * (1.0 - 0.9999999999999998)" + ] + }, + { + "cell_type": "markdown", + "id": "70eac81a", + "metadata": {}, + "source": [ + "*Both* results are wrong: `2e-13` is the correct answer.\n", + "\n", + "The size of the error will depend on the magnitude of the floating points:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f03bc37", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "1000.0 * 1e5 - 1000.0 * 0.9999999999999998e5" + ] + }, + { + "cell_type": "markdown", + "id": "2733eec2", + "metadata": {}, + "source": [ + "The result should be `2e-8`." + ] + }, + { + "cell_type": "markdown", + "id": "3b1f0994", + "metadata": {}, + "source": [ + "### Comparing floating points\n", + "\n", + "Use the \"approx\", for a default of a relative tolerance of $10^{-6}$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6036e48", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "from pytest import approx\n", + "assert 0.7 == approx(0.7 + 1e-7) " + ] + }, + { + "cell_type": "markdown", + "id": "fec43678", + "metadata": {}, + "source": [ + "Or be more explicit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4e4a37d", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "magnitude = 0.7\n", + "assert 0.7 == approx(0.701 , rel=0.1, abs=0.1)" + ] + }, + { + "cell_type": "markdown", + "id": "a1c83c3d", + "metadata": {}, + "source": [ + "Choosing tolerances is a big area of [debate](https://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html)." + ] + }, + { + "cell_type": "markdown", + "id": "aa7b5ad4", + "metadata": {}, + "source": [ + "### Comparing vectors of floating points\n", + "\n", + "Numerical vectors are best represented using [numpy](http://www.numpy.org/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d8ea491", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "from numpy import array, pi\n", + "\n", + "vector_of_reals = array([0.1, 0.2, 0.3, 0.4]) * pi" + ] + }, + { + "cell_type": "markdown", + "id": "b833f4b5", + "metadata": {}, + "source": [ + "Numpy ships with a number of assertions (in ``numpy.testing``) to make\n", + "comparison easy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "375a6be3", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "from numpy import array, pi\n", + "from numpy.testing import assert_allclose\n", + "expected = array([0.1, 0.2, 0.3, 0.4, 1e-12]) * pi\n", + "actual = array([0.1, 0.2, 0.3, 0.4, 2e-12]) * pi\n", + "actual[:-1] += 1e-6\n", + "\n", + "assert_allclose(actual, expected, rtol=1e-5, atol=1e-8)" + ] + }, + { + "cell_type": "markdown", + "id": "ebd29cf1", + "metadata": {}, + "source": [ + "It compares the difference between `actual` and `expected` to ``atol + rtol * abs(expected)``." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Test Frameworks" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/03pytest.ipynb.py b/ch03tests/03pytest.ipynb.py new file mode 100644 index 000000000..1a6373c2f --- /dev/null +++ b/ch03tests/03pytest.ipynb.py @@ -0,0 +1,230 @@ +# --- +# jupyter: +# jekyll: +# display_name: Test Frameworks +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Testing frameworks +# +# ### Why use testing frameworks? + +# %% [markdown] +# Frameworks should simplify our lives: +# +# * Should be easy to add simple test +# * Should be possible to create complex test: +# * Fixtures +# * Setup/Tear down +# * Parameterized tests (same test, mostly same input) +# * Find all our tests in a complicated code-base +# * Run all our tests with a quick command +# * Run only some tests, e.g. ``test --only "tests about fields"`` +# * **Report failing tests** +# * Additional goodies, such as code coverage + +# %% [markdown] +# ### Common testing frameworks + +# %% [markdown] +# * Language agnostic: [CTest](http://www.cmake.org/cmake/help/v2.8.12/ctest.html) +# * Test runner for executables, bash scripts, etc... +# * Great for legacy code hardening +# +# +# * C unit-tests: +# * all c++ frameworks, +# * [Check](https://libcheck.github.io/check/), +# * [CUnit](http://cunit.sourceforge.net) +# +# +# * C++ unit-tests: +# * [CppTest](http://cpptest.sourceforge.net/), +# * [Boost::Test](http://www.boost.org/doc/libs/1_55_0/libs/test/doc/html/index.html), +# * [google-test](https://code.google.com/p/googletest/), +# * [Catch](https://github.com/philsquared/Catch) (best) +# +# +# * Python unit-tests: +# * [nose](https://nose.readthedocs.org/en/latest/) includes test discovery, coverage, etc +# * [unittest](https://docs.python.org/3/library/unittest.html) comes with standard python library +# * [pytest](https://docs.pytest.org/en/latest/index.html), branched off of nose +# +# +# * R unit-tests: +# * [RUnit](http://cran.r-project.org/web/packages/RUnit/index.html), +# * [svUnit](http://cran.r-project.org/web/packages/svUnit/index.html) +# * (works with [SciViews](http://www.sciviews.org/) GUI) +# +# +# * Fortran unit-tests: +# * [funit](https://rubygems.org/gems/funit), +# * [pfunit](http://sourceforge.net/projects/pfunit/)(works with MPI) + +# %% [markdown] +# ### pytest framework: usage +# +# [pytest](https://docs.pytest.org/en/latest/) is a recommended python testing framework. + +# %% [markdown] +# We can use its tools in the notebook for on-the-fly tests in the notebook. This, happily, includes the negative-tests example we were looking for a moment ago. + +# %% +def I_only_accept_positive_numbers(number): + # Check input + if number < 0: + raise ValueError("Input {} is negative".format(number)) + + # Do something + + +# %% +from pytest import raises + +# %% +with raises(ValueError): + I_only_accept_positive_numbers(-5) + + +# %% [markdown] +# but the real power comes when we write a test file alongside our code files in our homemade packages: + +# %% language="bash" +# mkdir -p saskatchewan +# touch saskatchewan/__init__.py + +# %% +# %%writefile saskatchewan/overlap.py +def overlap(field1, field2): + left1, bottom1, top1, right1 = field1 + left2, bottom2, top2, right2 = field2 + + overlap_left = max(left1, left2) + overlap_bottom = max(bottom1, bottom2) + overlap_right = min(right1, right2) + overlap_top = min(top1, top2) + # Here's our wrong code again + overlap_height = (overlap_top - overlap_bottom) + overlap_width = (overlap_right - overlap_left) + + return overlap_height * overlap_width + + +# %% +# %%writefile saskatchewan/test_overlap.py +from .overlap import overlap + +def test_full_overlap(): + assert overlap((1.,1.,4.,4.), (2.,2.,3.,3.)) == 1.0 + +def test_partial_overlap(): + assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0 + +def test_no_overlap(): + assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0 + + +# %% attributes={"classes": [" bash"], "id": ""} magic_args="--no-raise-error" language="bash" +# cd saskatchewan +# pytest + +# %% [markdown] +# Note that it reported **which** test had failed, how many tests ran, and how many failed. + +# %% [markdown] +# The symbol `..F` means there were three tests, of which the third one failed. + +# %% [markdown] +# Pytest will: +# +# * automagically finds files ``test_*.py`` +# * collects all subroutines called ``test_*`` +# * runs tests and reports results + +# %% [markdown] +# Some options: +# +# * help: `pytest --help` +# * run only tests for a given feature: `pytest -k foo` # tests with 'foo' in the test name + +# %% [markdown] +# ## Testing with floating points +# +# ### Floating points are not reals +# +# +# Floating points are inaccurate representations of real numbers: +# +# `1.0 == 0.99999999999999999` is true to the last bit. + +# %% [markdown] +# This can lead to numerical errors during calculations: $1000 (a - b) \neq 1000a - 1000b$ + +# %% +1000.0 * 1.0 - 1000.0 * 0.9999999999999998 + +# %% attributes={"classes": [" python"], "id": ""} +1000.0 * (1.0 - 0.9999999999999998) + +# %% [markdown] +# *Both* results are wrong: `2e-13` is the correct answer. +# +# The size of the error will depend on the magnitude of the floating points: + +# %% attributes={"classes": [" python"], "id": ""} +1000.0 * 1e5 - 1000.0 * 0.9999999999999998e5 + +# %% [markdown] +# The result should be `2e-8`. + +# %% [markdown] +# ### Comparing floating points +# +# Use the "approx", for a default of a relative tolerance of $10^{-6}$ + +# %% attributes={"classes": [" python"], "id": ""} +from pytest import approx +assert 0.7 == approx(0.7 + 1e-7) + +# %% [markdown] +# Or be more explicit: + +# %% attributes={"classes": [" python"], "id": ""} +magnitude = 0.7 +assert 0.7 == approx(0.701 , rel=0.1, abs=0.1) + +# %% [markdown] +# Choosing tolerances is a big area of [debate](https://software-carpentry.org/blog/2014/10/why-we-dont-teach-testing.html). + +# %% [markdown] +# ### Comparing vectors of floating points +# +# Numerical vectors are best represented using [numpy](http://www.numpy.org/). + +# %% attributes={"classes": [" python"], "id": ""} +from numpy import array, pi + +vector_of_reals = array([0.1, 0.2, 0.3, 0.4]) * pi + +# %% [markdown] +# Numpy ships with a number of assertions (in ``numpy.testing``) to make +# comparison easy: + +# %% attributes={"classes": [" python"], "id": ""} +from numpy import array, pi +from numpy.testing import assert_allclose +expected = array([0.1, 0.2, 0.3, 0.4, 1e-12]) * pi +actual = array([0.1, 0.2, 0.3, 0.4, 2e-12]) * pi +actual[:-1] += 1e-6 + +assert_allclose(actual, expected, rtol=1e-5, atol=1e-8) + +# %% [markdown] +# It compares the difference between `actual` and `expected` to ``atol + rtol * abs(expected)``. diff --git a/ch03tests/04EnergyExample.html b/ch03tests/04EnergyExample.html new file mode 100644 index 000000000..5367a5c27 --- /dev/null +++ b/ch03tests/04EnergyExample.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Energy Example + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Classroom exercise: energy calculation

Diffusion model in 1D

Description: A one-dimensional diffusion model. (Could be a gas of particles, or a bunch of crowded people in a corridor, or animals in a valley habitat...)

+
    +
  • Agents are on a 1d axis
  • +
  • Agents do not want to be where there are other agents
  • +
  • This is represented as an 'energy': the higher the energy, the more unhappy the agents.
  • +
+

Implementation:

+
    +
  • Given a vector $n$ of positive integers, and of arbitrary length
  • +
  • Compute the energy, $E(n) = \sum_i n_i(n_i - 1)$
  • +
  • Later, we will have the likelyhood of an agent moving depend on the change in energy.
  • +
+
+
+
+
+
+
In [1]:
+
+
+
import numpy as np
+from matplotlib import pyplot as plt
+%matplotlib inline
+
+density =  np.array([0, 0, 3, 5, 8, 4, 2, 1])
+fig, ax = plt.subplots()
+ax.bar(np.arange(len(density)), density)
+ax.xrange=[-0.5, len(density)-0.5]
+ax.set_ylabel("Particle count $n_i$")
+ax.set_xlabel("Position $i$")
+
+
+
+
+
+
+
+
Out[1]:
+
+
Text(0.5, 0, 'Position $i$')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Here, the total energy due to position 2 is $3 (3-1)=6$, and due to column 7 is $1 (1-1)=0$. We need to sum these to get the +total energy.

+
+
+
+
+
+
+

Starting point

Create a Python module:

+
+
+
+
+
+
In [2]:
+
+
+
%%bash
+mkdir -p diffusion
+touch diffusion/__init__.py
+
+
+
+
+
+
+
+
+
    +
  • Implementation file: diffusion_model.py
  • +
+
+
+
+
+
+
In [3]:
+
+
+
%%writefile diffusion/model.py
+def energy(density, coeff=1.0):
+    """ 
+    Energy associated with the diffusion model
+
+    Parameters
+    ----------
+
+    density: array of positive integers
+        Number of particles at each position i in the array
+    coeff: float
+        Diffusion coefficient.
+    """
+  # implementation goes here
+
+
+
+
+
+
+
+
+
+
Writing diffusion/model.py
+
+
+
+
+
+
+
+
+
+
    +
  • Testing file: test_diffusion_model.py
  • +
+
+
+
+
+
+
In [4]:
+
+
+
%%writefile diffusion/test_model.py
+from .model import energy
+def test_energy():
+    """ Optional description for reporting """
+    # Test something
+
+
+
+
+
+
+
+
+
+
Writing diffusion/test_model.py
+
+
+
+
+
+
+
+
+
+

Invoke the tests:

+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+cd diffusion
+pytest
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/diffusion
+plugins: cov-4.1.0, anyio-3.7.1
+collected 1 item
+
+test_model.py .                                                          [100%]
+
+============================== 1 passed in 0.01s ===============================
+
+
+
+
+
+
+
+
+
+

Now, write your code (in model.py), and tests (in test_model.py), testing as you do.

+
+
+
+
+
+
+

Solution

Don't look until after you've tried!

+
+
+
+
+
+
In [6]:
+
+
+
%%writefile diffusion/model.py
+"""  Simplistic 1-dimensional diffusion model """
+
+def energy(density):
+    """ 
+    Energy associated with the diffusion model
+    
+    :Parameters:
+    
+    density: array of positive integers
+      Number of particles at each position i in the array/geometry
+    """
+    from numpy import array, any, sum
+
+    # Make sure input is an numpy array
+    density = array(density)
+
+    # ...of the right kind (integer). Unless it is zero length, 
+    #    in which case type does not matter.
+      
+    if density.dtype.kind != 'i' and len(density) > 0:
+        raise TypeError("Density should be a array of *integers*.")
+    # and the right values (positive or null)
+    if any(density < 0):
+        raise ValueError("Density should be an array of *positive* integers.")
+    if density.ndim != 1:
+        raise ValueError("Density should be an a *1-dimensional*" + 
+                         "array of positive integers.")
+    
+    return sum(density * (density - 1))
+
+
+
+
+
+
+
+
+
+
Overwriting diffusion/model.py
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
%%writefile diffusion/test_model.py
+""" Unit tests for a diffusion model """
+
+from pytest import raises
+from .model import energy
+
+def test_energy_fails_on_non_integer_density():
+    with raises(TypeError) as exception: 
+        energy([1.0, 2, 3])
+    
+def test_energy_fails_on_negative_density():
+    with raises(ValueError) as exception: energy(
+            [-1, 2, 3])
+        
+def test_energy_fails_ndimensional_density():
+    with raises(ValueError) as exception: energy(
+            [[1, 2, 3], [3, 4, 5]])
+
+def test_zero_energy_cases():
+    # Zero energy at zero density
+    densities = [ [], [0], [0, 0, 0] ]
+    for density in densities: 
+        assert energy(density) == 0
+
+def test_derivative():
+    from numpy.random import randint
+
+    # Loop over vectors of different sizes (but not empty)
+    for vector_size in randint(1, 1000, size=30): 
+
+        # Create random density of size N
+        density = randint(50, size=vector_size)
+
+        # will do derivative at this index
+        element_index = randint(vector_size)
+
+        # modified densities
+        density_plus_one = density.copy()
+        density_plus_one[element_index] += 1
+
+        # Compute and check result
+        # d(n^2-1)/dn = 2n
+        expected = (2.0 * density[element_index] 
+                    if density[element_index] > 0 
+                    else 0 )
+        actual = energy(density_plus_one) - energy(density) 
+        assert expected == actual
+
+def test_derivative_no_self_energy():
+    """ If particle is alone, then its participation to energy is zero """
+    from numpy import array
+
+    density = array([1, 0, 1, 10, 15, 0])
+    density_plus_one = density.copy()
+    density[1] += 1 
+
+    expected = 0
+    actual = energy(density_plus_one) - energy(density) 
+    assert expected == actual
+
+
+
+
+
+
+
+
+
+
Overwriting diffusion/test_model.py
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+cd diffusion
+pytest
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/diffusion
+plugins: cov-4.1.0, anyio-3.7.1
+collected 6 items
+
+test_model.py ......                                                     [100%]
+
+============================== 6 passed in 0.10s ===============================
+
+
+
+
+
+
+
+
+
+

Coverage

With py.test, you can use the "pytest-cov" plugin to measure test coverage

+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+cd diffusion
+pytest --cov="diffusion"
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/diffusion
+plugins: cov-4.1.0, anyio-3.7.1
+collected 6 items
+
+test_model.py ......                                                     [100%]
+
+---------- coverage: platform linux, python 3.8.18-final-0 -----------
+Name            Stmts   Miss  Cover
+-----------------------------------
+__init__.py         0      0   100%
+model.py           10      0   100%
+test_model.py      31      0   100%
+-----------------------------------
+TOTAL              41      0   100%
+
+
+============================== 6 passed in 0.15s ===============================
+
+
+
+
+
+
+
+
+
+

Or an html report:

+
+
+
+
+
+
In [10]:
+
+
+
%%bash
+cd diffusion
+pytest --cov="diffusion" --cov-report html
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/diffusion
+plugins: cov-4.1.0, anyio-3.7.1
+collected 6 items
+
+test_model.py ......                                                     [100%]
+
+---------- coverage: platform linux, python 3.8.18-final-0 -----------
+Coverage HTML written to dir htmlcov
+
+
+============================== 6 passed in 0.16s ===============================
+
+
+
+
+
+
+
+
+
+

Look at the coverage results

+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/04EnergyExample.ipynb b/ch03tests/04EnergyExample.ipynb new file mode 100644 index 000000000..381376742 --- /dev/null +++ b/ch03tests/04EnergyExample.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9487b614", + "metadata": {}, + "source": [ + "## Classroom exercise: energy calculation\n", + "\n", + "### Diffusion model in 1D\n", + "\n", + "Description: A one-dimensional diffusion model. (Could be a gas of particles, or a bunch of crowded people in a corridor, or animals in a valley habitat...)\n", + "\n", + "- Agents are on a 1d axis\n", + "- Agents do not want to be where there are other agents\n", + "- This is represented as an 'energy': the higher the energy, the more unhappy the agents.\n", + "\n", + "Implementation:\n", + "\n", + "- Given a vector $n$ of positive integers, and of arbitrary length\n", + "- Compute the energy, $E(n) = \\sum_i n_i(n_i - 1)$\n", + "- Later, we will have the likelyhood of an agent moving depend on the change in energy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d985df78", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "\n", + "density = np.array([0, 0, 3, 5, 8, 4, 2, 1])\n", + "fig, ax = plt.subplots()\n", + "ax.bar(np.arange(len(density)), density)\n", + "ax.xrange=[-0.5, len(density)-0.5]\n", + "ax.set_ylabel(\"Particle count $n_i$\")\n", + "ax.set_xlabel(\"Position $i$\")" + ] + }, + { + "cell_type": "markdown", + "id": "320f9cf4", + "metadata": {}, + "source": [ + "Here, the total energy due to position 2 is $3 (3-1)=6$, and due to column 7 is $1 (1-1)=0$. We need to sum these to get the\n", + "total energy." + ] + }, + { + "cell_type": "markdown", + "id": "48691efb", + "metadata": {}, + "source": [ + "### Starting point\n", + "\n", + "Create a Python module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8165009a", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p diffusion\n", + "touch diffusion/__init__.py" + ] + }, + { + "cell_type": "markdown", + "id": "1abfe615", + "metadata": {}, + "source": [ + "* Implementation file: diffusion_model.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "676b71c3", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "%%writefile diffusion/model.py\n", + "def energy(density, coeff=1.0):\n", + " \"\"\" \n", + " Energy associated with the diffusion model\n", + "\n", + " Parameters\n", + " ----------\n", + "\n", + " density: array of positive integers\n", + " Number of particles at each position i in the array\n", + " coeff: float\n", + " Diffusion coefficient.\n", + " \"\"\"\n", + " # implementation goes here" + ] + }, + { + "cell_type": "markdown", + "id": "8730dd63", + "metadata": {}, + "source": [ + "* Testing file: test_diffusion_model.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86bf1fa0", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "%%writefile diffusion/test_model.py\n", + "from .model import energy\n", + "def test_energy():\n", + " \"\"\" Optional description for reporting \"\"\"\n", + " # Test something" + ] + }, + { + "cell_type": "markdown", + "id": "d1b43049", + "metadata": {}, + "source": [ + "Invoke the tests:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95109496", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd diffusion\n", + "pytest" + ] + }, + { + "cell_type": "markdown", + "id": "01902ac6", + "metadata": {}, + "source": [ + "Now, write your code (in `model.py`), and tests (in `test_model.py`), testing as you do." + ] + }, + { + "cell_type": "markdown", + "id": "d4ddec74", + "metadata": {}, + "source": [ + "### Solution\n", + "\n", + "Don't look until after you've tried!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0d24927", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile diffusion/model.py\n", + "\"\"\" Simplistic 1-dimensional diffusion model \"\"\"\n", + "\n", + "def energy(density):\n", + " \"\"\" \n", + " Energy associated with the diffusion model\n", + " \n", + " :Parameters:\n", + " \n", + " density: array of positive integers\n", + " Number of particles at each position i in the array/geometry\n", + " \"\"\"\n", + " from numpy import array, any, sum\n", + "\n", + " # Make sure input is an numpy array\n", + " density = array(density)\n", + "\n", + " # ...of the right kind (integer). Unless it is zero length, \n", + " # in which case type does not matter.\n", + " \n", + " if density.dtype.kind != 'i' and len(density) > 0:\n", + " raise TypeError(\"Density should be a array of *integers*.\")\n", + " # and the right values (positive or null)\n", + " if any(density < 0):\n", + " raise ValueError(\"Density should be an array of *positive* integers.\")\n", + " if density.ndim != 1:\n", + " raise ValueError(\"Density should be an a *1-dimensional*\" + \n", + " \"array of positive integers.\")\n", + " \n", + " return sum(density * (density - 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90b3b59f", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile diffusion/test_model.py\n", + "\"\"\" Unit tests for a diffusion model \"\"\"\n", + "\n", + "from pytest import raises\n", + "from .model import energy\n", + "\n", + "def test_energy_fails_on_non_integer_density():\n", + " with raises(TypeError) as exception: \n", + " energy([1.0, 2, 3])\n", + " \n", + "def test_energy_fails_on_negative_density():\n", + " with raises(ValueError) as exception: energy(\n", + " [-1, 2, 3])\n", + " \n", + "def test_energy_fails_ndimensional_density():\n", + " with raises(ValueError) as exception: energy(\n", + " [[1, 2, 3], [3, 4, 5]])\n", + "\n", + "def test_zero_energy_cases():\n", + " # Zero energy at zero density\n", + " densities = [ [], [0], [0, 0, 0] ]\n", + " for density in densities: \n", + " assert energy(density) == 0\n", + "\n", + "def test_derivative():\n", + " from numpy.random import randint\n", + "\n", + " # Loop over vectors of different sizes (but not empty)\n", + " for vector_size in randint(1, 1000, size=30): \n", + "\n", + " # Create random density of size N\n", + " density = randint(50, size=vector_size)\n", + "\n", + " # will do derivative at this index\n", + " element_index = randint(vector_size)\n", + "\n", + " # modified densities\n", + " density_plus_one = density.copy()\n", + " density_plus_one[element_index] += 1\n", + "\n", + " # Compute and check result\n", + " # d(n^2-1)/dn = 2n\n", + " expected = (2.0 * density[element_index] \n", + " if density[element_index] > 0 \n", + " else 0 )\n", + " actual = energy(density_plus_one) - energy(density) \n", + " assert expected == actual\n", + "\n", + "def test_derivative_no_self_energy():\n", + " \"\"\" If particle is alone, then its participation to energy is zero \"\"\"\n", + " from numpy import array\n", + "\n", + " density = array([1, 0, 1, 10, 15, 0])\n", + " density_plus_one = density.copy()\n", + " density[1] += 1 \n", + "\n", + " expected = 0\n", + " actual = energy(density_plus_one) - energy(density) \n", + " assert expected == actual" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcca2b0a", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd diffusion\n", + "pytest" + ] + }, + { + "cell_type": "markdown", + "id": "014ecb11", + "metadata": {}, + "source": [ + "### Coverage\n", + "\n", + "With py.test, you can use the [\"pytest-cov\" plugin](https://github.com/pytest-dev/pytest-cov) to measure test coverage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b14e4b4", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd diffusion\n", + "pytest --cov=\"diffusion\"" + ] + }, + { + "cell_type": "markdown", + "id": "43bcd1a6", + "metadata": {}, + "source": [ + "Or an html report:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "569f85be", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd diffusion\n", + "pytest --cov=\"diffusion\" --cov-report html" + ] + }, + { + "cell_type": "markdown", + "id": "83d2922a", + "metadata": {}, + "source": [ + "Look at the [coverage results](diffusion/htmlcov/index.html)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b76e01f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jekyll": { + "display_name": "Energy Example" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/04EnergyExample.ipynb.py b/ch03tests/04EnergyExample.ipynb.py new file mode 100644 index 000000000..d1de4afc7 --- /dev/null +++ b/ch03tests/04EnergyExample.ipynb.py @@ -0,0 +1,220 @@ +# --- +# jupyter: +# jekyll: +# display_name: Energy Example +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Classroom exercise: energy calculation +# +# ### Diffusion model in 1D +# +# Description: A one-dimensional diffusion model. (Could be a gas of particles, or a bunch of crowded people in a corridor, or animals in a valley habitat...) +# +# - Agents are on a 1d axis +# - Agents do not want to be where there are other agents +# - This is represented as an 'energy': the higher the energy, the more unhappy the agents. +# +# Implementation: +# +# - Given a vector $n$ of positive integers, and of arbitrary length +# - Compute the energy, $E(n) = \sum_i n_i(n_i - 1)$ +# - Later, we will have the likelyhood of an agent moving depend on the change in energy. + +# %% +import numpy as np +from matplotlib import pyplot as plt +# %matplotlib inline + +density = np.array([0, 0, 3, 5, 8, 4, 2, 1]) +fig, ax = plt.subplots() +ax.bar(np.arange(len(density)), density) +ax.xrange=[-0.5, len(density)-0.5] +ax.set_ylabel("Particle count $n_i$") +ax.set_xlabel("Position $i$") + + +# %% [markdown] +# Here, the total energy due to position 2 is $3 (3-1)=6$, and due to column 7 is $1 (1-1)=0$. We need to sum these to get the +# total energy. + +# %% [markdown] +# ### Starting point +# +# Create a Python module: + +# %% language="bash" +# mkdir -p diffusion +# touch diffusion/__init__.py + +# %% [markdown] +# * Implementation file: diffusion_model.py + +# %% attributes={"classes": [" python"], "id": ""} +# %%writefile diffusion/model.py +def energy(density, coeff=1.0): + """ + Energy associated with the diffusion model + + Parameters + ---------- + + density: array of positive integers + Number of particles at each position i in the array + coeff: float + Diffusion coefficient. + """ + # implementation goes here + + +# %% [markdown] +# * Testing file: test_diffusion_model.py + +# %% attributes={"classes": [" python"], "id": ""} +# %%writefile diffusion/test_model.py +from .model import energy +def test_energy(): + """ Optional description for reporting """ + # Test something + + +# %% [markdown] +# Invoke the tests: + +# %% language="bash" +# cd diffusion +# pytest + +# %% [markdown] +# Now, write your code (in `model.py`), and tests (in `test_model.py`), testing as you do. + +# %% [markdown] +# ### Solution +# +# Don't look until after you've tried! + +# %% +# %%writefile diffusion/model.py +""" Simplistic 1-dimensional diffusion model """ + +def energy(density): + """ + Energy associated with the diffusion model + + :Parameters: + + density: array of positive integers + Number of particles at each position i in the array/geometry + """ + from numpy import array, any, sum + + # Make sure input is an numpy array + density = array(density) + + # ...of the right kind (integer). Unless it is zero length, + # in which case type does not matter. + + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be a array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + + return sum(density * (density - 1)) + + +# %% +# %%writefile diffusion/test_model.py +""" Unit tests for a diffusion model """ + +from pytest import raises +from .model import energy + +def test_energy_fails_on_non_integer_density(): + with raises(TypeError) as exception: + energy([1.0, 2, 3]) + +def test_energy_fails_on_negative_density(): + with raises(ValueError) as exception: energy( + [-1, 2, 3]) + +def test_energy_fails_ndimensional_density(): + with raises(ValueError) as exception: energy( + [[1, 2, 3], [3, 4, 5]]) + +def test_zero_energy_cases(): + # Zero energy at zero density + densities = [ [], [0], [0, 0, 0] ] + for density in densities: + assert energy(density) == 0 + +def test_derivative(): + from numpy.random import randint + + # Loop over vectors of different sizes (but not empty) + for vector_size in randint(1, 1000, size=30): + + # Create random density of size N + density = randint(50, size=vector_size) + + # will do derivative at this index + element_index = randint(vector_size) + + # modified densities + density_plus_one = density.copy() + density_plus_one[element_index] += 1 + + # Compute and check result + # d(n^2-1)/dn = 2n + expected = (2.0 * density[element_index] + if density[element_index] > 0 + else 0 ) + actual = energy(density_plus_one) - energy(density) + assert expected == actual + +def test_derivative_no_self_energy(): + """ If particle is alone, then its participation to energy is zero """ + from numpy import array + + density = array([1, 0, 1, 10, 15, 0]) + density_plus_one = density.copy() + density[1] += 1 + + expected = 0 + actual = energy(density_plus_one) - energy(density) + assert expected == actual + +# %% language="bash" +# cd diffusion +# pytest + +# %% [markdown] +# ### Coverage +# +# With py.test, you can use the ["pytest-cov" plugin](https://github.com/pytest-dev/pytest-cov) to measure test coverage + +# %% language="bash" +# cd diffusion +# pytest --cov="diffusion" + +# %% [markdown] +# Or an html report: + +# %% language="bash" +# cd diffusion +# pytest --cov="diffusion" --cov-report html + +# %% [markdown] +# Look at the [coverage results](diffusion/htmlcov/index.html) + +# %% diff --git a/ch03tests/05Mocks.html b/ch03tests/05Mocks.html new file mode 100644 index 000000000..f45094bc5 --- /dev/null +++ b/ch03tests/05Mocks.html @@ -0,0 +1,869 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mocks + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Mocking

+
+
+
+
+
+
+

Definition

Mock: verb,

+
    +
  1. to tease or laugh at in a scornful or contemptuous manner
  2. +
  3. to make a replica or imitation of something
  4. +
+
+
+
+
+
+
+

Mocking

+
    +
  • Replace a real object with a pretend object, which records how it is called, and can assert if it is called wrong
  • +
+
+
+
+
+
+
+

Mocking frameworks

+
+
+
+
+
+
+

Recording calls with mock

Mock objects record the calls made to them:

+
+
+
+
+
+
In [1]:
+
+
+
from unittest.mock import Mock
+function = Mock(name="myroutine", return_value=2)
+
+
+
+
+
+
+
+
In [2]:
+
+
+
function(1)
+
+
+
+
+
+
+
+
Out[2]:
+
+
2
+
+
+
+
+
+
+
+
In [3]:
+
+
+
function(5, "hello", a=True)
+
+
+
+
+
+
+
+
Out[3]:
+
+
2
+
+
+
+
+
+
+
+
In [4]:
+
+
+
function.mock_calls
+
+
+
+
+
+
+
+
Out[4]:
+
+
[call(1), call(5, 'hello', a=True)]
+
+
+
+
+
+
+
+
+

The arguments of each call can be recovered

+
+
+
+
+
+
In [5]:
+
+
+
name, args, kwargs = function.mock_calls[1]
+args, kwargs
+
+
+
+
+
+
+
+
Out[5]:
+
+
((5, 'hello'), {'a': True})
+
+
+
+
+
+
+
+
+

Mock objects can return different values for each call

+
+
+
+
+
+
In [6]:
+
+
+
function = Mock(name="myroutine", side_effect=[2, "xyz"])
+
+
+
+
+
+
+
+
In [7]:
+
+
+
function(1)
+
+
+
+
+
+
+
+
Out[7]:
+
+
2
+
+
+
+
+
+
+
+
In [8]:
+
+
+
function(1, "hello", {'a': True})
+
+
+
+
+
+
+
+
Out[8]:
+
+
'xyz'
+
+
+
+
+
+
+
+
+

We expect an error if there are no return values left in the list:

+
+
+
+
+
+
In [9]:
+
+
+
function()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+StopIteration                             Traceback (most recent call last)
+Cell In[9], line 1
+----> 1 function()
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/unittest/mock.py:1081, in CallableMixin.__call__(self, *args, **kwargs)
+   1079 self._mock_check_sig(*args, **kwargs)
+   1080 self._increment_mock_call(*args, **kwargs)
+-> 1081 return self._mock_call(*args, **kwargs)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/unittest/mock.py:1085, in CallableMixin._mock_call(self, *args, **kwargs)
+   1084 def _mock_call(self, /, *args, **kwargs):
+-> 1085     return self._execute_mock_call(*args, **kwargs)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/unittest/mock.py:1142, in CallableMixin._execute_mock_call(self, *args, **kwargs)
+   1140     raise effect
+   1141 elif not _callable(effect):
+-> 1142     result = next(effect)
+   1143     if _is_exception(result):
+   1144         raise result
+
+StopIteration: 
+
+
+
+
+
+
+
+
+

Using mocks to model test resources

+
+
+
+
+
+
+

Often we want to write tests for code which interacts with remote resources. (E.g. databases, the internet, or data files.)

+
+
+
+
+
+
+

We don't want to have our tests actually interact with the remote resource, as this would mean our tests failed +due to lost internet connections, for example.

+
+
+
+
+
+
+

Instead, we can use mocks to assert that our code does the right thing in terms of the messages it sends: the parameters of the +function calls it makes to the remote resource.

+
+
+
+
+
+
+

For example, consider the following code that downloads a map from the internet:

+
+
+
+
+
+
In [10]:
+
+
+
import requests
+
+def map_at(lat, long, satellite=False, zoom=12, 
+           size=(400, 400)):
+
+    base = "https://static-maps.yandex.ru/1.x/?"
+    
+    params = dict(
+        z = zoom,
+        size = ",".join(map(str,size)),
+        ll = ",".join(map(str,(long,lat))),
+        lang = "en_US")
+    
+    if satellite:
+        params["l"] = "sat"
+    else:
+        params["l"] = "map"
+        
+    return requests.get(base, params=params)
+
+
+
+
+
+
+
+
In [11]:
+
+
+
london_map = map_at(51.5073509, -0.1277583)
+from IPython.display import Image
+
+
+
+
+
+
+
+
In [12]:
+
+
+
%matplotlib inline
+Image(london_map.content)
+
+
+
+
+
+
+
+
Out[12]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We would like to test that it is building the parameters correctly. We can do this by mocking the requests object. We need to temporarily replace a method in the library with a mock. We can use "patch" to do this:

+
+
+
+
+
+
In [13]:
+
+
+
from unittest.mock import patch
+with patch.object(requests,'get') as mock_get:
+    london_map = map_at(51.5073509, -0.1277583)
+    print(mock_get.mock_calls)
+
+
+
+
+
+
+
+
+
+
[call('https://static-maps.yandex.ru/1.x/?', params={'z': 12, 'size': '400,400', 'll': '-0.1277583,51.5073509', 'lang': 'en_US', 'l': 'map'})]
+
+
+
+
+
+
+
+
+
+

Our tests then look like:

+
+
+
+
+
+
In [14]:
+
+
+
def test_build_default_params():
+    with patch.object(requests,'get') as mock_get:
+        default_map = map_at(51.0, 0.0)
+        mock_get.assert_called_with(
+        "https://static-maps.yandex.ru/1.x/?",
+        params={
+            'z':12,
+            'size':'400,400',
+            'll':'0.0,51.0',
+            'lang':'en_US',
+            'l': 'map'
+        }
+    )
+test_build_default_params()
+
+
+
+
+
+
+
+
+

That was quiet, so it passed. When I'm writing tests, I usually modify one of the expectations, to something 'wrong', just to check it's not +passing "by accident", run the tests, then change it back!

+
+
+
+
+
+
+

Testing functions that call other functions

+
+
+
+
+
+
In [15]:
+
+
+
def partial_derivative(function, at, direction, delta=1.0):
+    f_x = function(at)
+    x_plus_delta = at[:]
+    x_plus_delta[direction] += delta
+    f_x_plus_delta = function(x_plus_delta)
+    return (f_x_plus_delta - f_x) / delta
+
+
+
+
+
+
+
+
+

We want to test that the above function does the right thing. It is supposed to compute the derivative of a function +of a vector in a particular direction.

+
+
+
+
+
+
+

E.g.:

+
+
+
+
+
+
In [16]:
+
+
+
partial_derivative(sum, [0,0,0], 1)
+
+
+
+
+
+
+
+
Out[16]:
+
+
1.0
+
+
+
+
+
+
+
+
+

How do we assert that it is doing the right thing? With tests like this:

+
+
+
+
+
+
In [17]:
+
+
+
from unittest.mock import MagicMock
+
+def test_derivative_2d_y_direction():
+    func = MagicMock()
+    partial_derivative(func, [0,0], 1)
+    func.assert_any_call([0, 1.0])
+    func.assert_any_call([0, 0])
+    
+
+test_derivative_2d_y_direction()
+
+
+
+
+
+
+
+
+

We made our mock a "Magic Mock" because otherwise, the mock results f_x_plus_delta and f_x can't be subtracted:

+
+
+
+
+
+
In [18]:
+
+
+
MagicMock() - MagicMock()
+
+
+
+
+
+
+
+
Out[18]:
+
+
<MagicMock name='mock.__sub__()' id='139857891272448'>
+
+
+
+
+
+
+
+
In [19]:
+
+
+
Mock() - Mock()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[19], line 1
+----> 1 Mock() - Mock()
+
+TypeError: unsupported operand type(s) for -: 'Mock' and 'Mock'
+
+
+
+
+
+
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/05Mocks.ipynb b/ch03tests/05Mocks.ipynb new file mode 100644 index 000000000..dae9b7e85 --- /dev/null +++ b/ch03tests/05Mocks.ipynb @@ -0,0 +1,466 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8bb075d4", + "metadata": {}, + "source": [ + "## Mocking" + ] + }, + { + "cell_type": "markdown", + "id": "b498cc40", + "metadata": {}, + "source": [ + "### Definition\n", + "\n", + "**Mock**: *verb*,\n", + "\n", + "1. to tease or laugh at in a scornful or contemptuous manner\n", + "2. to make a replica or imitation of something\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4cf4e18d", + "metadata": {}, + "source": [ + "**Mocking**\n", + "\n", + "- Replace a real object with a pretend object, which records how it is called, and can assert if it is called wrong" + ] + }, + { + "cell_type": "markdown", + "id": "e6dfffdd", + "metadata": {}, + "source": [ + "### Mocking frameworks\n", + "\n", + "* C: [CMocka](http://www.cmocka.org/)\n", + "* C++: [googletest](https://github.com/google/googletest)\n", + "* Python: [unittest.mock](http://docs.python.org/3/library/unittest.mock)" + ] + }, + { + "cell_type": "markdown", + "id": "bd9560f5", + "metadata": {}, + "source": [ + "### Recording calls with mock\n", + "\n", + "Mock objects record the calls made to them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1dc1765", + "metadata": {}, + "outputs": [], + "source": [ + "from unittest.mock import Mock\n", + "function = Mock(name=\"myroutine\", return_value=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0970bfd5", + "metadata": {}, + "outputs": [], + "source": [ + "function(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e07b3c59", + "metadata": {}, + "outputs": [], + "source": [ + "function(5, \"hello\", a=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d73b2c1d", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "function.mock_calls" + ] + }, + { + "cell_type": "markdown", + "id": "d29bfa46", + "metadata": {}, + "source": [ + "The arguments of each call can be recovered" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "583374a2", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "name, args, kwargs = function.mock_calls[1]\n", + "args, kwargs" + ] + }, + { + "cell_type": "markdown", + "id": "3b1b21e2", + "metadata": {}, + "source": [ + "Mock objects can return different values for each call" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edc13895", + "metadata": {}, + "outputs": [], + "source": [ + "function = Mock(name=\"myroutine\", side_effect=[2, \"xyz\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87a3c1d3", + "metadata": {}, + "outputs": [], + "source": [ + "function(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51bbcfaa", + "metadata": {}, + "outputs": [], + "source": [ + "function(1, \"hello\", {'a': True})" + ] + }, + { + "cell_type": "markdown", + "id": "7e2ec22f", + "metadata": {}, + "source": [ + "We expect an error if there are no return values left in the list:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d51ffd21", + "metadata": {}, + "outputs": [], + "source": [ + "function()" + ] + }, + { + "cell_type": "markdown", + "id": "ba7acf47", + "metadata": {}, + "source": [ + "### Using mocks to model test resources" + ] + }, + { + "cell_type": "markdown", + "id": "9f289e51", + "metadata": {}, + "source": [ + "Often we want to write tests for code which interacts with remote resources. (E.g. databases, the internet, or data files.)" + ] + }, + { + "cell_type": "markdown", + "id": "1bccd9fe", + "metadata": {}, + "source": [ + "We don't want to have our tests *actually* interact with the remote resource, as this would mean our tests failed\n", + "due to lost internet connections, for example." + ] + }, + { + "cell_type": "markdown", + "id": "5d948652", + "metadata": {}, + "source": [ + "Instead, we can use mocks to assert that our code does the right thing in terms of the *messages it sends*: the parameters of the\n", + "function calls it makes to the remote resource." + ] + }, + { + "cell_type": "markdown", + "id": "66d9cae1", + "metadata": {}, + "source": [ + "For example, consider the following code that downloads a map from the internet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0cd06c4f", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "def map_at(lat, long, satellite=False, zoom=12, \n", + " size=(400, 400)):\n", + "\n", + " base = \"https://static-maps.yandex.ru/1.x/?\"\n", + " \n", + " params = dict(\n", + " z = zoom,\n", + " size = \",\".join(map(str,size)),\n", + " ll = \",\".join(map(str,(long,lat))),\n", + " lang = \"en_US\")\n", + " \n", + " if satellite:\n", + " params[\"l\"] = \"sat\"\n", + " else:\n", + " params[\"l\"] = \"map\"\n", + " \n", + " return requests.get(base, params=params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc171b05", + "metadata": {}, + "outputs": [], + "source": [ + "london_map = map_at(51.5073509, -0.1277583)\n", + "from IPython.display import Image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02b13cc0", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "Image(london_map.content)" + ] + }, + { + "cell_type": "markdown", + "id": "f68b584e", + "metadata": {}, + "source": [ + "We would like to test that it is building the parameters correctly. We can do this by **mocking** the requests object. We need to temporarily replace a method in the library with a mock. We can use \"patch\" to do this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96a7186d", + "metadata": {}, + "outputs": [], + "source": [ + "from unittest.mock import patch\n", + "with patch.object(requests,'get') as mock_get:\n", + " london_map = map_at(51.5073509, -0.1277583)\n", + " print(mock_get.mock_calls)" + ] + }, + { + "cell_type": "markdown", + "id": "895d99c1", + "metadata": {}, + "source": [ + "Our tests then look like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7eabdfd6", + "metadata": {}, + "outputs": [], + "source": [ + "def test_build_default_params():\n", + " with patch.object(requests,'get') as mock_get:\n", + " default_map = map_at(51.0, 0.0)\n", + " mock_get.assert_called_with(\n", + " \"https://static-maps.yandex.ru/1.x/?\",\n", + " params={\n", + " 'z':12,\n", + " 'size':'400,400',\n", + " 'll':'0.0,51.0',\n", + " 'lang':'en_US',\n", + " 'l': 'map'\n", + " }\n", + " )\n", + "test_build_default_params()" + ] + }, + { + "cell_type": "markdown", + "id": "db4e052e", + "metadata": {}, + "source": [ + "That was quiet, so it passed. When I'm writing tests, I usually modify one of the expectations, to something 'wrong', just to check it's not\n", + "passing \"by accident\", run the tests, then change it back!" + ] + }, + { + "cell_type": "markdown", + "id": "7ff0dc04", + "metadata": {}, + "source": [ + "### Testing functions that call other functions\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70c16561", + "metadata": { + "attributes": { + "classes": [ + " python" + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "def partial_derivative(function, at, direction, delta=1.0):\n", + " f_x = function(at)\n", + " x_plus_delta = at[:]\n", + " x_plus_delta[direction] += delta\n", + " f_x_plus_delta = function(x_plus_delta)\n", + " return (f_x_plus_delta - f_x) / delta" + ] + }, + { + "cell_type": "markdown", + "id": "47337843", + "metadata": {}, + "source": [ + "We want to test that the above function does the right thing. It is supposed to compute the derivative of a function\n", + "of a vector in a particular direction." + ] + }, + { + "cell_type": "markdown", + "id": "167a9c88", + "metadata": {}, + "source": [ + "E.g.:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c32cbe42", + "metadata": {}, + "outputs": [], + "source": [ + "partial_derivative(sum, [0,0,0], 1)" + ] + }, + { + "cell_type": "markdown", + "id": "16d8ad7d", + "metadata": {}, + "source": [ + "How do we assert that it is doing the right thing? With tests like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9bb7bb7", + "metadata": {}, + "outputs": [], + "source": [ + "from unittest.mock import MagicMock\n", + "\n", + "def test_derivative_2d_y_direction():\n", + " func = MagicMock()\n", + " partial_derivative(func, [0,0], 1)\n", + " func.assert_any_call([0, 1.0])\n", + " func.assert_any_call([0, 0])\n", + " \n", + "\n", + "test_derivative_2d_y_direction()" + ] + }, + { + "cell_type": "markdown", + "id": "6c00964a", + "metadata": {}, + "source": [ + "We made our mock a \"Magic Mock\" because otherwise, the mock results `f_x_plus_delta` and `f_x` can't be subtracted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a06ed4b6", + "metadata": {}, + "outputs": [], + "source": [ + "MagicMock() - MagicMock()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a32cd8b", + "metadata": {}, + "outputs": [], + "source": [ + "Mock() - Mock()" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Mocks" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/05Mocks.ipynb.py b/ch03tests/05Mocks.ipynb.py new file mode 100644 index 000000000..87da88360 --- /dev/null +++ b/ch03tests/05Mocks.ipynb.py @@ -0,0 +1,209 @@ +# --- +# jupyter: +# jekyll: +# display_name: Mocks +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Mocking + +# %% [markdown] +# ### Definition +# +# **Mock**: *verb*, +# +# 1. to tease or laugh at in a scornful or contemptuous manner +# 2. to make a replica or imitation of something +# +# + +# %% [markdown] +# **Mocking** +# +# - Replace a real object with a pretend object, which records how it is called, and can assert if it is called wrong + +# %% [markdown] +# ### Mocking frameworks +# +# * C: [CMocka](http://www.cmocka.org/) +# * C++: [googletest](https://github.com/google/googletest) +# * Python: [unittest.mock](http://docs.python.org/3/library/unittest.mock) + +# %% [markdown] +# ### Recording calls with mock +# +# Mock objects record the calls made to them: + +# %% +from unittest.mock import Mock +function = Mock(name="myroutine", return_value=2) + +# %% +function(1) + +# %% +function(5, "hello", a=True) + +# %% attributes={"classes": [" python"], "id": ""} +function.mock_calls + +# %% [markdown] +# The arguments of each call can be recovered + +# %% attributes={"classes": [" python"], "id": ""} +name, args, kwargs = function.mock_calls[1] +args, kwargs + +# %% [markdown] +# Mock objects can return different values for each call + +# %% +function = Mock(name="myroutine", side_effect=[2, "xyz"]) + +# %% +function(1) + +# %% +function(1, "hello", {'a': True}) + +# %% [markdown] +# We expect an error if there are no return values left in the list: + +# %% +function() + +# %% [markdown] +# ### Using mocks to model test resources + +# %% [markdown] +# Often we want to write tests for code which interacts with remote resources. (E.g. databases, the internet, or data files.) + +# %% [markdown] +# We don't want to have our tests *actually* interact with the remote resource, as this would mean our tests failed +# due to lost internet connections, for example. + +# %% [markdown] +# Instead, we can use mocks to assert that our code does the right thing in terms of the *messages it sends*: the parameters of the +# function calls it makes to the remote resource. + +# %% [markdown] +# For example, consider the following code that downloads a map from the internet: + +# %% +import requests + +def map_at(lat, long, satellite=False, zoom=12, + size=(400, 400)): + + base = "https://static-maps.yandex.ru/1.x/?" + + params = dict( + z = zoom, + size = ",".join(map(str,size)), + ll = ",".join(map(str,(long,lat))), + lang = "en_US") + + if satellite: + params["l"] = "sat" + else: + params["l"] = "map" + + return requests.get(base, params=params) + + +# %% +london_map = map_at(51.5073509, -0.1277583) +from IPython.display import Image + +# %% +# %matplotlib inline +Image(london_map.content) + +# %% [markdown] +# We would like to test that it is building the parameters correctly. We can do this by **mocking** the requests object. We need to temporarily replace a method in the library with a mock. We can use "patch" to do this: + +# %% +from unittest.mock import patch +with patch.object(requests,'get') as mock_get: + london_map = map_at(51.5073509, -0.1277583) + print(mock_get.mock_calls) + + +# %% [markdown] +# Our tests then look like: + +# %% +def test_build_default_params(): + with patch.object(requests,'get') as mock_get: + default_map = map_at(51.0, 0.0) + mock_get.assert_called_with( + "https://static-maps.yandex.ru/1.x/?", + params={ + 'z':12, + 'size':'400,400', + 'll':'0.0,51.0', + 'lang':'en_US', + 'l': 'map' + } + ) +test_build_default_params() + + +# %% [markdown] +# That was quiet, so it passed. When I'm writing tests, I usually modify one of the expectations, to something 'wrong', just to check it's not +# passing "by accident", run the tests, then change it back! + +# %% [markdown] +# ### Testing functions that call other functions +# +#
+ +# %% attributes={"classes": [" python"], "id": ""} +def partial_derivative(function, at, direction, delta=1.0): + f_x = function(at) + x_plus_delta = at[:] + x_plus_delta[direction] += delta + f_x_plus_delta = function(x_plus_delta) + return (f_x_plus_delta - f_x) / delta + + +# %% [markdown] +# We want to test that the above function does the right thing. It is supposed to compute the derivative of a function +# of a vector in a particular direction. + +# %% [markdown] +# E.g.: + +# %% +partial_derivative(sum, [0,0,0], 1) + +# %% [markdown] +# How do we assert that it is doing the right thing? With tests like this: + +# %% +from unittest.mock import MagicMock + +def test_derivative_2d_y_direction(): + func = MagicMock() + partial_derivative(func, [0,0], 1) + func.assert_any_call([0, 1.0]) + func.assert_any_call([0, 0]) + + +test_derivative_2d_y_direction() + +# %% [markdown] +# We made our mock a "Magic Mock" because otherwise, the mock results `f_x_plus_delta` and `f_x` can't be subtracted: + +# %% +MagicMock() - MagicMock() + +# %% +Mock() - Mock() diff --git a/ch03tests/06Debugger.html b/ch03tests/06Debugger.html new file mode 100644 index 000000000..6d2c73e0d --- /dev/null +++ b/ch03tests/06Debugger.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Debugger + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Using a debugger

Stepping through the code

Debuggers are programs that can be used to test other programs. They allow programmers to suspend execution of the target program and inspect variables at that point.

+ +
+
+
+
+
+
+

Using the python debugger

+
+
+
+
+
+
+

Unfortunately this doesn't work nicely in the notebook. But from the command line, you can run a python program with:

+
+
+
+
+
+
+
python -m pdb my_program.py
+
+
+
+
+
+
+
+

Basic navigation:

Basic command to navigate the code and the python debugger:

+
    +
  • help: prints the help
  • +
  • help n: prints help about command n
  • +
  • n(ext): executes one line of code. Executes and steps over functions.
  • +
  • s(tep): step into current function in line of code
  • +
  • l(ist): list program around current position
  • +
  • w(where): prints current stack (where we are in code)
  • +
  • [enter]: repeats last command
  • +
  • anypythonvariable: print the value of that variable
  • +
+

The python debugger is a python shell: it can print and compute values, and even change the values +of the variables at that point in the program.

+

Breakpoints

Break points tell debugger where and when to stop +We say

+
    +
  • b somefunctionname
  • +
+
+
+
+
+
+
In [1]:
+
+
+
%%writefile solutions/diffusionmodel/energy_example.py
+from diffusion_model import energy
+print(energy([5, 6, 7, 8, 0, 1]))
+
+
+
+
+
+
+
+
+
+
Writing solutions/diffusionmodel/energy_example.py
+
+
+
+
+
+
+
+
+
+

The debugger is, of course, most used interactively, but here I'm showing a prewritten debugger script:

+
+
+
+
+
+
In [2]:
+
+
+
%%writefile commands
+restart  # restart session
+n
+b energy # program will stop when entering energy
+c        # continue program until break point is reached
+print(density) # We are now "inside" the energy function and can print any variable.
+
+
+
+
+
+
+
+
+
+
Writing commands
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+python -m pdb solutions/diffusionmodel/energy_example.py < commands
+
+
+
+
+
+
+
+
+
+
> /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/energy_example.py(1)<module>()
+-> from diffusion_model import energy
+(Pdb) Restarting /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/energy_example.py with arguments:
+	# restart session
+> /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/energy_example.py(1)<module>()
+-> from diffusion_model import energy
+(Pdb) > /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/energy_example.py(2)<module>()
+-> print(energy([5, 6, 7, 8, 0, 1]))
+(Pdb) Breakpoint 1 at /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/diffusion_model.py:2
+(Pdb) > /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/solutions/diffusionmodel/diffusion_model.py(9)energy()
+-> from numpy import array, any, sum
+(Pdb) [5, 6, 7, 8, 0, 1]
+(Pdb) 
+
+
+
+
+
+
+
+
+
+

Alternatively, break-points can be set on files: b file.py:20 will stop on line 20 of file.py.

+
+
+
+
+
+
+

Post-mortem

Debugging when something goes wrong:

+
    +
  1. Have a crash somewhere in the code
  2. +
  3. run python -m pdb file.py or run the cell with %pdb on
  4. +
+

The program should stop where the exception was raised

+
    +
  1. use w and l for position in code and in call stack
  2. +
  3. use up and down to navigate up and down the call stack
  4. +
  5. inspect variables along the way to understand failure
  6. +
+
+
+
+
+
+
+

This does work in the notebook.

+
+
+
+
+
+
+
%pdb on
+from diffusion.model import energy
+partial_derivative(energy,[5,6,7,8,0,1],5)
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/06Debugger.ipynb b/ch03tests/06Debugger.ipynb new file mode 100644 index 000000000..f00dc515f --- /dev/null +++ b/ch03tests/06Debugger.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "14aead5f", + "metadata": {}, + "source": [ + "## Using a debugger\n", + "\n", + "### Stepping through the code\n", + "\n", + "Debuggers are programs that can be used to test other programs. They allow programmers to suspend execution of the target program and inspect variables at that point.\n", + "\n", + "* Mac - compiled languages:\n", + " [Xcode](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/chapters/quickstart.html)\n", + "* Windows - compiled languages:\n", + " [Visual Studio](http://msdn.microsoft.com/en-us/library/bb483011.aspx)\n", + "* Linux: [DDD](https://www.gnu.org/software/ddd/)\n", + "* all platforms: [eclipse](http://www.eclipse.org), [gdb](http://www.sourceware.org/gdb/) (DDD and\n", + " eclipse are GUIs for gdb)\n", + "* python: [spyder](https://www.spyder-ide.org/),\n", + "* [pdb](https://docs.python.org/3.6/library/pdb.html)\n", + "* R: [RStudio](http://www.rstudio.com/ide/docs/debugging/overview),\n", + " [debug](http://stat.ethz.ch/R-manual/R-devel/library/base/html/debug.html),\n", + " [browser](http://stat.ethz.ch/R-manual/R-devel/library/base/html/browser.html)" + ] + }, + { + "cell_type": "markdown", + "id": "b94e286d", + "metadata": {}, + "source": [ + "### Using the python debugger" + ] + }, + { + "cell_type": "markdown", + "id": "45549124", + "metadata": {}, + "source": [ + "Unfortunately this doesn't work nicely in the notebook. But from the command line, you can run a python program with:" + ] + }, + { + "cell_type": "markdown", + "id": "7ecc7ad0", + "metadata": {}, + "source": [ + "``` bash\n", + "python -m pdb my_program.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "911877df", + "metadata": {}, + "source": [ + "### Basic navigation:\n", + "\n", + "Basic command to navigate the code and the python debugger:\n", + "\n", + "* `help`: prints the help\n", + "* `help n`: prints help about command `n`\n", + "* `n`(ext): executes one line of code. Executes and steps **over** functions.\n", + "* `s`(tep): step into current function in line of code\n", + "* `l`(ist): list program around current position\n", + "* `w`(where): prints current stack (where we are in code)\n", + "* `[enter]`: repeats last command\n", + "* `anypythonvariable`: print the value of that variable\n", + "\n", + "The python debugger is **a python shell**: it can print and compute values, and even change the values\n", + "of the variables at that point in the program.\n", + "\n", + "### Breakpoints\n", + "\n", + "Break points tell debugger where and when to stop\n", + "We say\n", + "* `b somefunctionname` " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af3f136a", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile solutions/diffusionmodel/energy_example.py\n", + "from diffusion_model import energy\n", + "print(energy([5, 6, 7, 8, 0, 1]))" + ] + }, + { + "cell_type": "markdown", + "id": "45884407", + "metadata": {}, + "source": [ + "The debugger is, of course, most used interactively, but here I'm showing a prewritten debugger script:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "517e4447", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile commands\n", + "restart # restart session\n", + "n\n", + "b energy # program will stop when entering energy\n", + "c # continue program until break point is reached\n", + "print(density) # We are now \"inside\" the energy function and can print any variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e012f2d3", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "python -m pdb solutions/diffusionmodel/energy_example.py < commands\n" + ] + }, + { + "cell_type": "markdown", + "id": "d5cd97df", + "metadata": {}, + "source": [ + "Alternatively, break-points can be set on files: `b file.py:20` will stop on line 20 of `file.py`." + ] + }, + { + "cell_type": "markdown", + "id": "cfc8ba72", + "metadata": {}, + "source": [ + "### Post-mortem\n", + "\n", + "Debugging when something goes wrong:\n", + "\n", + "1. Have a crash somewhere in the code\n", + "1. run `python -m pdb file.py` or run the cell with `%pdb on`\n", + "\n", + "The program should stop where the exception was raised\n", + "\n", + "1. use `w` and `l` for position in code and in call stack\n", + "1. use `up` and `down` to navigate up and down the call stack\n", + "1. inspect variables along the way to understand failure" + ] + }, + { + "cell_type": "markdown", + "id": "6dffdf66", + "metadata": {}, + "source": [ + "This **does** work in the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "0bb562a9", + "metadata": {}, + "source": [ + "```\n", + "%pdb on\n", + "from diffusion.model import energy\n", + "partial_derivative(energy,[5,6,7,8,0,1],5)\n", + "```" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Debugger" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/06Debugger.ipynb.py b/ch03tests/06Debugger.ipynb.py new file mode 100644 index 000000000..9da1623a7 --- /dev/null +++ b/ch03tests/06Debugger.ipynb.py @@ -0,0 +1,113 @@ +# --- +# jupyter: +# jekyll: +# display_name: Debugger +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Using a debugger +# +# ### Stepping through the code +# +# Debuggers are programs that can be used to test other programs. They allow programmers to suspend execution of the target program and inspect variables at that point. +# +# * Mac - compiled languages: +# [Xcode](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/chapters/quickstart.html) +# * Windows - compiled languages: +# [Visual Studio](http://msdn.microsoft.com/en-us/library/bb483011.aspx) +# * Linux: [DDD](https://www.gnu.org/software/ddd/) +# * all platforms: [eclipse](http://www.eclipse.org), [gdb](http://www.sourceware.org/gdb/) (DDD and +# eclipse are GUIs for gdb) +# * python: [spyder](https://www.spyder-ide.org/), +# * [pdb](https://docs.python.org/3.6/library/pdb.html) +# * R: [RStudio](http://www.rstudio.com/ide/docs/debugging/overview), +# [debug](http://stat.ethz.ch/R-manual/R-devel/library/base/html/debug.html), +# [browser](http://stat.ethz.ch/R-manual/R-devel/library/base/html/browser.html) + +# %% [markdown] +# ### Using the python debugger + +# %% [markdown] +# Unfortunately this doesn't work nicely in the notebook. But from the command line, you can run a python program with: + +# %% [markdown] +# ``` bash +# python -m pdb my_program.py +# ``` + +# %% [markdown] +# ### Basic navigation: +# +# Basic command to navigate the code and the python debugger: +# +# * `help`: prints the help +# * `help n`: prints help about command `n` +# * `n`(ext): executes one line of code. Executes and steps **over** functions. +# * `s`(tep): step into current function in line of code +# * `l`(ist): list program around current position +# * `w`(where): prints current stack (where we are in code) +# * `[enter]`: repeats last command +# * `anypythonvariable`: print the value of that variable +# +# The python debugger is **a python shell**: it can print and compute values, and even change the values +# of the variables at that point in the program. +# +# ### Breakpoints +# +# Break points tell debugger where and when to stop +# We say +# * `b somefunctionname` + +# %% +# %%writefile solutions/diffusionmodel/energy_example.py +from diffusion_model import energy +print(energy([5, 6, 7, 8, 0, 1])) + +# %% [markdown] +# The debugger is, of course, most used interactively, but here I'm showing a prewritten debugger script: + +# %% +# %%writefile commands +restart # restart session +n +b energy # program will stop when entering energy +c # continue program until break point is reached +print(density) # We are now "inside" the energy function and can print any variable. + +# %% language="bash" +# python -m pdb solutions/diffusionmodel/energy_example.py < commands +# + +# %% [markdown] +# Alternatively, break-points can be set on files: `b file.py:20` will stop on line 20 of `file.py`. + +# %% [markdown] +# ### Post-mortem +# +# Debugging when something goes wrong: +# +# 1. Have a crash somewhere in the code +# 1. run `python -m pdb file.py` or run the cell with `%pdb on` +# +# The program should stop where the exception was raised +# +# 1. use `w` and `l` for position in code and in call stack +# 1. use `up` and `down` to navigate up and down the call stack +# 1. inspect variables along the way to understand failure + +# %% [markdown] +# This **does** work in the notebook. + +# %% [markdown] +# ``` +# %pdb on +# from diffusion.model import energy +# partial_derivative(energy,[5,6,7,8,0,1],5) +# ``` diff --git a/ch03tests/07CI.html b/ch03tests/07CI.html new file mode 100644 index 000000000..926eef8a2 --- /dev/null +++ b/ch03tests/07CI.html @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Continuous Integration + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Continuous Integration

Continuous integration (CI) is a software development practice that involves integrating new code to a shared repository regularly (typically at least once a day). The integrated changes are then automatically checked by the CI system on test servers, which allows to detect problems early.

+

Test servers

The test servers of the CI system might be configured to:

+
    +
  1. run tests nightly
  2. +
  3. run tests after each commit to GitHub (or other shared repository)
  4. +
  5. run tests on different platforms (e.g. to check that tests pass for different Python versions, or on different operating systems)
  6. +
+

There are a number of technologies that can be used to set up a CI system to work with a GitHub repository either on your own server or on a remote machine (GitHub Actions, Travis CI, CircleCI, ...). Various UCL research groups run servers that can be used to do this automatically. We currently recommend GitHub Actions as the CI system of choice, which has a quite generous offering for open source projects.

+

When configuring a CI system, it's important to weigh up the usefulness of the test settings you cover against the energy consumption that will incur from running the tests frequently. For example, you might want to set up the CI system to run a more extensive suite of tests when a PR to the main branch is opened, and only run a small number of important tests at every commit. You could also decide that you don't need to test your code for all Python versions, but only for an old version and a recent one.

+
+
+
+
+
+
+

Memory and profiling

For compiled languages (C, C++, Fortran):

+
    +
  • Checking for memory leaks with valgrind: +valgrind --leak-check=full program
  • +
  • Checking cache hits and cache misses with +cachegrind: +valgrind --tool=cachegrind program
  • +
  • Profiling the code with callgrind: +valgrind --tool=callgrind program
  • +
+
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/07CI.ipynb b/ch03tests/07CI.ipynb new file mode 100644 index 000000000..2c04e1015 --- /dev/null +++ b/ch03tests/07CI.ipynb @@ -0,0 +1,63 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "84ba5fa7", + "metadata": {}, + "source": [ + "## Continuous Integration\n", + "\n", + "Continuous integration (CI) is a software development practice that involves integrating new code to a shared repository regularly (typically at least once a day). The integrated changes are then automatically checked by the CI system on test servers, which allows to detect problems early.\n", + "\n", + "### Test servers\n", + "\n", + "The test servers of the CI system might be configured to: \n", + "\n", + "1. run tests nightly\n", + "2. run tests after each commit to GitHub (or other shared repository)\n", + "3. run tests on different platforms (e.g. to check that tests pass for different Python versions, or on different operating systems)\n", + "\n", + "There are a number of technologies that can be used to set up a CI system to work with a GitHub repository either on your own server or on a remote machine ([GitHub Actions](https://docs.github.com/en/actions), [Travis CI](https://blog.travis-ci.com/2019-05-30-setting-up-a-ci-cd-process-on-github), [CircleCI](https://circleci.com/), ...). Various UCL research groups run servers that can be used to do this automatically. We currently recommend GitHub Actions as the CI system of choice, which has a quite generous offering for open source projects.\n", + "\n", + "When configuring a CI system, it's important to weigh up the usefulness of the test settings you cover against the energy consumption that will incur from running the tests frequently. For example, you might want to set up the CI system to run a more extensive suite of tests when a PR to the `main` branch is opened, and only run a small number of important tests at every commit. You could also decide that you don't need to test your code for all Python versions, but only for an old version and a recent one." + ] + }, + { + "cell_type": "markdown", + "id": "6ed6b859", + "metadata": {}, + "source": [ + "### Memory and profiling\n", + "\n", + "For compiled languages (C, C++, Fortran):\n", + "* Checking for memory leaks with [valgrind](http://valgrind.org/):\n", + " `valgrind --leak-check=full program`\n", + "* Checking cache hits and cache misses with\n", + " [cachegrind](http://valgrind.org/docs/manual/cg-manual.html):\n", + " `valgrind --tool=cachegrind program`\n", + "* Profiling the code with [callgrind](http://valgrind.org/docs/manual/cl-manual.html):\n", + " `valgrind --tool=callgrind program`" + ] + }, + { + "cell_type": "markdown", + "id": "6970924b", + "metadata": {}, + "source": [ + "* Python: profile with [the standard library](https://docs.python.org/3/library/profile.html) or [runsnake](http://www.vrplumber.com/programming/runsnakerun/)\n", + "* R: [Rprof](http://stat.ethz.ch/R-manual/R-devel/library/utils/html/Rprof.html)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Continuous Integration" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/07CI.ipynb.py b/ch03tests/07CI.ipynb.py new file mode 100644 index 000000000..388cf869d --- /dev/null +++ b/ch03tests/07CI.ipynb.py @@ -0,0 +1,45 @@ +# --- +# jupyter: +# jekyll: +# display_name: Continuous Integration +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Continuous Integration +# +# Continuous integration (CI) is a software development practice that involves integrating new code to a shared repository regularly (typically at least once a day). The integrated changes are then automatically checked by the CI system on test servers, which allows to detect problems early. +# +# ### Test servers +# +# The test servers of the CI system might be configured to: +# +# 1. run tests nightly +# 2. run tests after each commit to GitHub (or other shared repository) +# 3. run tests on different platforms (e.g. to check that tests pass for different Python versions, or on different operating systems) +# +# There are a number of technologies that can be used to set up a CI system to work with a GitHub repository either on your own server or on a remote machine ([GitHub Actions](https://docs.github.com/en/actions), [Travis CI](https://blog.travis-ci.com/2019-05-30-setting-up-a-ci-cd-process-on-github), [CircleCI](https://circleci.com/), ...). Various UCL research groups run servers that can be used to do this automatically. We currently recommend GitHub Actions as the CI system of choice, which has a quite generous offering for open source projects. +# +# When configuring a CI system, it's important to weigh up the usefulness of the test settings you cover against the energy consumption that will incur from running the tests frequently. For example, you might want to set up the CI system to run a more extensive suite of tests when a PR to the `main` branch is opened, and only run a small number of important tests at every commit. You could also decide that you don't need to test your code for all Python versions, but only for an old version and a recent one. + +# %% [markdown] +# ### Memory and profiling +# +# For compiled languages (C, C++, Fortran): +# * Checking for memory leaks with [valgrind](http://valgrind.org/): +# `valgrind --leak-check=full program` +# * Checking cache hits and cache misses with +# [cachegrind](http://valgrind.org/docs/manual/cg-manual.html): +# `valgrind --tool=cachegrind program` +# * Profiling the code with [callgrind](http://valgrind.org/docs/manual/cl-manual.html): +# `valgrind --tool=callgrind program` + +# %% [markdown] +# * Python: profile with [the standard library](https://docs.python.org/3/library/profile.html) or [runsnake](http://www.vrplumber.com/programming/runsnakerun/) +# * R: [Rprof](http://stat.ethz.ch/R-manual/R-devel/library/utils/html/Rprof.html) diff --git a/ch03tests/08DiffusionExample.html b/ch03tests/08DiffusionExample.html new file mode 100644 index 000000000..3ee1395e9 --- /dev/null +++ b/ch03tests/08DiffusionExample.html @@ -0,0 +1,70201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Diffusion Example + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Recap example: Monte-Carlo

+
+
+
+
+
+
+

Problem: Implement and test a simple Monte-Carlo algorithm

Given an input function (energy) and starting point (density) and a temperature $T$:

+
    +
  1. Compute energy at current density.
  2. +
  3. Move randomly chosen agent randomly left or right.
  4. +
  5. Compute second energy.
  6. +
  7. Compare the two energies:
  8. +
  9. If second energy is lower, accept move.
  10. +
  11. $\beta$ is a parameter which determines how likely +the simulation is to move from a 'less favourable' situation to a 'more favourable' one.
  12. +
  13. Compute $P_0=e^{-\beta (E_1 - E_0)}$ and $P_1$ a uniformly distributed random number between 0 and 1,
  14. +
  15. If $P_0 > P_1$, do the move anyway.
  16. +
  17. Repeat.
  18. +
+
+
+
+
+
+
+
    +
  • the algorithm should work for (m)any energy function(s).
  • +
  • there should be separate tests for separate steps! What constitutes a step?
  • +
  • tests for the Monte-Carlo should not depend on other parts of code.
  • +
  • Use matplotlib to plot density at each iteration, and make an animation
  • +
+
+
+
+
+
+
+

Solution

+
+
+
+
+
+
+

We need to break our problem down into pieces:

+
+
+
+
+
+
+
    +
  1. A function to generate a random change: random_agent(), random_direction()
  2. +
  3. A function to compute the energy before the change and after it: energy()
  4. +
  5. A function to determine the probability of a change given the energy difference (1 if decreases, otherwise based on exponential): change_density()
  6. +
  7. A function to determine whether to execute a change or not by drawing a random numberaccept_change()
  8. +
  9. A method to iterate the above procedure: step()
  10. +
+
+
+
+
+
+
+

Next Step: Think about the possible unit tests

+
+
+
+
+
+
+
    +
  1. Input insanity: e.g. density should non-negative integer; testing by giving negative values etc.
  2. +
  3. change_density(): density is change by a particle hopping left or right? Do all positions have an equal chance of moving?
  4. +
  5. accept_change() will move be accepted when second energy is lower?
  6. +
  7. Make a small test case for the main algorithm. (Hint: by using mocking, we can pre-set who to move where.)
  8. +
+
+
+
+
+
+
In [1]:
+
+
+
%%bash
+mkdir -p DiffusionExample
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%writefile DiffusionExample/MonteCarlo.py
+import matplotlib.pyplot as plt
+from numpy import sum, array
+from numpy.random import randint, choice
+
+
+
+class MonteCarlo(object):
+    """ A simple Monte Carlo implementation """
+
+    def __init__(self, energy, density, temperature=1, itermax=1000):
+        from numpy import any, array
+        density = array(density)
+        self.itermax = itermax
+
+        if temperature == 0:
+            raise NotImplementedError(
+                "Zero temperature not implemented")
+        if temperature < 0e0:
+            raise ValueError(
+                "Negative temperature makes no sense")
+
+        if len(density) < 2:
+            raise ValueError("Density is too short")
+        # of the right kind (integer). Unless it is zero length,
+        # in which case type does not matter.
+        if density.dtype.kind != 'i' and len(density) > 0:
+            raise TypeError("Density should be an array of *integers*.")
+        # and the right values (positive or null)
+        if any(density < 0):
+            raise ValueError("Density should be an array of" +
+                             "*positive* integers.")
+        if density.ndim != 1:
+            raise ValueError("Density should be an a *1-dimensional*" +
+                             "array of positive integers.")
+        if sum(density) == 0:
+            raise ValueError("Density is empty.")
+
+        self.current_energy = energy(density)
+        self.temperature = temperature
+        self.density = density
+
+    def random_direction(self): return choice([-1, 1])
+
+    def random_agent(self, density):
+        # Particle index
+        particle = randint(sum(density))
+        current = 0
+        for location, n in enumerate(density):
+            current += n
+            if current > particle:
+                break
+        return location
+
+    def change_density(self, density):
+        """ Move one particle left or right. """
+
+        location = self.random_agent(density)
+
+        # Move direction
+        if(density[location]-1 < 0):
+            return array(density)
+        if location == 0:
+            direction = 1
+        elif location == len(density) - 1:
+            direction = -1
+        else:
+            direction = self.random_direction()
+
+        # Now make change
+        result = array(density)
+        result[location] -= 1
+        result[location + direction] += 1
+        return result
+
+    def accept_change(self, prior, successor):
+        """ Returns true if should accept change. """
+        from numpy import exp
+        from numpy.random import uniform
+        if successor <= prior:
+            return True
+        else:
+            return exp(-(successor - prior) / self.temperature) > uniform()
+
+    def step(self):
+        iteration = 0
+        while iteration < self.itermax:
+            new_density = self.change_density(self.density)
+            new_energy = energy(new_density)
+
+            accept = self.accept_change(self.current_energy, new_energy)
+            if accept:
+                self.density, self.current_energy = new_density, new_energy
+            iteration += 1
+
+        return self.current_energy, self.density
+
+
+def energy(density, coefficient=1):
+    """ Energy associated with the diffusion model
+        :Parameters:
+        density: array of positive integers
+        Number of particles at each position i in the array/geometry
+    """
+    from numpy import array, any, sum
+
+    # Make sure input is an array
+    density = array(density)
+
+    # of the right kind (integer). Unless it is zero length, in which case type does not matter.
+    if density.dtype.kind != 'i' and len(density) > 0:
+        raise TypeError("Density should be an array of *integers*.")
+    # and the right values (positive or null)
+    if any(density < 0):
+        raise ValueError("Density should be an array" +
+                         "of *positive* integers.")
+    if density.ndim != 1:
+        raise ValueError("Density should be an a *1-dimensional*" +
+                         "array of positive integers.")
+
+    return coefficient * 0.5 * sum(density * (density - 1))
+
+
+
+
+
+
+
+
+
+
Writing DiffusionExample/MonteCarlo.py
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
import sys
+sys.path.append('DiffusionExample')
+from MonteCarlo import MonteCarlo, energy
+import numpy as np
+import numpy.random as random
+from matplotlib import animation
+from matplotlib import pyplot as plt
+from IPython.display import HTML
+
+
+Temperature = 0.1
+density = [np.sin(i) for i in np.linspace(0.1, 3, 100)]
+density = np.array(density)*100
+density = density.astype(int)
+
+fig = plt.figure()
+ax = plt.axes(xlim=(-1, len(density)), ylim=(0, np.max(density)+1))
+image = ax.scatter(range(len(density)), density)
+
+txt_energy = plt.text(0, 100, 'Energy = 0')
+plt.xlabel('Temperature = 0.1')
+plt.ylabel('Energy Density')
+
+
+mc = MonteCarlo(energy, density, temperature=Temperature)
+
+
+def simulate(step):
+    energy, density = mc.step()
+    image.set_offsets(np.vstack((range(len(density)), density)).T)
+    txt_energy.set_text('Energy = {}'.format(energy))
+
+
+anim = animation.FuncAnimation(fig, simulate, frames=200,
+                               interval=50)
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[3]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [4]:
+
+
+
%%writefile DiffusionExample/test_model.py
+from MonteCarlo import MonteCarlo
+from unittest.mock import MagicMock
+from pytest import raises, approx
+
+
+def test_input_sanity():
+    """ Check incorrect input do fail """
+    energy = MagicMock()
+
+    with raises(NotImplementedError) as exception:
+        MonteCarlo(sum, [1, 1, 1], 0e0)
+    with raises(ValueError) as exception:
+        MonteCarlo(energy, [1, 1, 1], temperature=-1e0)
+
+    with raises(TypeError) as exception:
+        MonteCarlo(energy, [1.0, 2, 3])
+    with raises(ValueError) as exception:
+        MonteCarlo(energy, [-1, 2, 3])
+    with raises(ValueError) as exception:
+        MonteCarlo(energy, [[1, 2, 3], [3, 4, 5]])
+    with raises(ValueError) as exception:
+        MonteCarlo(energy, [3])
+    with raises(ValueError) as exception:
+        MonteCarlo(energy, [0, 0])
+
+
+def test_move_particle_one_over():
+    """ Check density is change by a particle hopping left or right. """
+    from numpy import nonzero, multiply
+    from numpy.random import randint
+
+    energy = MagicMock()
+
+    for i in range(100):
+        # Do this n times, to avoid
+        # issues with random numbers
+        # Create density
+
+        density = randint(50, size=randint(2, 6))
+        mc = MonteCarlo(energy, density)
+        # Change it
+        new_density = mc.change_density(density)
+
+        # Make sure any movement is by one
+        indices = nonzero(density - new_density)[0]
+        assert len(indices) == 2, "densities differ in two places"
+        assert \
+            multiply.reduce((density - new_density)[indices]) == -1, \
+            "densities differ by + and - 1"
+
+
+def test_equal_probability():
+    """ Check particles have equal probability of movement. """
+    from numpy import array, sqrt, count_nonzero
+
+    energy = MagicMock()
+
+    density = array([1, 0, 99])
+    mc = MonteCarlo(energy, density)
+    changes_at_zero = [
+        (density - mc.change_density(density))[0] != 0 for i in range(10000)]
+    assert count_nonzero(changes_at_zero) \
+            == approx(0.01 * len(changes_at_zero), 0.5 * sqrt(len(changes_at_zero)))
+
+
+def test_accept_change():
+    """ Check that move is accepted if second energy is lower """
+    from numpy import sqrt, count_nonzero, exp
+
+    energy = MagicMock
+    mc = MonteCarlo(energy, [1, 1, 1], temperature=100.0)
+    # Should always be true.
+    # But do more than one draw,
+    # in case randomness incorrectly crept into
+    # implementation
+    for i in range(10):
+        assert mc.accept_change(0.5, 0.4)
+        assert mc.accept_change(0.5, 0.5)
+
+    # This should be accepted only part of the time,
+    # depending on exponential distribution
+    prior, successor = 0.4, 0.5
+    accepted = [mc.accept_change(prior, successor) for i in range(10000)]
+    assert count_nonzero(accepted) / float(len(accepted)) \
+        == approx(exp(-(successor - prior) / mc.temperature), 3e0 / sqrt(len(accepted)))
+
+
+
+def test_main_algorithm():
+    import numpy as np
+    from numpy import testing
+    from unittest.mock import Mock
+
+    density=[1, 1, 1, 1, 1]
+    energy=MagicMock()
+    mc=MonteCarlo(energy, density, itermax = 5)
+
+    acceptance=[True, True, True, True, True]
+    mc.accept_change=Mock(side_effect = acceptance)
+    mc.random_agent=Mock(side_effect = [0, 1, 2, 3, 4])
+    mc.random_direction=Mock(side_effect = [1, 1, 1, 1, -1])
+    np.testing.assert_equal(mc.step()[1], [0, 1, 1, 2, 1])
+
+
+
+
+
+
+
+
+
+
Writing DiffusionExample/test_model.py
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+cd DiffusionExample
+py.test
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch03tests/DiffusionExample
+plugins: cov-4.1.0, anyio-3.7.1
+collected 5 items
+
+test_model.py .....                                                      [100%]
+
+============================== 5 passed in 0.52s ===============================
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/08DiffusionExample.ipynb b/ch03tests/08DiffusionExample.ipynb new file mode 100644 index 000000000..aaf078c18 --- /dev/null +++ b/ch03tests/08DiffusionExample.ipynb @@ -0,0 +1,412 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "83cc0067", + "metadata": {}, + "source": [ + "## Recap example: Monte-Carlo" + ] + }, + { + "cell_type": "markdown", + "id": "c5567bd8", + "metadata": {}, + "source": [ + "### Problem: Implement and test a simple Monte-Carlo algorithm\n", + "\n", + "Given an input function (energy) and starting point (density) and a temperature $T$: \n", + "\n", + "1. Compute energy at current density.\n", + "1. Move randomly chosen agent randomly left or right.\n", + "1. Compute second energy.\n", + "1. Compare the two energies:\n", + "1. If second energy is lower, accept move.\n", + "1. $\\beta$ is a parameter which determines how likely\n", + " the simulation is to move from a 'less favourable' situation to a 'more favourable' one.\n", + "1. Compute $P_0=e^{-\\beta (E_1 - E_0)}$ and $P_1$ a uniformly distributed random number between 0 and 1,\n", + "1. If $P_0 > P_1$, do the move anyway.\n", + "1. Repeat." + ] + }, + { + "cell_type": "markdown", + "id": "d53d08f3", + "metadata": {}, + "source": [ + "* the algorithm should work for (m)any energy function(s).\n", + "* there should be separate tests for separate steps! What constitutes a step?\n", + "* tests for the Monte-Carlo should not depend on other parts of code.\n", + "* Use [matplotlib](http://matplotlib.org/) to plot density at each iteration, and make an animation" + ] + }, + { + "cell_type": "markdown", + "id": "fce9c0ac", + "metadata": {}, + "source": [ + "### Solution" + ] + }, + { + "cell_type": "markdown", + "id": "56c71d20", + "metadata": {}, + "source": [ + "We need to break our problem down into pieces:" + ] + }, + { + "cell_type": "markdown", + "id": "42713ade", + "metadata": {}, + "source": [ + "1. A function to generate a random change: `random_agent()`, `random_direction()`\n", + "1. A function to compute the energy before the change and after it: `energy()`\n", + "1. A function to determine the probability of a change given the energy difference (1 if decreases, otherwise based on exponential): `change_density()`\n", + "1. A function to determine whether to execute a change or not by drawing a random number`accept_change()`\n", + "1. A method to iterate the above procedure: `step()`" + ] + }, + { + "cell_type": "markdown", + "id": "ed86938d", + "metadata": {}, + "source": [ + "Next Step: Think about the possible unit tests" + ] + }, + { + "cell_type": "markdown", + "id": "91fd0acf", + "metadata": {}, + "source": [ + "1. Input insanity: e.g. density should non-negative integer; testing by giving negative values etc.\n", + "1. `change_density()`: density is change by a particle hopping left or right? Do all positions have an equal chance of moving?\n", + "1. `accept_change()` will move be accepted when second energy is lower?\n", + "1. Make a small test case for the main algorithm. (Hint: by using mocking, we can pre-set who to move where.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d6c3f04", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p DiffusionExample" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "778e486e", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile DiffusionExample/MonteCarlo.py\n", + "import matplotlib.pyplot as plt\n", + "from numpy import sum, array\n", + "from numpy.random import randint, choice\n", + "\n", + "\n", + "\n", + "class MonteCarlo(object):\n", + " \"\"\" A simple Monte Carlo implementation \"\"\"\n", + "\n", + " def __init__(self, energy, density, temperature=1, itermax=1000):\n", + " from numpy import any, array\n", + " density = array(density)\n", + " self.itermax = itermax\n", + "\n", + " if temperature == 0:\n", + " raise NotImplementedError(\n", + " \"Zero temperature not implemented\")\n", + " if temperature < 0e0:\n", + " raise ValueError(\n", + " \"Negative temperature makes no sense\")\n", + "\n", + " if len(density) < 2:\n", + " raise ValueError(\"Density is too short\")\n", + " # of the right kind (integer). Unless it is zero length,\n", + " # in which case type does not matter.\n", + " if density.dtype.kind != 'i' and len(density) > 0:\n", + " raise TypeError(\"Density should be an array of *integers*.\")\n", + " # and the right values (positive or null)\n", + " if any(density < 0):\n", + " raise ValueError(\"Density should be an array of\" +\n", + " \"*positive* integers.\")\n", + " if density.ndim != 1:\n", + " raise ValueError(\"Density should be an a *1-dimensional*\" +\n", + " \"array of positive integers.\")\n", + " if sum(density) == 0:\n", + " raise ValueError(\"Density is empty.\")\n", + "\n", + " self.current_energy = energy(density)\n", + " self.temperature = temperature\n", + " self.density = density\n", + "\n", + " def random_direction(self): return choice([-1, 1])\n", + "\n", + " def random_agent(self, density):\n", + " # Particle index\n", + " particle = randint(sum(density))\n", + " current = 0\n", + " for location, n in enumerate(density):\n", + " current += n\n", + " if current > particle:\n", + " break\n", + " return location\n", + "\n", + " def change_density(self, density):\n", + " \"\"\" Move one particle left or right. \"\"\"\n", + "\n", + " location = self.random_agent(density)\n", + "\n", + " # Move direction\n", + " if(density[location]-1 < 0):\n", + " return array(density)\n", + " if location == 0:\n", + " direction = 1\n", + " elif location == len(density) - 1:\n", + " direction = -1\n", + " else:\n", + " direction = self.random_direction()\n", + "\n", + " # Now make change\n", + " result = array(density)\n", + " result[location] -= 1\n", + " result[location + direction] += 1\n", + " return result\n", + "\n", + " def accept_change(self, prior, successor):\n", + " \"\"\" Returns true if should accept change. \"\"\"\n", + " from numpy import exp\n", + " from numpy.random import uniform\n", + " if successor <= prior:\n", + " return True\n", + " else:\n", + " return exp(-(successor - prior) / self.temperature) > uniform()\n", + "\n", + " def step(self):\n", + " iteration = 0\n", + " while iteration < self.itermax:\n", + " new_density = self.change_density(self.density)\n", + " new_energy = energy(new_density)\n", + "\n", + " accept = self.accept_change(self.current_energy, new_energy)\n", + " if accept:\n", + " self.density, self.current_energy = new_density, new_energy\n", + " iteration += 1\n", + "\n", + " return self.current_energy, self.density\n", + "\n", + "\n", + "def energy(density, coefficient=1):\n", + " \"\"\" Energy associated with the diffusion model\n", + " :Parameters:\n", + " density: array of positive integers\n", + " Number of particles at each position i in the array/geometry\n", + " \"\"\"\n", + " from numpy import array, any, sum\n", + "\n", + " # Make sure input is an array\n", + " density = array(density)\n", + "\n", + " # of the right kind (integer). Unless it is zero length, in which case type does not matter.\n", + " if density.dtype.kind != 'i' and len(density) > 0:\n", + " raise TypeError(\"Density should be an array of *integers*.\")\n", + " # and the right values (positive or null)\n", + " if any(density < 0):\n", + " raise ValueError(\"Density should be an array\" +\n", + " \"of *positive* integers.\")\n", + " if density.ndim != 1:\n", + " raise ValueError(\"Density should be an a *1-dimensional*\" +\n", + " \"array of positive integers.\")\n", + "\n", + " return coefficient * 0.5 * sum(density * (density - 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ef79add", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('DiffusionExample')\n", + "from MonteCarlo import MonteCarlo, energy\n", + "import numpy as np\n", + "import numpy.random as random\n", + "from matplotlib import animation\n", + "from matplotlib import pyplot as plt\n", + "from IPython.display import HTML\n", + "\n", + "\n", + "Temperature = 0.1\n", + "density = [np.sin(i) for i in np.linspace(0.1, 3, 100)]\n", + "density = np.array(density)*100\n", + "density = density.astype(int)\n", + "\n", + "fig = plt.figure()\n", + "ax = plt.axes(xlim=(-1, len(density)), ylim=(0, np.max(density)+1))\n", + "image = ax.scatter(range(len(density)), density)\n", + "\n", + "txt_energy = plt.text(0, 100, 'Energy = 0')\n", + "plt.xlabel('Temperature = 0.1')\n", + "plt.ylabel('Energy Density')\n", + "\n", + "\n", + "mc = MonteCarlo(energy, density, temperature=Temperature)\n", + "\n", + "\n", + "def simulate(step):\n", + " energy, density = mc.step()\n", + " image.set_offsets(np.vstack((range(len(density)), density)).T)\n", + " txt_energy.set_text('Energy = {}'.format(energy))\n", + "\n", + "\n", + "anim = animation.FuncAnimation(fig, simulate, frames=200,\n", + " interval=50)\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c5b9360", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile DiffusionExample/test_model.py\n", + "from MonteCarlo import MonteCarlo\n", + "from unittest.mock import MagicMock\n", + "from pytest import raises, approx\n", + "\n", + "\n", + "def test_input_sanity():\n", + " \"\"\" Check incorrect input do fail \"\"\"\n", + " energy = MagicMock()\n", + "\n", + " with raises(NotImplementedError) as exception:\n", + " MonteCarlo(sum, [1, 1, 1], 0e0)\n", + " with raises(ValueError) as exception:\n", + " MonteCarlo(energy, [1, 1, 1], temperature=-1e0)\n", + "\n", + " with raises(TypeError) as exception:\n", + " MonteCarlo(energy, [1.0, 2, 3])\n", + " with raises(ValueError) as exception:\n", + " MonteCarlo(energy, [-1, 2, 3])\n", + " with raises(ValueError) as exception:\n", + " MonteCarlo(energy, [[1, 2, 3], [3, 4, 5]])\n", + " with raises(ValueError) as exception:\n", + " MonteCarlo(energy, [3])\n", + " with raises(ValueError) as exception:\n", + " MonteCarlo(energy, [0, 0])\n", + "\n", + "\n", + "def test_move_particle_one_over():\n", + " \"\"\" Check density is change by a particle hopping left or right. \"\"\"\n", + " from numpy import nonzero, multiply\n", + " from numpy.random import randint\n", + "\n", + " energy = MagicMock()\n", + "\n", + " for i in range(100):\n", + " # Do this n times, to avoid\n", + " # issues with random numbers\n", + " # Create density\n", + "\n", + " density = randint(50, size=randint(2, 6))\n", + " mc = MonteCarlo(energy, density)\n", + " # Change it\n", + " new_density = mc.change_density(density)\n", + "\n", + " # Make sure any movement is by one\n", + " indices = nonzero(density - new_density)[0]\n", + " assert len(indices) == 2, \"densities differ in two places\"\n", + " assert \\\n", + " multiply.reduce((density - new_density)[indices]) == -1, \\\n", + " \"densities differ by + and - 1\"\n", + "\n", + "\n", + "def test_equal_probability():\n", + " \"\"\" Check particles have equal probability of movement. \"\"\"\n", + " from numpy import array, sqrt, count_nonzero\n", + "\n", + " energy = MagicMock()\n", + "\n", + " density = array([1, 0, 99])\n", + " mc = MonteCarlo(energy, density)\n", + " changes_at_zero = [\n", + " (density - mc.change_density(density))[0] != 0 for i in range(10000)]\n", + " assert count_nonzero(changes_at_zero) \\\n", + " == approx(0.01 * len(changes_at_zero), 0.5 * sqrt(len(changes_at_zero)))\n", + "\n", + "\n", + "def test_accept_change():\n", + " \"\"\" Check that move is accepted if second energy is lower \"\"\"\n", + " from numpy import sqrt, count_nonzero, exp\n", + "\n", + " energy = MagicMock\n", + " mc = MonteCarlo(energy, [1, 1, 1], temperature=100.0)\n", + " # Should always be true.\n", + " # But do more than one draw,\n", + " # in case randomness incorrectly crept into\n", + " # implementation\n", + " for i in range(10):\n", + " assert mc.accept_change(0.5, 0.4)\n", + " assert mc.accept_change(0.5, 0.5)\n", + "\n", + " # This should be accepted only part of the time,\n", + " # depending on exponential distribution\n", + " prior, successor = 0.4, 0.5\n", + " accepted = [mc.accept_change(prior, successor) for i in range(10000)]\n", + " assert count_nonzero(accepted) / float(len(accepted)) \\\n", + " == approx(exp(-(successor - prior) / mc.temperature), 3e0 / sqrt(len(accepted)))\n", + "\n", + "\n", + "\n", + "def test_main_algorithm():\n", + " import numpy as np\n", + " from numpy import testing\n", + " from unittest.mock import Mock\n", + "\n", + " density=[1, 1, 1, 1, 1]\n", + " energy=MagicMock()\n", + " mc=MonteCarlo(energy, density, itermax = 5)\n", + "\n", + " acceptance=[True, True, True, True, True]\n", + " mc.accept_change=Mock(side_effect = acceptance)\n", + " mc.random_agent=Mock(side_effect = [0, 1, 2, 3, 4])\n", + " mc.random_direction=Mock(side_effect = [1, 1, 1, 1, -1])\n", + " np.testing.assert_equal(mc.step()[1], [0, 1, 1, 2, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "551355ca", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd DiffusionExample\n", + "py.test\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Diffusion Example" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch03tests/08DiffusionExample.ipynb.py b/ch03tests/08DiffusionExample.ipynb.py new file mode 100644 index 000000000..5dda3f8f5 --- /dev/null +++ b/ch03tests/08DiffusionExample.ipynb.py @@ -0,0 +1,334 @@ +# --- +# jupyter: +# jekyll: +# display_name: Diffusion Example +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Recap example: Monte-Carlo + +# %% [markdown] +# ### Problem: Implement and test a simple Monte-Carlo algorithm +# +# Given an input function (energy) and starting point (density) and a temperature $T$: +# +# 1. Compute energy at current density. +# 1. Move randomly chosen agent randomly left or right. +# 1. Compute second energy. +# 1. Compare the two energies: +# 1. If second energy is lower, accept move. +# 1. $\beta$ is a parameter which determines how likely +# the simulation is to move from a 'less favourable' situation to a 'more favourable' one. +# 1. Compute $P_0=e^{-\beta (E_1 - E_0)}$ and $P_1$ a uniformly distributed random number between 0 and 1, +# 1. If $P_0 > P_1$, do the move anyway. +# 1. Repeat. + +# %% [markdown] +# * the algorithm should work for (m)any energy function(s). +# * there should be separate tests for separate steps! What constitutes a step? +# * tests for the Monte-Carlo should not depend on other parts of code. +# * Use [matplotlib](http://matplotlib.org/) to plot density at each iteration, and make an animation + +# %% [markdown] +# ### Solution + +# %% [markdown] +# We need to break our problem down into pieces: + +# %% [markdown] +# 1. A function to generate a random change: `random_agent()`, `random_direction()` +# 1. A function to compute the energy before the change and after it: `energy()` +# 1. A function to determine the probability of a change given the energy difference (1 if decreases, otherwise based on exponential): `change_density()` +# 1. A function to determine whether to execute a change or not by drawing a random number`accept_change()` +# 1. A method to iterate the above procedure: `step()` + +# %% [markdown] +# Next Step: Think about the possible unit tests + +# %% [markdown] +# 1. Input insanity: e.g. density should non-negative integer; testing by giving negative values etc. +# 1. `change_density()`: density is change by a particle hopping left or right? Do all positions have an equal chance of moving? +# 1. `accept_change()` will move be accepted when second energy is lower? +# 1. Make a small test case for the main algorithm. (Hint: by using mocking, we can pre-set who to move where.) + +# %% language="bash" +# mkdir -p DiffusionExample + +# %% +# %%writefile DiffusionExample/MonteCarlo.py +import matplotlib.pyplot as plt +from numpy import sum, array +from numpy.random import randint, choice + + + +class MonteCarlo(object): + """ A simple Monte Carlo implementation """ + + def __init__(self, energy, density, temperature=1, itermax=1000): + from numpy import any, array + density = array(density) + self.itermax = itermax + + if temperature == 0: + raise NotImplementedError( + "Zero temperature not implemented") + if temperature < 0e0: + raise ValueError( + "Negative temperature makes no sense") + + if len(density) < 2: + raise ValueError("Density is too short") + # of the right kind (integer). Unless it is zero length, + # in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of" + + "*positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + if sum(density) == 0: + raise ValueError("Density is empty.") + + self.current_energy = energy(density) + self.temperature = temperature + self.density = density + + def random_direction(self): return choice([-1, 1]) + + def random_agent(self, density): + # Particle index + particle = randint(sum(density)) + current = 0 + for location, n in enumerate(density): + current += n + if current > particle: + break + return location + + def change_density(self, density): + """ Move one particle left or right. """ + + location = self.random_agent(density) + + # Move direction + if(density[location]-1 < 0): + return array(density) + if location == 0: + direction = 1 + elif location == len(density) - 1: + direction = -1 + else: + direction = self.random_direction() + + # Now make change + result = array(density) + result[location] -= 1 + result[location + direction] += 1 + return result + + def accept_change(self, prior, successor): + """ Returns true if should accept change. """ + from numpy import exp + from numpy.random import uniform + if successor <= prior: + return True + else: + return exp(-(successor - prior) / self.temperature) > uniform() + + def step(self): + iteration = 0 + while iteration < self.itermax: + new_density = self.change_density(self.density) + new_energy = energy(new_density) + + accept = self.accept_change(self.current_energy, new_energy) + if accept: + self.density, self.current_energy = new_density, new_energy + iteration += 1 + + return self.current_energy, self.density + + +def energy(density, coefficient=1): + """ Energy associated with the diffusion model + :Parameters: + density: array of positive integers + Number of particles at each position i in the array/geometry + """ + from numpy import array, any, sum + + # Make sure input is an array + density = array(density) + + # of the right kind (integer). Unless it is zero length, in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array" + + "of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + + return coefficient * 0.5 * sum(density * (density - 1)) + + +# %% +import sys +sys.path.append('DiffusionExample') +from MonteCarlo import MonteCarlo, energy +import numpy as np +import numpy.random as random +from matplotlib import animation +from matplotlib import pyplot as plt +from IPython.display import HTML + + +Temperature = 0.1 +density = [np.sin(i) for i in np.linspace(0.1, 3, 100)] +density = np.array(density)*100 +density = density.astype(int) + +fig = plt.figure() +ax = plt.axes(xlim=(-1, len(density)), ylim=(0, np.max(density)+1)) +image = ax.scatter(range(len(density)), density) + +txt_energy = plt.text(0, 100, 'Energy = 0') +plt.xlabel('Temperature = 0.1') +plt.ylabel('Energy Density') + + +mc = MonteCarlo(energy, density, temperature=Temperature) + + +def simulate(step): + energy, density = mc.step() + image.set_offsets(np.vstack((range(len(density)), density)).T) + txt_energy.set_text('Energy = {}'.format(energy)) + + +anim = animation.FuncAnimation(fig, simulate, frames=200, + interval=50) +HTML(anim.to_jshtml()) + +# %% +# %%writefile DiffusionExample/test_model.py +from MonteCarlo import MonteCarlo +from unittest.mock import MagicMock +from pytest import raises, approx + + +def test_input_sanity(): + """ Check incorrect input do fail """ + energy = MagicMock() + + with raises(NotImplementedError) as exception: + MonteCarlo(sum, [1, 1, 1], 0e0) + with raises(ValueError) as exception: + MonteCarlo(energy, [1, 1, 1], temperature=-1e0) + + with raises(TypeError) as exception: + MonteCarlo(energy, [1.0, 2, 3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [-1, 2, 3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [[1, 2, 3], [3, 4, 5]]) + with raises(ValueError) as exception: + MonteCarlo(energy, [3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [0, 0]) + + +def test_move_particle_one_over(): + """ Check density is change by a particle hopping left or right. """ + from numpy import nonzero, multiply + from numpy.random import randint + + energy = MagicMock() + + for i in range(100): + # Do this n times, to avoid + # issues with random numbers + # Create density + + density = randint(50, size=randint(2, 6)) + mc = MonteCarlo(energy, density) + # Change it + new_density = mc.change_density(density) + + # Make sure any movement is by one + indices = nonzero(density - new_density)[0] + assert len(indices) == 2, "densities differ in two places" + assert \ + multiply.reduce((density - new_density)[indices]) == -1, \ + "densities differ by + and - 1" + + +def test_equal_probability(): + """ Check particles have equal probability of movement. """ + from numpy import array, sqrt, count_nonzero + + energy = MagicMock() + + density = array([1, 0, 99]) + mc = MonteCarlo(energy, density) + changes_at_zero = [ + (density - mc.change_density(density))[0] != 0 for i in range(10000)] + assert count_nonzero(changes_at_zero) \ + == approx(0.01 * len(changes_at_zero), 0.5 * sqrt(len(changes_at_zero))) + + +def test_accept_change(): + """ Check that move is accepted if second energy is lower """ + from numpy import sqrt, count_nonzero, exp + + energy = MagicMock + mc = MonteCarlo(energy, [1, 1, 1], temperature=100.0) + # Should always be true. + # But do more than one draw, + # in case randomness incorrectly crept into + # implementation + for i in range(10): + assert mc.accept_change(0.5, 0.4) + assert mc.accept_change(0.5, 0.5) + + # This should be accepted only part of the time, + # depending on exponential distribution + prior, successor = 0.4, 0.5 + accepted = [mc.accept_change(prior, successor) for i in range(10000)] + assert count_nonzero(accepted) / float(len(accepted)) \ + == approx(exp(-(successor - prior) / mc.temperature), 3e0 / sqrt(len(accepted))) + + + +def test_main_algorithm(): + import numpy as np + from numpy import testing + from unittest.mock import Mock + + density=[1, 1, 1, 1, 1] + energy=MagicMock() + mc=MonteCarlo(energy, density, itermax = 5) + + acceptance=[True, True, True, True, True] + mc.accept_change=Mock(side_effect = acceptance) + mc.random_agent=Mock(side_effect = [0, 1, 2, 3, 4]) + mc.random_direction=Mock(side_effect = [1, 1, 1, 1, -1]) + np.testing.assert_equal(mc.step()[1], [0, 1, 1, 2, 1]) + +# %% language="bash" +# cd DiffusionExample +# py.test +# diff --git a/ch03tests/DiffusionExample/MonteCarlo.py b/ch03tests/DiffusionExample/MonteCarlo.py new file mode 100644 index 000000000..4342420c9 --- /dev/null +++ b/ch03tests/DiffusionExample/MonteCarlo.py @@ -0,0 +1,120 @@ +import matplotlib.pyplot as plt +from numpy import sum, array +from numpy.random import randint, choice + + + +class MonteCarlo(object): + """ A simple Monte Carlo implementation """ + + def __init__(self, energy, density, temperature=1, itermax=1000): + from numpy import any, array + density = array(density) + self.itermax = itermax + + if temperature == 0: + raise NotImplementedError( + "Zero temperature not implemented") + if temperature < 0e0: + raise ValueError( + "Negative temperature makes no sense") + + if len(density) < 2: + raise ValueError("Density is too short") + # of the right kind (integer). Unless it is zero length, + # in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of" + + "*positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + if sum(density) == 0: + raise ValueError("Density is empty.") + + self.current_energy = energy(density) + self.temperature = temperature + self.density = density + + def random_direction(self): return choice([-1, 1]) + + def random_agent(self, density): + # Particle index + particle = randint(sum(density)) + current = 0 + for location, n in enumerate(density): + current += n + if current > particle: + break + return location + + def change_density(self, density): + """ Move one particle left or right. """ + + location = self.random_agent(density) + + # Move direction + if(density[location]-1 < 0): + return array(density) + if location == 0: + direction = 1 + elif location == len(density) - 1: + direction = -1 + else: + direction = self.random_direction() + + # Now make change + result = array(density) + result[location] -= 1 + result[location + direction] += 1 + return result + + def accept_change(self, prior, successor): + """ Returns true if should accept change. """ + from numpy import exp + from numpy.random import uniform + if successor <= prior: + return True + else: + return exp(-(successor - prior) / self.temperature) > uniform() + + def step(self): + iteration = 0 + while iteration < self.itermax: + new_density = self.change_density(self.density) + new_energy = energy(new_density) + + accept = self.accept_change(self.current_energy, new_energy) + if accept: + self.density, self.current_energy = new_density, new_energy + iteration += 1 + + return self.current_energy, self.density + + +def energy(density, coefficient=1): + """ Energy associated with the diffusion model + :Parameters: + density: array of positive integers + Number of particles at each position i in the array/geometry + """ + from numpy import array, any, sum + + # Make sure input is an array + density = array(density) + + # of the right kind (integer). Unless it is zero length, in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array" + + "of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + + return coefficient * 0.5 * sum(density * (density - 1)) diff --git a/ch03tests/DiffusionExample/test_model.py b/ch03tests/DiffusionExample/test_model.py new file mode 100644 index 000000000..7185c90de --- /dev/null +++ b/ch03tests/DiffusionExample/test_model.py @@ -0,0 +1,102 @@ +from MonteCarlo import MonteCarlo +from unittest.mock import MagicMock +from pytest import raises, approx + + +def test_input_sanity(): + """ Check incorrect input do fail """ + energy = MagicMock() + + with raises(NotImplementedError) as exception: + MonteCarlo(sum, [1, 1, 1], 0e0) + with raises(ValueError) as exception: + MonteCarlo(energy, [1, 1, 1], temperature=-1e0) + + with raises(TypeError) as exception: + MonteCarlo(energy, [1.0, 2, 3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [-1, 2, 3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [[1, 2, 3], [3, 4, 5]]) + with raises(ValueError) as exception: + MonteCarlo(energy, [3]) + with raises(ValueError) as exception: + MonteCarlo(energy, [0, 0]) + + +def test_move_particle_one_over(): + """ Check density is change by a particle hopping left or right. """ + from numpy import nonzero, multiply + from numpy.random import randint + + energy = MagicMock() + + for i in range(100): + # Do this n times, to avoid + # issues with random numbers + # Create density + + density = randint(50, size=randint(2, 6)) + mc = MonteCarlo(energy, density) + # Change it + new_density = mc.change_density(density) + + # Make sure any movement is by one + indices = nonzero(density - new_density)[0] + assert len(indices) == 2, "densities differ in two places" + assert \ + multiply.reduce((density - new_density)[indices]) == -1, \ + "densities differ by + and - 1" + + +def test_equal_probability(): + """ Check particles have equal probability of movement. """ + from numpy import array, sqrt, count_nonzero + + energy = MagicMock() + + density = array([1, 0, 99]) + mc = MonteCarlo(energy, density) + changes_at_zero = [ + (density - mc.change_density(density))[0] != 0 for i in range(10000)] + assert count_nonzero(changes_at_zero) \ + == approx(0.01 * len(changes_at_zero), 0.5 * sqrt(len(changes_at_zero))) + + +def test_accept_change(): + """ Check that move is accepted if second energy is lower """ + from numpy import sqrt, count_nonzero, exp + + energy = MagicMock + mc = MonteCarlo(energy, [1, 1, 1], temperature=100.0) + # Should always be true. + # But do more than one draw, + # in case randomness incorrectly crept into + # implementation + for i in range(10): + assert mc.accept_change(0.5, 0.4) + assert mc.accept_change(0.5, 0.5) + + # This should be accepted only part of the time, + # depending on exponential distribution + prior, successor = 0.4, 0.5 + accepted = [mc.accept_change(prior, successor) for i in range(10000)] + assert count_nonzero(accepted) / float(len(accepted)) \ + == approx(exp(-(successor - prior) / mc.temperature), 3e0 / sqrt(len(accepted))) + + + +def test_main_algorithm(): + import numpy as np + from numpy import testing + from unittest.mock import Mock + + density=[1, 1, 1, 1, 1] + energy=MagicMock() + mc=MonteCarlo(energy, density, itermax = 5) + + acceptance=[True, True, True, True, True] + mc.accept_change=Mock(side_effect = acceptance) + mc.random_agent=Mock(side_effect = [0, 1, 2, 3, 4]) + mc.random_direction=Mock(side_effect = [1, 1, 1, 1, -1]) + np.testing.assert_equal(mc.step()[1], [0, 1, 1, 2, 1]) diff --git a/ch03tests/commands b/ch03tests/commands new file mode 100644 index 000000000..e66498781 --- /dev/null +++ b/ch03tests/commands @@ -0,0 +1,5 @@ +restart # restart session +n +b energy # program will stop when entering energy +c # continue program until break point is reached +print(density) # We are now "inside" the energy function and can print any variable. diff --git a/ch03tests/diffusion/htmlcov/coverage_html.js b/ch03tests/diffusion/htmlcov/coverage_html.js new file mode 100644 index 000000000..593488286 --- /dev/null +++ b/ch03tests/diffusion/htmlcov/coverage_html.js @@ -0,0 +1,624 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + const child = cell.firstElementChild + if (child instanceof HTMLTimeElement && child.dateTime) { + return child.dateTime + } else if (child instanceof HTMLDataElement && child.value) { + return child.value + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + if (currentSortOrder === "none") { + th.setAttribute("aria-sort", th.dataset.defaultSortOrder || "ascending"); + } else { + th.setAttribute("aria-sort", currentSortOrder === "ascending" ? "descending" : "ascending"); + } + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (th.getAttribute("aria-sort") === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr) ); +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + // Observe filter keyevents. + document.getElementById("filter").addEventListener("input", debounce(event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = new Array(table.rows[0].cells.length).fill(0); + // Accumulate the percentage as fraction + totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection + + // Hide / show elements. + table_body_rows.forEach(row => { + if (!row.cells[0].textContent.includes(event.target.value)) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 1; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (column === totals.length - 1) { + // Last column contains percentage + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + const footer = table.tFoot.rows[0]; + // Calculate new dynamic sum values based on visible rows. + for (let column = 1; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + + // Set value into dynamic footer cell element. + if (column === totals.length - 1) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + + if (stored_list) { + const {column, direction} = JSON.parse(stored_list); + const th = document.querySelector("[data-sortable]").tHead.rows[0].cells[column]; // nosemgrep: eslint.detect-object-injection + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() + } + + // Watch for page unload events so we can save the final sort settings: + window.addEventListener("unload", function () { + const th = document.querySelector('[data-sortable] th[aria-sort="ascending"], [data-sortable] [aria-sort="descending"]'); + if (!th) { + return; + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + column: [...th.parentElement.cells].indexOf(th), + direction: th.getAttribute("aria-sort"), + })); + }); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } else { + coverage.pyfile_ready(); + } +}); diff --git a/ch03tests/diffusion/htmlcov/favicon_32.png b/ch03tests/diffusion/htmlcov/favicon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..8649f0475d8d20793b2ec431fe25a186a414cf10 GIT binary patch literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+

+ coverage.py v7.3.2, + created at 2023-11-22 15:47 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
__init__.py000100%
model.py1000100%
test_model.py3100100%
Total4100100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/ch03tests/diffusion/htmlcov/keybd_closed.png b/ch03tests/diffusion/htmlcov/keybd_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..ba119c47df81ed2bbd27a06988abf700139c4f99 GIT binary patch literal 9004 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 literal 0 HcmV?d00001 diff --git a/ch03tests/diffusion/htmlcov/keybd_open.png b/ch03tests/diffusion/htmlcov/keybd_open.png new file mode 100644 index 0000000000000000000000000000000000000000..a8bac6c9de256626c680f9e9e3f8ee81d9713ecd GIT binary patch literal 9003 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 G%6|bpD&^7u literal 0 HcmV?d00001 diff --git a/ch03tests/diffusion/htmlcov/model_py.html b/ch03tests/diffusion/htmlcov/model_py.html new file mode 100644 index 000000000..717300a03 --- /dev/null +++ b/ch03tests/diffusion/htmlcov/model_py.html @@ -0,0 +1,126 @@ + + + + + Coverage for model.py: 100% + + + + + +
+ +
+
+

1""" Simplistic 1-dimensional diffusion model """ 

+

2 

+

3def energy(density): 

+

4 """  

+

5 Energy associated with the diffusion model 

+

6  

+

7 :Parameters: 

+

8  

+

9 density: array of positive integers 

+

10 Number of particles at each position i in the array/geometry 

+

11 """ 

+

12 from numpy import array, any, sum 

+

13 

+

14 # Make sure input is an numpy array 

+

15 density = array(density) 

+

16 

+

17 # ...of the right kind (integer). Unless it is zero length,  

+

18 # in which case type does not matter. 

+

19 

+

20 if density.dtype.kind != 'i' and len(density) > 0: 

+

21 raise TypeError("Density should be a array of *integers*.") 

+

22 # and the right values (positive or null) 

+

23 if any(density < 0): 

+

24 raise ValueError("Density should be an array of *positive* integers.") 

+

25 if density.ndim != 1: 

+

26 raise ValueError("Density should be an a *1-dimensional*" + 

+

27 "array of positive integers.") 

+

28 

+

29 return sum(density * (density - 1)) 

+
+ + + diff --git a/ch03tests/diffusion/htmlcov/status.json b/ch03tests/diffusion/htmlcov/status.json new file mode 100644 index 000000000..2a3df4833 --- /dev/null +++ b/ch03tests/diffusion/htmlcov/status.json @@ -0,0 +1 @@ +{"format":2,"version":"7.3.2","globals":"e88d389d5514e7d953650311e1bcb90d","files":{"__init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"nums":[0,1,0,0,0,0,0,0],"html_filename":"__init___py.html","relative_filename":"__init__.py"}},"model_py":{"hash":"43c7eada4262c382c0def8d97a79d5dd","index":{"nums":[0,1,10,0,0,0,0,0],"html_filename":"model_py.html","relative_filename":"model.py"}},"test_model_py":{"hash":"19bbfeab31a2b1d92e5e7ab1dedf121c","index":{"nums":[0,1,31,0,0,0,0,0],"html_filename":"test_model_py.html","relative_filename":"test_model.py"}}}} \ No newline at end of file diff --git a/ch03tests/diffusion/htmlcov/style.css b/ch03tests/diffusion/htmlcov/style.css new file mode 100644 index 000000000..11b24c4e7 --- /dev/null +++ b/ch03tests/diffusion/htmlcov/style.css @@ -0,0 +1,309 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.2em; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } + +#filter_container input:focus { border-color: #007acc; } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { margin-top: -4em; padding-top: 4em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; } + +#index th { font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"]::after { font-family: sans-serif; content: " ↑"; } + +#index th[aria-sort="descending"]::after { font-family: sans-serif; content: " ↓"; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.file:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } + +#index tr.file:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/ch03tests/diffusion/htmlcov/test_model_py.html b/ch03tests/diffusion/htmlcov/test_model_py.html new file mode 100644 index 000000000..f4c1a3657 --- /dev/null +++ b/ch03tests/diffusion/htmlcov/test_model_py.html @@ -0,0 +1,155 @@ + + + + + Coverage for test_model.py: 100% + + + + + +
+
+

+ Coverage for test_model.py: + 100% +

+ +

+ 31 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.3.2, + created at 2023-11-22 15:47 +0000 +

+ +
+
+
+

1""" Unit tests for a diffusion model """ 

+

2 

+

3from pytest import raises 

+

4from .model import energy 

+

5 

+

6def test_energy_fails_on_non_integer_density(): 

+

7 with raises(TypeError) as exception: 

+

8 energy([1.0, 2, 3]) 

+

9 

+

10def test_energy_fails_on_negative_density(): 

+

11 with raises(ValueError) as exception: energy( 

+

12 [-1, 2, 3]) 

+

13 

+

14def test_energy_fails_ndimensional_density(): 

+

15 with raises(ValueError) as exception: energy( 

+

16 [[1, 2, 3], [3, 4, 5]]) 

+

17 

+

18def test_zero_energy_cases(): 

+

19 # Zero energy at zero density 

+

20 densities = [ [], [0], [0, 0, 0] ] 

+

21 for density in densities: 

+

22 assert energy(density) == 0 

+

23 

+

24def test_derivative(): 

+

25 from numpy.random import randint 

+

26 

+

27 # Loop over vectors of different sizes (but not empty) 

+

28 for vector_size in randint(1, 1000, size=30): 

+

29 

+

30 # Create random density of size N 

+

31 density = randint(50, size=vector_size) 

+

32 

+

33 # will do derivative at this index 

+

34 element_index = randint(vector_size) 

+

35 

+

36 # modified densities 

+

37 density_plus_one = density.copy() 

+

38 density_plus_one[element_index] += 1 

+

39 

+

40 # Compute and check result 

+

41 # d(n^2-1)/dn = 2n 

+

42 expected = (2.0 * density[element_index] 

+

43 if density[element_index] > 0 

+

44 else 0 ) 

+

45 actual = energy(density_plus_one) - energy(density) 

+

46 assert expected == actual 

+

47 

+

48def test_derivative_no_self_energy(): 

+

49 """ If particle is alone, then its participation to energy is zero """ 

+

50 from numpy import array 

+

51 

+

52 density = array([1, 0, 1, 10, 15, 0]) 

+

53 density_plus_one = density.copy() 

+

54 density[1] += 1 

+

55 

+

56 expected = 0 

+

57 actual = energy(density_plus_one) - energy(density) 

+

58 assert expected == actual 

+
+ + + diff --git a/ch03tests/diffusion/model.py b/ch03tests/diffusion/model.py new file mode 100644 index 000000000..372b54b41 --- /dev/null +++ b/ch03tests/diffusion/model.py @@ -0,0 +1,29 @@ +""" Simplistic 1-dimensional diffusion model """ + +def energy(density): + """ + Energy associated with the diffusion model + + :Parameters: + + density: array of positive integers + Number of particles at each position i in the array/geometry + """ + from numpy import array, any, sum + + # Make sure input is an numpy array + density = array(density) + + # ...of the right kind (integer). Unless it is zero length, + # in which case type does not matter. + + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be a array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional*" + + "array of positive integers.") + + return sum(density * (density - 1)) diff --git a/ch03tests/diffusion/test_model.py b/ch03tests/diffusion/test_model.py new file mode 100644 index 000000000..34f390a4f --- /dev/null +++ b/ch03tests/diffusion/test_model.py @@ -0,0 +1,58 @@ +""" Unit tests for a diffusion model """ + +from pytest import raises +from .model import energy + +def test_energy_fails_on_non_integer_density(): + with raises(TypeError) as exception: + energy([1.0, 2, 3]) + +def test_energy_fails_on_negative_density(): + with raises(ValueError) as exception: energy( + [-1, 2, 3]) + +def test_energy_fails_ndimensional_density(): + with raises(ValueError) as exception: energy( + [[1, 2, 3], [3, 4, 5]]) + +def test_zero_energy_cases(): + # Zero energy at zero density + densities = [ [], [0], [0, 0, 0] ] + for density in densities: + assert energy(density) == 0 + +def test_derivative(): + from numpy.random import randint + + # Loop over vectors of different sizes (but not empty) + for vector_size in randint(1, 1000, size=30): + + # Create random density of size N + density = randint(50, size=vector_size) + + # will do derivative at this index + element_index = randint(vector_size) + + # modified densities + density_plus_one = density.copy() + density_plus_one[element_index] += 1 + + # Compute and check result + # d(n^2-1)/dn = 2n + expected = (2.0 * density[element_index] + if density[element_index] > 0 + else 0 ) + actual = energy(density_plus_one) - energy(density) + assert expected == actual + +def test_derivative_no_self_energy(): + """ If particle is alone, then its participation to energy is zero """ + from numpy import array + + density = array([1, 0, 1, 10, 15, 0]) + density_plus_one = density.copy() + density[1] += 1 + + expected = 0 + actual = energy(density_plus_one) - energy(density) + assert expected == actual diff --git a/ch03tests/figures/callgrind.png b/ch03tests/figures/callgrind.png new file mode 100644 index 0000000000000000000000000000000000000000..ba947a94a8106463466a97457c5eca79c7a7b8b9 GIT binary patch literal 274705 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

~Q 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

@Fmvt_ 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≧*5TAz-(rVa2OHa3mdae-7@o}|< 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�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

G_qYQIp=L8W5sLLd=ZeBOiTJQ%LvZ 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 literal 0 HcmV?d00001 diff --git a/ch03tests/figures/coverage.png b/ch03tests/figures/coverage.png new file mode 100644 index 0000000000000000000000000000000000000000..b7ae7370e43a5700bf66ab07e926456706f02bd7 GIT binary patch literal 53222 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 AT>t<8 literal 0 HcmV?d00001 diff --git a/ch03tests/figures/jenkins.png b/ch03tests/figures/jenkins.png new file mode 100644 index 0000000000000000000000000000000000000000..c7a52353639b7cd2f20d36ab7e6381a40bb3ff31 GIT binary patch literal 108319 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 z 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 checkTest + writeCode -> checkAllTests + + cleanup -> writeTest [tailport=e, headport=n] + checkTest -> writeTest [label="passed", tailport=w, headport=w] + checkTest -> writeCode [label="failed"] + checkAllTests -> cleanup [label="passed"] + checkAllTests -> writeCode [label="failed", tailport=w, headport=w] + + writeTest [shape=box, label="(re-) write test"] + writeCode [shape=box, label="write code"] + cleanup [shape=box, label="cleanup code, refactor"] + + checkTest [shape=diamond, label="run test"] + checkAllTests [shape=diamond, label="run all tests"] +} diff --git a/ch03tests/index.html b/ch03tests/index.html new file mode 100644 index 000000000..25f061fa9 --- /dev/null +++ b/ch03tests/index.html @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Testing + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Why test?
  • +
  • Unit testing and regression testing
  • +
  • Negative testing
  • +
  • Mocking
  • +
  • Debugging
  • +
  • Continuous Integration
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch03tests/saskatchewan/overlap.py b/ch03tests/saskatchewan/overlap.py new file mode 100644 index 000000000..b963a715f --- /dev/null +++ b/ch03tests/saskatchewan/overlap.py @@ -0,0 +1,13 @@ +def overlap(field1, field2): + left1, bottom1, top1, right1 = field1 + left2, bottom2, top2, right2 = field2 + + overlap_left = max(left1, left2) + overlap_bottom = max(bottom1, bottom2) + overlap_right = min(right1, right2) + overlap_top = min(top1, top2) + # Here's our wrong code again + overlap_height = (overlap_top - overlap_bottom) + overlap_width = (overlap_right - overlap_left) + + return overlap_height * overlap_width diff --git a/ch03tests/saskatchewan/test_overlap.py b/ch03tests/saskatchewan/test_overlap.py new file mode 100644 index 000000000..fedab8dd0 --- /dev/null +++ b/ch03tests/saskatchewan/test_overlap.py @@ -0,0 +1,10 @@ +from .overlap import overlap + +def test_full_overlap(): + assert overlap((1.,1.,4.,4.), (2.,2.,3.,3.)) == 1.0 + +def test_partial_overlap(): + assert overlap((1,1,4,4), (2,2,3,4.5)) == 2.0 + +def test_no_overlap(): + assert overlap((1,1,4,4), (4.5,4.5,5,5)) == 0.0 diff --git a/ch03tests/solutions/diffusionmodel/LICENSE.md b/ch03tests/solutions/diffusionmodel/LICENSE.md new file mode 100644 index 000000000..de16cca79 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 James Hetherington + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ch03tests/solutions/diffusionmodel/README.md b/ch03tests/solutions/diffusionmodel/README.md new file mode 100644 index 000000000..93cd8c207 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/README.md @@ -0,0 +1,4 @@ +Diffusion model +=============== + +This example code is used as part of the UCL [Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course. diff --git a/ch03tests/solutions/diffusionmodel/diffusion_model.py b/ch03tests/solutions/diffusionmodel/diffusion_model.py new file mode 100644 index 000000000..4262418a3 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/diffusion_model.py @@ -0,0 +1,47 @@ +""" Simplistic 1-dimensional diffusion model """ +def energy(density, coefficient=1): + """ Energy associated with the diffusion model + + :Parameters: + density: array of positive integers + Number of particles at each position i in the array/geometry + """ + from numpy import array, any, sum + + # Make sure input is an array + density = array(density) + + # of the right kind (integer). Unless it is zero length, in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional* array of positive integers.") + + return coefficient * 0.5 * sum(density * (density - 1)) + + + +def partial_derivative(function, x, index): + """ Computes right derivative of function over integers + + :Parameters: + function: callable object + The function for which to compute the delta/derivative + x: array of integers + The point at which to compute the right-derivative + index: integer + Partial derivative direction. + """ + from numpy import array + # Computes left value + left_value = function(x) + + # Copies and modifies x. Could do it without copy, but that complicates mocking. + x = array(x) + x[index] += 1 + right_value = function(x) + + return right_value - left_value diff --git a/ch03tests/solutions/diffusionmodel/energy_example.py b/ch03tests/solutions/diffusionmodel/energy_example.py new file mode 100644 index 000000000..ff30a2464 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/energy_example.py @@ -0,0 +1,2 @@ +from diffusion_model import energy +print(energy([5, 6, 7, 8, 0, 1])) diff --git a/ch03tests/solutions/diffusionmodel/test_derivatives.py b/ch03tests/solutions/diffusionmodel/test_derivatives.py new file mode 100644 index 000000000..6c8a423a0 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/test_derivatives.py @@ -0,0 +1,35 @@ +""" Tests partial derivative via mocking """ +from mock import MagicMock +from nose.tools import assert_equal, assert_true +from diffusion_model import partial_derivative + +def test_partial_derivative(): + """ Mocks a call to partial derivative. """ + from numpy import array, sum, abs + # setups arguments + function = MagicMock(side_effect=[3, 2]) + density = array([0, 1, 2]) + + # Makes call + result = partial_derivative(function, density, 1) + # Check magnitude of result: sign depends on order of call since we are faking it. + assert_equal(abs(result), 1, "Magnitude of partial derivative") + + # Check function was called twice + assert_equal(function.call_count, 2, "Function was called twice") + + for name, args, kwargs in function.mock_calls: + # Called function itself + assert_equal(name, '') + # No keyword arguments + assert_equal(kwargs, {}) + # Single argument + assert_equal(len(args), 1) + + # Density is at most off by one + assert_true(abs(sum(args[0] - density)) <= 1) + + # Checks that args in two calls are off by one and that sign of result is correct + first_density = function.mock_calls[0][1][0] + second_density = function.mock_calls[1][1][0] + assert_equal(sum(first_density - second_density), result) diff --git a/ch03tests/solutions/diffusionmodel/test_diffusion_model.py b/ch03tests/solutions/diffusionmodel/test_diffusion_model.py new file mode 100644 index 000000000..2ee1cabc7 --- /dev/null +++ b/ch03tests/solutions/diffusionmodel/test_diffusion_model.py @@ -0,0 +1,62 @@ +""" Unit tests for a diffusion model """ +from nose.tools import assert_raises, assert_almost_equal +from diffusion_model import energy + +# def test_energy_fails_on_non_integer_density(): +# with assert_raises(TypeError) as exception: energy([1.0, 2, 3]) +# +# def test_energy_fails_on_negative_density(): +# with assert_raises(ValueError) as exception: energy([-1, 2, 3]) +# +# def test_energy_fails_ndimensional_density(): +# with assert_raises(ValueError) as exception: energy([[1, 2, 3], [3, 4, 5]]) + +def test_zero_energy_cases(): + # Zero energy at zero density + densities = [ [], [0], [0, 0, 0] ] + for density in densities: assert_almost_equal(energy(density), 0) + + # Zero energy for coefficient == 0 + assert_almost_equal(energy([1, 1, 1], coefficient=0), 0) + +def test_derivative(): + from numpy.random import randint + + # Loop over vectors of different sizes (but not empty) + for vector_size in randint(1, 1000, size=30): + + # Create random density of size N + density = randint(50, size=vector_size) + + # will do derivative at this index + element_index = randint(vector_size) + + # modified densities + density_plus_one = density.copy() + density_plus_one[element_index] += 1 + + # Compute and check result + expected = density[element_index] if density[element_index] > 0 else 0 + actual = energy(density_plus_one) - energy(density) + assert_almost_equal(expected, actual) + +def test_derivative_no_self_energy(): + """ If particle is alone, then its participation to energy is zero """ + from numpy import array + + density = array([1, 0, 1, 10, 15, 0]) + density_plus_one = density.copy() + density[1] += 1 + + expected = 0 + actual = energy(density_plus_one) - energy(density) + assert_almost_equal(expected, actual) + +def test_coefficient_is_linear(): + from numpy import array + + density = array([1, 0, 1, 10, 15, 0]) + + value = energy(density, coefficient = 1) + twice = energy(density, coefficient = 2e0) + assert_almost_equal(value + value, twice) diff --git a/ch03tests/solutions/montecarlo/LICENSE.md b/ch03tests/solutions/montecarlo/LICENSE.md new file mode 100644 index 000000000..de16cca79 --- /dev/null +++ b/ch03tests/solutions/montecarlo/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 James Hetherington + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ch03tests/solutions/montecarlo/README.md b/ch03tests/solutions/montecarlo/README.md new file mode 100644 index 000000000..da0d42ecb --- /dev/null +++ b/ch03tests/solutions/montecarlo/README.md @@ -0,0 +1,4 @@ +Monte Carlo +=========== + +This example code is used as part of the UCL [Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course. diff --git a/ch03tests/solutions/montecarlo/monte_carlo.py b/ch03tests/solutions/montecarlo/monte_carlo.py new file mode 100644 index 000000000..5619b294c --- /dev/null +++ b/ch03tests/solutions/montecarlo/monte_carlo.py @@ -0,0 +1,82 @@ +class MonteCarlo(object): + """ A simple Monte Carlo implementation """ + def __init__(self, temperature=100, itermax=100): + + if temperature == 0: raise NotImplementedError("Zero temperature not implemented") + if temperature < 0e0: raise ValueError("Negative temperature makes no sense") + + self.temperature = temperature + """ Temperature at which to run simulation """ + self.itermax = itermax + """ Maximum number of iterations """ + + def change_density(self, density): + """ Move one particle left or right. """ + from numpy import sum, array + from numpy.random import randint, choice + + # Particle index + particle = randint(sum(density)) + # Location + current = 0 + for location, n in enumerate(density): + current += n + if n > particle: break + + # Move direction + if location == 0: direction = 1 + elif location == len(density) - 1: direction = -1 + else: direction = choice([-1, 1]) + + # Now make change + result = array(density) + result[location] -= 1 + result[location + direction] += 1 + return result + + def accept_change(self, prior, successor): + """ Returns true if should accept change. """ + from numpy import exp + from numpy.random import uniform + if successor <= prior: return True + return exp(-(successor - prior) / self.temperature) > uniform() + + def __call__(self, energy, density): + """ Runs Monte-carlo """ + from numpy import any, array + + density = array(density) + if len(density) < 2: + raise ValueError("Density is too short") + # of the right kind (integer). Unless it is zero length, in which case type does not matter. + if density.dtype.kind != 'i' and len(density) > 0: + raise TypeError("Density should be an array of *integers*.") + # and the right values (positive or null) + if any(density < 0): + raise ValueError("Density should be an array of *positive* integers.") + if density.ndim != 1: + raise ValueError("Density should be an a *1-dimensional* array of positive integers.") + if sum(density) == 0: + raise ValueError("Density is empty.") + + iteration = 0 + current_energy = energy(density) + while iteration < self.itermax or self.itermax < 0: + + new_density = self.change_density(density) + new_energy = energy(new_density) + + accept = self.accept_change(current_energy, new_energy) + if accept: density, current_energy = new_density, new_energy + + if not self.observe(iteration, accept, density, current_energy): break + + iteration += 1 + + def observe(self, iteration, accepted, density, energy): + """ Called at every step to observe simulation. + + :returns: True if simulation should keep going. + """ + return True + diff --git a/ch03tests/solutions/montecarlo/test_monte_carlo.py b/ch03tests/solutions/montecarlo/test_monte_carlo.py new file mode 100644 index 000000000..c71477332 --- /dev/null +++ b/ch03tests/solutions/montecarlo/test_monte_carlo.py @@ -0,0 +1,130 @@ +""" Tests Monte-Carlo method, in isolation of diffusion model """ + +from nose.tools import assert_equal, assert_almost_equal, assert_true, assert_raises +from monte_carlo import MonteCarlo + +def test_input_sanity(): + """ Check incorrect input do fail """ + + with assert_raises(NotImplementedError) as exception: MonteCarlo(temperature=0e0) + with assert_raises(ValueError) as exception: MonteCarlo(temperature=-1e0) + + mc = MonteCarlo() + with assert_raises(TypeError) as exception: mc(lambda x: 0, [1.0, 2, 3]) + with assert_raises(ValueError) as exception: mc(lambda x: 0, [-1, 2, 3]) + with assert_raises(ValueError) as exception: mc(lambda x: 0, [[1, 2, 3], [3, 4, 5]]) + with assert_raises(ValueError) as exception: mc(lambda x: 0, [3]) + with assert_raises(ValueError) as exception: mc(lambda x: 0, [0, 0]) + +def test_move_particle_one_over(): + """ Check density is change by a particle hopping left or right. """ + from numpy import nonzero, multiply + from numpy.random import randint + + mc = MonteCarlo() + + for i in range(100): # Do this n times, to avoid issues with random numbers + # Create density + density = randint(50, size=randint(2, 6)) + # Change it + new_density = mc.change_density(density) + + # Make sure any movement is by one + indices = nonzero(density - new_density)[0] + assert_equal(len(indices), 2, "densities differ in two places") + assert_equal( + multiply.reduce((density - new_density)[indices]), + -1, + "densities differ by + and - 1" + ) + +def test_equal_probability(): + """ Check particles have equal probability of movement. """ + from numpy import array, sqrt, count_nonzero + + mc = MonteCarlo() + density = array([1, 0, 99]) + changes_at_zero = [(density - mc.change_density(density))[0] != 0 for i in range(10000)] + assert_almost_equal( + count_nonzero(changes_at_zero), + 0.01 * len(changes_at_zero), + delta = 0.5 * sqrt(len(changes_at_zero)) + ) + +def test_accept_change(): + """ Check that move is accepted if second energy is lower """ + from numpy import sqrt, count_nonzero, exp + + mc = MonteCarlo(temperature=100.0) + # Should always be true. But do more than one draw, in case random incorrectly crept into + # implementation + for i in range(10): + assert_true(mc.accept_change(0.5, 0.4)) + assert_true(mc.accept_change(0.5, 0.5)) + + # This should be accepted only part of the time, depending on exponential distribution + prior, successor = 0.4, 0.5 + accepted = [mc.accept_change(prior, successor) for i in range(10000)] + assert_almost_equal( + count_nonzero(accepted) / float(len(accepted)), + exp(-(successor - prior) / mc.temperature), + delta = 3e0 / sqrt(len(accepted)) + ) + +def test_main_algorithm(): + """ Check set path through main algorithm """ + from mock import Mock, call + + mc = MonteCarlo(temperature=100.0, itermax=4) + + # Patch mc so that it takes a pre-determined path through + acceptance = [True, True, False, True] + mc.accept_change = Mock(side_effect=acceptance) + densities = ( + [0, 0, 1, 0], + [0, 1, 1, 0], + [2, 2, 2, 2], + [2, 3, 3, 2], + [5, 3, 3, 5], + ) + mc.change_density = Mock(side_effect=densities[1:]) + mc.observe = Mock(return_value=True) + + # Fake energy method + energies = [0.1, -0.1, -0.2, -0.15, -0.25] + energy = Mock(side_effect=energies) + + # Call simulation + mc(energy, densities[0]) + + # Now, analyze path. First check length. + assert_equal(len(mc.accept_change.mock_calls), 4) + assert_equal(len(mc.change_density.mock_calls), 4) + assert_equal(len(mc.observe.mock_calls), 4) + assert_equal(len(energy.mock_calls), 5) # one extra call to get first energy + + # Easiest to look at observe, since it should have all the info about the step + observe_path = [ + call(0, acceptance[0], densities[1], energies[1]), + call(1, acceptance[1], densities[2], energies[2]), + call(2, acceptance[2], densities[2], energies[2]), + call(3, acceptance[3], densities[4], energies[4]) + ] + assert_equal(observe_path, mc.observe.call_args_list) + +def test_stop_simulation(): + """ Checks that if observe returns False, iteration stops. """ + from mock import Mock + mc = MonteCarlo(temperature=100.0, itermax=8) + + # Make a fake observer + mc.observe = Mock(side_effect=[True, False, True]) + # Fake energy method + energies = [0.1, -0.1, -0.2, -0.15, -0.25] + energy = Mock(side_effect=energies) + # Call simulation + mc(energy, [0, 1, 2, 3]) + + assert_equal(len(mc.observe.mock_calls), 2) + assert_equal(len(energy.mock_calls), 3) # one extra call to get first energy + diff --git a/ch04packaging/010Installation.html b/ch04packaging/010Installation.html new file mode 100644 index 000000000..8cf4805ac --- /dev/null +++ b/ch04packaging/010Installation.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Installing Libraries + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Packaging your code

+
+
+
+
+
+
+

Installing Libraries

+
+
+
+
+
+
+

We've seen that there are lots of python libraries. But how do we install them?

+
+
+
+
+
+
+

The main problem is this: libraries need other libraries

+
+
+
+
+
+
+

So you can't just install a library by copying code to the computer: you'll find yourself wandering down a tree +of "dependencies"; libraries needed by libraries needed by the library you want.

+
+
+
+
+
+
+

This is actually a good thing; it means that people are making use of each others' +code. There's a real problem in scientific programming, of people who think they're really clever writing their own +twenty-fifth version of the same thing.

+
+
+
+
+
+
+

So using other people's libraries is good.

+
+
+
+
+
+
+

Why don't we do it more? Because it can often be quite difficult to install other peoples' libraries!

+
+
+
+
+
+
+

Python has developed a good tool for avoiding this: pip.

+
+
+
+
+
+
+

Installing Geopy using Pip

+
+
+
+
+
+
+

On a computer you control, on which you have installed python via Anaconda, you will need to open a terminal +to invoke the library-installer program, pip.

+
+
+
+
+
+
+
    +
  • On windows, go to Start -> Anaconda3 -> Anaconda Prompt
  • +
  • On mac, start Terminal.
  • +
  • On linux, open a bash shell.
  • +
+
+
+
+
+
+
+

Into this shell, type:

+

pip install geopy

+
+
+
+
+
+
+

The computer will install the package and its dependencies automatically from PyPI (a repository of packages, which we'll talk about later).

+
+
+
+
+
+
+

Now, close the Jupyter notebook if you have it open, and reopen it. Check your new library is installed with:

+
+
+
+
+
+
In [1]:
+
+
+
import geopy
+geocoder = geopy.geocoders.Nominatim(user_agent="mphy0021") 
+
+
+
+
+
+
+
+
In [2]:
+
+
+
geocoder.geocode('Cambridge', exactly_one=False)
+
+
+
+
+
+
+
+
Out[2]:
+
+
[Location(Cambridge, Cambridgeshire, Cambridgeshire and Peterborough, England, United Kingdom, (52.2055314, 0.1186637, 0.0)),
+ Location(Cambridge, Middlesex County, Massachusetts, United States, (42.3655767, -71.1040018, 0.0)),
+ Location(Cambridge, Region of Waterloo, Ontario, Canada, (43.3600536, -80.3123023, 0.0)),
+ Location(Cambridge, Henry County, Illinois, United States, (41.3025257, -90.1962861, 0.0)),
+ Location(Cambridge, Isanti County, Minnesota, 55008, United States, (45.5727408, -93.2243921, 0.0)),
+ Location(Cambridge, Story County, Iowa, United States, (41.8990768, -93.5294029, 0.0)),
+ Location(Cambridge, Dorchester County, Maryland, 21613, United States, (38.5714624, -76.0763177, 0.0)),
+ Location(Cambridge, Guernsey County, Ohio, 43725, United States, (40.031183, -81.5884561, 0.0)),
+ Location(Cambridge, Jefferson County, Kentucky, United States, (38.2217369, -85.616627, 0.0)),
+ Location(Cambridge, Cowley County, Kansas, United States, (37.316988, -96.66633224663678, 0.0))]
+
+
+
+
+
+
+
+
+

That was actually pretty easy, I hope. This is how you'll install new libraries when you need them.

+
+
+
+
+
+
+

Troubleshooting:

+

On mac or linux, you might get a complaint that you need "superuser", "root", or "administrator" access. If so type:

+
    +
  • pip install --user geopy
  • +
+

If you get a complaint like: 'pip is not recognized as an internal or external command', try the following:

+
    +
  • conda install pip (if you are using Anaconda - though it should be already available)
  • +
  • or follow the official instructions otherwise.
  • +
+
+
+
+
+
+
+

Installing binary dependencies with Conda

+
+
+
+
+
+
+

pip is the usual Python tool for installing libraries. But there's one area of library installation that is still awkward: +some python libraries depend not on other python libraries, but on libraries in C++ or Fortran.

+
+
+
+
+
+
+

This can cause you to run into difficulties installing some libraries. +Fortunately, for lots of these, Continuum, the makers of Anaconda, provide a carefully managed set of scripts for installing +these awkward non-python libraries too. You can do this with the conda command line tool, if you're using Anaconda.

+
+
+
+
+
+
+

Simply type

+
    +
  • conda install <whatever>
  • +
+

instead of pip install. This will fetch the python package not from PyPI, but from Anaconda's distribution for your platform, and manage any non-python dependencies too.

+
+
+
+
+
+
+

Typically, if you're using Anaconda, whenever you come across a python package you want, you should check if Anaconda +package it first using conda search. If it is there you can conda install it, you'll likely have less problems. But Anaconda doesn't package everything, so you'll need to pip install from time to time.

+

The maintainers of packages may have also provided releases of their software via conda-forge, a community-driven project that provides a collection of packages for the anaconda environment. In such case you can add conda-forge to your anaconda installation and use search and install as explained above.

+
+
+
+
+
+
+

Where do these libraries go?

+
+
+
+
+
+
In [3]:
+
+
+
geopy.__path__
+
+
+
+
+
+
+
+
Out[3]:
+
+
['/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/geopy']
+
+
+
+
+
+
+
+
+

Your computer will be configured to keep installed Python packages in a particular place.

+
+
+
+
+
+
+

Python knows where to look for possible library installations in a list of places, called the $PYTHONPATH (%PYTHONPATH% in Windows). +It will try each of these places in turn, until it finds a matching library name.

+
+
+
+
+
+
In [4]:
+
+
+
import sys
+sys.path
+
+
+
+
+
+
+
+
Out[4]:
+
+
['/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging',
+ '/opt/hostedtoolcache/Python/3.8.18/x64/lib/python38.zip',
+ '/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8',
+ '/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/lib-dynload',
+ '',
+ '/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages']
+
+
+
+
+
+
+
+
+

You can add (append) more paths to this list, and so allow libraries to be load from there. Thought this is not a recommended practice, let's do it once to understand how the import works.

+
    +
  1. Create a new directory (e.g., myexemplar),
  2. +
  3. create a file inside that directory (exemplar.py),
  4. +
  5. write a function inside such file (exemplar_works),
  6. +
  7. open python, import sys and add the path of myexemplar to sys.path,
  8. +
  9. import your new file, and
  10. +
  11. run the function.
  12. +
+
+
+
+
+
+
+

Libraries not in PyPI

+
+
+
+
+
+
+

Sometimes you'll need to download the source code +directly. This won't automatically follow the dependency tree, but for simple standalone libraries, is sometimes necessary.

+
+
+
+
+
+
+

To install these on windows, download and unzip the library into a folder of your choice, e.g. my_python_libs.

+

On windows, a reasonable choice +is the folder you end up in when you open the Anaconda terminal. You can get a graphical view on this folder by typing: explorer .

+
+
+
+
+
+
+

Make a new folder for your download and unzip the library there.

+
+
+
+
+
+
+

Now, you need to move so you're inside your download in the terminal:

+
+
+
+
+
+
+
    +
  • cd my_python_libs
  • +
  • cd <library name> (e.g. cd JSAnimation-master)
  • +
+
+
+
+
+
+
+

Now, manually install the library in your PythonPath:

+
+
+
+
+
+
+
    +
  • pip install --user .
  • +
+
+
+
+
+
+
+

This is all pretty awkward, but it is worth practising this stuff, as most of the power of using programming for +research resides in all the libraries that are out there.

+
+
+
+
+
+
+

Python virtual environments

+
+
+
+
+
+
+

Sometimes you need to have different versions of a package installed, or you would like to install a set of libraries that you don't want to affect the rest of the installation in your system. In such cases you can create environments that are isolated from the rest.

+

There are multiple solutions to this, only for python or for anaconda. +Find more information on how to create and use the virtual enviroments.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/010Installation.ipynb b/ch04packaging/010Installation.ipynb new file mode 100644 index 000000000..c208c10c5 --- /dev/null +++ b/ch04packaging/010Installation.ipynb @@ -0,0 +1,397 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6909bc4e", + "metadata": {}, + "source": [ + "# Packaging your code" + ] + }, + { + "cell_type": "markdown", + "id": "59a159ea", + "metadata": {}, + "source": [ + "## Installing Libraries" + ] + }, + { + "cell_type": "markdown", + "id": "d7fed472", + "metadata": {}, + "source": [ + "We've seen that there are lots of python libraries. But how do we install them?" + ] + }, + { + "cell_type": "markdown", + "id": "cc34a5b9", + "metadata": {}, + "source": [ + "The main problem is this: *libraries need other libraries*" + ] + }, + { + "cell_type": "markdown", + "id": "9a94ad6c", + "metadata": {}, + "source": [ + "So you can't just install a library by copying code to the computer: you'll find yourself wandering down a tree\n", + "of \"dependencies\"; libraries needed by libraries needed by the library you want." + ] + }, + { + "cell_type": "markdown", + "id": "ab1d928c", + "metadata": {}, + "source": [ + "This is actually a good thing; it means that people are making use of each others'\n", + "code. There's a real problem in scientific programming, of people who think they're really clever writing their own\n", + "twenty-fifth version of the same thing." + ] + }, + { + "cell_type": "markdown", + "id": "66859ff5", + "metadata": {}, + "source": [ + "So using other people's libraries is good." + ] + }, + { + "cell_type": "markdown", + "id": "4ec2af0f", + "metadata": {}, + "source": [ + "Why don't we do it more? Because it can often be quite difficult to **install** other peoples' libraries!" + ] + }, + { + "cell_type": "markdown", + "id": "15a38e29", + "metadata": {}, + "source": [ + "Python has developed a good tool for avoiding this: **pip**." + ] + }, + { + "cell_type": "markdown", + "id": "f21466c1", + "metadata": {}, + "source": [ + "### Installing Geopy using Pip" + ] + }, + { + "cell_type": "markdown", + "id": "fe142cc7", + "metadata": {}, + "source": [ + "On a computer you control, on which you have installed python via Anaconda, you will need to open a **terminal**\n", + "to invoke the library-installer program, `pip`." + ] + }, + { + "cell_type": "markdown", + "id": "c80c823f", + "metadata": {}, + "source": [ + "* On windows, go to Start -> Anaconda3 -> Anaconda Prompt\n", + "* On mac, start *Terminal*. \n", + "* On linux, open a bash shell." + ] + }, + { + "cell_type": "markdown", + "id": "0f46eb3e", + "metadata": {}, + "source": [ + "Into this shell, type:\n", + " \n", + "`pip install geopy`" + ] + }, + { + "cell_type": "markdown", + "id": "7586dd6c", + "metadata": {}, + "source": [ + "The computer will install the package and its dependencies automatically from PyPI (a repository of packages, which we'll talk about later)." + ] + }, + { + "cell_type": "markdown", + "id": "9187c31c", + "metadata": {}, + "source": [ + "Now, close the Jupyter notebook if you have it open, and reopen it. Check your new library is installed with:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c53b363b", + "metadata": {}, + "outputs": [], + "source": [ + "import geopy\n", + "geocoder = geopy.geocoders.Nominatim(user_agent=\"mphy0021\") " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10441b49", + "metadata": {}, + "outputs": [], + "source": [ + "geocoder.geocode('Cambridge', exactly_one=False)" + ] + }, + { + "cell_type": "markdown", + "id": "a50f9e99", + "metadata": {}, + "source": [ + "That was actually pretty easy, I hope. This is how you'll install new libraries when you need them." + ] + }, + { + "cell_type": "markdown", + "id": "e14f97c4", + "metadata": {}, + "source": [ + "Troubleshooting:\n", + " \n", + "On mac or linux, you *might* get a complaint that you need \"superuser\", \"root\", or \"administrator\" access. If so type:\n", + "\n", + "* `pip install --user geopy`\n", + " \n", + "If you get a complaint like: 'pip is not recognized as an internal or external command', try the following:\n", + " \n", + "* `conda install pip` (if you are using Anaconda - though it should be already available)\n", + "* or follow the [official instructions](https://packaging.python.org/tutorials/installing-packages/#ensure-you-can-run-pip-from-the-command-line) otherwise.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5461b20e", + "metadata": {}, + "source": [ + "### Installing binary dependencies with Conda" + ] + }, + { + "cell_type": "markdown", + "id": "a4aafc90", + "metadata": {}, + "source": [ + "`pip` is the usual Python tool for installing libraries. But there's one area of library installation that is still awkward:\n", + "some python libraries depend not on other **python** libraries, but on libraries in C++ or Fortran." + ] + }, + { + "cell_type": "markdown", + "id": "eb7fcc17", + "metadata": {}, + "source": [ + "This can cause you to run into difficulties installing some libraries. \n", + "Fortunately, for lots of these, Continuum, the makers of Anaconda, provide a carefully managed set of scripts for installing\n", + "these awkward non-python libraries too. You can do this with the `conda` command line tool, if you're using Anaconda." + ] + }, + { + "cell_type": "markdown", + "id": "07292921", + "metadata": {}, + "source": [ + "Simply type\n", + "\n", + "* `conda install `\n", + "\n", + "instead of `pip install`. This will fetch the python package not from PyPI, but from Anaconda's distribution for your platform, and manage any non-python dependencies too." + ] + }, + { + "cell_type": "markdown", + "id": "74942923", + "metadata": {}, + "source": [ + "Typically, if you're using Anaconda, whenever you come across a python package you want, you should check if Anaconda\n", + "package it first using `conda search`. If it is there you can `conda install` it, you'll likely have less problems. But Anaconda doesn't package everything, so you'll need to `pip install` from time to time.\n", + "\n", + "The maintainers of packages may have also provided releases of their software via [conda-forge](https://conda-forge.org/), a community-driven project that provides a collection of packages for the anaconda environment. In such case you can [add conda-forge](https://conda-forge.org/#about) to your anaconda installation and use `search` and `install` as explained above." + ] + }, + { + "cell_type": "markdown", + "id": "ac84df47", + "metadata": {}, + "source": [ + "### Where do these libraries go? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "024c880d", + "metadata": {}, + "outputs": [], + "source": [ + "geopy.__path__" + ] + }, + { + "cell_type": "markdown", + "id": "2ff47c8a", + "metadata": {}, + "source": [ + "Your computer will be configured to keep installed Python packages in a particular place." + ] + }, + { + "cell_type": "markdown", + "id": "828ef4b6", + "metadata": {}, + "source": [ + "Python knows where to look for possible library installations in a list of places, called the `$PYTHONPATH` (`%PYTHONPATH%` in Windows).\n", + "It will try each of these places in turn, until it finds a matching library name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f3823b0", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path" + ] + }, + { + "cell_type": "markdown", + "id": "d4f1a3a8", + "metadata": {}, + "source": [ + "You can add (`append`) more paths to this list, and so allow libraries to be load from there. Thought this is not a recommended practice, let's do it once to understand how the import works.\n", + "\n", + "1. Create a new directory (_e.g._, `myexemplar`),\n", + "1. create a file inside that directory (`exemplar.py`),\n", + "1. write a function inside such file (`exemplar_works`),\n", + "1. open python, import `sys` and add the path of `myexemplar` to `sys.path`,\n", + "1. import your new file, and \n", + "1. run the function." + ] + }, + { + "cell_type": "markdown", + "id": "bf6c5e2d", + "metadata": {}, + "source": [ + "### Libraries not in PyPI" + ] + }, + { + "cell_type": "markdown", + "id": "a910f8d5", + "metadata": {}, + "source": [ + "Sometimes you'll need to download the source code\n", + "directly. This won't automatically follow the dependency tree, but for simple standalone libraries, is sometimes necessary." + ] + }, + { + "cell_type": "markdown", + "id": "5b0b2703", + "metadata": {}, + "source": [ + "To install these on windows, download and unzip the library into a folder of your choice, e.g. `my_python_libs`. \n", + "\n", + "On windows, a reasonable choice\n", + "is the folder you end up in when you open the Anaconda terminal. You can get a graphical view on this folder by typing: `explorer .`" + ] + }, + { + "cell_type": "markdown", + "id": "0de2f086", + "metadata": {}, + "source": [ + "Make a new folder for your download and unzip the library there." + ] + }, + { + "cell_type": "markdown", + "id": "d0a9fa09", + "metadata": {}, + "source": [ + "Now, you need to move so you're inside your download in the terminal:" + ] + }, + { + "cell_type": "markdown", + "id": "65fc9150", + "metadata": {}, + "source": [ + "* `cd my_python_libs`\n", + "* `cd ` (e.g. `cd JSAnimation-master`) " + ] + }, + { + "cell_type": "markdown", + "id": "c7db5d5f", + "metadata": {}, + "source": [ + "Now, manually install the library in your PythonPath:" + ] + }, + { + "cell_type": "markdown", + "id": "49191bac", + "metadata": {}, + "source": [ + "* `pip install --user .`" + ] + }, + { + "cell_type": "markdown", + "id": "2deb868d", + "metadata": {}, + "source": [ + "This is all pretty awkward, but it is worth practising this stuff, as most of the power of using programming for\n", + "research resides in all the libraries that are out there. " + ] + }, + { + "cell_type": "markdown", + "id": "0e6f29ab", + "metadata": {}, + "source": [ + "### Python virtual environments" + ] + }, + { + "cell_type": "markdown", + "id": "b57df57d", + "metadata": {}, + "source": [ + "Sometimes you need to have different versions of a package installed, or you would like to install a set of libraries that you don't want to affect the rest of the installation in your system. In such cases you can create environments that are isolated from the rest.\n", + "\n", + "There are multiple solutions to this, only for [python](https://docs.python.org/3.6/library/venv.html) or for [anaconda](https://conda.io/docs/user-guide/tasks/manage-environments.html).\n", + "Find more information on [how to create and use the virtual enviroments](https://realpython.com/python-virtual-environments-a-primer/)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Installing Libraries" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/010Installation.ipynb.py b/ch04packaging/010Installation.ipynb.py new file mode 100644 index 000000000..7371c3b0e --- /dev/null +++ b/ch04packaging/010Installation.ipynb.py @@ -0,0 +1,182 @@ +# --- +# jupyter: +# jekyll: +# display_name: Installing Libraries +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Packaging your code + +# %% [markdown] +# ## Installing Libraries + +# %% [markdown] +# We've seen that there are lots of python libraries. But how do we install them? + +# %% [markdown] +# The main problem is this: *libraries need other libraries* + +# %% [markdown] +# So you can't just install a library by copying code to the computer: you'll find yourself wandering down a tree +# of "dependencies"; libraries needed by libraries needed by the library you want. + +# %% [markdown] +# This is actually a good thing; it means that people are making use of each others' +# code. There's a real problem in scientific programming, of people who think they're really clever writing their own +# twenty-fifth version of the same thing. + +# %% [markdown] +# So using other people's libraries is good. + +# %% [markdown] +# Why don't we do it more? Because it can often be quite difficult to **install** other peoples' libraries! + +# %% [markdown] +# Python has developed a good tool for avoiding this: **pip**. + +# %% [markdown] +# ### Installing Geopy using Pip + +# %% [markdown] +# On a computer you control, on which you have installed python via Anaconda, you will need to open a **terminal** +# to invoke the library-installer program, `pip`. + +# %% [markdown] +# * On windows, go to Start -> Anaconda3 -> Anaconda Prompt +# * On mac, start *Terminal*. +# * On linux, open a bash shell. + +# %% [markdown] +# Into this shell, type: +# +# `pip install geopy` + +# %% [markdown] +# The computer will install the package and its dependencies automatically from PyPI (a repository of packages, which we'll talk about later). + +# %% [markdown] +# Now, close the Jupyter notebook if you have it open, and reopen it. Check your new library is installed with: + +# %% +import geopy +geocoder = geopy.geocoders.Nominatim(user_agent="mphy0021") + +# %% +geocoder.geocode('Cambridge', exactly_one=False) + +# %% [markdown] +# That was actually pretty easy, I hope. This is how you'll install new libraries when you need them. + +# %% [markdown] +# Troubleshooting: +# +# On mac or linux, you *might* get a complaint that you need "superuser", "root", or "administrator" access. If so type: +# +# * `pip install --user geopy` +# +# If you get a complaint like: 'pip is not recognized as an internal or external command', try the following: +# +# * `conda install pip` (if you are using Anaconda - though it should be already available) +# * or follow the [official instructions](https://packaging.python.org/tutorials/installing-packages/#ensure-you-can-run-pip-from-the-command-line) otherwise. +# + +# %% [markdown] +# ### Installing binary dependencies with Conda + +# %% [markdown] +# `pip` is the usual Python tool for installing libraries. But there's one area of library installation that is still awkward: +# some python libraries depend not on other **python** libraries, but on libraries in C++ or Fortran. + +# %% [markdown] +# This can cause you to run into difficulties installing some libraries. +# Fortunately, for lots of these, Continuum, the makers of Anaconda, provide a carefully managed set of scripts for installing +# these awkward non-python libraries too. You can do this with the `conda` command line tool, if you're using Anaconda. + +# %% [markdown] +# Simply type +# +# * `conda install ` +# +# instead of `pip install`. This will fetch the python package not from PyPI, but from Anaconda's distribution for your platform, and manage any non-python dependencies too. + +# %% [markdown] +# Typically, if you're using Anaconda, whenever you come across a python package you want, you should check if Anaconda +# package it first using `conda search`. If it is there you can `conda install` it, you'll likely have less problems. But Anaconda doesn't package everything, so you'll need to `pip install` from time to time. +# +# The maintainers of packages may have also provided releases of their software via [conda-forge](https://conda-forge.org/), a community-driven project that provides a collection of packages for the anaconda environment. In such case you can [add conda-forge](https://conda-forge.org/#about) to your anaconda installation and use `search` and `install` as explained above. + +# %% [markdown] +# ### Where do these libraries go? + +# %% +geopy.__path__ + +# %% [markdown] +# Your computer will be configured to keep installed Python packages in a particular place. + +# %% [markdown] +# Python knows where to look for possible library installations in a list of places, called the `$PYTHONPATH` (`%PYTHONPATH%` in Windows). +# It will try each of these places in turn, until it finds a matching library name. + +# %% +import sys +sys.path + +# %% [markdown] +# You can add (`append`) more paths to this list, and so allow libraries to be load from there. Thought this is not a recommended practice, let's do it once to understand how the import works. +# +# 1. Create a new directory (_e.g._, `myexemplar`), +# 1. create a file inside that directory (`exemplar.py`), +# 1. write a function inside such file (`exemplar_works`), +# 1. open python, import `sys` and add the path of `myexemplar` to `sys.path`, +# 1. import your new file, and +# 1. run the function. + +# %% [markdown] +# ### Libraries not in PyPI + +# %% [markdown] +# Sometimes you'll need to download the source code +# directly. This won't automatically follow the dependency tree, but for simple standalone libraries, is sometimes necessary. + +# %% [markdown] +# To install these on windows, download and unzip the library into a folder of your choice, e.g. `my_python_libs`. +# +# On windows, a reasonable choice +# is the folder you end up in when you open the Anaconda terminal. You can get a graphical view on this folder by typing: `explorer .` + +# %% [markdown] +# Make a new folder for your download and unzip the library there. + +# %% [markdown] +# Now, you need to move so you're inside your download in the terminal: + +# %% [markdown] +# * `cd my_python_libs` +# * `cd ` (e.g. `cd JSAnimation-master`) + +# %% [markdown] +# Now, manually install the library in your PythonPath: + +# %% [markdown] +# * `pip install --user .` + +# %% [markdown] +# This is all pretty awkward, but it is worth practising this stuff, as most of the power of using programming for +# research resides in all the libraries that are out there. + +# %% [markdown] +# ### Python virtual environments + +# %% [markdown] +# Sometimes you need to have different versions of a package installed, or you would like to install a set of libraries that you don't want to affect the rest of the installation in your system. In such cases you can create environments that are isolated from the rest. +# +# There are multiple solutions to this, only for [python](https://docs.python.org/3.6/library/venv.html) or for [anaconda](https://conda.io/docs/user-guide/tasks/manage-environments.html). +# Find more information on [how to create and use the virtual enviroments](https://realpython.com/python-virtual-environments-a-primer/). diff --git a/ch04packaging/01Libraries.html b/ch04packaging/01Libraries.html new file mode 100644 index 000000000..a827fbb8c --- /dev/null +++ b/ch04packaging/01Libraries.html @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Libraries + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Libraries

+
+
+
+
+
+
+

Libraries are awesome

+
+
+
+
+
+
+

The strength of a language lies as much in the set of libraries available, as it does +in the language itself.

+

A great set of libraries allows for a very powerful programming style:

+
    +
  • Write minimal code yourself
  • +
  • Choose the right libraries
  • +
  • Plug them together
  • +
  • Create impressive results
  • +
+

Not only is this efficient with your programming time, it's also more efficient with computer +time.

+

The chances are any algorithm you might want to use has already been programmed better by someone else.

+
+
+
+
+
+
+

Drawbacks of libraries.

+
+
+
+
+
+
+
    +
  • Sometimes, libraries are not looked after by their creator: code that is not maintained rots:

    +
      +
    • It no longer works with later versions of upstream libraries.
    • +
    • It doesn't work on newer platforms or systems.
    • +
    • Features that are needed now, because the field has moved on, are not added
    • +
    +
  • +
  • Sometimes, libraries are hard to get working:

    +
      +
    • For libraries in pure python, this is almost never a problem
    • +
    • But many libraries involve compiled components: these can be hard to install.
    • +
    +
  • +
+
+
+
+
+
+
+

Contribute, don't duplicate

+
+
+
+
+
+
+
    +
  • You have a duty to the ecosystem of scholarly software:
      +
    • If there's a tool or algorithm you need, find a project which provides it.
    • +
    • If there are features missing, or problems with it, fix them, don't create your own library.
    • +
    +
  • +
+
+
+
+
+
+
+

How to choose a library

+
+
+
+
+
+
+
    +
  • Is the code on an open version control tool like GitHub?
      +
    • When was the last commit?
    • +
    • How often are there commits?
    • +
    +
  • +
  • Can you find the lead contributor on the internet?
  • +
  • Do they respond when approached: +
  • +
  • Are there contributors other than the lead contributor?
  • +
  • Is there discussion of the library on Stack Exchange?
  • +
  • Is it on standard package repositories? (PyPI, apt/yum/brew)
  • +
  • Are there any tests?
  • +
  • Download it. Can you build it? Do the tests pass?
  • +
  • Is there an open test dashboard? (Travis/Jenkins/CDash)
  • +
  • What dependencies does the library itself have? Do they pass this list?
  • +
  • Are different versions of the library clearly labeled with version numbers?
  • +
  • Is there a changelog?
  • +
+
+
+
+
+
+
+

Sensible Version Numbering

+
+
+
+
+
+
+

The best approach to version numbers clearly distinguishes kinds of change:

+

Given a version number MAJOR.MINOR.PATCH, e.g. 2.11.14 increment the:

+
    +
  • MAJOR version when you make incompatible API changes,
  • +
  • MINOR version when you add functionality in a backwards-compatible manner, and
  • +
  • PATCH version when you make backwards-compatible bug fixes.
  • +
+

This is called Semantic Versioning.

+
+
+
+
+
+
+

The Python Standard Library

+
+
+
+
+
+
+

Python comes with a powerful standard library.

+

Learning python is as much about learning this library as learning the language itself.

+

You've already seen a few packages in this library: math, pdb, datetime.

+
+
+
+
+
+
+

The Python Package Index

+
+
+
+
+
+
+

Python's real power, however, comes with the Python Package Index: PyPI. +This is a huge array of libraries, with all kinds of capabilities, all easily installable from the +command line or through your Python distribution.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/01Libraries.ipynb b/ch04packaging/01Libraries.ipynb new file mode 100644 index 000000000..8c8a7b415 --- /dev/null +++ b/ch04packaging/01Libraries.ipynb @@ -0,0 +1,196 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7b61aaa4", + "metadata": {}, + "source": [ + "## Libraries" + ] + }, + { + "cell_type": "markdown", + "id": "8101d8a6", + "metadata": {}, + "source": [ + "### Libraries are awesome" + ] + }, + { + "cell_type": "markdown", + "id": "d5f20240", + "metadata": {}, + "source": [ + "\n", + "The strength of a language lies as much in the set of libraries available, as it does\n", + "in the language itself.\n", + "\n", + "A great set of libraries allows for a very powerful programming style:\n", + "\n", + "* Write minimal code yourself\n", + "* Choose the right libraries\n", + "* Plug them together\n", + "* Create impressive results\n", + "\n", + "Not only is this efficient with your programming time, it's also more efficient with computer\n", + "time.\n", + "\n", + "The chances are any algorithm you might want to use has already been programmed better by someone else." + ] + }, + { + "cell_type": "markdown", + "id": "6f1b90f0", + "metadata": {}, + "source": [ + "### Drawbacks of libraries." + ] + }, + { + "cell_type": "markdown", + "id": "3517e7c1", + "metadata": {}, + "source": [ + "\n", + "* Sometimes, libraries are not looked after by their creator: code that is not maintained *rots*:\n", + " * It no longer works with later versions of *upstream* libraries.\n", + " * It doesn't work on newer platforms or systems.\n", + " * Features that are needed now, because the field has moved on, are not added\n", + "\n", + "* Sometimes, libraries are hard to get working:\n", + " * For libraries in pure python, this is almost never a problem\n", + " * But many libraries involve *compiled components*: these can be hard to install.\n" + ] + }, + { + "cell_type": "markdown", + "id": "69574fe8", + "metadata": {}, + "source": [ + "### Contribute, don't duplicate" + ] + }, + { + "cell_type": "markdown", + "id": "32c2052b", + "metadata": {}, + "source": [ + "\n", + "* You have a duty to the ecosystem of scholarly software:\n", + " * If there's a tool or algorithm you need, find a project which provides it.\n", + " * If there are features missing, or problems with it, fix them, [don't create your own](http://xkcd.com/927/) library.\n" + ] + }, + { + "cell_type": "markdown", + "id": "56209fdd", + "metadata": {}, + "source": [ + "### How to choose a library" + ] + }, + { + "cell_type": "markdown", + "id": "eeb2bbb6", + "metadata": {}, + "source": [ + "\n", + "* Is the code on an open version control tool like GitHub?\n", + " * When was the last commit?\n", + " * How often are there commits?\n", + "* Can you find the lead contributor on the internet?\n", + "* Do they respond when approached:\n", + " * emails to developer list\n", + " * personal emails\n", + " * tweets\n", + " * [irc](https://freenode.net)/[gitter](https://gitter.im/)/[slack](https://slack.com/)/[[matrix]](https://element.io/)\n", + " * issues raised on GitHub\n", + "* Are there contributors other than the lead contributor?\n", + "* Is there discussion of the library on Stack Exchange?\n", + "* Is it on standard package repositories? (PyPI, apt/yum/brew)\n", + "* Are there any tests?\n", + "* Download it. Can you build it? Do the tests pass?\n", + "* Is there an open test dashboard? (Travis/Jenkins/CDash)\n", + "* What dependencies does the library itself have? Do they pass this list?\n", + "* Are different versions of the library clearly labeled with version numbers?\n", + "* Is there a changelog?\n" + ] + }, + { + "cell_type": "markdown", + "id": "d5d737d0", + "metadata": {}, + "source": [ + "### Sensible Version Numbering" + ] + }, + { + "cell_type": "markdown", + "id": "a1f2f3f5", + "metadata": {}, + "source": [ + "\n", + "The best approach to version numbers clearly distinguishes kinds of change:\n", + "\n", + "Given a version number MAJOR.MINOR.PATCH, e.g. 2.11.14 increment the:\n", + "\n", + "* MAJOR version when you make incompatible API changes,\n", + "* MINOR version when you add functionality in a backwards-compatible manner, and\n", + "* PATCH version when you make backwards-compatible bug fixes.\n", + "\n", + "This is called [Semantic Versioning](http://semver.org).\n" + ] + }, + { + "cell_type": "markdown", + "id": "4aea2a7a", + "metadata": {}, + "source": [ + "### The Python Standard Library" + ] + }, + { + "cell_type": "markdown", + "id": "d3710aaa", + "metadata": {}, + "source": [ + "\n", + "Python comes with a powerful [standard library](https://docs.python.org/3/library/).\n", + "\n", + "Learning python is as much about learning this library as learning the language itself.\n", + "\n", + "You've already seen a few packages in this library: `math`, `pdb`, `datetime`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "4b425980", + "metadata": {}, + "source": [ + "### The Python Package Index" + ] + }, + { + "cell_type": "markdown", + "id": "754e2a2b", + "metadata": {}, + "source": [ + "\n", + "Python's real power, however, comes with the Python Package Index: [PyPI](https://pypi.org/).\n", + "This is a huge array of libraries, with all kinds of capabilities, all easily installable from the \n", + "command line or through your Python distribution.\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Libraries" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/01Libraries.ipynb.py b/ch04packaging/01Libraries.ipynb.py new file mode 100644 index 000000000..1aff6e8f6 --- /dev/null +++ b/ch04packaging/01Libraries.ipynb.py @@ -0,0 +1,124 @@ +# --- +# jupyter: +# jekyll: +# display_name: Libraries +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Libraries + +# %% [markdown] +# ### Libraries are awesome + +# %% [markdown] +# +# The strength of a language lies as much in the set of libraries available, as it does +# in the language itself. +# +# A great set of libraries allows for a very powerful programming style: +# +# * Write minimal code yourself +# * Choose the right libraries +# * Plug them together +# * Create impressive results +# +# Not only is this efficient with your programming time, it's also more efficient with computer +# time. +# +# The chances are any algorithm you might want to use has already been programmed better by someone else. + +# %% [markdown] +# ### Drawbacks of libraries. + +# %% [markdown] +# +# * Sometimes, libraries are not looked after by their creator: code that is not maintained *rots*: +# * It no longer works with later versions of *upstream* libraries. +# * It doesn't work on newer platforms or systems. +# * Features that are needed now, because the field has moved on, are not added +# +# * Sometimes, libraries are hard to get working: +# * For libraries in pure python, this is almost never a problem +# * But many libraries involve *compiled components*: these can be hard to install. +# + +# %% [markdown] +# ### Contribute, don't duplicate + +# %% [markdown] +# +# * You have a duty to the ecosystem of scholarly software: +# * If there's a tool or algorithm you need, find a project which provides it. +# * If there are features missing, or problems with it, fix them, [don't create your own](http://xkcd.com/927/) library. +# + +# %% [markdown] +# ### How to choose a library + +# %% [markdown] +# +# * Is the code on an open version control tool like GitHub? +# * When was the last commit? +# * How often are there commits? +# * Can you find the lead contributor on the internet? +# * Do they respond when approached: +# * emails to developer list +# * personal emails +# * tweets +# * [irc](https://freenode.net)/[gitter](https://gitter.im/)/[slack](https://slack.com/)/[[matrix]](https://element.io/) +# * issues raised on GitHub +# * Are there contributors other than the lead contributor? +# * Is there discussion of the library on Stack Exchange? +# * Is it on standard package repositories? (PyPI, apt/yum/brew) +# * Are there any tests? +# * Download it. Can you build it? Do the tests pass? +# * Is there an open test dashboard? (Travis/Jenkins/CDash) +# * What dependencies does the library itself have? Do they pass this list? +# * Are different versions of the library clearly labeled with version numbers? +# * Is there a changelog? +# + +# %% [markdown] +# ### Sensible Version Numbering + +# %% [markdown] +# +# The best approach to version numbers clearly distinguishes kinds of change: +# +# Given a version number MAJOR.MINOR.PATCH, e.g. 2.11.14 increment the: +# +# * MAJOR version when you make incompatible API changes, +# * MINOR version when you add functionality in a backwards-compatible manner, and +# * PATCH version when you make backwards-compatible bug fixes. +# +# This is called [Semantic Versioning](http://semver.org). +# + +# %% [markdown] +# ### The Python Standard Library + +# %% [markdown] +# +# Python comes with a powerful [standard library](https://docs.python.org/3/library/). +# +# Learning python is as much about learning this library as learning the language itself. +# +# You've already seen a few packages in this library: `math`, `pdb`, `datetime`. +# + +# %% [markdown] +# ### The Python Package Index + +# %% [markdown] +# +# Python's real power, however, comes with the Python Package Index: [PyPI](https://pypi.org/). +# This is a huge array of libraries, with all kinds of capabilities, all easily installable from the +# command line or through your Python distribution. +# diff --git a/ch04packaging/025TextFiles.html b/ch04packaging/025TextFiles.html new file mode 100644 index 000000000..064495b51 --- /dev/null +++ b/ch04packaging/025TextFiles.html @@ -0,0 +1,842 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Writing Libraries + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Python not in the Notebook

+
+
+
+
+
+
+

We will often want to save our Python classes, for use in multiple Notebooks. +We can do this by writing text files with a .py extension, and then importing them.

+
+
+
+
+
+
+

Writing Python in Text Files

+
+
+
+
+
+
+

You can use a text editor like VS Code or Spyder. If you create your own Python files ending in .py, then you can import them with import just like external libraries.

+
+
+
+
+
+
+

You can also maintain your library code in a Notebook, and use %%writefile to create your library, though this is not encouraged!

+
+
+
+
+
+
+

Libraries are usually structured with multiple files, one for each class.

+
+
+
+
+
+
+

We will be turning the code we have written for the maze into a library, so that other code can reuse it.

+
+
+
+
+
+
+

We group our modules into packages, by putting them together into a folder. You can do this with explorer, or using a shell, or even with Python:

+
+
+
+
+
+
In [1]:
+
+
+
import os
+if 'mazetool' not in os.listdir(os.getcwd()):
+    os.mkdir('mazetool')
+
+
+
+
+
+
+
+
In [2]:
+
+
+
%%writefile mazetool/maze.py
+
+from .room import Room
+from .person import Person
+
+class Maze(object):
+    def __init__(self, name):
+        self.name = name
+        self.rooms = []
+        self.occupants = []
+        
+    def add_room(self, name, capacity):
+        result = Room(name, capacity)
+        self.rooms.append(result)
+        return result
+        
+    def add_exit(self, name, source, target, reverse= None):
+        source.add_exit(name, target)
+        if reverse:
+            target.add_exit(reverse, source)
+            
+    def add_occupant(self, name, room):
+        self.occupants.append(Person(name, room))
+        room.occupancy += 1
+    
+    def wander(self):
+        "Move all the people in a random direction"
+        for occupant in self.occupants:
+            occupant.wander()
+                
+    def describe(self):
+        for occupant in self.occupants:
+            occupant.describe()
+            
+    def step(self):
+        house.describe()
+        print()
+        house.wander()
+        print()
+        
+    def simulate(self, steps):
+        for _ in range(steps):
+            self.step()
+
+
+
+
+
+
+
+
+
+
Writing mazetool/maze.py
+
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%writefile mazetool/room.py
+from .exit import Exit
+
+
+class Room(object):
+    def __init__(self, name, capacity):
+        self.name = name
+        self.capacity = capacity
+        self.occupancy = 0
+        self.exits = []
+        
+    def has_space(self):
+        return self.occupancy < self.capacity
+    
+    def available_exits(self):
+        return [exit for exit in self.exits if exit.valid() ]
+            
+    def random_valid_exit(self):
+        import random
+        if not self.available_exits():
+            return None
+        return random.choice(self.available_exits())
+    
+    def add_exit(self, name, target):
+        self.exits.append(Exit(name, target))
+    
+
+
+
+
+
+
+
+
+
+
Writing mazetool/room.py
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%writefile mazetool/person.py
+
+class Person(object):
+    def __init__(self, name, room = None):
+        self.name=name
+        self.room=room
+    
+    def use(self, exit):
+        self.room.occupancy -= 1
+        destination=exit.target
+        destination.occupancy +=1
+        self.room=destination
+        print(self.name, "goes", exit.name, "to the", destination.name)
+    
+    def wander(self):
+        exit = self.room.random_valid_exit()
+        if exit:
+            self.use(exit)
+            
+    def describe(self):
+        print(self.name, "is in the", self.room.name)
+
+
+
+
+
+
+
+
+
+
Writing mazetool/person.py
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%writefile mazetool/exit.py
+
+class Exit(object):
+    def __init__(self, name, target):
+        self.name = name
+        self.target = target
+    
+    def valid(self):
+        return self.target.has_space()
+
+
+
+
+
+
+
+
+
+
Writing mazetool/exit.py
+
+
+
+
+
+
+
+
+
+

In order to tell Python that our "mazetool" folder is a Python package, +we have to make a special file called __init__.py. If you import things in there, they are imported as part of the package:

+
+
+
+
+
+
In [6]:
+
+
+
%%writefile mazetool/__init__.py
+from .maze import Maze # Python 3 relative import
+
+
+
+
+
+
+
+
+
+
Writing mazetool/__init__.py
+
+
+
+
+
+
+
+
+
+

In this case we are making it easier to import Maze as we are making it available one level above.

+
+
+
+
+
+
+

Loading Our Package

+
+
+
+
+
+
+

We just wrote the files, there is no "Maze" class in this notebook yet:

+
+
+
+
+
+
In [7]:
+
+
+
myhouse = Maze('My New House')
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+NameError                                 Traceback (most recent call last)
+Cell In[7], line 1
+----> 1 myhouse = Maze('My New House')
+
+NameError: name 'Maze' is not defined
+
+
+
+
+
+
+
+
+

But now, we can import Maze, (and the other files will get imported via the chained Import statements, starting from the __init__.py file.

+
+
+
+
+
+
In [8]:
+
+
+
import mazetool
+
+
+
+
+
+
+
+
+

Let's see how we can access the files we created:

+
+
+
+
+
+
In [9]:
+
+
+
mazetool.exit.Exit
+
+
+
+
+
+
+
+
Out[9]:
+
+
mazetool.exit.Exit
+
+
+
+
+
+
+
+
In [10]:
+
+
+
from mazetool import Maze
+
+
+
+
+
+
+
+
In [11]:
+
+
+
house = Maze('My New House')
+living = house.add_room('livingroom', 2)
+
+
+
+
+
+
+
+
+

Note the files we have created are on the disk in the folder we made:

+
+
+
+
+
+
In [12]:
+
+
+
import os
+
+
+
+
+
+
+
+
In [13]:
+
+
+
os.listdir(os.path.join(os.getcwd(), 'mazetool') )
+
+
+
+
+
+
+
+
Out[13]:
+
+
['exit.py', '__init__.py', 'person.py', 'room.py', 'maze.py', '__pycache__']
+
+
+
+
+
+
+
+
+

You may get also .pyc files. Those are "Compiled" temporary python files that the system generates to speed things up. They'll be regenerated +on the fly when your .py files change. They may appear inside the __pycache__ directory.

+
+
+
+
+
+
+

The Python Path

+
+
+
+
+
+
+

We want to import these from notebooks elsewhere on our computer: +it would be a bad idea to keep all our Python work in one folder.

+
+
+
+
+
+
+

The best way to do this is to learn how to make our code +into a proper module that we can install. We'll see more on that in a few lectures' time (notebook).

+
+
+
+
+
+
+

Alternatively, we can add a folder to the "PYTHONPATH", where python searches for modules:

+
+
+
+
+
+
In [14]:
+
+
+
import sys
+print('\n'.join(sys.path[-3:]))
+
+
+
+
+
+
+
+
+
+
/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/lib-dynload
+
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
from pathlib import Path
+sys.path.append(os.path.join(Path.home(), 'devel', 'libraries', 'python'))
+
+
+
+
+
+
+
+
In [16]:
+
+
+
print(sys.path[-1])
+
+
+
+
+
+
+
+
+
+
/home/runner/devel/libraries/python
+
+
+
+
+
+
+
+
+
+

I've thus added a folder to the list of places searched. If you want to do this permanently, you should set the PYTHONPATH Environment Variable, +which you can learn about in a shell course, or can read about online for your operating system.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/025TextFiles.ipynb b/ch04packaging/025TextFiles.ipynb new file mode 100644 index 000000000..e6f403e7a --- /dev/null +++ b/ch04packaging/025TextFiles.ipynb @@ -0,0 +1,452 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dd0e8f93", + "metadata": {}, + "source": [ + "## Python not in the Notebook" + ] + }, + { + "cell_type": "markdown", + "id": "7c5784af", + "metadata": {}, + "source": [ + "We will often want to save our Python classes, for use in multiple Notebooks.\n", + "We can do this by writing text files with a .py extension, and then `importing` them." + ] + }, + { + "cell_type": "markdown", + "id": "97d7816e", + "metadata": {}, + "source": [ + "### Writing Python in Text Files" + ] + }, + { + "cell_type": "markdown", + "id": "798fa4c6", + "metadata": {}, + "source": [ + "You can use a text editor like [VS Code](https://code.visualstudio.com/) or [Spyder](https://www.spyder-ide.org/). If you create your own Python files ending in `.py`, then you can import them with `import` just like external libraries." + ] + }, + { + "cell_type": "markdown", + "id": "75be5f36", + "metadata": {}, + "source": [ + "You can also maintain your library code in a Notebook, and use `%%writefile` to create your library, though this is not encouraged!" + ] + }, + { + "cell_type": "markdown", + "id": "ea06a4f0", + "metadata": {}, + "source": [ + "Libraries are usually structured with multiple files, one for each class." + ] + }, + { + "cell_type": "markdown", + "id": "f1abe2a0", + "metadata": {}, + "source": [ + "We will be turning the code we have written for the maze into a library, so that other code can reuse it." + ] + }, + { + "cell_type": "markdown", + "id": "13a9fa69", + "metadata": {}, + "source": [ + "We group our modules into packages, by putting them together into a folder. You can do this with explorer, or using a shell, or even with Python:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5e02449", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "if 'mazetool' not in os.listdir(os.getcwd()):\n", + " os.mkdir('mazetool')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf15cbeb", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mazetool/maze.py\n", + "\n", + "from .room import Room\n", + "from .person import Person\n", + "\n", + "class Maze(object):\n", + " def __init__(self, name):\n", + " self.name = name\n", + " self.rooms = []\n", + " self.occupants = []\n", + " \n", + " def add_room(self, name, capacity):\n", + " result = Room(name, capacity)\n", + " self.rooms.append(result)\n", + " return result\n", + " \n", + " def add_exit(self, name, source, target, reverse= None):\n", + " source.add_exit(name, target)\n", + " if reverse:\n", + " target.add_exit(reverse, source)\n", + " \n", + " def add_occupant(self, name, room):\n", + " self.occupants.append(Person(name, room))\n", + " room.occupancy += 1\n", + " \n", + " def wander(self):\n", + " \"Move all the people in a random direction\"\n", + " for occupant in self.occupants:\n", + " occupant.wander()\n", + " \n", + " def describe(self):\n", + " for occupant in self.occupants:\n", + " occupant.describe()\n", + " \n", + " def step(self):\n", + " house.describe()\n", + " print()\n", + " house.wander()\n", + " print()\n", + " \n", + " def simulate(self, steps):\n", + " for _ in range(steps):\n", + " self.step()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7282e17", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mazetool/room.py\n", + "from .exit import Exit\n", + "\n", + "\n", + "class Room(object):\n", + " def __init__(self, name, capacity):\n", + " self.name = name\n", + " self.capacity = capacity\n", + " self.occupancy = 0\n", + " self.exits = []\n", + " \n", + " def has_space(self):\n", + " return self.occupancy < self.capacity\n", + " \n", + " def available_exits(self):\n", + " return [exit for exit in self.exits if exit.valid() ]\n", + " \n", + " def random_valid_exit(self):\n", + " import random\n", + " if not self.available_exits():\n", + " return None\n", + " return random.choice(self.available_exits())\n", + " \n", + " def add_exit(self, name, target):\n", + " self.exits.append(Exit(name, target))\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9163c6d5", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mazetool/person.py\n", + "\n", + "class Person(object):\n", + " def __init__(self, name, room = None):\n", + " self.name=name\n", + " self.room=room\n", + " \n", + " def use(self, exit):\n", + " self.room.occupancy -= 1\n", + " destination=exit.target\n", + " destination.occupancy +=1\n", + " self.room=destination\n", + " print(self.name, \"goes\", exit.name, \"to the\", destination.name)\n", + " \n", + " def wander(self):\n", + " exit = self.room.random_valid_exit()\n", + " if exit:\n", + " self.use(exit)\n", + " \n", + " def describe(self):\n", + " print(self.name, \"is in the\", self.room.name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bb71fb7", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mazetool/exit.py\n", + "\n", + "class Exit(object):\n", + " def __init__(self, name, target):\n", + " self.name = name\n", + " self.target = target\n", + " \n", + " def valid(self):\n", + " return self.target.has_space()" + ] + }, + { + "cell_type": "markdown", + "id": "1cc9635c", + "metadata": {}, + "source": [ + "In order to tell Python that our \"mazetool\" folder is a Python package, \n", + "we have to make a special file called `__init__.py`. If you import things in there, they are imported as part of the package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9213f2c0", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mazetool/__init__.py\n", + "from .maze import Maze # Python 3 relative import" + ] + }, + { + "cell_type": "markdown", + "id": "e5539a5d", + "metadata": {}, + "source": [ + "In this case we are making it easier to import `Maze` as we are making it available one level above." + ] + }, + { + "cell_type": "markdown", + "id": "c4cf0593", + "metadata": {}, + "source": [ + "### Loading Our Package" + ] + }, + { + "cell_type": "markdown", + "id": "d0a99aa6", + "metadata": {}, + "source": [ + "We just wrote the files, there is no \"Maze\" class in this notebook yet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60d519be", + "metadata": {}, + "outputs": [], + "source": [ + "myhouse = Maze('My New House')" + ] + }, + { + "cell_type": "markdown", + "id": "5365b365", + "metadata": {}, + "source": [ + "But now, we can import Maze, (and the other files will get imported via the chained Import statements, starting from the `__init__.py` file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64a8699d", + "metadata": {}, + "outputs": [], + "source": [ + "import mazetool" + ] + }, + { + "cell_type": "markdown", + "id": "97ee1924", + "metadata": {}, + "source": [ + "Let's see how we can access the files we created:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f883c26a", + "metadata": {}, + "outputs": [], + "source": [ + "mazetool.exit.Exit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b73b68da", + "metadata": {}, + "outputs": [], + "source": [ + "from mazetool import Maze" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b228a516", + "metadata": {}, + "outputs": [], + "source": [ + "house = Maze('My New House')\n", + "living = house.add_room('livingroom', 2)" + ] + }, + { + "cell_type": "markdown", + "id": "f268aef2", + "metadata": {}, + "source": [ + "Note the files we have created are on the disk in the folder we made:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be6ee4f1", + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a72fa5f", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "os.listdir(os.path.join(os.getcwd(), 'mazetool') )" + ] + }, + { + "cell_type": "markdown", + "id": "77cae70b", + "metadata": {}, + "source": [ + "You may get also `.pyc` files. Those are \"Compiled\" temporary python files that the system generates to speed things up. They'll be regenerated\n", + "on the fly when your `.py` files change. They may appear inside the `__pycache__` directory." + ] + }, + { + "cell_type": "markdown", + "id": "5dc1e949", + "metadata": {}, + "source": [ + "### The Python Path" + ] + }, + { + "cell_type": "markdown", + "id": "eddf6eb9", + "metadata": {}, + "source": [ + "We want to `import` these from notebooks elsewhere on our computer:\n", + "it would be a bad idea to keep all our Python work in one folder." + ] + }, + { + "cell_type": "markdown", + "id": "26ad80e4", + "metadata": {}, + "source": [ + "The best way to do this is to learn how to make our code\n", + "into a proper module that we can install. We'll see more on that in a [few lectures' time](./03Packaging.html) ([notebook](./03Packaging.ipynb))." + ] + }, + { + "cell_type": "markdown", + "id": "51908e50", + "metadata": {}, + "source": [ + "Alternatively, we can add a folder to the \"`PYTHONPATH`\", where python searches for modules:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be8f1b0e", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "print('\\n'.join(sys.path[-3:]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b56532a1", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "sys.path.append(os.path.join(Path.home(), 'devel', 'libraries', 'python'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6766683f", + "metadata": {}, + "outputs": [], + "source": [ + "print(sys.path[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "f9968e34", + "metadata": {}, + "source": [ + "I've thus added a folder to the list of places searched. If you want to do this permanently, you should set the `PYTHONPATH` Environment Variable,\n", + "which you can learn about in a shell course, or can read about online for your operating system." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Writing Libraries" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/025TextFiles.ipynb.py b/ch04packaging/025TextFiles.ipynb.py new file mode 100644 index 000000000..472fcac02 --- /dev/null +++ b/ch04packaging/025TextFiles.ipynb.py @@ -0,0 +1,235 @@ +# --- +# jupyter: +# jekyll: +# display_name: Writing Libraries +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Python not in the Notebook + +# %% [markdown] +# We will often want to save our Python classes, for use in multiple Notebooks. +# We can do this by writing text files with a .py extension, and then `importing` them. + +# %% [markdown] +# ### Writing Python in Text Files + +# %% [markdown] +# You can use a text editor like [VS Code](https://code.visualstudio.com/) or [Spyder](https://www.spyder-ide.org/). If you create your own Python files ending in `.py`, then you can import them with `import` just like external libraries. + +# %% [markdown] +# You can also maintain your library code in a Notebook, and use `%%writefile` to create your library, though this is not encouraged! + +# %% [markdown] +# Libraries are usually structured with multiple files, one for each class. + +# %% [markdown] +# We will be turning the code we have written for the maze into a library, so that other code can reuse it. + +# %% [markdown] +# We group our modules into packages, by putting them together into a folder. You can do this with explorer, or using a shell, or even with Python: + +# %% +import os +if 'mazetool' not in os.listdir(os.getcwd()): + os.mkdir('mazetool') + +# %% +# %%writefile mazetool/maze.py + +from .room import Room +from .person import Person + +class Maze(object): + def __init__(self, name): + self.name = name + self.rooms = [] + self.occupants = [] + + def add_room(self, name, capacity): + result = Room(name, capacity) + self.rooms.append(result) + return result + + def add_exit(self, name, source, target, reverse= None): + source.add_exit(name, target) + if reverse: + target.add_exit(reverse, source) + + def add_occupant(self, name, room): + self.occupants.append(Person(name, room)) + room.occupancy += 1 + + def wander(self): + "Move all the people in a random direction" + for occupant in self.occupants: + occupant.wander() + + def describe(self): + for occupant in self.occupants: + occupant.describe() + + def step(self): + house.describe() + print() + house.wander() + print() + + def simulate(self, steps): + for _ in range(steps): + self.step() + + +# %% +# %%writefile mazetool/room.py +from .exit import Exit + + +class Room(object): + def __init__(self, name, capacity): + self.name = name + self.capacity = capacity + self.occupancy = 0 + self.exits = [] + + def has_space(self): + return self.occupancy < self.capacity + + def available_exits(self): + return [exit for exit in self.exits if exit.valid() ] + + def random_valid_exit(self): + import random + if not self.available_exits(): + return None + return random.choice(self.available_exits()) + + def add_exit(self, name, target): + self.exits.append(Exit(name, target)) + + + +# %% +# %%writefile mazetool/person.py + +class Person(object): + def __init__(self, name, room = None): + self.name=name + self.room=room + + def use(self, exit): + self.room.occupancy -= 1 + destination=exit.target + destination.occupancy +=1 + self.room=destination + print(self.name, "goes", exit.name, "to the", destination.name) + + def wander(self): + exit = self.room.random_valid_exit() + if exit: + self.use(exit) + + def describe(self): + print(self.name, "is in the", self.room.name) + + +# %% +# %%writefile mazetool/exit.py + +class Exit(object): + def __init__(self, name, target): + self.name = name + self.target = target + + def valid(self): + return self.target.has_space() + + +# %% [markdown] +# In order to tell Python that our "mazetool" folder is a Python package, +# we have to make a special file called `__init__.py`. If you import things in there, they are imported as part of the package: + +# %% +# %%writefile mazetool/__init__.py +from .maze import Maze # Python 3 relative import + +# %% [markdown] +# In this case we are making it easier to import `Maze` as we are making it available one level above. + +# %% [markdown] +# ### Loading Our Package + +# %% [markdown] +# We just wrote the files, there is no "Maze" class in this notebook yet: + +# %% +myhouse = Maze('My New House') + +# %% [markdown] +# But now, we can import Maze, (and the other files will get imported via the chained Import statements, starting from the `__init__.py` file. + +# %% +import mazetool + +# %% [markdown] +# Let's see how we can access the files we created: + +# %% +mazetool.exit.Exit + +# %% +from mazetool import Maze + +# %% +house = Maze('My New House') +living = house.add_room('livingroom', 2) + +# %% [markdown] +# Note the files we have created are on the disk in the folder we made: + +# %% +import os + +# %% +os.listdir(os.path.join(os.getcwd(), 'mazetool') ) + + +# %% [markdown] +# You may get also `.pyc` files. Those are "Compiled" temporary python files that the system generates to speed things up. They'll be regenerated +# on the fly when your `.py` files change. They may appear inside the `__pycache__` directory. + +# %% [markdown] +# ### The Python Path + +# %% [markdown] +# We want to `import` these from notebooks elsewhere on our computer: +# it would be a bad idea to keep all our Python work in one folder. + +# %% [markdown] +# The best way to do this is to learn how to make our code +# into a proper module that we can install. We'll see more on that in a [few lectures' time](./03Packaging.html) ([notebook](./03Packaging.ipynb)). + +# %% [markdown] +# Alternatively, we can add a folder to the "`PYTHONPATH`", where python searches for modules: + +# %% +import sys +print('\n'.join(sys.path[-3:])) + +# %% +from pathlib import Path +sys.path.append(os.path.join(Path.home(), 'devel', 'libraries', 'python')) + +# %% +print(sys.path[-1]) + +# %% [markdown] +# I've thus added a folder to the list of places searched. If you want to do this permanently, you should set the `PYTHONPATH` Environment Variable, +# which you can learn about in a shell course, or can read about online for your operating system. diff --git a/ch04packaging/02Argparse.html b/ch04packaging/02Argparse.html new file mode 100644 index 000000000..02a32fa56 --- /dev/null +++ b/ch04packaging/02Argparse.html @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Argparse + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Argparse

+
+
+
+
+
+
+

This is the standard library for building programs with a command-line interface. Here we show a short introduction to it, but we recommend to read the official tutorial.

+
+
+
+
+
+
+

Let's start by creating a simple greet function that accepts some parameters.

+
+
+
+
+
+
In [1]:
+
+
+
def greet(personal, family, title="", polite=False):
+    greeting = "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+
+
+
+
+
+
+
+

Now we have a function that greets whoever we want.

+
+
+
+
+
+
In [2]:
+
+
+
greet("John", "Cleese", polite=True)
+
+
+
+
+
+
+
+
Out[2]:
+
+
'How do you do, John Cleese.'
+
+
+
+
+
+
+
+
+

If we want to create a command line interface for this function, we need to save it on its own file. To add the capability to accept inputs from the command line we are going to use argparse.

+

Rememer, what's under the if __name__ == "__main__": block is what's get executed when you run the file!

+
+
+
+
+
+
In [3]:
+
+
+
%%writefile greeter.py
+#!/usr/bin/env python
+from argparse import ArgumentParser
+
+def greet(personal, family, title="", polite=False):
+    greeting = "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+if __name__ == "__main__":
+    parser = ArgumentParser(description="Generate appropriate greetings")
+    parser.add_argument('--title', '-t')
+    parser.add_argument('--polite','-p', action="store_true")
+    parser.add_argument('personal')
+    parser.add_argument('family')
+    arguments= parser.parse_args()
+    
+    message = greet(arguments.personal, arguments.family,
+                    arguments.title, arguments.polite)
+    print(message)
+
+
+
+
+
+
+
+
+
+
Writing greeter.py
+
+
+
+
+
+
+
+
+
+

Note that we've created arguments for each argument greet accepts and kept what's optional in the function (the keyword arguments) to be also optional for its command-line interface (can you spot how?).

+
+
+
+
+
+
+

We need to tell the computer that this file can be executed to be able to run this script without calling it with python everytime. The computer will know what to use by reading the shebang) #!. If you are using MacOS or Linux, you do the following to create an executable:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+chmod u+x greeter.py
+
+
+
+
+
+
+
+
+

and then running it as:

+
+
+
+
+
+
In [5]:
+
+
+
%%bash --no-raise-error
+./greeter.py
+
+
+
+
+
+
+
+
+
+
usage: greeter.py [-h] [--title TITLE] [--polite] personal family
+greeter.py: error: the following arguments are required: personal, family
+
+
+
+
+
+
+
+
+
+

if you are using Windows' commands or powershell terminal (instead of git-bash), then the shebang is ignored and you will have to call python explicitily. Additionally, for the notebooks cells, you need to change bash by cmd.

+
%%cmd
+python greeter.py John Cleese
+
+
+
+
+
+
+
In [6]:
+
+
+
%%bash
+./greeter.py John Cleese
+
+
+
+
+
+
+
+
+
+
Hey, John Cleese.
+
+
+
+
+
+
+
+
+
+

We can then use the optional arguments as:

+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+./greeter.py --polite John Cleese
+
+
+
+
+
+
+
+
+
+
How do you do, John Cleese.
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%bash
+./greeter.py John Cleese --title Dr
+
+
+
+
+
+
+
+
+
+
Hey, Dr John Cleese.
+
+
+
+
+
+
+
+
+
+

Yes, he is!

+
+
+
+
+
+
+

From the error we got above when we called greeter.py without arguments, you may have noticed that in the usage message there's also a -h optional argument. We know it's optional because it's shown within square brackes, like for [--polite]. This new argument, as the usage message seen above, is generated automatically by argparse and you can use it to see the help.

+
+
+
+
+
+
In [9]:
+
+
+
%%bash
+./greeter.py --help
+
+
+
+
+
+
+
+
+
+
usage: greeter.py [-h] [--title TITLE] [--polite] personal family
+
+Generate appropriate greetings
+
+positional arguments:
+  personal
+  family
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --title TITLE, -t TITLE
+  --polite, -p
+
+
+
+
+
+
+
+
+
+

Before we move into the next section, let's clean up our if __name__ == "__main__": block by creating a function that keeps the argparse magic. We will call that function process.

+
+
+
+
+
+
In [10]:
+
+
+
%%writefile greeter.py
+#!/usr/bin/env python
+from argparse import ArgumentParser
+
+def greet(personal, family, title="", polite=False):
+    greeting = "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+def process():
+    parser = ArgumentParser(description="Generate appropriate greetings")
+
+    parser.add_argument('--title', '-t')
+    parser.add_argument('--polite', '-p', action="store_true")
+    parser.add_argument('personal')
+    parser.add_argument('family')
+
+    arguments = parser.parse_args()
+
+    print(greet(arguments.personal, arguments.family,
+                arguments.title, arguments.polite))    
+
+if __name__ == "__main__":
+    process()
+
+
+
+
+
+
+
+
+
+
Overwriting greeter.py
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/02Argparse.ipynb b/ch04packaging/02Argparse.ipynb new file mode 100644 index 000000000..6053eda9f --- /dev/null +++ b/ch04packaging/02Argparse.ipynb @@ -0,0 +1,286 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "df83069f", + "metadata": {}, + "source": [ + "## Argparse" + ] + }, + { + "cell_type": "markdown", + "id": "03b923e1", + "metadata": {}, + "source": [ + "This is the standard library for building programs with a command-line interface. Here we show a short introduction to it, but we recommend to read the [official tutorial](https://docs.python.org/3/howto/argparse.html)." + ] + }, + { + "cell_type": "markdown", + "id": "05e92321", + "metadata": {}, + "source": [ + "Let's start by creating a simple `greet` function that accepts some parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d035a8b5", + "metadata": {}, + "outputs": [], + "source": [ + "def greet(personal, family, title=\"\", polite=False):\n", + " greeting = \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting" + ] + }, + { + "cell_type": "markdown", + "id": "9ddb327a", + "metadata": {}, + "source": [ + "Now we have a function that greets whoever we want." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12172054", + "metadata": {}, + "outputs": [], + "source": [ + "greet(\"John\", \"Cleese\", polite=True)" + ] + }, + { + "cell_type": "markdown", + "id": "0a701865", + "metadata": {}, + "source": [ + "If we want to create a command line interface for this function, we need to save it on its own file. To add the capability to accept inputs from the command line we are going to use `argparse`.\n", + "\n", + "Rememer, what's under the `if __name__ == \"__main__\":` block is what's get executed when you run the file!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00fe5b19", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greeter.py\n", + "#!/usr/bin/env python\n", + "from argparse import ArgumentParser\n", + "\n", + "def greet(personal, family, title=\"\", polite=False):\n", + " greeting = \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting\n", + "\n", + "if __name__ == \"__main__\":\n", + " parser = ArgumentParser(description=\"Generate appropriate greetings\")\n", + " parser.add_argument('--title', '-t')\n", + " parser.add_argument('--polite','-p', action=\"store_true\")\n", + " parser.add_argument('personal')\n", + " parser.add_argument('family')\n", + " arguments= parser.parse_args()\n", + " \n", + " message = greet(arguments.personal, arguments.family,\n", + " arguments.title, arguments.polite)\n", + " print(message)" + ] + }, + { + "cell_type": "markdown", + "id": "07734e8c", + "metadata": {}, + "source": [ + "Note that we've created arguments for each argument `greet` accepts and kept what's optional in the function (the keyword arguments) to be also optional for its command-line interface (can you spot how?)." + ] + }, + { + "cell_type": "markdown", + "id": "5df1760b", + "metadata": {}, + "source": [ + "We need to tell the computer that this file can be executed to be able to run this script without calling it with `python` everytime. The computer will know what to use by reading the [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) `#!`. If you are using MacOS or Linux, you do the following to create an executable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63cb6939", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "chmod u+x greeter.py" + ] + }, + { + "cell_type": "markdown", + "id": "c9983096", + "metadata": {}, + "source": [ + "and then running it as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11d2bd4f", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "./greeter.py" + ] + }, + { + "cell_type": "markdown", + "id": "1540caa1", + "metadata": {}, + "source": [ + "if you are using Windows' commands or powershell terminal (instead of git-bash), then the shebang is ignored and you will have to call `python` explicitily. Additionally, for the notebooks cells, you need to change `bash` by `cmd`.\n", + "\n", + "```\n", + "%%cmd\n", + "python greeter.py John Cleese\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cec9aa3", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "./greeter.py John Cleese" + ] + }, + { + "cell_type": "markdown", + "id": "6e21c899", + "metadata": {}, + "source": [ + "We can then use the optional arguments as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f31f9657", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "./greeter.py --polite John Cleese" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e3a1a06", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "./greeter.py John Cleese --title Dr" + ] + }, + { + "cell_type": "markdown", + "id": "422c0ace", + "metadata": {}, + "source": [ + "Yes, [he is](https://en.wikipedia.org/wiki/John_Cleese#Honours_and_tributes)!" + ] + }, + { + "cell_type": "markdown", + "id": "87caafcf", + "metadata": {}, + "source": [ + "From the error we got above when we called `greeter.py` without arguments, you may have noticed that in the usage message there's also a `-h` optional argument. We know it's optional because it's shown within square brackes, like for `[--polite]`. This new argument, as the usage message seen above, is generated automatically by argparse and you can use it to see the help." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e39495f", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "./greeter.py --help" + ] + }, + { + "cell_type": "markdown", + "id": "d19c8730", + "metadata": {}, + "source": [ + "Before we move into the next section, let's clean up our `if __name__ == \"__main__\":` block by creating a function that keeps the `argparse` magic. We will call that function `process`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e130196b", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greeter.py\n", + "#!/usr/bin/env python\n", + "from argparse import ArgumentParser\n", + "\n", + "def greet(personal, family, title=\"\", polite=False):\n", + " greeting = \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting\n", + "\n", + "def process():\n", + " parser = ArgumentParser(description=\"Generate appropriate greetings\")\n", + "\n", + " parser.add_argument('--title', '-t')\n", + " parser.add_argument('--polite', '-p', action=\"store_true\")\n", + " parser.add_argument('personal')\n", + " parser.add_argument('family')\n", + "\n", + " arguments = parser.parse_args()\n", + "\n", + " print(greet(arguments.personal, arguments.family,\n", + " arguments.title, arguments.polite)) \n", + "\n", + "if __name__ == \"__main__\":\n", + " process()" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Argparse" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/02Argparse.ipynb.py b/ch04packaging/02Argparse.ipynb.py new file mode 100644 index 000000000..be1061a61 --- /dev/null +++ b/ch04packaging/02Argparse.ipynb.py @@ -0,0 +1,143 @@ +# --- +# jupyter: +# jekyll: +# display_name: Argparse +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Argparse + +# %% [markdown] +# This is the standard library for building programs with a command-line interface. Here we show a short introduction to it, but we recommend to read the [official tutorial](https://docs.python.org/3/howto/argparse.html). + +# %% [markdown] +# Let's start by creating a simple `greet` function that accepts some parameters. + +# %% +def greet(personal, family, title="", polite=False): + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + + +# %% [markdown] +# Now we have a function that greets whoever we want. + +# %% +greet("John", "Cleese", polite=True) + +# %% [markdown] +# If we want to create a command line interface for this function, we need to save it on its own file. To add the capability to accept inputs from the command line we are going to use `argparse`. +# +# Rememer, what's under the `if __name__ == "__main__":` block is what's get executed when you run the file! + +# %% +# %%writefile greeter.py +# #!/usr/bin/env python +from argparse import ArgumentParser + +def greet(personal, family, title="", polite=False): + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + +if __name__ == "__main__": + parser = ArgumentParser(description="Generate appropriate greetings") + parser.add_argument('--title', '-t') + parser.add_argument('--polite','-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + arguments= parser.parse_args() + + message = greet(arguments.personal, arguments.family, + arguments.title, arguments.polite) + print(message) + +# %% [markdown] +# Note that we've created arguments for each argument `greet` accepts and kept what's optional in the function (the keyword arguments) to be also optional for its command-line interface (can you spot how?). + +# %% [markdown] +# We need to tell the computer that this file can be executed to be able to run this script without calling it with `python` everytime. The computer will know what to use by reading the [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) `#!`. If you are using MacOS or Linux, you do the following to create an executable: + +# %% language="bash" +# chmod u+x greeter.py + +# %% [markdown] +# and then running it as: + +# %% magic_args="--no-raise-error" language="bash" +# ./greeter.py + +# %% [markdown] +# if you are using Windows' commands or powershell terminal (instead of git-bash), then the shebang is ignored and you will have to call `python` explicitily. Additionally, for the notebooks cells, you need to change `bash` by `cmd`. +# +# ``` +# %%cmd +# python greeter.py John Cleese +# ``` + +# %% language="bash" +# ./greeter.py John Cleese + +# %% [markdown] +# We can then use the optional arguments as: + +# %% language="bash" +# ./greeter.py --polite John Cleese + +# %% language="bash" +# ./greeter.py John Cleese --title Dr + +# %% [markdown] +# Yes, [he is](https://en.wikipedia.org/wiki/John_Cleese#Honours_and_tributes)! + +# %% [markdown] +# From the error we got above when we called `greeter.py` without arguments, you may have noticed that in the usage message there's also a `-h` optional argument. We know it's optional because it's shown within square brackes, like for `[--polite]`. This new argument, as the usage message seen above, is generated automatically by argparse and you can use it to see the help. + +# %% language="bash" +# ./greeter.py --help + +# %% [markdown] +# Before we move into the next section, let's clean up our `if __name__ == "__main__":` block by creating a function that keeps the `argparse` magic. We will call that function `process`. + +# %% +# %%writefile greeter.py +# #!/usr/bin/env python +from argparse import ArgumentParser + +def greet(personal, family, title="", polite=False): + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + print(greet(arguments.personal, arguments.family, + arguments.title, arguments.polite)) + +if __name__ == "__main__": + process() diff --git a/ch04packaging/03Packaging.html b/ch04packaging/03Packaging.html new file mode 100644 index 000000000..f97495ceb --- /dev/null +++ b/ch04packaging/03Packaging.html @@ -0,0 +1,2247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Packaging + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Packaging

+
+
+
+
+
+
+

Once we've made a working program, we'd like to be able to share it with others.

+

A good cross-platform build tool is the most important thing: you can always +have collaborators build from source.

+
+
+
+
+
+
+

Distribution tools

+
+
+
+
+
+
+

Distribution tools allow one to obtain a working copy of someone else's package.

+
    +
  • Language-specific tools:

    +
  • +
  • python: PyPI,

    +
  • +
  • ruby: Ruby Gems,

    +
  • +
  • perl: CPAN,

    +
  • +
  • R: CRAN

    +
  • +
  • Platform specific packagers e.g.:

    +
  • +
  • brew for MacOS,

    +
  • +
  • apt/dnf/pacman for Linux or

    +
  • +
  • choco for Windows.

    +
  • +
+
+
+
+
+
+
+

Laying out a project

+
+
+
+
+
+
+

When planning to package a project for distribution, defining a suitable +project layout is essential. A typical layout might look like this:

+
repository_name
+|-- module_name
+|   |-- __init__.py
+|   |-- python_file.py
+|   |-- another_python_file.py
+|   `-- test
+|       |-- fixtures
+|       |   `-- fixture_file.yaml
+|       |-- __init__.py
+|       `-- test_python_file.py
+|-- LICENSE.md
+|-- CITATION.md
+|-- README.md
+`-- setup.py
+
+
+
+
+
+
+
+

To achieve this for our greetings.py file from the previous session, we can use the commands shown below. We can start by making our directory structure. You can create many nested directories at once using the -p switch on mkdir.

+
+
+
+
+
+
In [1]:
+
+
+
%%bash
+mkdir -p greetings_repo/greetings/test/fixtures
+
+
+
+
+
+
+
+
+

For this notebook, since we are going to be modifying the files bit by bit, we are going to use the autoreload ipython magic so that we don't need to restart the kernel.

+
+
+
+
+
+
In [2]:
+
+
+
%load_ext autoreload
+%autoreload 2
+
+
+
+
+
+
+
+
+

Using pyproject.toml

+
+
+
+
+
+
+

Since June 2020, python's recommendation for creating a package is to specify package information in a pyproject.toml file. +Older projects used a setup.py or setup.cfg file instead - and in fact the new pyproject.toml file in many ways mirrors this old format. +A lot of projects and packages have not yet switched over from setup.py to pyproject.toml, so don't be surprised to see a mixture of the two formats when you're looking at other people's packages.

+
+
+
+
+
+
+

For our greetings package, right now we are adding only the name of the package and its version number. +This information is included in the project section of our pyproject.toml file.

+

But we also need to tell users how to build the package from these specifications. +This information is specified in the build-system section of our toml file. +In this case, we'll be using setuptools to build our package, so we list it in the requires field. +We also need setuptools_scm[toml] so that setuptools can understand the settings we give it in our .toml file, and wheel to make the package distribution.

+

Finally, we can set specific options for setuptools using additional sections in pyproject.toml: in this case, we will tell setuptools that it needs to find and include all of the files in our greetings folder.

+
+
+
+
+
+
In [3]:
+
+
+
%%writefile greetings_repo/pyproject.toml
+
+[project]
+name = "Greetings"
+version = "0.1.0"
+
+[build-system]
+requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"]
+
+[tool.setuptools.packages.find]
+include = ["greetings*"]
+
+[tool.setuptools_scm]
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/pyproject.toml
+
+
+
+
+
+
+
+
+
+

We can now install this "package" with pip:

+
+
+
+
+
+
In [4]:
+
+
+
%%bash
+cd greetings_repo
+pip install .
+
+
+
+
+
+
+
+
+
+
Processing /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+  Installing build dependencies: started
+  Installing build dependencies: finished with status 'done'
+  Getting requirements to build wheel: started
+  Getting requirements to build wheel: finished with status 'error'
+
+
+
+
+
+
+
  error: subprocess-exited-with-error
+  
+  × Getting requirements to build wheel did not run successfully.
+   exit code: 1
+  ╰─> [37 lines of output]
+      Traceback (most recent call last):
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
+          main()
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
+          json_out['return_val'] = hook(**hook_input['kwargs'])
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 118, in get_requires_for_build_wheel
+          return hook(config_settings)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in get_requires_for_build_wheel
+          return self._get_build_requires(config_settings, requirements=['wheel'])
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 295, in _get_build_requires
+          self.run_setup()
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 480, in run_setup
+          super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 311, in run_setup
+          exec(code, locals())
+        File "<string>", line 1, in <module>
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 103, in setup
+          return distutils.core.setup(**attrs)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 147, in setup
+          _setup_distribution = dist = klass(attrs)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 303, in __init__
+          _Distribution.__init__(self, dist_attrs)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
+          self.finalize_options()
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 654, in finalize_options
+          ep(self)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
+          _assign_version(dist, config)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
+          _version_missing(config)
+        File "/tmp/pip-build-env-3ak9tyee/overlay/lib/python3.8/site-packages/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
+          raise LookupError(
+      LookupError: setuptools-scm was unable to detect version for /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo.
+      
+      Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.
+      
+      For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj
+      [end of output]
+  
+  note: This error originates from a subprocess, and is likely not a problem with pip.
+error: subprocess-exited-with-error
+
+× Getting requirements to build wheel did not run successfully.
+ exit code: 1
+╰─> See above for output.
+
+note: This error originates from a subprocess, and is likely not a problem with pip.
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[4], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'cd greetings_repo\npip install .\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'cd greetings_repo\npip install .\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

And the package will be then available to use everywhere on the system. But so far this package doesn't contain anything and there's nothing we can run! We need to add some files first.

+
+
+
+
+
+
+

To create a regular package, we needed to have __init__.py files on each subdirectory that we want to be able to import. This is, since version 3.3 and the introduction of Implicit Namespaces Packages, not needed anymore. However, if you want to use relative imports and pytest, then you still need to have these files.

+

The __init__.py files can contain any initialisation code you want to run when the (sub)module is imported.

+

For this example, and because we are using relative imports in the tests, we are creating the needed __init__.py files.

+
+
+
+
+
+
In [5]:
+
+
+
%%bash
+
+touch greetings_repo/greetings/__init__.py
+
+
+
+
+
+
+
+
+

And we can copy the greet function from the previous section in the greeter.py file.

+
+
+
+
+
+
In [6]:
+
+
+
%%writefile greetings_repo/greetings/greeter.py
+
+def greet(personal, family, title="", polite=False):
+    greeting = "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/greetings/greeter.py
+
+
+
+
+
+
+
+
+
+

For the changes to take effect, we need to reinstall the library:

+
+
+
+
+
+
In [7]:
+
+
+
%%bash
+cd greetings_repo
+pip install .
+
+
+
+
+
+
+
+
+
+
Processing /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+  Installing build dependencies: started
+  Installing build dependencies: finished with status 'done'
+  Getting requirements to build wheel: started
+  Getting requirements to build wheel: finished with status 'error'
+
+
+
+
+
+
+
  error: subprocess-exited-with-error
+  
+  × Getting requirements to build wheel did not run successfully.
+   exit code: 1
+  ╰─> [37 lines of output]
+      Traceback (most recent call last):
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
+          main()
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
+          json_out['return_val'] = hook(**hook_input['kwargs'])
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 118, in get_requires_for_build_wheel
+          return hook(config_settings)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in get_requires_for_build_wheel
+          return self._get_build_requires(config_settings, requirements=['wheel'])
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 295, in _get_build_requires
+          self.run_setup()
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 480, in run_setup
+          super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 311, in run_setup
+          exec(code, locals())
+        File "<string>", line 1, in <module>
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 103, in setup
+          return distutils.core.setup(**attrs)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 147, in setup
+          _setup_distribution = dist = klass(attrs)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 303, in __init__
+          _Distribution.__init__(self, dist_attrs)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
+          self.finalize_options()
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 654, in finalize_options
+          ep(self)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
+          _assign_version(dist, config)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
+          _version_missing(config)
+        File "/tmp/pip-build-env-035jj1xf/overlay/lib/python3.8/site-packages/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
+          raise LookupError(
+      LookupError: setuptools-scm was unable to detect version for /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo.
+      
+      Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.
+      
+      For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj
+      [end of output]
+  
+  note: This error originates from a subprocess, and is likely not a problem with pip.
+error: subprocess-exited-with-error
+
+× Getting requirements to build wheel did not run successfully.
+ exit code: 1
+╰─> See above for output.
+
+note: This error originates from a subprocess, and is likely not a problem with pip.
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[7], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'cd greetings_repo\npip install .\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'cd greetings_repo\npip install .\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

And now we are able to import it and use it:

+
+
+
+
+
+
In [8]:
+
+
+
from greetings.greeter import greet
+greet("Terry","Gilliam")
+
+
+
+
+
+
+
+
Out[8]:
+
+
'Hey, Terry Gilliam.'
+
+
+
+
+
+
+
+
+

Convert the script to a module

+
+
+
+
+
+
+

Of course, there's more to do when taking code from a quick script and turning it into a proper module:

+
+
+
+
+
+
+

We need to add docstrings to our functions, so people can know how to use them.

+
+
+
+
+
+
In [9]:
+
+
+
%%writefile greetings_repo/greetings/greeter.py
+
+def greet(personal, family, title="", polite=False):
+    """ Generate a greeting string for a person.
+    Parameters
+    ----------
+    personal: str
+        A given name, such as Will or Jean-Luc
+    family: str
+        A family name, such as Riker or Picard
+    title: str
+        An optional title, such as Captain or Reverend
+    polite: bool
+        True for a formal greeting, False for informal.
+    Returns
+    -------
+    string
+        An appropriate greeting
+    Examples
+    --------
+    >>> from greetings.greeter import greet
+    >>> greet("Terry", "Jones")
+    'Hey, Terry Jones.
+    """
+
+    greeting = "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/greetings/greeter.py
+
+
+
+
+
+
+
+
+
+

We can see the documentation using help.

+
+
+
+
+
+
In [10]:
+
+
+
help(greet)
+
+
+
+
+
+
+
+
+
+
Help on function greet in module greetings.greeter:
+
+greet(personal, family, title='', polite=False)
+    Generate a greeting string for a person.
+    
+    Parameters
+    ----------
+    personal: str
+        A given name, such as Will or Jean-Luc
+    family: str
+        A family name, such as Riker or Picard
+    title: str
+        An optional title, such as Captain or Reverend
+    polite: bool
+        True for a formal greeting, False for informal.
+    
+    Returns
+    -------
+    string
+        An appropriate greeting
+    
+    Examples
+    --------
+    >>> from greetings.greeter import greet
+    >>> greet("Terry", "Jones")
+    'Hey, Terry Jones.
+
+
+
+
+
+
+
+
+
+
+

The documentation string explains how to use the function; don't worry about this for now, we'll consider +this on the next section (notebook version).

+
+
+
+
+
+
+

Write an executable script

+
+
+
+
+
+
+
+
+
+
+
+
+

We can create an executable script, command.py that uses our greeting functionality and the process function we created in the previous section.

+

Note how we are importing greet using relative imports, where .greeter means to look for a greeter module within the same directory.

+
+
+
+
+
+
In [11]:
+
+
+
%%writefile greetings_repo/greetings/command.py
+
+from argparse import ArgumentParser
+
+from .greeter import greet
+
+
+def process():
+    parser = ArgumentParser(description="Generate appropriate greetings")
+
+    parser.add_argument('--title', '-t')
+    parser.add_argument('--polite', '-p', action="store_true")
+    parser.add_argument('personal')
+    parser.add_argument('family')
+
+    arguments = parser.parse_args()
+
+    print(greet(arguments.personal, arguments.family,
+                arguments.title, arguments.polite))
+
+
+if __name__ == "__main__":
+    process()
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/greetings/command.py
+
+
+
+
+
+
+
+
+
+

Specify entry point

+
+
+
+
+
+
+

This allows us to create a command to execute part of our library. In this case when we execute greet on the terminal, we will be calling the process function under greetings/command.py.

+

We can encode this into our package information by specifying the project.scripts field in our pyproject.toml file.

+
+
+
+
+
+
In [12]:
+
+
+
%%writefile greetings_repo/pyproject.toml
+
+[project]
+name = "Greetings"
+version = "0.1.0"
+
+[project.scripts]
+greet = "greetings.command:process"
+
+[build-system]
+requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"]
+
+[tool.setuptools.packages.find]
+include = ["greetings*"]
+
+[tool.setuptools_scm]
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/pyproject.toml
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
%%bash
+cd greetings_repo
+pip install -e .
+
+
+
+
+
+
+
+
+
+
Obtaining file:///home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+  Installing build dependencies: started
+  Installing build dependencies: finished with status 'done'
+  Checking if build backend supports build_editable: started
+  Checking if build backend supports build_editable: finished with status 'done'
+  Getting requirements to build editable: started
+  Getting requirements to build editable: finished with status 'error'
+
+
+
+
+
+
+
  error: subprocess-exited-with-error
+  
+  × Getting requirements to build editable did not run successfully.
+   exit code: 1
+  ╰─> [39 lines of output]
+      Traceback (most recent call last):
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
+          main()
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
+          json_out['return_val'] = hook(**hook_input['kwargs'])
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 132, in get_requires_for_build_editable
+          return hook(config_settings)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 441, in get_requires_for_build_editable
+          return self.get_requires_for_build_wheel(config_settings)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in get_requires_for_build_wheel
+          return self._get_build_requires(config_settings, requirements=['wheel'])
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 295, in _get_build_requires
+          self.run_setup()
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 480, in run_setup
+          super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 311, in run_setup
+          exec(code, locals())
+        File "<string>", line 1, in <module>
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 103, in setup
+          return distutils.core.setup(**attrs)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 147, in setup
+          _setup_distribution = dist = klass(attrs)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 303, in __init__
+          _Distribution.__init__(self, dist_attrs)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
+          self.finalize_options()
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 654, in finalize_options
+          ep(self)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
+          _assign_version(dist, config)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
+          _version_missing(config)
+        File "/tmp/pip-build-env-mhbjy45t/overlay/lib/python3.8/site-packages/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
+          raise LookupError(
+      LookupError: setuptools-scm was unable to detect version for /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo.
+      
+      Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.
+      
+      For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj
+      [end of output]
+  
+  note: This error originates from a subprocess, and is likely not a problem with pip.
+error: subprocess-exited-with-error
+
+× Getting requirements to build editable did not run successfully.
+ exit code: 1
+╰─> See above for output.
+
+note: This error originates from a subprocess, and is likely not a problem with pip.
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[13], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'cd greetings_repo\npip install -e .\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'cd greetings_repo\npip install -e .\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
+

And the scripts are now available as command line commands, so the following commands can now be run:

+
+
+
+
+
+
In [14]:
+
+
+
%%bash
+greet --help
+
+
+
+
+
+
+
+
+
+
usage: greet [-h] [--title TITLE] [--polite] personal family
+
+Generate appropriate greetings
+
+positional arguments:
+  personal
+  family
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --title TITLE, -t TITLE
+  --polite, -p
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
%%bash
+greet Terry Gilliam
+greet --polite Terry Gilliam
+greet Terry Gilliam --title Cartoonist
+
+
+
+
+
+
+
+
+
+
Hey, Terry Gilliam.
+How do you do, Terry Gilliam.
+Hey, Cartoonist Terry Gilliam.
+
+
+
+
+
+
+
+
+
+

Specify dependencies

+
+
+
+
+
+
+

Let's give some life to our output using ascii art

+
+
+
+
+
+
In [16]:
+
+
+
%%writefile greetings_repo/greetings/command.py
+
+from argparse import ArgumentParser
+
+from art import art
+
+from .greeter import greet
+
+
+def process():
+    parser = ArgumentParser(description="Generate appropriate greetings")
+
+    parser.add_argument('--title', '-t')
+    parser.add_argument('--polite', '-p', action="store_true")
+    parser.add_argument('personal')
+    parser.add_argument('family')
+
+    arguments = parser.parse_args()
+
+    message = greet(arguments.personal, arguments.family,
+                    arguments.title, arguments.polite)
+    print(art("cute face"), message)
+
+if __name__ == "__main__":
+    process()
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/greetings/command.py
+
+
+
+
+
+
+
+
+
+

We use the dependencies field of the project section in our pyproject.toml file to specify the packages we depend on. +We provide the names of the packages as a list of strings.

+
+
+
+
+
+
In [17]:
+
+
+
%%writefile greetings_repo/pyproject.toml
+
+[project]
+name = "Greetings"
+version = "0.1.0"
+dependencies = [
+    "art",
+]
+
+[project.scripts]
+greet = "greetings.command:process"
+
+[build-system]
+requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"]
+
+[tool.setuptools.packages.find]
+include = ["greetings*"]
+
+[tool.setuptools_scm]
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/pyproject.toml
+
+
+
+
+
+
+
+
+
+

When installing the package now, pip will also install the dependencies automatically.

+
+
+
+
+
+
In [18]:
+
+
+
%%bash
+cd greetings_repo
+pip install -e .
+
+
+
+
+
+
+
+
+
+
Obtaining file:///home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+  Installing build dependencies: started
+  Installing build dependencies: finished with status 'done'
+  Checking if build backend supports build_editable: started
+  Checking if build backend supports build_editable: finished with status 'done'
+  Getting requirements to build editable: started
+  Getting requirements to build editable: finished with status 'error'
+
+
+
+
+
+
+
  error: subprocess-exited-with-error
+  
+  × Getting requirements to build editable did not run successfully.
+   exit code: 1
+  ╰─> [39 lines of output]
+      Traceback (most recent call last):
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
+          main()
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
+          json_out['return_val'] = hook(**hook_input['kwargs'])
+        File "/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 132, in get_requires_for_build_editable
+          return hook(config_settings)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 441, in get_requires_for_build_editable
+          return self.get_requires_for_build_wheel(config_settings)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in get_requires_for_build_wheel
+          return self._get_build_requires(config_settings, requirements=['wheel'])
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 295, in _get_build_requires
+          self.run_setup()
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 480, in run_setup
+          super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 311, in run_setup
+          exec(code, locals())
+        File "<string>", line 1, in <module>
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 103, in setup
+          return distutils.core.setup(**attrs)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 147, in setup
+          _setup_distribution = dist = klass(attrs)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 303, in __init__
+          _Distribution.__init__(self, dist_attrs)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
+          self.finalize_options()
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 654, in finalize_options
+          ep(self)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
+          _assign_version(dist, config)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
+          _version_missing(config)
+        File "/tmp/pip-build-env-zi_eyiu_/overlay/lib/python3.8/site-packages/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
+          raise LookupError(
+      LookupError: setuptools-scm was unable to detect version for /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo.
+      
+      Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.
+      
+      For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj
+      [end of output]
+  
+  note: This error originates from a subprocess, and is likely not a problem with pip.
+error: subprocess-exited-with-error
+
+× Getting requirements to build editable did not run successfully.
+ exit code: 1
+╰─> See above for output.
+
+note: This error originates from a subprocess, and is likely not a problem with pip.
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+CalledProcessError                        Traceback (most recent call last)
+Cell In[18], line 1
+----> 1 get_ipython().run_cell_magic('bash', '', 'cd greetings_repo\npip install -e .\n')
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py:2478, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
+   2476 with self.builtin_trap:
+   2477     args = (magic_arg_s, cell)
+-> 2478     result = fn(*args, **kwargs)
+   2480 # The code below prevents the output from being displayed
+   2481 # when using magics with decodator @output_can_be_silenced
+   2482 # when the last Python token in the expression is a ';'.
+   2483 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:153, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
+    151 else:
+    152     line = script
+--> 153 return self.shebang(line, cell)
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/IPython/core/magics/script.py:305, in ScriptMagics.shebang(self, line, cell)
+    300 if args.raise_error and p.returncode != 0:
+    301     # If we get here and p.returncode is still None, we must have
+    302     # killed it but not yet seen its return code. We don't wait for it,
+    303     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
+    304     rc = p.returncode or -9
+--> 305     raise CalledProcessError(rc, cell)
+
+CalledProcessError: Command 'b'cd greetings_repo\npip install -e .\n'' returned non-zero exit status 1.
+
+
+
+
+
+
+
+
In [19]:
+
+
+
%%bash
+greet Terry Gilliam
+
+
+
+
+
+
+
+
+
+
Hey, Terry Gilliam.
+
+
+
+
+
+
+
+
+
+

Installing from GitHub

+
+
+
+
+
+
+

We could now submit "greeter" to PyPI for approval, so everyone could pip install it.

+

However, when using git, we don't even need to do that: we can install directly from any git URL:

+
+
+
+
+
+
+
pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter
+
+
+
+
+
+
+
+
$ greet Lancelot the-Brave --title Sir
+Hey, Sir Lancelot the-Brave.
+
+
+
+
+
+
+
+
+There are a few additional text files that are important to add to a package: a readme file, a licence file and a citation file. + + +
+
+
+
+
+
+
+
+
+
+
+
+

Write a readme file

+
+
+
+
+
+
+

The readme file might look like this:

+
+
+
+
+
+
In [20]:
+
+
+
%%writefile greetings_repo/README.md
+
+# Greetings!
+
+This is a very simple example package used as part of the UCL
+[Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course.
+
+## Installation
+
+```bash
+pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter
+```
+
+## Usage
+    
+Invoke the tool with `greet <FirstName> <Secondname>` or use it on your own library:
+
+```python
+from greeting import greeter
+
+greeter.greet(user.name, user.lastname)
+```
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/README.md
+
+
+
+
+
+
+
+
+
+

Write a license file

+
+
+
+
+
+
+

We will discus more about licensing in a later section. For now let's assume we want to release this package into the public domain:

+
+
+
+
+
+
In [21]:
+
+
+
%%writefile greetings_repo/LICENSE.md
+
+(C) University College London 2014
+
+This "greetings" example package is granted into the public domain.
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/LICENSE.md
+
+
+
+
+
+
+
+
+
+

Write a citation file

+
+
+
+
+
+
+

A citation file will inform our users how we would like to be cited when refering to our software:

+
+
+
+
+
+
In [22]:
+
+
+
%%writefile greetings_repo/CITATION.md
+
+If you wish to refer to this course, please cite the URL
+http://github-pages.ucl.ac.uk/rsd-engineeringcourse/
+
+Portions of the material are taken from [Software Carpentry](http://software-carpentry.org/)
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/CITATION.md
+
+
+
+
+
+
+
+
+
+

You may well want to formalise this using the codemeta.json standard or the citation file format - these don't have wide adoption yet, but we recommend it.

+
+
+
+
+
+
+

Define packages and executables

+
+
+
+
+
+
+

We need to create __init__ files for the source and the tests.

+
touch greetings/greetings/test/__init__.py
+touch greetings/greetings/__init__.py
+
+
+
+
+
+
+
+

Write some unit tests

+
+
+
+
+
+
+

We can now write some tests to our library.

+

Remember, that we need to create the empty __init__.py files so that pytest can follow the relative imports.

+
+
+
+
+
+
In [23]:
+
+
+
%%bash
+touch greetings_repo/greetings/test/__init__.py
+
+
+
+
+
+
+
+
+

Separating the script from the logical module made this possible.

+
+
+
+
+
+
In [24]:
+
+
+
%%writefile greetings_repo/greetings/test/test_greeter.py
+
+import os
+
+import yaml
+
+from ..greeter import greet
+
+def test_greet():
+    with open(os.path.join(os.path.dirname(__file__),
+                           'fixtures',
+                           'samples.yaml')) as fixtures_file:
+        fixtures = yaml.safe_load(fixtures_file)
+        for fixture in fixtures:
+            answer = fixture.pop('answer')
+            assert greet(**fixture) == answer
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/greetings/test/test_greeter.py
+
+
+
+
+
+
+
+
+
+

Add a fixtures file:

+
+
+
+
+
+
In [25]:
+
+
+
%%writefile greetings_repo/greetings/test/fixtures/samples.yaml
+
+- personal: Eric
+  family: Idle
+  answer: "Hey, Eric Idle."
+- personal: Graham
+  family: Chapman
+  polite: True
+  answer: "How do you do, Graahm Chapman."
+- personal: Michael
+  family: Palin
+  title: CBE
+  answer: "Hey, CBE Mike Palin."  
+
+
+
+
+
+
+
+
+
+
Writing greetings_repo/greetings/test/fixtures/samples.yaml
+
+
+
+
+
+
+
+
+
+

We can now run pytest

+
+
+
+
+
+
In [26]:
+
+
+
%%bash --no-raise-error
+
+cd greetings_repo
+pytest
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+plugins: cov-4.1.0, anyio-3.7.1
+collected 1 item
+
+greetings/test/test_greeter.py F                                         [100%]
+
+=================================== FAILURES ===================================
+__________________________________ test_greet __________________________________
+
+    def test_greet():
+        with open(os.path.join(os.path.dirname(__file__),
+                               'fixtures',
+                               'samples.yaml')) as fixtures_file:
+            fixtures = yaml.safe_load(fixtures_file)
+            for fixture in fixtures:
+                answer = fixture.pop('answer')
+>               assert greet(**fixture) == answer
+E               AssertionError: assert 'How do you d...aham Chapman.' == 'How do you d...aahm Chapman.'
+E                 - How do you do, Graahm Chapman.
+E                 ?                    -
+E                 + How do you do, Graham Chapman.
+E                 ?                   +
+
+greetings/test/test_greeter.py:15: AssertionError
+=========================== short test summary info ============================
+FAILED greetings/test/test_greeter.py::test_greet - AssertionError: assert 'How do you d...aham Chapman.' == 'How do you d...aahm Chapman.'
+  - How do you do, Graahm Chapman.
+  ?                    -
+  + How do you do, Graham Chapman.
+  ?                   +
+============================== 1 failed in 0.11s ===============================
+
+
+
+
+
+
+
+
+
+

However, this hasn't told us that also the third test is wrong too! A better aproach is to parametrize the testfile greetings_repo/greetings/test/test_greeter.py as follows:

+
+
+
+
+
+
In [27]:
+
+
+
%%writefile greetings_repo/greetings/test/test_greeter.py
+
+import os
+
+import pytest
+import yaml
+
+from ..greeter import greet
+
+def read_fixture():
+    with open(os.path.join(os.path.dirname(__file__),
+                           'fixtures',
+                           'samples.yaml')) as fixtures_file:
+        fixtures = yaml.safe_load(fixtures_file)
+    return fixtures
+
+@pytest.mark.parametrize("fixture", read_fixture())
+def test_greeter(fixture):
+    answer = fixture.pop('answer')
+    assert greet(**fixture) == answer
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/greetings/test/test_greeter.py
+
+
+
+
+
+
+
+
+
+

Now when we run pytest, we get a failure per element in our fixture and we know all that fails.

+
+
+
+
+
+
In [28]:
+
+
+
%%bash --no-raise-error
+
+cd greetings_repo
+pytest
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+plugins: cov-4.1.0, anyio-3.7.1
+collected 3 items
+
+greetings/test/test_greeter.py .FF                                       [100%]
+
+=================================== FAILURES ===================================
+____________________________ test_greeter[fixture1] ____________________________
+
+fixture = {'family': 'Chapman', 'personal': 'Graham', 'polite': True}
+
+    @pytest.mark.parametrize("fixture", read_fixture())
+    def test_greeter(fixture):
+        answer = fixture.pop('answer')
+>       assert greet(**fixture) == answer
+E       AssertionError: assert 'How do you d...aham Chapman.' == 'How do you d...aahm Chapman.'
+E         - How do you do, Graahm Chapman.
+E         ?                    -
+E         + How do you do, Graham Chapman.
+E         ?                   +
+
+greetings/test/test_greeter.py:19: AssertionError
+____________________________ test_greeter[fixture2] ____________________________
+
+fixture = {'family': 'Palin', 'personal': 'Michael', 'title': 'CBE'}
+
+    @pytest.mark.parametrize("fixture", read_fixture())
+    def test_greeter(fixture):
+        answer = fixture.pop('answer')
+>       assert greet(**fixture) == answer
+E       AssertionError: assert 'Hey, CBE Michael Palin.' == 'Hey, CBE Mike Palin.'
+E         - Hey, CBE Mike Palin.
+E         ?            ^
+E         + Hey, CBE Michael Palin.
+E         ?            ^^^ +
+
+greetings/test/test_greeter.py:19: AssertionError
+=========================== short test summary info ============================
+FAILED greetings/test/test_greeter.py::test_greeter[fixture1] - AssertionError: assert 'How do you d...aham Chapman.' == 'How do you d...aahm Chapman.'
+  - How do you do, Graahm Chapman.
+  ?                    -
+  + How do you do, Graham Chapman.
+  ?                   +
+FAILED greetings/test/test_greeter.py::test_greeter[fixture2] - AssertionError: assert 'Hey, CBE Michael Palin.' == 'Hey, CBE Mike Palin.'
+  - Hey, CBE Mike Palin.
+  ?            ^
+  + Hey, CBE Michael Palin.
+  ?            ^^^ +
+========================= 2 failed, 1 passed in 0.11s ==========================
+
+
+
+
+
+
+
+
+
+

We can also make pytest to check whether the docstrings are correct by adding the --doctest-modules flag. We run pytest --doctest-modules and obtain the following output:

+
+
+
+
+
+
In [29]:
+
+
+
%%bash --no-raise-error
+
+cd greetings_repo
+pytest --doctest-modules
+
+
+
+
+
+
+
+
+
+
============================= test session starts ==============================
+platform linux -- Python 3.8.18, pytest-7.4.3, pluggy-1.3.0
+rootdir: /home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings_repo
+plugins: cov-4.1.0, anyio-3.7.1
+collected 4 items / 1 error
+
+==================================== ERRORS ====================================
+____________________ ERROR collecting greetings/command.py _____________________
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/_pytest/runner.py:341: in from_call
+    result: Optional[TResult] = func()
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/_pytest/runner.py:372: in <lambda>
+    call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/_pytest/doctest.py:567: in collect
+    module = import_path(
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/_pytest/pathlib.py:567: in import_path
+    importlib.import_module(module_name)
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/importlib/__init__.py:127: in import_module
+    return _bootstrap._gcd_import(name[level:], package, level)
+<frozen importlib._bootstrap>:1014: in _gcd_import
+    ???
+<frozen importlib._bootstrap>:991: in _find_and_load
+    ???
+<frozen importlib._bootstrap>:975: in _find_and_load_unlocked
+    ???
+<frozen importlib._bootstrap>:671: in _load_unlocked
+    ???
+<frozen importlib._bootstrap_external>:843: in exec_module
+    ???
+<frozen importlib._bootstrap>:219: in _call_with_frames_removed
+    ???
+greetings/command.py:4: in <module>
+    from art import art
+E   ModuleNotFoundError: No module named 'art'
+=========================== short test summary info ============================
+ERROR greetings/command.py - ModuleNotFoundError: No module named 'art'
+!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
+=============================== 1 error in 0.20s ===============================
+
+
+
+
+
+
+
+
+
+

Finally, we typically don't want to include the tests when we distribute our software for our users. +We can make sure they are not included using the exclude option on when telling setuptools to find packages.

+

Additionally, we can make sure that our README and LICENSE are included in our package metadata by declaring them in the readme and license fields under the project section. +If you're using a particularly common or standard license, you can even provide the name of the license, rather than the file, and your package builder will take care of the rest!

+
+
+
+
+
+
In [30]:
+
+
+
%%writefile greetings_repo/pyproject.toml
+
+[project]
+name = "Greetings"
+version = "0.1.0"
+readme = "README.md"
+license = { file = "LICENSE.md" }
+dependencies = [
+    "art",
+    "pyyaml",
+]
+
+[project.scripts]
+greet = "greetings.command:process"
+
+[build-system]
+requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"]
+
+[tool.setuptools.packages.find]
+include = ["greetings*"]
+exclude = ["tests*"]
+
+[tool.setuptools_scm]
+
+
+
+
+
+
+
+
+
+
Overwriting greetings_repo/pyproject.toml
+
+
+
+
+
+
+
+
+
+

Developer Install

+
+
+
+
+
+
+

If you modify your source files, you would now find it appeared as if the program doesn't change.

+

That's because pip install copies the files.

+

If you want to install a package, but keep working on it, you can do:

+
+
+
+
+
+
+
pip install --editable .
+
+

or, its shorter version:

+
pip install -e .
+
+
+
+
+
+
+
+

Distributing compiled code

+
+
+
+
+
+
+

If you're working in C++ or Fortran, there is no language specific repository. +You'll need to write platform installers for as many platforms as you want to +support.

+

Typically:

+
    +
  • dpkg for apt-get on Ubuntu and Debian
  • +
  • rpm for yum/dnf on Redhat and Fedora
  • +
  • homebrew on OSX (Possibly macports as well)
  • +
  • An executable msi installer for Windows.
  • +
+
+
+
+
+
+
+

Homebrew

+
+
+
+
+
+
+

Homebrew: A ruby DSL, you host off your own webpage

+

See an installer for the cppcourse example

+

If you're on OSX, do:

+
+
+
+
+
+
+
brew tap jamespjh/homebrew-reactor
+brew install reactor
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/03Packaging.ipynb b/ch04packaging/03Packaging.ipynb new file mode 100644 index 000000000..b536f5c2c --- /dev/null +++ b/ch04packaging/03Packaging.ipynb @@ -0,0 +1,1214 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "579f83c5", + "metadata": {}, + "source": [ + "## Packaging" + ] + }, + { + "cell_type": "markdown", + "id": "ac1a9535", + "metadata": {}, + "source": [ + "\n", + "Once we've made a working program, we'd like to be able to share it with others.\n", + "\n", + "A good cross-platform build tool is the most important thing: you can always\n", + "have collaborators build from source.\n" + ] + }, + { + "cell_type": "markdown", + "id": "256e992d", + "metadata": {}, + "source": [ + "### Distribution tools" + ] + }, + { + "cell_type": "markdown", + "id": "5846e82f", + "metadata": {}, + "source": [ + "Distribution tools allow one to obtain a working copy of someone else's package.\n", + "\n", + "- Language-specific tools: \n", + " - python: PyPI,\n", + " - ruby: Ruby Gems, \n", + " - perl: CPAN,\n", + " - R: CRAN\n", + " \n", + "- Platform specific packagers e.g.:\n", + " - [`brew`](https://brew.sh/) for MacOS, \n", + " - `apt`/`dnf`/`pacman` for Linux or \n", + " - [`choco`](https://chocolatey.org/) for Windows." + ] + }, + { + "cell_type": "markdown", + "id": "92422f92", + "metadata": {}, + "source": [ + "### Laying out a project" + ] + }, + { + "cell_type": "markdown", + "id": "83186e81", + "metadata": {}, + "source": [ + "\n", + "When planning to package a project for distribution, defining a suitable\n", + "project layout is essential. A typical layout might look like this:\n", + "\n", + "```\n", + "repository_name\n", + "|-- module_name\n", + "| |-- __init__.py\n", + "| |-- python_file.py\n", + "| |-- another_python_file.py\n", + "| `-- test\n", + "| |-- fixtures\n", + "| | `-- fixture_file.yaml\n", + "| |-- __init__.py\n", + "| `-- test_python_file.py\n", + "|-- LICENSE.md\n", + "|-- CITATION.md\n", + "|-- README.md\n", + "`-- setup.py\n", + "```\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "97084f80", + "metadata": {}, + "source": [ + "To achieve this for our `greetings.py` file from the previous session, we can use the commands shown below. We can start by making our directory structure. You can create many nested directories at once using the `-p` switch on `mkdir`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccdf6dc2", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "mkdir -p greetings_repo/greetings/test/fixtures" + ] + }, + { + "cell_type": "markdown", + "id": "588371b6", + "metadata": {}, + "source": [ + "For this notebook, since we are going to be modifying the files bit by bit, we are going to use the [autoreload ipython magic](https://ipython.readthedocs.io/en/stable/config/extensions/autoreload.html) so that we don't need to restart the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31f200ab", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "id": "d8ee4cad", + "metadata": {}, + "source": [ + "### Using pyproject.toml" + ] + }, + { + "cell_type": "markdown", + "id": "2341f0cf", + "metadata": {}, + "source": [ + "Since June 2020, python's recommendation for creating a package is to specify package information in a `pyproject.toml` file.\n", + "Older projects used a `setup.py` or `setup.cfg` file instead - and in fact the new `pyproject.toml` file in many ways mirrors this old format.\n", + "A lot of projects and packages have not yet switched over from `setup.py` to `pyproject.toml`, so don't be surprised to see a mixture of the two formats when you're looking at other people's packages." + ] + }, + { + "cell_type": "markdown", + "id": "865c5279", + "metadata": {}, + "source": [ + "For our `greetings` package, right now we are adding only the name of the package and its version number.\n", + "This information is included in the `project` section of our `pyproject.toml` file.\n", + "\n", + "But we also need to tell users how to build the package from these specifications.\n", + "This information is specified in the `build-system` section of our `toml` file.\n", + "In this case, we'll be using `setuptools` to build our package, so we list it in the `requires` field.\n", + "We also need `setuptools_scm[toml]` so that `setuptools` can understand the settings we give it in our `.toml` file, and `wheel` to make the package distribution.\n", + "\n", + "Finally, we can set specific options for `setuptools` using additional sections in `pyproject.toml`: in this case, we will tell `setuptools` that it needs to find **and include** all of the files in our `greetings` folder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c7639c", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/pyproject.toml\n", + "\n", + "[project]\n", + "name = \"Greetings\"\n", + "version = \"0.1.0\"\n", + "\n", + "[build-system]\n", + "requires = [\"setuptools\", \"setuptools_scm[toml]>=6.2\", \"wheel\"]\n", + "\n", + "[tool.setuptools.packages.find]\n", + "include = [\"greetings*\"]\n", + "\n", + "[tool.setuptools_scm]" + ] + }, + { + "cell_type": "markdown", + "id": "2013773a", + "metadata": {}, + "source": [ + "We can now install this \"package\" with pip:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69736992", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd greetings_repo\n", + "pip install ." + ] + }, + { + "cell_type": "markdown", + "id": "1f9e556b", + "metadata": {}, + "source": [ + "\n", + "And the package will be then available to use everywhere on the system. But so far this package doesn't contain anything and there's nothing we can run! We need to add some files first.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5f939197", + "metadata": {}, + "source": [ + "\n", + "To create a regular package, we needed to have `__init__.py` files on each subdirectory that we want to be able to import. This is, since version 3.3 and the introduction of [Implicit Namespaces Packages](https://www.python.org/dev/peps/pep-0420/), not needed anymore. However, if you want to use relative imports and `pytest`, then you [still need to have these files](https://github.com/pytest-dev/pytest/issues/1927).\n", + "\n", + "The `__init__.py` files can contain any initialisation code you want to run when the (sub)module is imported.\n", + "\n", + "For this example, and because we are using relative imports in the tests, we are creating the needed `__init__.py` files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7201c583", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "touch greetings_repo/greetings/__init__.py" + ] + }, + { + "cell_type": "markdown", + "id": "eb911c55", + "metadata": {}, + "source": [ + "And we can copy the `greet` function from the [previous section](https://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch04packaging/02Argparse.html) in the `greeter.py` file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d2c6794", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/greeter.py\n", + "\n", + "def greet(personal, family, title=\"\", polite=False):\n", + " greeting = \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8daa4e8a", + "metadata": {}, + "source": [ + "For the changes to take effect, we need to reinstall the library: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2fff3bc", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd greetings_repo\n", + "pip install ." + ] + }, + { + "cell_type": "markdown", + "id": "9f3c8900", + "metadata": {}, + "source": [ + "And now we are able to import it and use it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ad902f8", + "metadata": {}, + "outputs": [], + "source": [ + "from greetings.greeter import greet\n", + "greet(\"Terry\",\"Gilliam\")" + ] + }, + { + "cell_type": "markdown", + "id": "98d7e39f", + "metadata": {}, + "source": [ + "### Convert the script to a module" + ] + }, + { + "cell_type": "markdown", + "id": "227e967b", + "metadata": {}, + "source": [ + "\n", + "Of course, there's more to do when taking code from a quick script and turning it into a proper module:" + ] + }, + { + "cell_type": "markdown", + "id": "5ad54c12", + "metadata": {}, + "source": [ + "We need to add docstrings to our functions, so people can know how to use them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef5a7e82", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/greeter.py\n", + "\n", + "def greet(personal, family, title=\"\", polite=False):\n", + " \"\"\" Generate a greeting string for a person.\n", + " Parameters\n", + " ----------\n", + " personal: str\n", + " A given name, such as Will or Jean-Luc\n", + " family: str\n", + " A family name, such as Riker or Picard\n", + " title: str\n", + " An optional title, such as Captain or Reverend\n", + " polite: bool\n", + " True for a formal greeting, False for informal.\n", + " Returns\n", + " -------\n", + " string\n", + " An appropriate greeting\n", + " Examples\n", + " --------\n", + " >>> from greetings.greeter import greet\n", + " >>> greet(\"Terry\", \"Jones\")\n", + " 'Hey, Terry Jones.\n", + " \"\"\"\n", + "\n", + " greeting = \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting" + ] + }, + { + "cell_type": "markdown", + "id": "18452271", + "metadata": {}, + "source": [ + "We can see the documentation using `help`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16bec3ad", + "metadata": {}, + "outputs": [], + "source": [ + "help(greet)" + ] + }, + { + "cell_type": "markdown", + "id": "7f9f84d4", + "metadata": {}, + "source": [ + "The documentation string explains how to use the function; don't worry about this for now, we'll consider\n", + "this on [the next section](./04documentation.html) ([notebook version](./04documentation.ipynb))." + ] + }, + { + "cell_type": "markdown", + "id": "23e8cb6c", + "metadata": {}, + "source": [ + "### Write an executable script" + ] + }, + { + "cell_type": "markdown", + "id": "5df137ea", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "356b2b8e", + "metadata": {}, + "source": [ + "We can create an executable script, `command.py` that uses our greeting functionality and the `process` function we created in the previous section.\n", + "\n", + "Note how we are importing `greet` using [relative imports](https://www.python.org/dev/peps/pep-0328/), where `.greeter` means to look for a `greeter` module within the same directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61c6c6de", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/command.py\n", + "\n", + "from argparse import ArgumentParser\n", + "\n", + "from .greeter import greet\n", + "\n", + "\n", + "def process():\n", + " parser = ArgumentParser(description=\"Generate appropriate greetings\")\n", + "\n", + " parser.add_argument('--title', '-t')\n", + " parser.add_argument('--polite', '-p', action=\"store_true\")\n", + " parser.add_argument('personal')\n", + " parser.add_argument('family')\n", + "\n", + " arguments = parser.parse_args()\n", + "\n", + " print(greet(arguments.personal, arguments.family,\n", + " arguments.title, arguments.polite))\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " process()" + ] + }, + { + "cell_type": "markdown", + "id": "4b40cf25", + "metadata": {}, + "source": [ + "#### Specify entry point" + ] + }, + { + "cell_type": "markdown", + "id": "2948ce02", + "metadata": {}, + "source": [ + "This allows us to create a command to execute part of our library. In this case when we execute `greet` on the terminal, we will be calling the `process` function under `greetings/command.py`.\n", + "\n", + "We can encode this into our package information by specifying the `project.scripts` field in our `pyproject.toml` file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0793b6e6", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/pyproject.toml\n", + "\n", + "[project]\n", + "name = \"Greetings\"\n", + "version = \"0.1.0\"\n", + "\n", + "[project.scripts]\n", + "greet = \"greetings.command:process\"\n", + "\n", + "[build-system]\n", + "requires = [\"setuptools\", \"setuptools_scm[toml]>=6.2\", \"wheel\"]\n", + "\n", + "[tool.setuptools.packages.find]\n", + "include = [\"greetings*\"]\n", + "\n", + "[tool.setuptools_scm]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a854031", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd greetings_repo\n", + "pip install -e ." + ] + }, + { + "cell_type": "markdown", + "id": "c1c7ad65", + "metadata": {}, + "source": [ + "\n", + "And the scripts are now available as command line commands, so the following commands can now be run:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02d9176a", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "greet --help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "270b140b", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "greet Terry Gilliam\n", + "greet --polite Terry Gilliam\n", + "greet Terry Gilliam --title Cartoonist" + ] + }, + { + "cell_type": "markdown", + "id": "92ab406c", + "metadata": {}, + "source": [ + "### Specify dependencies" + ] + }, + { + "cell_type": "markdown", + "id": "61e2c423", + "metadata": {}, + "source": [ + "Let's give some life to our output using ascii art" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "261ea608", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/command.py\n", + "\n", + "from argparse import ArgumentParser\n", + "\n", + "from art import art\n", + "\n", + "from .greeter import greet\n", + "\n", + "\n", + "def process():\n", + " parser = ArgumentParser(description=\"Generate appropriate greetings\")\n", + "\n", + " parser.add_argument('--title', '-t')\n", + " parser.add_argument('--polite', '-p', action=\"store_true\")\n", + " parser.add_argument('personal')\n", + " parser.add_argument('family')\n", + "\n", + " arguments = parser.parse_args()\n", + "\n", + " message = greet(arguments.personal, arguments.family,\n", + " arguments.title, arguments.polite)\n", + " print(art(\"cute face\"), message)\n", + "\n", + "if __name__ == \"__main__\":\n", + " process()" + ] + }, + { + "cell_type": "markdown", + "id": "9f289291", + "metadata": {}, + "source": [ + "We use the `dependencies` field of the `project` section in our `pyproject.toml` file to specify the packages we depend on.\n", + "We provide the names of the packages as a list of strings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf022ab8", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/pyproject.toml\n", + "\n", + "[project]\n", + "name = \"Greetings\"\n", + "version = \"0.1.0\"\n", + "dependencies = [\n", + " \"art\",\n", + "]\n", + "\n", + "[project.scripts]\n", + "greet = \"greetings.command:process\"\n", + "\n", + "[build-system]\n", + "requires = [\"setuptools\", \"setuptools_scm[toml]>=6.2\", \"wheel\"]\n", + "\n", + "[tool.setuptools.packages.find]\n", + "include = [\"greetings*\"]\n", + "\n", + "[tool.setuptools_scm]" + ] + }, + { + "cell_type": "markdown", + "id": "e82ff7c4", + "metadata": {}, + "source": [ + "When installing the package now, pip will also install the dependencies automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7928aada", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd greetings_repo\n", + "pip install -e ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8b6e192", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "greet Terry Gilliam" + ] + }, + { + "cell_type": "markdown", + "id": "b29ad0b9", + "metadata": {}, + "source": [ + "### Installing from GitHub" + ] + }, + { + "cell_type": "markdown", + "id": "db0f128c", + "metadata": {}, + "source": [ + "\n", + "We could now submit \"greeter\" to PyPI for approval, so everyone could `pip install` it.\n", + "\n", + "However, when using git, we don't even need to do that: we can install directly from any git URL:\n" + ] + }, + { + "cell_type": "markdown", + "id": "1e35697d", + "metadata": {}, + "source": [ + "```bash\n", + "pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "358456b6", + "metadata": {}, + "source": [ + "```bash\n", + "$ greet Lancelot the-Brave --title Sir\n", + "Hey, Sir Lancelot the-Brave.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "fe79e80a", + "metadata": {}, + "source": [ + "
\n", + "There are a few additional text files that are important to add to a package: a readme file, a licence file and a citation file." + ] + }, + { + "cell_type": "markdown", + "id": "acb9a621", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "dd39ac81", + "metadata": {}, + "source": [ + "### Write a readme file" + ] + }, + { + "cell_type": "markdown", + "id": "90a39899", + "metadata": {}, + "source": [ + "The readme file might look like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "274ddc13", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/README.md\n", + "\n", + "# Greetings!\n", + "\n", + "This is a very simple example package used as part of the UCL\n", + "[Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course.\n", + "\n", + "## Installation\n", + "\n", + "```bash\n", + "pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter\n", + "```\n", + "\n", + "## Usage\n", + " \n", + "Invoke the tool with `greet ` or use it on your own library:\n", + "\n", + "```python\n", + "from greeting import greeter\n", + "\n", + "greeter.greet(user.name, user.lastname)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "52605c7c", + "metadata": {}, + "source": [ + "### Write a license file" + ] + }, + { + "cell_type": "markdown", + "id": "8c28daba", + "metadata": {}, + "source": [ + "We will discus more about [licensing in a later section](https://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch04packaging/07Licensing.html). For now let's assume we want to release this package into the public domain:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c40fef51", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/LICENSE.md\n", + "\n", + "(C) University College London 2014\n", + "\n", + "This \"greetings\" example package is granted into the public domain." + ] + }, + { + "cell_type": "markdown", + "id": "a5407284", + "metadata": {}, + "source": [ + "### Write a citation file" + ] + }, + { + "cell_type": "markdown", + "id": "8c1309cc", + "metadata": {}, + "source": [ + "A citation file will inform our users how we would like to be cited when refering to our software:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc21bbe6", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/CITATION.md\n", + "\n", + "If you wish to refer to this course, please cite the URL\n", + "http://github-pages.ucl.ac.uk/rsd-engineeringcourse/\n", + "\n", + "Portions of the material are taken from [Software Carpentry](http://software-carpentry.org/)" + ] + }, + { + "cell_type": "markdown", + "id": "6f9a11fe", + "metadata": {}, + "source": [ + "You may well want to formalise this using the [codemeta.json](https://codemeta.github.io/) standard or the [citation file format](http://citation-file-format.github.io/) - these don't have wide adoption yet, but we recommend it." + ] + }, + { + "cell_type": "markdown", + "id": "50bfdc95", + "metadata": {}, + "source": [ + "### Define packages and executables" + ] + }, + { + "cell_type": "markdown", + "id": "4f214dd5", + "metadata": {}, + "source": [ + "We need to create `__init__` files for the source and the tests.\n", + "```bash\n", + "touch greetings/greetings/test/__init__.py\n", + "touch greetings/greetings/__init__.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3924c57d", + "metadata": {}, + "source": [ + "### Write some unit tests" + ] + }, + { + "cell_type": "markdown", + "id": "a0c2f9ef", + "metadata": {}, + "source": [ + "We can now write some tests to our library. \n", + "\n", + "Remember, that we need to create the empty `__init__.py` files so that `pytest` can follow the relative imports." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20999c08", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "touch greetings_repo/greetings/test/__init__.py" + ] + }, + { + "cell_type": "markdown", + "id": "575bef8c", + "metadata": {}, + "source": [ + "\n", + "Separating the script from the logical module made this possible.\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d506521", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/test/test_greeter.py\n", + "\n", + "import os\n", + "\n", + "import yaml\n", + "\n", + "from ..greeter import greet\n", + "\n", + "def test_greet():\n", + " with open(os.path.join(os.path.dirname(__file__),\n", + " 'fixtures',\n", + " 'samples.yaml')) as fixtures_file:\n", + " fixtures = yaml.safe_load(fixtures_file)\n", + " for fixture in fixtures:\n", + " answer = fixture.pop('answer')\n", + " assert greet(**fixture) == answer" + ] + }, + { + "cell_type": "markdown", + "id": "ae83c98c", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Add a fixtures file:\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a80cf33", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/test/fixtures/samples.yaml\n", + "\n", + "- personal: Eric\n", + " family: Idle\n", + " answer: \"Hey, Eric Idle.\"\n", + "- personal: Graham\n", + " family: Chapman\n", + " polite: True\n", + " answer: \"How do you do, Graahm Chapman.\"\n", + "- personal: Michael\n", + " family: Palin\n", + " title: CBE\n", + " answer: \"Hey, CBE Mike Palin.\" " + ] + }, + { + "cell_type": "markdown", + "id": "35e0b4f7", + "metadata": {}, + "source": [ + "We can now run `pytest`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70249a02", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "\n", + "cd greetings_repo\n", + "pytest" + ] + }, + { + "cell_type": "markdown", + "id": "66faf0d3", + "metadata": {}, + "source": [ + "However, this hasn't told us that also the third test is wrong too! A better aproach is to parametrize the testfile `greetings_repo/greetings/test/test_greeter.py` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6589dc51", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/greetings/test/test_greeter.py\n", + "\n", + "import os\n", + "\n", + "import pytest\n", + "import yaml\n", + "\n", + "from ..greeter import greet\n", + "\n", + "def read_fixture():\n", + " with open(os.path.join(os.path.dirname(__file__),\n", + " 'fixtures',\n", + " 'samples.yaml')) as fixtures_file:\n", + " fixtures = yaml.safe_load(fixtures_file)\n", + " return fixtures\n", + "\n", + "@pytest.mark.parametrize(\"fixture\", read_fixture())\n", + "def test_greeter(fixture):\n", + " answer = fixture.pop('answer')\n", + " assert greet(**fixture) == answer" + ] + }, + { + "cell_type": "markdown", + "id": "df31a70d", + "metadata": {}, + "source": [ + "Now when we run `pytest`, we get a failure per element in our fixture and we know all that fails." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2843ae61", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "\n", + "cd greetings_repo\n", + "pytest" + ] + }, + { + "cell_type": "markdown", + "id": "662c31ca", + "metadata": {}, + "source": [ + "We can also make pytest to check whether the docstrings are correct by adding the `--doctest-modules` flag. We run `pytest --doctest-modules` and obtain the following output:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c86e1502", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "\n", + "cd greetings_repo\n", + "pytest --doctest-modules" + ] + }, + { + "cell_type": "markdown", + "id": "bb64f84b", + "metadata": {}, + "source": [ + "Finally, we typically don't want to include the tests when we distribute our software for our users.\n", + "We can make sure they are not included using the `exclude` option on when telling `setuptools` to find packages.\n", + "\n", + "Additionally, we can make sure that our README and LICENSE are included in our package metadata by declaring them in the `readme` and `license` fields under the `project` section.\n", + "If you're using a particularly common or standard license, you can even provide the name of the license, rather than the file, and your package builder will take care of the rest!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3700aa48", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings_repo/pyproject.toml\n", + "\n", + "[project]\n", + "name = \"Greetings\"\n", + "version = \"0.1.0\"\n", + "readme = \"README.md\"\n", + "license = { file = \"LICENSE.md\" }\n", + "dependencies = [\n", + " \"art\",\n", + " \"pyyaml\",\n", + "]\n", + "\n", + "[project.scripts]\n", + "greet = \"greetings.command:process\"\n", + "\n", + "[build-system]\n", + "requires = [\"setuptools\", \"setuptools_scm[toml]>=6.2\", \"wheel\"]\n", + "\n", + "[tool.setuptools.packages.find]\n", + "include = [\"greetings*\"]\n", + "exclude = [\"tests*\"]\n", + "\n", + "[tool.setuptools_scm]" + ] + }, + { + "cell_type": "markdown", + "id": "850a57bd", + "metadata": {}, + "source": [ + "### Developer Install" + ] + }, + { + "cell_type": "markdown", + "id": "58a1f08f", + "metadata": {}, + "source": [ + "\n", + "If you modify your source files, you would now find it appeared as if the program doesn't change.\n", + "\n", + "That's because pip install **copies** the files.\n", + "\n", + "If you want to install a package, but keep working on it, you can do:" + ] + }, + { + "cell_type": "markdown", + "id": "4c4b16f3", + "metadata": {}, + "source": [ + "```bash\n", + "pip install --editable .\n", + "```\n", + "\n", + "or, its shorter version:\n", + "\n", + "```bash\n", + "pip install -e .\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "0340e52e", + "metadata": {}, + "source": [ + "### Distributing compiled code" + ] + }, + { + "cell_type": "markdown", + "id": "9ffc7444", + "metadata": {}, + "source": [ + "\n", + "If you're working in C++ or Fortran, there is no language specific repository.\n", + "You'll need to write platform installers for as many platforms as you want to\n", + "support.\n", + "\n", + "Typically:\n", + "\n", + "* `dpkg` for `apt-get` on Ubuntu and Debian\n", + "* `rpm` for `yum`/`dnf` on Redhat and Fedora\n", + "* `homebrew` on OSX (Possibly `macports` as well)\n", + "* An executable `msi` installer for Windows.\n" + ] + }, + { + "cell_type": "markdown", + "id": "02642c46", + "metadata": {}, + "source": [ + "#### Homebrew" + ] + }, + { + "cell_type": "markdown", + "id": "19227084", + "metadata": {}, + "source": [ + "\n", + "Homebrew: A ruby DSL, you host off your own webpage\n", + "\n", + "See an [installer for the cppcourse example](http://github.com/jamespjh/homebrew-reactor)\n", + "\n", + "If you're on OSX, do:\n" + ] + }, + { + "cell_type": "markdown", + "id": "8c25ac04", + "metadata": {}, + "source": [ + "```\n", + "brew tap jamespjh/homebrew-reactor\n", + "brew install reactor\n", + "```" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Packaging" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/03Packaging.ipynb.py b/ch04packaging/03Packaging.ipynb.py new file mode 100644 index 000000000..787cd893d --- /dev/null +++ b/ch04packaging/03Packaging.ipynb.py @@ -0,0 +1,685 @@ +# --- +# jupyter: +# jekyll: +# display_name: Packaging +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Packaging + +# %% [markdown] +# +# Once we've made a working program, we'd like to be able to share it with others. +# +# A good cross-platform build tool is the most important thing: you can always +# have collaborators build from source. +# + +# %% [markdown] +# ### Distribution tools + +# %% [markdown] +# Distribution tools allow one to obtain a working copy of someone else's package. +# +# - Language-specific tools: +# - python: PyPI, +# - ruby: Ruby Gems, +# - perl: CPAN, +# - R: CRAN +# +# - Platform specific packagers e.g.: +# - [`brew`](https://brew.sh/) for MacOS, +# - `apt`/`dnf`/`pacman` for Linux or +# - [`choco`](https://chocolatey.org/) for Windows. + +# %% [markdown] +# ### Laying out a project + +# %% [markdown] +# +# When planning to package a project for distribution, defining a suitable +# project layout is essential. A typical layout might look like this: +# +# ``` +# repository_name +# |-- module_name +# | |-- __init__.py +# | |-- python_file.py +# | |-- another_python_file.py +# | `-- test +# | |-- fixtures +# | | `-- fixture_file.yaml +# | |-- __init__.py +# | `-- test_python_file.py +# |-- LICENSE.md +# |-- CITATION.md +# |-- README.md +# `-- setup.py +# ``` +# +# +# +# + +# %% [markdown] +# To achieve this for our `greetings.py` file from the previous session, we can use the commands shown below. We can start by making our directory structure. You can create many nested directories at once using the `-p` switch on `mkdir`. + +# %% language="bash" +# mkdir -p greetings_repo/greetings/test/fixtures + +# %% [markdown] +# For this notebook, since we are going to be modifying the files bit by bit, we are going to use the [autoreload ipython magic](https://ipython.readthedocs.io/en/stable/config/extensions/autoreload.html) so that we don't need to restart the kernel. + +# %% +# %load_ext autoreload +# %autoreload 2 + +# %% [markdown] +# ### Using pyproject.toml + +# %% [markdown] +# Since June 2020, python's recommendation for creating a package is to specify package information in a `pyproject.toml` file. +# Older projects used a `setup.py` or `setup.cfg` file instead - and in fact the new `pyproject.toml` file in many ways mirrors this old format. +# A lot of projects and packages have not yet switched over from `setup.py` to `pyproject.toml`, so don't be surprised to see a mixture of the two formats when you're looking at other people's packages. + +# %% [markdown] +# For our `greetings` package, right now we are adding only the name of the package and its version number. +# This information is included in the `project` section of our `pyproject.toml` file. +# +# But we also need to tell users how to build the package from these specifications. +# This information is specified in the `build-system` section of our `toml` file. +# In this case, we'll be using `setuptools` to build our package, so we list it in the `requires` field. +# We also need `setuptools_scm[toml]` so that `setuptools` can understand the settings we give it in our `.toml` file, and `wheel` to make the package distribution. +# +# Finally, we can set specific options for `setuptools` using additional sections in `pyproject.toml`: in this case, we will tell `setuptools` that it needs to find **and include** all of the files in our `greetings` folder. + +# %% +# %%writefile greetings_repo/pyproject.toml + +[project] +name = "Greetings" +version = "0.1.0" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"] + +[tool.setuptools.packages.find] +include = ["greetings*"] + +[tool.setuptools_scm] + + +# %% [markdown] +# We can now install this "package" with pip: + +# %% language="bash" +# cd greetings_repo +# pip install . + +# %% [markdown] +# +# And the package will be then available to use everywhere on the system. But so far this package doesn't contain anything and there's nothing we can run! We need to add some files first. +# + +# %% [markdown] +# +# To create a regular package, we needed to have `__init__.py` files on each subdirectory that we want to be able to import. This is, since version 3.3 and the introduction of [Implicit Namespaces Packages](https://www.python.org/dev/peps/pep-0420/), not needed anymore. However, if you want to use relative imports and `pytest`, then you [still need to have these files](https://github.com/pytest-dev/pytest/issues/1927). +# +# The `__init__.py` files can contain any initialisation code you want to run when the (sub)module is imported. +# +# For this example, and because we are using relative imports in the tests, we are creating the needed `__init__.py` files. + +# %% language="bash" +# +# touch greetings_repo/greetings/__init__.py + +# %% [markdown] +# And we can copy the `greet` function from the [previous section](https://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch04packaging/02Argparse.html) in the `greeter.py` file. + +# %% +# %%writefile greetings_repo/greetings/greeter.py + +def greet(personal, family, title="", polite=False): + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + + + +# %% [markdown] +# For the changes to take effect, we need to reinstall the library: + +# %% language="bash" +# cd greetings_repo +# pip install . + +# %% [markdown] +# And now we are able to import it and use it: + +# %% +from greetings.greeter import greet +greet("Terry","Gilliam") + + +# %% [markdown] +# ### Convert the script to a module + +# %% [markdown] +# +# Of course, there's more to do when taking code from a quick script and turning it into a proper module: + +# %% [markdown] +# We need to add docstrings to our functions, so people can know how to use them. + +# %% +# %%writefile greetings_repo/greetings/greeter.py + +def greet(personal, family, title="", polite=False): + """ Generate a greeting string for a person. + Parameters + ---------- + personal: str + A given name, such as Will or Jean-Luc + family: str + A family name, such as Riker or Picard + title: str + An optional title, such as Captain or Reverend + polite: bool + True for a formal greeting, False for informal. + Returns + ------- + string + An appropriate greeting + Examples + -------- + >>> from greetings.greeter import greet + >>> greet("Terry", "Jones") + 'Hey, Terry Jones. + """ + + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + + +# %% [markdown] +# We can see the documentation using `help`. + +# %% +help(greet) + +# %% [markdown] +# The documentation string explains how to use the function; don't worry about this for now, we'll consider +# this on [the next section](./04documentation.html) ([notebook version](./04documentation.ipynb)). + +# %% [markdown] +# ### Write an executable script + +# %% [markdown] +# +# +# +# +# +# + +# %% [markdown] +# We can create an executable script, `command.py` that uses our greeting functionality and the `process` function we created in the previous section. +# +# Note how we are importing `greet` using [relative imports](https://www.python.org/dev/peps/pep-0328/), where `.greeter` means to look for a `greeter` module within the same directory. + +# %% +# %%writefile greetings_repo/greetings/command.py + +from argparse import ArgumentParser + +from .greeter import greet + + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + print(greet(arguments.personal, arguments.family, + arguments.title, arguments.polite)) + + +if __name__ == "__main__": + process() + +# %% [markdown] +# #### Specify entry point + +# %% [markdown] +# This allows us to create a command to execute part of our library. In this case when we execute `greet` on the terminal, we will be calling the `process` function under `greetings/command.py`. +# +# We can encode this into our package information by specifying the `project.scripts` field in our `pyproject.toml` file. + +# %% +# %%writefile greetings_repo/pyproject.toml + +[project] +name = "Greetings" +version = "0.1.0" + +[project.scripts] +greet = "greetings.command:process" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"] + +[tool.setuptools.packages.find] +include = ["greetings*"] + +[tool.setuptools_scm] + +# %% language="bash" +# cd greetings_repo +# pip install -e . + +# %% [markdown] +# +# And the scripts are now available as command line commands, so the following commands can now be run: +# +# +# + +# %% language="bash" +# greet --help + +# %% language="bash" +# greet Terry Gilliam +# greet --polite Terry Gilliam +# greet Terry Gilliam --title Cartoonist + +# %% [markdown] +# ### Specify dependencies + +# %% [markdown] +# Let's give some life to our output using ascii art + +# %% +# %%writefile greetings_repo/greetings/command.py + +from argparse import ArgumentParser + +from art import art + +from .greeter import greet + + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + message = greet(arguments.personal, arguments.family, + arguments.title, arguments.polite) + print(art("cute face"), message) + +if __name__ == "__main__": + process() + +# %% [markdown] +# We use the `dependencies` field of the `project` section in our `pyproject.toml` file to specify the packages we depend on. +# We provide the names of the packages as a list of strings. + +# %% +# %%writefile greetings_repo/pyproject.toml + +[project] +name = "Greetings" +version = "0.1.0" +dependencies = [ + "art", +] + +[project.scripts] +greet = "greetings.command:process" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"] + +[tool.setuptools.packages.find] +include = ["greetings*"] + +[tool.setuptools_scm] + +# %% [markdown] +# When installing the package now, pip will also install the dependencies automatically. + +# %% language="bash" +# cd greetings_repo +# pip install -e . + +# %% language="bash" +# greet Terry Gilliam + +# %% [markdown] +# ### Installing from GitHub + +# %% [markdown] +# +# We could now submit "greeter" to PyPI for approval, so everyone could `pip install` it. +# +# However, when using git, we don't even need to do that: we can install directly from any git URL: +# + +# %% [markdown] +# ```bash +# pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter +# ``` + +# %% [markdown] +# ```bash +# $ greet Lancelot the-Brave --title Sir +# Hey, Sir Lancelot the-Brave. +# ``` + +# %% [markdown] +#
+# There are a few additional text files that are important to add to a package: a readme file, a licence file and a citation file. + +# %% [markdown] +# +# + +# %% [markdown] +# ### Write a readme file + +# %% [markdown] +# The readme file might look like this: + +# %% +# %%writefile greetings_repo/README.md + +# Greetings! + +This is a very simple example package used as part of the UCL +[Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course. + +## Installation + +```bash +pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter +``` + +## Usage + +Invoke the tool with `greet ` or use it on your own library: + +```python +from greeting import greeter + +greeter.greet(user.name, user.lastname) +``` + +# %% [markdown] +# ### Write a license file + +# %% [markdown] +# We will discus more about [licensing in a later section](https://github-pages.ucl.ac.uk/rsd-engineeringcourse/ch04packaging/07Licensing.html). For now let's assume we want to release this package into the public domain: + +# %% +# %%writefile greetings_repo/LICENSE.md + +(C) University College London 2014 + +This "greetings" example package is granted into the public domain. + +# %% [markdown] +# ### Write a citation file + +# %% [markdown] +# A citation file will inform our users how we would like to be cited when refering to our software: + +# %% +# %%writefile greetings_repo/CITATION.md + +If you wish to refer to this course, please cite the URL +http://github-pages.ucl.ac.uk/rsd-engineeringcourse/ + +Portions of the material are taken from [Software Carpentry](http://software-carpentry.org/) + +# %% [markdown] +# You may well want to formalise this using the [codemeta.json](https://codemeta.github.io/) standard or the [citation file format](http://citation-file-format.github.io/) - these don't have wide adoption yet, but we recommend it. + +# %% [markdown] +# ### Define packages and executables + +# %% [markdown] +# We need to create `__init__` files for the source and the tests. +# ```bash +# touch greetings/greetings/test/__init__.py +# touch greetings/greetings/__init__.py +# ``` + +# %% [markdown] +# ### Write some unit tests + +# %% [markdown] +# We can now write some tests to our library. +# +# Remember, that we need to create the empty `__init__.py` files so that `pytest` can follow the relative imports. + +# %% language="bash" +# touch greetings_repo/greetings/test/__init__.py + +# %% [markdown] +# +# Separating the script from the logical module made this possible. +# +# +# +# +# +# + +# %% +# %%writefile greetings_repo/greetings/test/test_greeter.py + +import os + +import yaml + +from ..greeter import greet + +def test_greet(): + with open(os.path.join(os.path.dirname(__file__), + 'fixtures', + 'samples.yaml')) as fixtures_file: + fixtures = yaml.safe_load(fixtures_file) + for fixture in fixtures: + answer = fixture.pop('answer') + assert greet(**fixture) == answer + + +# %% [markdown] +# +# +# +# Add a fixtures file: +# +# +# +# +# +# + +# %% +# %%writefile greetings_repo/greetings/test/fixtures/samples.yaml + +- personal: Eric + family: Idle + answer: "Hey, Eric Idle." +- personal: Graham + family: Chapman + polite: True + answer: "How do you do, Graahm Chapman." +- personal: Michael + family: Palin + title: CBE + answer: "Hey, CBE Mike Palin." + +# %% [markdown] +# We can now run `pytest` + +# %% magic_args="--no-raise-error" language="bash" +# +# cd greetings_repo +# pytest + +# %% [markdown] +# However, this hasn't told us that also the third test is wrong too! A better aproach is to parametrize the testfile `greetings_repo/greetings/test/test_greeter.py` as follows: + +# %% +# %%writefile greetings_repo/greetings/test/test_greeter.py + +import os + +import pytest +import yaml + +from ..greeter import greet + +def read_fixture(): + with open(os.path.join(os.path.dirname(__file__), + 'fixtures', + 'samples.yaml')) as fixtures_file: + fixtures = yaml.safe_load(fixtures_file) + return fixtures + +@pytest.mark.parametrize("fixture", read_fixture()) +def test_greeter(fixture): + answer = fixture.pop('answer') + assert greet(**fixture) == answer + + +# %% [markdown] +# Now when we run `pytest`, we get a failure per element in our fixture and we know all that fails. + +# %% magic_args="--no-raise-error" language="bash" +# +# cd greetings_repo +# pytest + +# %% [markdown] +# We can also make pytest to check whether the docstrings are correct by adding the `--doctest-modules` flag. We run `pytest --doctest-modules` and obtain the following output: + +# %% magic_args="--no-raise-error" language="bash" +# +# cd greetings_repo +# pytest --doctest-modules + +# %% [markdown] +# Finally, we typically don't want to include the tests when we distribute our software for our users. +# We can make sure they are not included using the `exclude` option on when telling `setuptools` to find packages. +# +# Additionally, we can make sure that our README and LICENSE are included in our package metadata by declaring them in the `readme` and `license` fields under the `project` section. +# If you're using a particularly common or standard license, you can even provide the name of the license, rather than the file, and your package builder will take care of the rest! + +# %% +# %%writefile greetings_repo/pyproject.toml + +[project] +name = "Greetings" +version = "0.1.0" +readme = "README.md" +license = { file = "LICENSE.md" } +dependencies = [ + "art", + "pyyaml", +] + +[project.scripts] +greet = "greetings.command:process" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"] + +[tool.setuptools.packages.find] +include = ["greetings*"] +exclude = ["tests*"] + +[tool.setuptools_scm] + +# %% [markdown] +# ### Developer Install + +# %% [markdown] +# +# If you modify your source files, you would now find it appeared as if the program doesn't change. +# +# That's because pip install **copies** the files. +# +# If you want to install a package, but keep working on it, you can do: + +# %% [markdown] +# ```bash +# pip install --editable . +# ``` +# +# or, its shorter version: +# +# ```bash +# pip install -e . +# ``` + +# %% [markdown] +# ### Distributing compiled code + +# %% [markdown] +# +# If you're working in C++ or Fortran, there is no language specific repository. +# You'll need to write platform installers for as many platforms as you want to +# support. +# +# Typically: +# +# * `dpkg` for `apt-get` on Ubuntu and Debian +# * `rpm` for `yum`/`dnf` on Redhat and Fedora +# * `homebrew` on OSX (Possibly `macports` as well) +# * An executable `msi` installer for Windows. +# + +# %% [markdown] +# #### Homebrew + +# %% [markdown] +# +# Homebrew: A ruby DSL, you host off your own webpage +# +# See an [installer for the cppcourse example](http://github.com/jamespjh/homebrew-reactor) +# +# If you're on OSX, do: +# + +# %% [markdown] +# ``` +# brew tap jamespjh/homebrew-reactor +# brew install reactor +# ``` diff --git a/ch04packaging/04documentation.html b/ch04packaging/04documentation.html new file mode 100644 index 000000000..f4d8761ed --- /dev/null +++ b/ch04packaging/04documentation.html @@ -0,0 +1,887 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Documentation

+
+
+
+
+
+
+

Documentation is hard

+
+
+
+
+
+
+
    +
  • Good documentation is hard, and very expensive.
  • +
  • Bad documentation is detrimental.
  • +
  • Good documentation quickly becomes bad if not kept up-to-date with code changes.
  • +
  • Professional companies pay large teams of documentation writers.
  • +
+
+
+
+
+
+
+

Prefer readable code with tests and vignettes

+
+
+
+
+
+
+

If you don't have the capacity to maintain great documentation, +focus on:

+
    +
  • Readable code
  • +
  • Automated tests
  • +
  • Small code samples demonstrating how to use the api
  • +
+
+
+
+
+
+
+

Comment-based Documentation tools

+
+
+
+
+
+
+

Documentation tools can produce extensive documentation about your code by pulling out comments near the beginning of functions, +together with the signature, into a web page.

+

The most popular is Doxygen.

+

Here are some other documentation tools used in different languages, have a look at the generated and source examples:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageNameOutput examplesource
MultipleDoxygenArray docsArray docstring source
PythonSphinxnumpy.ones docsnumpy.ones docstring source
Rpkgdownstringr's str_uniquestringr's str_unique docstring source
JuliaDocumnenter.jlones docsones docstring source
FortanFORDarange docsarange docstring source
RustrustdocMatrix docsMatrix docstring source
+

Breathe can be used to make Sphinx and Doxygen work together (good to keep documentation, for example, of a C++ project that includes Python bindings). roxygen2 is another good option for R packages.

+
+
+
+
+
+
+

Example of using Sphinx

+
+
+
+
+
+
+

Write some docstrings

+
+
+
+
+
+
+

We're going to document our "greeter" example from the previous section using docstrings with Sphinx.

+

There are various conventions for how to write docstrings, but the native Sphinx one doesn't look nice when used with +the built in help system.

+

In writing Greeter, we used the docstring conventions from NumPy. +So we use the numpydoc sphinx extension to +support these (Note: you will need to install this extension for the later examples to work).

+
+
+
+
+
+
+
""" 
+Generate a greeting string for a person.
+
+Parameters
+----------
+personal: str
+    A given name, such as Will or Jean-Luc
+
+family: str
+    A family name, such as Riker or Picard
+
+title: str
+    An optional title, such as Captain or Reverend
+
+polite: bool
+    True for a formal greeting, False for informal.
+
+Returns
+-------
+string
+    An appropriate greeting
+"""
+
+
+
+
+
+
+
+

Set up Sphinx

+
+
+
+
+
+
+

Install Sphinx using the appropiate instructions for your system following the documentation online. +(Note that your output and the linked documentation may differ slightly depending on when you installed Sphinx and what version you're using.)

+
+
+
+
+
+
+

Invoke the sphinx-quickstart command to build Sphinx's +configuration file automatically based on questions +at the command line:

+
+
+
+
+
+
+
sphinx-quickstart
+
+
+
+
+
+
+
+

Which responds:

+
+
+
+
+
+
+
Welcome to the Sphinx 4.2.0 quickstart utility.
+
+Please enter values for the following settings (just press Enter to
+accept a default value, if one is given in brackets).
+
+Selected root path: .
+
+You have two options for placing the build directory for Sphinx output.
+Either, you use a directory "_build" within the root path, or you separate
+"source" and "build" directories within the root path.
+> Separate source and build directories (y/n) [n]:
+
+The project name will occur in several places in the built documentation.
+> Project name: Greetings
+> Author name(s): James Hetherington
+> Project release []: 0.1
+
+If the documents are to be written in a language other than English,
+you can select a language here by its language code. Sphinx will then
+translate text that it generates into that language.
+
+For a list of supported codes, see
+https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.
+> Project language [en]:
+
+Creating file ./conf.py.
+Creating file ./index.rst.
+Creating file ./Makefile.
+Creating file ./make.bat.
+
+Finished: An initial directory structure has been created.
+
+You should now populate your master file /tmp/index.rst and create other documentation
+source files. Use the Makefile to build the docs, like so:
+   make builder
+where "builder" is one of the supported builders, e.g. html, latex or linkcheck.
+
+
+
+
+
+
+
+

and then look at and adapt the generated config - a file called +conf.py in the root of the project - with, for example, the extensions we want to use. +This config file contains the project's Sphinx configuration, as Python variables:

+
+
+
+
+
+
+
#Add any Sphinx extension module names here, as strings. They can be
+#extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',  # Support automatic documentation
+    'sphinx.ext.coverage', # Automatically check if functions are documented
+    'sphinx.ext.mathjax',  # Allow support for algebra
+    'sphinx.ext.viewcode', # Include the source code in documentation
+    'numpydoc'             # Support NumPy style docstrings
+]
+
+
+
+
+
+
+
+

To proceed with the example, we'll copy a finished conf.py into our folder, though normally you'll always use sphinx-quickstart

+
+
+
+
+
+
In [1]:
+
+
+
%%writefile greetings/conf.py
+
+import sys
+import os
+
+# We need to tell Sphinx where to look for modules
+sys.path.insert(0, os.path.abspath('.'))
+
+extensions = [
+    'sphinx.ext.autodoc',  # Support automatic documentation
+    'sphinx.ext.coverage', # Automatically check if functions are documented
+    'sphinx.ext.mathjax',  # Allow support for algebra
+    'sphinx.ext.viewcode', # Include the source code in documentation
+    'numpydoc'             # Support NumPy style docstrings
+]
+templates_path = ['_templates']
+source_suffix = '.rst'
+master_doc = 'index'
+project = 'Greetings'
+copyright = '2014, James Hetherington'
+version = '0.1'
+release = '0.1'
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+html_theme = 'alabaster'
+pygments_style = 'sphinx'
+htmlhelp_basename = 'Greetingsdoc'
+latex_elements = {
+}
+
+latex_documents = [
+  ('index', 'Greetings.tex', 'Greetings Documentation',
+   'James Hetherington', 'manual'),
+]
+
+man_pages = [
+    ('index', 'greetings', 'Greetings Documentation',
+     ['James Hetherington'], 1)
+]
+
+texinfo_documents = [
+  ('index', 'Greetings', u'Greetings Documentation',
+   'James Hetherington', 'Greetings', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+
+
+
+
+
+
+
+
+
Overwriting greetings/conf.py
+
+
+
+
+
+
+
+
+
+

Define the root documentation page

+
+
+
+
+
+
+

Sphinx uses RestructuredText another wiki markup format similar to Markdown.

+

You define an "index.rst" file to contain any preamble text you want. The rest is autogenerated by sphinx-quickstart

+
+
+
+
+
+
In [2]:
+
+
+
%%writefile greetings/index.rst
+Welcome to Greetings's documentation!
+=====================================
+
+Simple "Hello, James" module developed to teach research software engineering.
+
+.. autofunction:: greetings.greeter.greet
+
+
+
+
+
+
+
+
+
+
Overwriting greetings/index.rst
+
+
+
+
+
+
+
+
+
+

Run sphinx

+
+
+
+
+
+
+

We can run Sphinx using:

+
+
+
+
+
+
In [3]:
+
+
+
%%bash
+cd greetings/
+sphinx-build . doc
+
+
+
+
+
+
+
+
+
+
Running Sphinx v7.1.2
+making output directory... done
+[autosummary] generating autosummary for: index.rst
+building [mo]: targets for 0 po files that are out of date
+writing output... 
+building [html]: targets for 1 source files that are out of date
+updating environment: [new config] 1 added, 0 changed, 0 removed
+reading sources... [100%] index
+looking for now-outdated files... none found
+pickling environment... done
+checking consistency... done
+preparing documents... done
+copying assets... copying static files... done
+copying extra files... done
+done
+writing output... [100%] index
+generating indices... genindex done
+highlighting module code... [100%] greetings.greeter
+writing additional pages... search done
+dumping search index in English (code: en)... done
+dumping object inventory... done
+build succeeded.
+
+The HTML pages are in doc.
+
+
+
+
+
+
+
+
+
+

Sphinx output

+
+
+
+
+
+
+

Sphinx's output is html. We just created a simple single function's documentation, but Sphinx will create +multiple nested pages of documentation automatically for many functions.

+
+
+
+
+
+
+

Doctest - testing your documentation is up to date

+
+
+
+
+
+
+

doctest is a module included in the standard library. It runs all the code within the docstrings and checks whether the output is what it's claimed on the documentation.

+

Let's add an example to our greeting function and check it with doctest. We are leaving the output with a small typo (missing the closing quote ') to see what's the type of output we get from doctest.

+
+
+
+
+
+
In [4]:
+
+
+
%%writefile greetings/greetings/greeter.py
+def greet(personal, family, title="", polite=False):
+    """ Generate a greeting string for a person.
+
+    Parameters
+    ----------
+    personal: str
+        A given name, such as Will or Jean-Luc
+    family: str
+        A family name, such as Riker or Picard
+    title: str
+        An optional title, such as Captain or Reverend
+    polite: bool
+        True for a formal greeting, False for informal.
+
+    Returns
+    -------
+    string
+        An appropriate greeting
+        
+    Examples
+    --------
+    >>> from greetings.greeter import greet
+    >>> greet("Terry", "Jones")
+    'Hey, Terry Jones.
+    """
+
+    greeting= "How do you do, " if polite else "Hey, "
+    if title:
+        greeting += f"{title} "
+
+    greeting += f"{personal} {family}."
+    return greeting
+
+
+
+
+
+
+
+
+
+
Overwriting greetings/greetings/greeter.py
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%%bash --no-raise-error
+python -m doctest greetings/greetings/greeter.py
+
+
+
+
+
+
+
+
+
+
**********************************************************************
+File "/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings/greetings/greeter.py", line 23, in greeter.greet
+Failed example:
+    greet("Terry", "Jones")
+Expected:
+    'Hey, Terry Jones.
+Got:
+    'Hey, Terry Jones.'
+**********************************************************************
+1 items had failures:
+   1 of   2 in greeter.greet
+***Test Failed*** 1 failures.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

which clearly identifies a tiny error in our example.

+
+
+
+
+
+
+

pytest can run the doctest too if you call it as:

+

pytest --doctest-modules

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/04documentation.ipynb b/ch04packaging/04documentation.ipynb new file mode 100644 index 000000000..b0cb3044e --- /dev/null +++ b/ch04packaging/04documentation.ipynb @@ -0,0 +1,542 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6aa7793a", + "metadata": {}, + "source": [ + "## Documentation" + ] + }, + { + "cell_type": "markdown", + "id": "4dd2c0cc", + "metadata": {}, + "source": [ + "### Documentation is hard" + ] + }, + { + "cell_type": "markdown", + "id": "701b301c", + "metadata": {}, + "source": [ + "\n", + "* Good documentation is hard, and very expensive.\n", + "* Bad documentation is detrimental.\n", + "* Good documentation quickly becomes bad if not kept up-to-date with code changes.\n", + "* Professional companies pay large teams of documentation writers.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b3198dff", + "metadata": {}, + "source": [ + "### Prefer readable code with tests and vignettes" + ] + }, + { + "cell_type": "markdown", + "id": "b18d131d", + "metadata": {}, + "source": [ + "\n", + "If you don't have the capacity to maintain great documentation,\n", + "focus on:\n", + "\n", + "* Readable code\n", + "* Automated tests\n", + "* Small code samples demonstrating how to use the api\n" + ] + }, + { + "cell_type": "markdown", + "id": "aad40696", + "metadata": {}, + "source": [ + "### Comment-based Documentation tools" + ] + }, + { + "cell_type": "markdown", + "id": "82de7a63", + "metadata": {}, + "source": [ + "\n", + "Documentation tools can produce extensive documentation about your code by pulling out comments near the beginning of functions,\n", + "together with the signature, into a web page.\n", + "\n", + "The most popular is [Doxygen](http://www.doxygen.nl/).\n", + "\n", + "Here are some other documentation tools used in different languages, have a look at the generated and source examples:\n", + "\n", + "\n", + "| Language | Name | Output example | source |\n", + "| --- | --- | --- | --- |\n", + "| Multiple | [Doxygen](http://www.doxygen.nl/) | [`Array` docs](https://eigen.tuxfamily.org/dox/classEigen_1_1Array.html) | [`Array` docstring source](https://gitlab.com/libeigen/eigen/-/blob/55e3ae02ac1f13fbcc7a83f5e37a39fd2b142db1/Eigen/src/Core/Array.h#L26-L45) |\n", + "| Python | [Sphinx](http://sphinx-doc.org/) | [`numpy.ones` docs](https://numpy.org/doc/1.21/reference/generated/numpy.ones.html) | [`numpy.ones` docstring source](https://github.com/numpy/numpy/blob/v1.21.0/numpy/core/numeric.py#L149-L206) |\n", + "| R | [pkgdown](https://pkgdown.r-lib.org/) | [`stringr`'s `str_unique`](https://stringr.tidyverse.org/reference/str_unique.html) | [`stringr`'s `str_unique` docstring source](https://github.com/tidyverse/stringr/blob/main/R/unique.R) |\n", + "| Julia | [Documnenter.jl](https://juliadocs.github.io/Documenter.jl/stable/) | [`ones` docs](https://docs.julialang.org/en/v1/base/arrays/#Base.ones) | [`ones` docstring source](https://github.com/JuliaLang/julia/blob/ae8452a9e0b973991c30f27beb2201db1b0ea0d3/base/array.jl#L475-L493) |\n", + "| Fortan | [FORD](https://github.com/Fortran-FOSS-Programmers/ford) | [`arange` docs](https://stdlib.fortran-lang.org/interface/arange.html) | [`arange` docstring source](https://github.com/fortran-lang/stdlib/blob/d14fca8e7cc36ed5f6f84d2bf576c91c2e54eb07/src/stdlib_math.fypp#L276-L290) |\n", + "| Rust | [rustdoc](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html) | [`Matrix` docs](https://docs.rs/nalgebra/0.18.0/nalgebra/base/struct.Matrix.html) | [`Matrix` docstring source](https://github.com/dimforge/nalgebra/blob/8ea8ac70d5ad4bae865e6246a48455bf0b3fa3d2/src/base/matrix.rs#L59-L157) |\n", + "\n", + "[Breathe](https://breathe.readthedocs.io/en/latest/) can be used to make Sphinx and Doxygen work together (good to keep documentation, for example, of a C++ project that includes Python bindings). [roxygen2](https://cran.r-project.org/web/packages/roxygen2/vignettes/roxygen2.html) is another good option for R packages.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "dc8f0d2c", + "metadata": {}, + "source": [ + "## Example of using Sphinx" + ] + }, + { + "cell_type": "markdown", + "id": "91141cb9", + "metadata": {}, + "source": [ + "### Write some docstrings" + ] + }, + { + "cell_type": "markdown", + "id": "8c8a33a7", + "metadata": {}, + "source": [ + "We're going to document our \"greeter\" example from the previous section using docstrings with Sphinx.\n", + "\n", + "There are various conventions for how to write docstrings, but the native Sphinx one doesn't look nice when used with\n", + "the built in `help` system.\n", + "\n", + "In writing Greeter, we used the [docstring conventions from NumPy](https://numpy.org/doc/stable/docs/howto_document.html).\n", + "So we use the [`numpydoc`](https://numpydoc.readthedocs.io/en/latest/) sphinx extension to\n", + "support these (Note: you will need to install this extension for the later examples to work)." + ] + }, + { + "cell_type": "markdown", + "id": "ccc8f45f", + "metadata": {}, + "source": [ + "```python\n", + "\"\"\" \n", + "Generate a greeting string for a person.\n", + "\n", + "Parameters\n", + "----------\n", + "personal: str\n", + " A given name, such as Will or Jean-Luc\n", + "\n", + "family: str\n", + " A family name, such as Riker or Picard\n", + "\n", + "title: str\n", + " An optional title, such as Captain or Reverend\n", + "\n", + "polite: bool\n", + " True for a formal greeting, False for informal.\n", + "\n", + "Returns\n", + "-------\n", + "string\n", + " An appropriate greeting\n", + "\"\"\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "0e4c7e3a", + "metadata": {}, + "source": [ + "### Set up Sphinx" + ] + }, + { + "cell_type": "markdown", + "id": "5afe79cc", + "metadata": {}, + "source": [ + "Install Sphinx using the [appropiate instructions](https://www.sphinx-doc.org/en/master/usage/installation.html) for your system following the documentation online.\n", + "(Note that your output and the linked documentation may differ slightly depending on when you installed Sphinx and what version you're using.)" + ] + }, + { + "cell_type": "markdown", + "id": "e083d6d3", + "metadata": {}, + "source": [ + "\n", + "Invoke the [sphinx-quickstart](https://www.sphinx-doc.org/en/master/usage/quickstart.html) command to build Sphinx's\n", + "configuration file automatically based on questions\n", + "at the command line:" + ] + }, + { + "cell_type": "markdown", + "id": "41cd3e2d", + "metadata": {}, + "source": [ + "``` bash\n", + "sphinx-quickstart\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a187b618", + "metadata": {}, + "source": [ + "Which responds:" + ] + }, + { + "cell_type": "markdown", + "id": "d903e76c", + "metadata": {}, + "source": [ + "```\n", + "Welcome to the Sphinx 4.2.0 quickstart utility.\n", + "\n", + "Please enter values for the following settings (just press Enter to\n", + "accept a default value, if one is given in brackets).\n", + "\n", + "Selected root path: .\n", + "\n", + "You have two options for placing the build directory for Sphinx output.\n", + "Either, you use a directory \"_build\" within the root path, or you separate\n", + "\"source\" and \"build\" directories within the root path.\n", + "> Separate source and build directories (y/n) [n]:\n", + "\n", + "The project name will occur in several places in the built documentation.\n", + "> Project name: Greetings\n", + "> Author name(s): James Hetherington\n", + "> Project release []: 0.1\n", + "\n", + "If the documents are to be written in a language other than English,\n", + "you can select a language here by its language code. Sphinx will then\n", + "translate text that it generates into that language.\n", + "\n", + "For a list of supported codes, see\n", + "https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.\n", + "> Project language [en]:\n", + "\n", + "Creating file ./conf.py.\n", + "Creating file ./index.rst.\n", + "Creating file ./Makefile.\n", + "Creating file ./make.bat.\n", + "\n", + "Finished: An initial directory structure has been created.\n", + "\n", + "You should now populate your master file /tmp/index.rst and create other documentation\n", + "source files. Use the Makefile to build the docs, like so:\n", + " make builder\n", + "where \"builder\" is one of the supported builders, e.g. html, latex or linkcheck.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3d8ea0e6", + "metadata": {}, + "source": [ + "and then look at and adapt the generated config - a file called\n", + "`conf.py` in the root of the project - with, for example, the extensions we want to use.\n", + "This config file contains the project's Sphinx configuration, as Python variables:" + ] + }, + { + "cell_type": "markdown", + "id": "b6d608dc", + "metadata": {}, + "source": [ + "``` python\n", + "#Add any Sphinx extension module names here, as strings. They can be\n", + "#extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n", + "# ones.\n", + "extensions = [\n", + " 'sphinx.ext.autodoc', # Support automatic documentation\n", + " 'sphinx.ext.coverage', # Automatically check if functions are documented\n", + " 'sphinx.ext.mathjax', # Allow support for algebra\n", + " 'sphinx.ext.viewcode', # Include the source code in documentation\n", + " 'numpydoc' # Support NumPy style docstrings\n", + "]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "ea7a9a68", + "metadata": {}, + "source": [ + "To proceed with the example, we'll copy a finished conf.py into our folder, though normally you'll always use `sphinx-quickstart`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24f75846", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile greetings/conf.py\n", + "\n", + "import sys\n", + "import os\n", + "\n", + "# We need to tell Sphinx where to look for modules\n", + "sys.path.insert(0, os.path.abspath('.'))\n", + "\n", + "extensions = [\n", + " 'sphinx.ext.autodoc', # Support automatic documentation\n", + " 'sphinx.ext.coverage', # Automatically check if functions are documented\n", + " 'sphinx.ext.mathjax', # Allow support for algebra\n", + " 'sphinx.ext.viewcode', # Include the source code in documentation\n", + " 'numpydoc' # Support NumPy style docstrings\n", + "]\n", + "templates_path = ['_templates']\n", + "source_suffix = '.rst'\n", + "master_doc = 'index'\n", + "project = 'Greetings'\n", + "copyright = '2014, James Hetherington'\n", + "version = '0.1'\n", + "release = '0.1'\n", + "exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n", + "html_theme = 'alabaster'\n", + "pygments_style = 'sphinx'\n", + "htmlhelp_basename = 'Greetingsdoc'\n", + "latex_elements = {\n", + "}\n", + "\n", + "latex_documents = [\n", + " ('index', 'Greetings.tex', 'Greetings Documentation',\n", + " 'James Hetherington', 'manual'),\n", + "]\n", + "\n", + "man_pages = [\n", + " ('index', 'greetings', 'Greetings Documentation',\n", + " ['James Hetherington'], 1)\n", + "]\n", + "\n", + "texinfo_documents = [\n", + " ('index', 'Greetings', u'Greetings Documentation',\n", + " 'James Hetherington', 'Greetings', 'One line description of project.',\n", + " 'Miscellaneous'),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "be82b2bc", + "metadata": {}, + "source": [ + "### Define the root documentation page" + ] + }, + { + "cell_type": "markdown", + "id": "614207ff", + "metadata": {}, + "source": [ + "\n", + "Sphinx uses [RestructuredText](https://docutils.sourceforge.io/rst.html) another wiki markup format similar to Markdown.\n", + "\n", + "You define an \"index.rst\" file to contain any preamble text you want. The rest is autogenerated by `sphinx-quickstart`\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48bbdbd3", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%writefile greetings/index.rst\n", + "Welcome to Greetings's documentation!\n", + "=====================================\n", + "\n", + "Simple \"Hello, James\" module developed to teach research software engineering.\n", + "\n", + ".. autofunction:: greetings.greeter.greet" + ] + }, + { + "cell_type": "markdown", + "id": "2e7ba2b5", + "metadata": {}, + "source": [ + "###  Run sphinx" + ] + }, + { + "cell_type": "markdown", + "id": "c548ad6a", + "metadata": {}, + "source": [ + "\n", + "We can run Sphinx using:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b177891", + "metadata": { + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "%%bash\n", + "cd greetings/\n", + "sphinx-build . doc" + ] + }, + { + "cell_type": "markdown", + "id": "2c8793ad", + "metadata": {}, + "source": [ + "### Sphinx output" + ] + }, + { + "cell_type": "markdown", + "id": "17dc86b5", + "metadata": {}, + "source": [ + "Sphinx's output is [html](./greetings/doc/index.html). We just created a simple single function's documentation, but Sphinx will create\n", + "multiple nested pages of documentation automatically for many functions.\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "28e942ed", + "metadata": {}, + "source": [ + "## Doctest - testing your documentation is up to date" + ] + }, + { + "cell_type": "markdown", + "id": "61007354", + "metadata": {}, + "source": [ + "`doctest` is a module included in the standard library. It runs all the code within the docstrings and checks whether the output is what it's claimed on the documentation.\n", + "\n", + "Let's add an example to our greeting function and check it with `doctest`. We are leaving the output with a small typo (missing the closing quote `'`) to see what's the type of output we get from `doctest`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dff465a7", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile greetings/greetings/greeter.py\n", + "def greet(personal, family, title=\"\", polite=False):\n", + " \"\"\" Generate a greeting string for a person.\n", + "\n", + " Parameters\n", + " ----------\n", + " personal: str\n", + " A given name, such as Will or Jean-Luc\n", + " family: str\n", + " A family name, such as Riker or Picard\n", + " title: str\n", + " An optional title, such as Captain or Reverend\n", + " polite: bool\n", + " True for a formal greeting, False for informal.\n", + "\n", + " Returns\n", + " -------\n", + " string\n", + " An appropriate greeting\n", + " \n", + " Examples\n", + " --------\n", + " >>> from greetings.greeter import greet\n", + " >>> greet(\"Terry\", \"Jones\")\n", + " 'Hey, Terry Jones.\n", + " \"\"\"\n", + "\n", + " greeting= \"How do you do, \" if polite else \"Hey, \"\n", + " if title:\n", + " greeting += f\"{title} \"\n", + "\n", + " greeting += f\"{personal} {family}.\"\n", + " return greeting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6de70464", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "python -m doctest greetings/greetings/greeter.py" + ] + }, + { + "cell_type": "markdown", + "id": "5b162565", + "metadata": {}, + "source": [ + " " + ] + }, + { + "cell_type": "markdown", + "id": "724a5753", + "metadata": {}, + "source": [ + "which clearly identifies a tiny error in our example." + ] + }, + { + "cell_type": "markdown", + "id": "5528e2ba", + "metadata": {}, + "source": [ + "pytest can run the doctest too if you call it as:\n", + "\n", + "`pytest --doctest-modules`" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Documentation" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/04documentation.ipynb.py b/ch04packaging/04documentation.ipynb.py new file mode 100644 index 000000000..79cd092dd --- /dev/null +++ b/ch04packaging/04documentation.ipynb.py @@ -0,0 +1,343 @@ +# --- +# jupyter: +# jekyll: +# display_name: Documentation +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Documentation + +# %% [markdown] +# ### Documentation is hard + +# %% [markdown] +# +# * Good documentation is hard, and very expensive. +# * Bad documentation is detrimental. +# * Good documentation quickly becomes bad if not kept up-to-date with code changes. +# * Professional companies pay large teams of documentation writers. +# + +# %% [markdown] +# ### Prefer readable code with tests and vignettes + +# %% [markdown] +# +# If you don't have the capacity to maintain great documentation, +# focus on: +# +# * Readable code +# * Automated tests +# * Small code samples demonstrating how to use the api +# + +# %% [markdown] +# ### Comment-based Documentation tools + +# %% [markdown] +# +# Documentation tools can produce extensive documentation about your code by pulling out comments near the beginning of functions, +# together with the signature, into a web page. +# +# The most popular is [Doxygen](http://www.doxygen.nl/). +# +# Here are some other documentation tools used in different languages, have a look at the generated and source examples: +# +# +# | Language | Name | Output example | source | +# | --- | --- | --- | --- | +# | Multiple | [Doxygen](http://www.doxygen.nl/) | [`Array` docs](https://eigen.tuxfamily.org/dox/classEigen_1_1Array.html) | [`Array` docstring source](https://gitlab.com/libeigen/eigen/-/blob/55e3ae02ac1f13fbcc7a83f5e37a39fd2b142db1/Eigen/src/Core/Array.h#L26-L45) | +# | Python | [Sphinx](http://sphinx-doc.org/) | [`numpy.ones` docs](https://numpy.org/doc/1.21/reference/generated/numpy.ones.html) | [`numpy.ones` docstring source](https://github.com/numpy/numpy/blob/v1.21.0/numpy/core/numeric.py#L149-L206) | +# | R | [pkgdown](https://pkgdown.r-lib.org/) | [`stringr`'s `str_unique`](https://stringr.tidyverse.org/reference/str_unique.html) | [`stringr`'s `str_unique` docstring source](https://github.com/tidyverse/stringr/blob/main/R/unique.R) | +# | Julia | [Documnenter.jl](https://juliadocs.github.io/Documenter.jl/stable/) | [`ones` docs](https://docs.julialang.org/en/v1/base/arrays/#Base.ones) | [`ones` docstring source](https://github.com/JuliaLang/julia/blob/ae8452a9e0b973991c30f27beb2201db1b0ea0d3/base/array.jl#L475-L493) | +# | Fortan | [FORD](https://github.com/Fortran-FOSS-Programmers/ford) | [`arange` docs](https://stdlib.fortran-lang.org/interface/arange.html) | [`arange` docstring source](https://github.com/fortran-lang/stdlib/blob/d14fca8e7cc36ed5f6f84d2bf576c91c2e54eb07/src/stdlib_math.fypp#L276-L290) | +# | Rust | [rustdoc](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html) | [`Matrix` docs](https://docs.rs/nalgebra/0.18.0/nalgebra/base/struct.Matrix.html) | [`Matrix` docstring source](https://github.com/dimforge/nalgebra/blob/8ea8ac70d5ad4bae865e6246a48455bf0b3fa3d2/src/base/matrix.rs#L59-L157) | +# +# [Breathe](https://breathe.readthedocs.io/en/latest/) can be used to make Sphinx and Doxygen work together (good to keep documentation, for example, of a C++ project that includes Python bindings). [roxygen2](https://cran.r-project.org/web/packages/roxygen2/vignettes/roxygen2.html) is another good option for R packages. +# +# +# + +# %% [markdown] +# ## Example of using Sphinx + +# %% [markdown] +# ### Write some docstrings + +# %% [markdown] +# We're going to document our "greeter" example from the previous section using docstrings with Sphinx. +# +# There are various conventions for how to write docstrings, but the native Sphinx one doesn't look nice when used with +# the built in `help` system. +# +# In writing Greeter, we used the [docstring conventions from NumPy](https://numpy.org/doc/stable/docs/howto_document.html). +# So we use the [`numpydoc`](https://numpydoc.readthedocs.io/en/latest/) sphinx extension to +# support these (Note: you will need to install this extension for the later examples to work). + +# %% [markdown] +# ```python +# """ +# Generate a greeting string for a person. +# +# Parameters +# ---------- +# personal: str +# A given name, such as Will or Jean-Luc +# +# family: str +# A family name, such as Riker or Picard +# +# title: str +# An optional title, such as Captain or Reverend +# +# polite: bool +# True for a formal greeting, False for informal. +# +# Returns +# ------- +# string +# An appropriate greeting +# """ +# ``` + +# %% [markdown] +# ### Set up Sphinx + +# %% [markdown] +# Install Sphinx using the [appropiate instructions](https://www.sphinx-doc.org/en/master/usage/installation.html) for your system following the documentation online. +# (Note that your output and the linked documentation may differ slightly depending on when you installed Sphinx and what version you're using.) + +# %% [markdown] +# +# Invoke the [sphinx-quickstart](https://www.sphinx-doc.org/en/master/usage/quickstart.html) command to build Sphinx's +# configuration file automatically based on questions +# at the command line: + +# %% [markdown] +# ``` bash +# sphinx-quickstart +# ``` + +# %% [markdown] +# Which responds: + +# %% [markdown] +# ``` +# Welcome to the Sphinx 4.2.0 quickstart utility. +# +# Please enter values for the following settings (just press Enter to +# accept a default value, if one is given in brackets). +# +# Selected root path: . +# +# You have two options for placing the build directory for Sphinx output. +# Either, you use a directory "_build" within the root path, or you separate +# "source" and "build" directories within the root path. +# > Separate source and build directories (y/n) [n]: +# +# The project name will occur in several places in the built documentation. +# > Project name: Greetings +# > Author name(s): James Hetherington +# > Project release []: 0.1 +# +# If the documents are to be written in a language other than English, +# you can select a language here by its language code. Sphinx will then +# translate text that it generates into that language. +# +# For a list of supported codes, see +# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language. +# > Project language [en]: +# +# Creating file ./conf.py. +# Creating file ./index.rst. +# Creating file ./Makefile. +# Creating file ./make.bat. +# +# Finished: An initial directory structure has been created. +# +# You should now populate your master file /tmp/index.rst and create other documentation +# source files. Use the Makefile to build the docs, like so: +# make builder +# where "builder" is one of the supported builders, e.g. html, latex or linkcheck. +# ``` + +# %% [markdown] +# and then look at and adapt the generated config - a file called +# `conf.py` in the root of the project - with, for example, the extensions we want to use. +# This config file contains the project's Sphinx configuration, as Python variables: + +# %% [markdown] +# ``` python +# #Add any Sphinx extension module names here, as strings. They can be +# #extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# # ones. +# extensions = [ +# 'sphinx.ext.autodoc', # Support automatic documentation +# 'sphinx.ext.coverage', # Automatically check if functions are documented +# 'sphinx.ext.mathjax', # Allow support for algebra +# 'sphinx.ext.viewcode', # Include the source code in documentation +# 'numpydoc' # Support NumPy style docstrings +# ] +# ``` + +# %% [markdown] +# To proceed with the example, we'll copy a finished conf.py into our folder, though normally you'll always use `sphinx-quickstart` +# + +# %% jupyter={"outputs_hidden": false} +# %%writefile greetings/conf.py + +import sys +import os + +# We need to tell Sphinx where to look for modules +sys.path.insert(0, os.path.abspath('.')) + +extensions = [ + 'sphinx.ext.autodoc', # Support automatic documentation + 'sphinx.ext.coverage', # Automatically check if functions are documented + 'sphinx.ext.mathjax', # Allow support for algebra + 'sphinx.ext.viewcode', # Include the source code in documentation + 'numpydoc' # Support NumPy style docstrings +] +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +project = 'Greetings' +copyright = '2014, James Hetherington' +version = '0.1' +release = '0.1' +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +html_theme = 'alabaster' +pygments_style = 'sphinx' +htmlhelp_basename = 'Greetingsdoc' +latex_elements = { +} + +latex_documents = [ + ('index', 'Greetings.tex', 'Greetings Documentation', + 'James Hetherington', 'manual'), +] + +man_pages = [ + ('index', 'greetings', 'Greetings Documentation', + ['James Hetherington'], 1) +] + +texinfo_documents = [ + ('index', 'Greetings', u'Greetings Documentation', + 'James Hetherington', 'Greetings', 'One line description of project.', + 'Miscellaneous'), +] + +# %% [markdown] +# ### Define the root documentation page + +# %% [markdown] +# +# Sphinx uses [RestructuredText](https://docutils.sourceforge.io/rst.html) another wiki markup format similar to Markdown. +# +# You define an "index.rst" file to contain any preamble text you want. The rest is autogenerated by `sphinx-quickstart` +# +# +# +# +# +# + +# %% jupyter={"outputs_hidden": false} +# %%writefile greetings/index.rst +Welcome to Greetings's documentation! +===================================== + +Simple "Hello, James" module developed to teach research software engineering. + +.. autofunction:: greetings.greeter.greet + + +# %% [markdown] +# ###  Run sphinx + +# %% [markdown] +# +# We can run Sphinx using: +# + +# %% jupyter={"outputs_hidden": false} language="bash" +# cd greetings/ +# sphinx-build . doc + +# %% [markdown] +# ### Sphinx output + +# %% [markdown] +# Sphinx's output is [html](./greetings/doc/index.html). We just created a simple single function's documentation, but Sphinx will create +# multiple nested pages of documentation automatically for many functions. +# +# +# +# + +# %% [markdown] +# ## Doctest - testing your documentation is up to date + +# %% [markdown] +# `doctest` is a module included in the standard library. It runs all the code within the docstrings and checks whether the output is what it's claimed on the documentation. +# +# Let's add an example to our greeting function and check it with `doctest`. We are leaving the output with a small typo (missing the closing quote `'`) to see what's the type of output we get from `doctest`. + +# %% +# %%writefile greetings/greetings/greeter.py +def greet(personal, family, title="", polite=False): + """ Generate a greeting string for a person. + + Parameters + ---------- + personal: str + A given name, such as Will or Jean-Luc + family: str + A family name, such as Riker or Picard + title: str + An optional title, such as Captain or Reverend + polite: bool + True for a formal greeting, False for informal. + + Returns + ------- + string + An appropriate greeting + + Examples + -------- + >>> from greetings.greeter import greet + >>> greet("Terry", "Jones") + 'Hey, Terry Jones. + """ + + greeting= "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + +# %% magic_args="--no-raise-error" language="bash" +# python -m doctest greetings/greetings/greeter.py + +# %% [markdown] +# + +# %% [markdown] +# which clearly identifies a tiny error in our example. + +# %% [markdown] +# pytest can run the doctest too if you call it as: +# +# `pytest --doctest-modules` diff --git a/ch04packaging/05Process.html b/ch04packaging/05Process.html new file mode 100644 index 000000000..28bab642c --- /dev/null +++ b/ch04packaging/05Process.html @@ -0,0 +1,637 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Agile and Waterfall + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Software Project Management

+
+
+
+
+
+
+

Software Engineering Stages

+
+
+
+
+
+
+
    +
  • Requirements
  • +
  • Functional Design
  • +
  • Architectural Design
  • +
  • Implementation
  • +
  • Integration
  • +
+
+
+
+
+
+
+

Requirements Engineering

+
+
+
+
+
+
+

Requirements capture obviously means describing the things the software needs to be able to do.

+

A common approach is to write down lots of "user stories", describing how the software helps the user achieve something:

+
+

As a clinician, when I finish an analysis, I want a report to be created on the test results, so that I can +send it to the patient.

+
+

As a role, when condition or circumstance applies I want a goal or desire so that benefits occur.

+

These are easy to map into the Gherkin behaviour driven design test language.

+
+
+
+
+
+
+

Functional and architectural design

+
+
+
+
+
+
+

Engineers try to separate the functional design, how the software appears to and is used by the user, from the +architectural design, how the software achieves that functionality.

+

Changes to functional design require users to adapt, and are thus often more costly than changes to architectural design.

+
+
+
+
+
+
+

Waterfall

+
+
+
+
+
+
+

The Waterfall design philosophy argues that the elements of design should occur in order: first requirements capture, then functional design, +then architectural design. This approach is based on the idea that if a mistake is made in the design, then programming effort is wasted, +so significant effort is spent in trying to ensure that requirements are well understood and that the design is correct before programming starts.

+
+
+
+
+
+
+

Why Waterfall?

+
+
+
+
+
+
+

Without a design approach, programmers resort to designing as we go, typing in code, trying what works, and making it up as we go along. +When trying to collaborate to make software with others this can result in lots of wasted time, software that only the author understands, +components built by colleagues that don't work together, or code that the programmer thinks is nice but that doesn't meet the user's requirements.

+
+
+
+
+
+
+

Problems with Waterfall

+
+
+
+
+
+
+

Waterfall results in a contractual approach to development, building an us-and-them relationship between users, business types, designers, and programmers.

+
+

I built what the design said, so I did my job.

+
+

Waterfall results in a paperwork culture, where people spend a long time designing standard forms to document each stage of the design, +with less time actually spent making things.

+

Waterfall results in excessive adherence to a plan, even when mistakes in the design are obvious to people doing the work.

+
+
+
+
+
+
+

Software is not made of bricks

+
+
+
+
+
+
+

The waterfall approach to software engineering comes from the engineering tradition applied to building physical objects, +where Architects and Engineers design buildings, and builders build them according to the design.

+

Software is intrinsically different: software is not made of bricks.

+
+
+
+
+
+
+
+

Software is not the same 'stuff' as that from which physical systems are constructed. +Software systems differ in material respects from physical systems. +Much of this has been rehearsed by Fred Brooks in his classic +'No Silver Bullet' paper.

+

First, complexity and scale are different in the case of software systems: relatively functionally simple software systems comprise more independent parts, placed +in relation to each other, than do physical systems of equivalent functional value.

+

Second, and clearly linked to this, we do not have well developed components and composition mechanisms from which to build +software systems (though clearly we are working hard on providing these) nor do we have a straightforward mathematical account that +permits us to reason about the effects of composition.

+

Third, software systems operate in a domain determined principally by arbitrary rules about information and symbolic communication whilst the +operation of physical systems is governed by the laws of physics. +Finally, software is readily changeable and thus is changed, it is used in settings where our uncertainty leads us to anticipate the need to change.

+
+

-- Prof. Anthony Finkelstein, UCL Dean of Engineering, and Professor of Software Systems Engineering

+
+
+
+
+
+
+

The Agile Manifesto

+
+
+
+
+
+
+

In 2001, authors including Martin Fowler, Ward Cunningham and Kent Beck met in a Utah ski resort, and published the following manifesto.

+

Manifesto for Agile Software Development

+

We are uncovering better ways of developing +software by doing it and helping others do it. +Through this work we have come to value:

+
    +
  • Individuals and interactions over processes and tools
  • +
  • Working software over comprehensive documentation
  • +
  • Customer collaboration over contract negotiation
  • +
  • Responding to change over following a plan
  • +
+

That is, while there is value in the items on +the right, we value the items on the left more.

+
+
+
+
+
+
+

Agile is not absence of process

+
+
+
+
+
+
+
+

The Agile movement is not anti-methodology, in fact, many of us want to restore credibility to the word methodology. +We want to restore a balance. We embrace modeling, but not in order to file some diagram in a dusty corporate repository. +We embrace documentation, but not hundreds of pages of never-maintained and rarely-used tomes. We plan, but recognize the +limits of planning in a turbulent environment. Those who would brand proponents of XP or SCRUM or any of the other +Agile Methodologies as "hackers" are ignorant of both the methodologies and the original definition of the term hacker

+
+

-- Jim Highsmith

+
+
+
+
+
+
+

Elements of an Agile Process

+
+
+
+
+
+
+
    +
  • Continuous delivery
  • +
  • Self-organising teams
  • +
  • Iterative development
  • +
  • Ongoing design
  • +
+
+
+
+
+
+
+

Ongoing Design

+
+
+
+
+
+
+

Agile development doesn't eschew design. Design documents should still be written, but treated as living documents, +updated as more insight is gained into the task, as work is done, and as requirements change.

+

Use of a Wiki or version control repository to store design documents thus works much better than using Word documents!

+

Test-driven design and refactoring are essential techniques to ensure that lack of "Big Design Up Front" doesn't produce +badly constructed spaghetti software which doesn't meet requirements. By continously scouring our code for smells, and +stopping to refactor, we evolve towards a well-structured design with weakly interacting units. By starting with tests +which describe how our code should behave, we create executable specifications, giving us confidence that the code does +what it is supposed to.

+
+
+
+
+
+
+

Iterative Development

+
+
+
+
+
+
+

Agile development maintains a backlog of features to be completed and bugs to be fixed. In each iteration, we start with a meeting where +we decide which backlog tasks will be attempted during the development cycle, estimating how long each will take, +and selecting an achievable set of goals for the "sprint". At the end of each cycle, we review the goals completed and missed, +and consider what went well, what went badly, and what could be improved.

+

We try not to add work to a cycle mid-sprint. New tasks that emerge are added to the backlog, and considered in the next planning meeting. +This reduces stress and distraction.

+
+
+
+
+
+
+

Continuous Delivery

+
+
+
+
+
+
+

In agile development, we try to get as quickly as possible to code that can be demonstrated to clients. A regular demo of progress +to clients at the end of each development iteration says so much more than sharing a design document. "Release early, release often" +is a common slogan. Most bugs are found by people using code -- so exposing code to users as early as possible will help find bugs quickly.

+
+
+
+
+
+
+

Self-organising teams

+
+
+
+
+
+
+

Code is created by people. People work best when they feel ownership and pride in their work. Division of responsiblities into designers +and programmers results in a "Code Monkey" role, where the craftspersonship and +sense of responsibility for code quality is lost. Agile approaches encourage programmers, designers, clients, and businesspeople to see +themselves as one team, working together, with fluid roles. Programmers grab issues from the backlog according to interest, aptitude, +and community spirit.

+
+
+
+
+
+
+

Agile in Research

+
+
+
+
+
+
+

Agile approaches, where we try to turn the instincts and practices which emerge naturally when smart programmers get together into +well-formulated best practices, have emerged as antidotes to both the chaotic free-form typing in of code, and the rigid +paperwork-driven approaches of Waterfall.

+

If these approaches have turned out to be better even in industrial contexts, where requirements for code can be well understood, +they are even more appropriate in a research context, where we are working in poorly understood fields with even less well captured +requirements.

+
+
+
+
+
+
+

Conclusion

+
+
+
+
+
+
+
    +
  • Don't ignore design.
  • +
  • See if there's a known design pattern that will help.
  • +
  • Do try to think about how your code will work before you start typing.
  • +
  • Do use design tools like UML to think about your design without coding straight away.
  • +
  • Do try to write down some user stories.
  • +
  • Do maintain design documents.
  • +
+

BUT

+
    +
  • Do change your design as you work, updating the documents if you have them.
  • +
  • Don't go dark -- never do more than a couple of weeks programming without showing what you've done to colleagues.
  • +
  • Don't get isolated from the reasons for your code's existence, stay involved in the research, don't be a Code Monkey.
  • +
  • Do keep a list of all the things your code needs, estimate and prioritise tasks carefully.
  • +
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/05Process.ipynb b/ch04packaging/05Process.ipynb new file mode 100644 index 000000000..49bcfc49d --- /dev/null +++ b/ch04packaging/05Process.ipynb @@ -0,0 +1,414 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8f3dc7ba", + "metadata": {}, + "source": [ + "## Software Project Management" + ] + }, + { + "cell_type": "markdown", + "id": "aa0fc110", + "metadata": {}, + "source": [ + "### Software Engineering Stages" + ] + }, + { + "cell_type": "markdown", + "id": "f76e0343", + "metadata": {}, + "source": [ + "\n", + "* Requirements\n", + "* Functional Design\n", + "* Architectural Design\n", + "* Implementation\n", + "* Integration\n" + ] + }, + { + "cell_type": "markdown", + "id": "94146e08", + "metadata": {}, + "source": [ + "### Requirements Engineering" + ] + }, + { + "cell_type": "markdown", + "id": "b2304feb", + "metadata": {}, + "source": [ + "\n", + "Requirements capture obviously means describing the things the software needs to be able to do.\n", + "\n", + "A common approach is to write down lots of \"user stories\", describing how the software helps the user achieve something:\n", + "\n", + "> As a clinician, when I finish an analysis, I want a report to be created on the test results, so that I can\n", + "> send it to the patient.\n", + "\n", + "As a *role*, when *condition or circumstance applies* I want *a goal or desire* so that *benefits occur*.\n", + "\n", + "These are easy to map into the [Gherkin behaviour driven design test language](https://en.wikipedia.org/wiki/Behavior-driven_development).\n" + ] + }, + { + "cell_type": "markdown", + "id": "2c4996d4", + "metadata": {}, + "source": [ + "### Functional and architectural design" + ] + }, + { + "cell_type": "markdown", + "id": "61c001de", + "metadata": {}, + "source": [ + "\n", + "Engineers try to separate the functional design, how the software appears to and is used by the user, from the\n", + "architectural design, how the software achieves that functionality.\n", + "\n", + "Changes to functional design require users to adapt, and are thus often more costly than changes to architectural design.\n" + ] + }, + { + "cell_type": "markdown", + "id": "f9ccf054", + "metadata": {}, + "source": [ + "### Waterfall" + ] + }, + { + "cell_type": "markdown", + "id": "ae5084f8", + "metadata": {}, + "source": [ + "\n", + "The _Waterfall_ design philosophy argues that the elements of design should occur in order: first requirements capture, then functional design,\n", + "then architectural design. This approach is based on the idea that if a mistake is made in the design, then programming effort is wasted,\n", + "so significant effort is spent in trying to ensure that requirements are well understood and that the design is correct before programming starts.\n" + ] + }, + { + "cell_type": "markdown", + "id": "efd529f0", + "metadata": {}, + "source": [ + "### Why Waterfall?" + ] + }, + { + "cell_type": "markdown", + "id": "e88d7820", + "metadata": {}, + "source": [ + "\n", + "Without a design approach, programmers resort to designing as we go, typing in code, trying what works, and making it up as we go along.\n", + "When trying to collaborate to make software with others this can result in lots of wasted time, software that only the author understands,\n", + "components built by colleagues that don't work together, or code that the programmer thinks is nice but that doesn't meet the user's requirements.\n" + ] + }, + { + "cell_type": "markdown", + "id": "fb22728c", + "metadata": {}, + "source": [ + "### Problems with Waterfall" + ] + }, + { + "cell_type": "markdown", + "id": "3e78c6ef", + "metadata": {}, + "source": [ + "\n", + "Waterfall results in a contractual approach to development, building an us-and-them relationship between users, business types, designers, and programmers.\n", + "\n", + "> I built what the design said, so I did my job.\n", + "\n", + "Waterfall results in a paperwork culture, where people spend a long time designing standard forms to document each stage of the design,\n", + "with less time actually spent *making things*.\n", + "\n", + "Waterfall results in excessive adherence to a plan, even when mistakes in the design are obvious to people doing the work.\n" + ] + }, + { + "cell_type": "markdown", + "id": "bf590088", + "metadata": {}, + "source": [ + "### Software is not made of bricks" + ] + }, + { + "cell_type": "markdown", + "id": "27772ebe", + "metadata": {}, + "source": [ + "\n", + "The waterfall approach to software engineering comes from the engineering tradition applied to building physical objects,\n", + "where Architects and Engineers design buildings, and builders build them according to the design.\n", + "\n", + "Software is intrinsically different: **software is not made of bricks.**\n" + ] + }, + { + "cell_type": "markdown", + "id": "e92fb74e", + "metadata": {}, + "source": [ + "\n", + "> Software is not the same 'stuff' as that from which physical systems are constructed.\n", + "Software systems differ in material respects from physical systems.\n", + "Much of this has been rehearsed by Fred Brooks in his classic\n", + "['No Silver Bullet'](http://www.cs.unc.edu/techreports/86-020.pdf) paper.\n", + ">\n", + ">First, complexity and scale are different in the case of software systems: relatively functionally simple software systems comprise more independent parts, placed\n", + "in relation to each other, than do physical systems of equivalent functional value.\n", + ">\n", + ">Second, and clearly linked to this, we do not have well developed components and composition mechanisms from which to build\n", + "software systems (though clearly we are working hard on providing these) nor do we have a straightforward mathematical account that\n", + "permits us to reason about the effects of composition.\n", + ">\n", + ">\n", + "> Third, software systems operate in a domain determined principally by arbitrary rules about information and symbolic communication whilst the\n", + "operation of physical systems is governed by the laws of physics.\n", + "Finally, software is readily changeable and thus is changed, it is used in settings where our uncertainty leads us to anticipate the need to change.\n", + "\n", + "-- Prof. [Anthony Finkelstein](http://www0.cs.ucl.ac.uk/staff/A.Finkelstein/), UCL Dean of Engineering, and Professor of Software Systems Engineering" + ] + }, + { + "cell_type": "markdown", + "id": "cac2f70a", + "metadata": {}, + "source": [ + "### The Agile Manifesto" + ] + }, + { + "cell_type": "markdown", + "id": "0dbdb41c", + "metadata": {}, + "source": [ + "\n", + "In 2001, authors including Martin Fowler, Ward Cunningham and Kent Beck met in a Utah ski resort, and published the following manifesto.\n", + "\n", + " [Manifesto for Agile Software Development](http://agilemanifesto.org/)\n", + "\n", + " We are uncovering better ways of developing\n", + " software by doing it and helping others do it.\n", + " Through this work we have come to value:\n", + "\n", + " * _Individuals and interactions_ over processes and tools\n", + " * _Working software_ over comprehensive documentation\n", + " * _Customer collaboration_ over contract negotiation\n", + " * _Responding to change_ over following a plan\n", + "\n", + " That is, while there is value in the items on\n", + " the right, we value the items on the left more.\n" + ] + }, + { + "cell_type": "markdown", + "id": "80db8e2c", + "metadata": {}, + "source": [ + "### Agile is not absence of process" + ] + }, + { + "cell_type": "markdown", + "id": "71dc1492", + "metadata": {}, + "source": [ + "\n", + "> The Agile movement is not anti-methodology, in fact, many of us want to restore credibility to the word methodology.\n", + "> We want to restore a balance. We embrace modeling, but not in order to file some diagram in a dusty corporate repository.\n", + "> We embrace documentation, but not hundreds of pages of never-maintained and rarely-used tomes. We plan, but recognize the\n", + "> limits of planning in a turbulent environment. Those who would brand proponents of XP or SCRUM or any of the other\n", + "> Agile Methodologies as \"hackers\" are ignorant of both the methodologies and the original definition of the term hacker\n", + "\n", + "-- Jim Highsmith\n" + ] + }, + { + "cell_type": "markdown", + "id": "9e67daa0", + "metadata": {}, + "source": [ + "### Elements of an Agile Process" + ] + }, + { + "cell_type": "markdown", + "id": "48adf0c3", + "metadata": {}, + "source": [ + "\n", + "* Continuous delivery\n", + "* Self-organising teams\n", + "* Iterative development\n", + "* Ongoing design\n" + ] + }, + { + "cell_type": "markdown", + "id": "3b8ac5d1", + "metadata": {}, + "source": [ + "### Ongoing Design" + ] + }, + { + "cell_type": "markdown", + "id": "e83a3480", + "metadata": {}, + "source": [ + "\n", + "Agile development doesn't eschew design. Design documents should still be written, but treated as living documents,\n", + "updated as more insight is gained into the task, as work is done, and as requirements change.\n", + "\n", + "Use of a Wiki or version control repository to store design documents thus works much better than using Word documents!\n", + "\n", + "Test-driven design and refactoring are essential techniques to ensure that lack of \"Big Design Up Front\" doesn't produce\n", + "badly constructed spaghetti software which doesn't meet requirements. By continously scouring our code for [smells](https://en.wikipedia.org/wiki/Code_smell), and\n", + "stopping to refactor, we evolve towards a well-structured design with weakly interacting units. By starting with tests\n", + "which describe how our code should behave, we create executable specifications, giving us confidence that the code does\n", + "what it is supposed to.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8e018f1c", + "metadata": {}, + "source": [ + "### Iterative Development" + ] + }, + { + "cell_type": "markdown", + "id": "16037f7b", + "metadata": {}, + "source": [ + "\n", + "Agile development maintains a backlog of features to be completed and bugs to be fixed. In each iteration, we start with a meeting where\n", + "we decide which backlog tasks will be attempted during the development cycle, estimating how long each will take,\n", + "and selecting an achievable set of goals for the \"sprint\". At the end of each cycle, we review the goals completed and missed,\n", + "and consider what went well, what went badly, and what could be improved.\n", + "\n", + "We try not to add work to a cycle mid-sprint. New tasks that emerge are added to the backlog, and considered in the next planning meeting.\n", + "This reduces stress and distraction.\n" + ] + }, + { + "cell_type": "markdown", + "id": "316edde2", + "metadata": {}, + "source": [ + "### Continuous Delivery" + ] + }, + { + "cell_type": "markdown", + "id": "57d4c7eb", + "metadata": {}, + "source": [ + "\n", + "In agile development, we try to get as quickly as possible to code that can be *demonstrated* to clients. A regular demo of progress\n", + "to clients at the end of each development iteration says so much more than sharing a design document. \"Release early, release often\"\n", + "is a common slogan. Most bugs are found by people *using* code -- so exposing code to users as early as possible will help find bugs quickly.\n" + ] + }, + { + "cell_type": "markdown", + "id": "6b32e86c", + "metadata": {}, + "source": [ + "### Self-organising teams" + ] + }, + { + "cell_type": "markdown", + "id": "e77252ee", + "metadata": {}, + "source": [ + "\n", + "Code is created by people. People work best when they feel ownership and pride in their work. Division of responsiblities into designers\n", + "and programmers results in a [\"Code Monkey\"](https://youtu.be/MNl3fTods9c) role, where the craftspersonship and \n", + "sense of responsibility for code quality is lost. Agile approaches encourage programmers, designers, clients, and businesspeople to see\n", + "themselves as one team, working together, with fluid roles. Programmers grab issues from the backlog according to interest, aptitude,\n", + "and community spirit.\n" + ] + }, + { + "cell_type": "markdown", + "id": "fa161a3a", + "metadata": {}, + "source": [ + "### Agile in Research" + ] + }, + { + "cell_type": "markdown", + "id": "70015079", + "metadata": {}, + "source": [ + "\n", + "Agile approaches, where we try to turn the instincts and practices which emerge naturally when smart programmers get together into\n", + "well-formulated best practices, have emerged as antidotes to both the chaotic free-form typing in of code, and the rigid\n", + "paperwork-driven approaches of Waterfall.\n", + "\n", + "If these approaches have turned out to be better even in industrial contexts, where requirements for code can be well understood,\n", + "they are even more appropriate in a research context, where we are working in poorly understood fields with even less well captured\n", + "requirements.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c728f723", + "metadata": {}, + "source": [ + "### Conclusion" + ] + }, + { + "cell_type": "markdown", + "id": "d27899f9", + "metadata": {}, + "source": [ + "\n", + "* Don't ignore design.\n", + "* See if there's a known design pattern that will help.\n", + "* Do try to think about how your code will work before you start typing.\n", + "* Do use design tools like UML to think about your design without coding straight away.\n", + "* Do try to write down some user stories.\n", + "* Do maintain design documents.\n", + "\n", + "BUT\n", + "\n", + "* Do change your design as you work, updating the documents if you have them.\n", + "* Don't go dark -- never do more than a couple of weeks programming without showing what you've done to colleagues.\n", + "* Don't get isolated from the reasons for your code's existence, stay involved in the research, don't be a Code Monkey.\n", + "* Do keep a list of all the things your code needs, estimate and prioritise tasks carefully.\n", + "\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Agile and Waterfall" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/05Process.ipynb.py b/ch04packaging/05Process.ipynb.py new file mode 100644 index 000000000..8f92232d7 --- /dev/null +++ b/ch04packaging/05Process.ipynb.py @@ -0,0 +1,257 @@ +# --- +# jupyter: +# jekyll: +# display_name: Agile and Waterfall +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Software Project Management + +# %% [markdown] +# ### Software Engineering Stages + +# %% [markdown] +# +# * Requirements +# * Functional Design +# * Architectural Design +# * Implementation +# * Integration +# + +# %% [markdown] +# ### Requirements Engineering + +# %% [markdown] +# +# Requirements capture obviously means describing the things the software needs to be able to do. +# +# A common approach is to write down lots of "user stories", describing how the software helps the user achieve something: +# +# > As a clinician, when I finish an analysis, I want a report to be created on the test results, so that I can +# > send it to the patient. +# +# As a *role*, when *condition or circumstance applies* I want *a goal or desire* so that *benefits occur*. +# +# These are easy to map into the [Gherkin behaviour driven design test language](https://en.wikipedia.org/wiki/Behavior-driven_development). +# + +# %% [markdown] +# ### Functional and architectural design + +# %% [markdown] +# +# Engineers try to separate the functional design, how the software appears to and is used by the user, from the +# architectural design, how the software achieves that functionality. +# +# Changes to functional design require users to adapt, and are thus often more costly than changes to architectural design. +# + +# %% [markdown] +# ### Waterfall + +# %% [markdown] +# +# The _Waterfall_ design philosophy argues that the elements of design should occur in order: first requirements capture, then functional design, +# then architectural design. This approach is based on the idea that if a mistake is made in the design, then programming effort is wasted, +# so significant effort is spent in trying to ensure that requirements are well understood and that the design is correct before programming starts. +# + +# %% [markdown] +# ### Why Waterfall? + +# %% [markdown] +# +# Without a design approach, programmers resort to designing as we go, typing in code, trying what works, and making it up as we go along. +# When trying to collaborate to make software with others this can result in lots of wasted time, software that only the author understands, +# components built by colleagues that don't work together, or code that the programmer thinks is nice but that doesn't meet the user's requirements. +# + +# %% [markdown] +# ### Problems with Waterfall + +# %% [markdown] +# +# Waterfall results in a contractual approach to development, building an us-and-them relationship between users, business types, designers, and programmers. +# +# > I built what the design said, so I did my job. +# +# Waterfall results in a paperwork culture, where people spend a long time designing standard forms to document each stage of the design, +# with less time actually spent *making things*. +# +# Waterfall results in excessive adherence to a plan, even when mistakes in the design are obvious to people doing the work. +# + +# %% [markdown] +# ### Software is not made of bricks + +# %% [markdown] +# +# The waterfall approach to software engineering comes from the engineering tradition applied to building physical objects, +# where Architects and Engineers design buildings, and builders build them according to the design. +# +# Software is intrinsically different: **software is not made of bricks.** +# + +# %% [markdown] +# +# > Software is not the same 'stuff' as that from which physical systems are constructed. +# Software systems differ in material respects from physical systems. +# Much of this has been rehearsed by Fred Brooks in his classic +# ['No Silver Bullet'](http://www.cs.unc.edu/techreports/86-020.pdf) paper. +# > +# >First, complexity and scale are different in the case of software systems: relatively functionally simple software systems comprise more independent parts, placed +# in relation to each other, than do physical systems of equivalent functional value. +# > +# >Second, and clearly linked to this, we do not have well developed components and composition mechanisms from which to build +# software systems (though clearly we are working hard on providing these) nor do we have a straightforward mathematical account that +# permits us to reason about the effects of composition. +# > +# > +# > Third, software systems operate in a domain determined principally by arbitrary rules about information and symbolic communication whilst the +# operation of physical systems is governed by the laws of physics. +# Finally, software is readily changeable and thus is changed, it is used in settings where our uncertainty leads us to anticipate the need to change. +# +# -- Prof. [Anthony Finkelstein](http://www0.cs.ucl.ac.uk/staff/A.Finkelstein/), UCL Dean of Engineering, and Professor of Software Systems Engineering + +# %% [markdown] +# ### The Agile Manifesto + +# %% [markdown] +# +# In 2001, authors including Martin Fowler, Ward Cunningham and Kent Beck met in a Utah ski resort, and published the following manifesto. +# +# [Manifesto for Agile Software Development](http://agilemanifesto.org/) +# +# We are uncovering better ways of developing +# software by doing it and helping others do it. +# Through this work we have come to value: +# +# * _Individuals and interactions_ over processes and tools +# * _Working software_ over comprehensive documentation +# * _Customer collaboration_ over contract negotiation +# * _Responding to change_ over following a plan +# +# That is, while there is value in the items on +# the right, we value the items on the left more. +# + +# %% [markdown] +# ### Agile is not absence of process + +# %% [markdown] +# +# > The Agile movement is not anti-methodology, in fact, many of us want to restore credibility to the word methodology. +# > We want to restore a balance. We embrace modeling, but not in order to file some diagram in a dusty corporate repository. +# > We embrace documentation, but not hundreds of pages of never-maintained and rarely-used tomes. We plan, but recognize the +# > limits of planning in a turbulent environment. Those who would brand proponents of XP or SCRUM or any of the other +# > Agile Methodologies as "hackers" are ignorant of both the methodologies and the original definition of the term hacker +# +# -- Jim Highsmith +# + +# %% [markdown] +# ### Elements of an Agile Process + +# %% [markdown] +# +# * Continuous delivery +# * Self-organising teams +# * Iterative development +# * Ongoing design +# + +# %% [markdown] +# ### Ongoing Design + +# %% [markdown] +# +# Agile development doesn't eschew design. Design documents should still be written, but treated as living documents, +# updated as more insight is gained into the task, as work is done, and as requirements change. +# +# Use of a Wiki or version control repository to store design documents thus works much better than using Word documents! +# +# Test-driven design and refactoring are essential techniques to ensure that lack of "Big Design Up Front" doesn't produce +# badly constructed spaghetti software which doesn't meet requirements. By continously scouring our code for [smells](https://en.wikipedia.org/wiki/Code_smell), and +# stopping to refactor, we evolve towards a well-structured design with weakly interacting units. By starting with tests +# which describe how our code should behave, we create executable specifications, giving us confidence that the code does +# what it is supposed to. +# + +# %% [markdown] +# ### Iterative Development + +# %% [markdown] +# +# Agile development maintains a backlog of features to be completed and bugs to be fixed. In each iteration, we start with a meeting where +# we decide which backlog tasks will be attempted during the development cycle, estimating how long each will take, +# and selecting an achievable set of goals for the "sprint". At the end of each cycle, we review the goals completed and missed, +# and consider what went well, what went badly, and what could be improved. +# +# We try not to add work to a cycle mid-sprint. New tasks that emerge are added to the backlog, and considered in the next planning meeting. +# This reduces stress and distraction. +# + +# %% [markdown] +# ### Continuous Delivery + +# %% [markdown] +# +# In agile development, we try to get as quickly as possible to code that can be *demonstrated* to clients. A regular demo of progress +# to clients at the end of each development iteration says so much more than sharing a design document. "Release early, release often" +# is a common slogan. Most bugs are found by people *using* code -- so exposing code to users as early as possible will help find bugs quickly. +# + +# %% [markdown] +# ### Self-organising teams + +# %% [markdown] +# +# Code is created by people. People work best when they feel ownership and pride in their work. Division of responsiblities into designers +# and programmers results in a ["Code Monkey"](https://youtu.be/MNl3fTods9c) role, where the craftspersonship and +# sense of responsibility for code quality is lost. Agile approaches encourage programmers, designers, clients, and businesspeople to see +# themselves as one team, working together, with fluid roles. Programmers grab issues from the backlog according to interest, aptitude, +# and community spirit. +# + +# %% [markdown] +# ### Agile in Research + +# %% [markdown] +# +# Agile approaches, where we try to turn the instincts and practices which emerge naturally when smart programmers get together into +# well-formulated best practices, have emerged as antidotes to both the chaotic free-form typing in of code, and the rigid +# paperwork-driven approaches of Waterfall. +# +# If these approaches have turned out to be better even in industrial contexts, where requirements for code can be well understood, +# they are even more appropriate in a research context, where we are working in poorly understood fields with even less well captured +# requirements. +# + +# %% [markdown] +# ### Conclusion + +# %% [markdown] +# +# * Don't ignore design. +# * See if there's a known design pattern that will help. +# * Do try to think about how your code will work before you start typing. +# * Do use design tools like UML to think about your design without coding straight away. +# * Do try to write down some user stories. +# * Do maintain design documents. +# +# BUT +# +# * Do change your design as you work, updating the documents if you have them. +# * Don't go dark -- never do more than a couple of weeks programming without showing what you've done to colleagues. +# * Don't get isolated from the reasons for your code's existence, stay involved in the research, don't be a Code Monkey. +# * Do keep a list of all the things your code needs, estimate and prioritise tasks carefully. +# +# diff --git a/ch04packaging/06Issues.html b/ch04packaging/06Issues.html new file mode 100644 index 000000000..fed69b58d --- /dev/null +++ b/ch04packaging/06Issues.html @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Issue Tracking + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Managing software issues

+
+
+
+
+
+
+

Issues

Code has bugs. It also has features, things it should do.

+

A good project has an organised way of managing these. Generally you should use an issue tracker.

+
+
+
+
+
+
+

Some Issue Trackers

There are lots of good issue trackers.

+

The most commonly used open source ones are Trac and Redmine.

+

Cloud based issue trackers include Lighthouse and GitHub.

+

Commercial solutions include Jira.

+
+
+
+
+
+
+

Anatomy of an issue

    +
  • Reporter
  • +
  • Description
  • +
  • Owner
  • +
  • Type [Bug, Feature]
  • +
  • Component
  • +
  • Status
  • +
  • Severity
  • +
+
+
+
+
+
+
+

Reporting a Bug

The description should make the bug reproducible:

+
    +
  • Version
  • +
  • Steps
  • +
+

If possible, submit a minimal reproducing code fragment - look at this detailed answer about how to create a minimal example for $LaTeX$.

+
+
+
+
+
+
+

Owning an issue

    +
  • Whoever the issue is assigned to works next.
  • +
  • If an issue needs someone else's work, assign it to them.
  • +
+
+
+
+
+
+
+

Status

    +
  • Submitted
  • +
  • Accepted
  • +
  • Underway
  • +
  • Blocked
  • +
+
+
+
+
+
+
+

Resolutions

    +
  • Resolved
  • +
  • Will Not Fix
  • +
  • Not reproducible
  • +
  • Not a bug (working as intended)
  • +
+
+
+
+
+
+
+

Bug triage

Some organisations use a severity matrix based on:

+
    +
  • Severity [Wrong answer, crash, unusable, workaround, cosmetic...]
  • +
  • Frequency [All users, most users, some users...]
  • +
+
+
+
+
+
+
+

The backlog

The list of all the bugs that need to be fixed or +features that have been requested is called the "backlog".

+
+
+
+
+
+
+

Development cycles

Development goes in cycles.

+

Cycles range in length from a week to three months.

+

In a given cycle:

+
    +
  • Decide which features should be implemented
  • +
  • Decide which bugs should be fixed
  • +
  • Move these issues from the Backlog into the current cycle. (Aka Sprint)
  • +
+
+
+
+
+
+
+

GitHub issues

GitHub doesn't have separate fields for status, component, severity etc. +Instead, it just has labels, which you can create and delete.

+

See for example Jupyter.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/06Issues.ipynb b/ch04packaging/06Issues.ipynb new file mode 100644 index 000000000..b5ae2fde4 --- /dev/null +++ b/ch04packaging/06Issues.ipynb @@ -0,0 +1,174 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "eb7233f7", + "metadata": {}, + "source": [ + "## Managing software issues" + ] + }, + { + "cell_type": "markdown", + "id": "5c6ebd9c", + "metadata": {}, + "source": [ + "### Issues\n", + "\n", + "Code has *bugs*. It also has *features*, things it should do.\n", + "\n", + "A good project has an organised way of managing these. Generally you should use an issue tracker." + ] + }, + { + "cell_type": "markdown", + "id": "a6e88611", + "metadata": {}, + "source": [ + "### Some Issue Trackers\n", + "\n", + "There are lots of good issue trackers.\n", + "\n", + "The most commonly used open source ones are [Trac](http://trac.edgewall.org/) and [Redmine](http://www.redmine.org/).\n", + "\n", + "Cloud based issue trackers include [Lighthouse](http://lighthouseapp.com/) and [GitHub](https://github.com/blog/831-issues-2-0-the-next-generation).\n", + "\n", + "Commercial solutions include [Jira](https://www.atlassian.com/software/jira)." + ] + }, + { + "cell_type": "markdown", + "id": "d8797d02", + "metadata": {}, + "source": [ + "### Anatomy of an issue\n", + "\n", + "* Reporter\n", + "* Description\n", + "* Owner\n", + "* Type [Bug, Feature]\n", + "* Component\n", + "* Status\n", + "* Severity" + ] + }, + { + "cell_type": "markdown", + "id": "8d690435", + "metadata": {}, + "source": [ + "### Reporting a Bug\n", + "\n", + "The description should make the bug reproducible:\n", + "\n", + "* Version\n", + "* Steps\n", + "\n", + "If possible, submit a minimal reproducing code fragment - look at this detailed answer about [how to create a minimal example for $LaTeX$](https://tex.meta.stackexchange.com/a/3225/10934)." + ] + }, + { + "cell_type": "markdown", + "id": "3b9ea027", + "metadata": {}, + "source": [ + "### Owning an issue\n", + "\n", + "* Whoever the issue is assigned to works next.\n", + "* If an issue needs someone else's work, assign it to them." + ] + }, + { + "cell_type": "markdown", + "id": "d006372d", + "metadata": {}, + "source": [ + "### Status \n", + "\n", + "* Submitted\n", + "* Accepted\n", + "* Underway\n", + "* Blocked" + ] + }, + { + "cell_type": "markdown", + "id": "805bc425", + "metadata": {}, + "source": [ + "### Resolutions\n", + "\n", + "* Resolved\n", + "* Will Not Fix\n", + "* Not reproducible\n", + "* Not a bug (working as intended)" + ] + }, + { + "cell_type": "markdown", + "id": "a84c8cc8", + "metadata": {}, + "source": [ + "### Bug triage\n", + "\n", + "Some organisations use a severity matrix based on:\n", + "\n", + "* Severity [Wrong answer, crash, unusable, workaround, cosmetic...]\n", + "* Frequency [All users, most users, some users...]" + ] + }, + { + "cell_type": "markdown", + "id": "32dde2d7", + "metadata": {}, + "source": [ + "### The backlog\n", + "\n", + "The list of all the bugs that need to be fixed or\n", + "features that have been requested is called the \"backlog\"." + ] + }, + { + "cell_type": "markdown", + "id": "55bf3100", + "metadata": {}, + "source": [ + "### Development cycles\n", + "\n", + "Development goes in *cycles*.\n", + "\n", + "Cycles range in length from a week to three months.\n", + "\n", + "In a given cycle:\n", + "\n", + "* Decide which features should be implemented\n", + "* Decide which bugs should be fixed\n", + "* Move these issues from the Backlog into the current cycle. (Aka Sprint)" + ] + }, + { + "cell_type": "markdown", + "id": "8f22c449", + "metadata": {}, + "source": [ + "### GitHub issues\n", + "\n", + "GitHub doesn't have separate fields for status, component, severity etc.\n", + "Instead, it just has labels, which you can create and delete.\n", + "\n", + "See for example [Jupyter](https://github.com/jupyter/notebook/issues?page=1&state=open)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Issue Tracking" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/06Issues.ipynb.py b/ch04packaging/06Issues.ipynb.py new file mode 100644 index 000000000..15469da7c --- /dev/null +++ b/ch04packaging/06Issues.ipynb.py @@ -0,0 +1,111 @@ +# --- +# jupyter: +# jekyll: +# display_name: Issue Tracking +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Managing software issues + +# %% [markdown] +# ### Issues +# +# Code has *bugs*. It also has *features*, things it should do. +# +# A good project has an organised way of managing these. Generally you should use an issue tracker. + +# %% [markdown] +# ### Some Issue Trackers +# +# There are lots of good issue trackers. +# +# The most commonly used open source ones are [Trac](http://trac.edgewall.org/) and [Redmine](http://www.redmine.org/). +# +# Cloud based issue trackers include [Lighthouse](http://lighthouseapp.com/) and [GitHub](https://github.com/blog/831-issues-2-0-the-next-generation). +# +# Commercial solutions include [Jira](https://www.atlassian.com/software/jira). + +# %% [markdown] +# ### Anatomy of an issue +# +# * Reporter +# * Description +# * Owner +# * Type [Bug, Feature] +# * Component +# * Status +# * Severity + +# %% [markdown] +# ### Reporting a Bug +# +# The description should make the bug reproducible: +# +# * Version +# * Steps +# +# If possible, submit a minimal reproducing code fragment - look at this detailed answer about [how to create a minimal example for $LaTeX$](https://tex.meta.stackexchange.com/a/3225/10934). + +# %% [markdown] +# ### Owning an issue +# +# * Whoever the issue is assigned to works next. +# * If an issue needs someone else's work, assign it to them. + +# %% [markdown] +# ### Status +# +# * Submitted +# * Accepted +# * Underway +# * Blocked + +# %% [markdown] +# ### Resolutions +# +# * Resolved +# * Will Not Fix +# * Not reproducible +# * Not a bug (working as intended) + +# %% [markdown] +# ### Bug triage +# +# Some organisations use a severity matrix based on: +# +# * Severity [Wrong answer, crash, unusable, workaround, cosmetic...] +# * Frequency [All users, most users, some users...] + +# %% [markdown] +# ### The backlog +# +# The list of all the bugs that need to be fixed or +# features that have been requested is called the "backlog". + +# %% [markdown] +# ### Development cycles +# +# Development goes in *cycles*. +# +# Cycles range in length from a week to three months. +# +# In a given cycle: +# +# * Decide which features should be implemented +# * Decide which bugs should be fixed +# * Move these issues from the Backlog into the current cycle. (Aka Sprint) + +# %% [markdown] +# ### GitHub issues +# +# GitHub doesn't have separate fields for status, component, severity etc. +# Instead, it just has labels, which you can create and delete. +# +# See for example [Jupyter](https://github.com/jupyter/notebook/issues?page=1&state=open). diff --git a/ch04packaging/07Licensing.html b/ch04packaging/07Licensing.html new file mode 100644 index 000000000..3a9ce593f --- /dev/null +++ b/ch04packaging/07Licensing.html @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Licensing + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Software Licensing

+
+
+
+
+
+
+

Reuse

This course is distributed under the Creative Commons By Attribution license, which means you can modify and reuse the materials, so long as you credit UCL Research IT Services.

+
+
+
+
+
+
+

Disclaimer

Here we attempt to give some basic advice on choosing a licence for your software. But:

+
    +
  • we are NOT lawyers (IANAL),
  • +
  • opinions differ (and flamewars are boring),
  • +
  • this training does NOT constitute legal advice.
  • +
+

For an in-depth discussion of software licences, read the O'Reilly book Understanding Open Source and Free Software Licensing.

+

Your department, or UCL, may have policies about applying licences to code you create while a UCL employee or student. This training doesn't address this issue, and does not represent UCL policy -- seek advice from your supervisor or manager if concerned.

+
+
+
+
+
+
+

Choose a licence

It is important to choose a licence and to create a license file to tell people what it is.

+

The licence lets people know whether they can reuse your code and +under what terms. This course has one, for example.

+

Your licence file should typically be called LICENSE.txt or similar. GitHub will offer to create a licence file automatically when you create a new repository.

+
+
+
+
+
+
+

Open source doesn't stop you making money

A common misconception about open source software is the thought that +open source means you can't make any money. This is wrong.

+

Plenty of people open source their software and profit from:

+
    +
  • The software under a different licence e.g. Saxon
  • +
  • Consulting. For example: Anaconda who help maintain NumPy.
  • +
  • Manuals. For example: VTK.
  • +
  • Add-ons. For example: Puppet.
  • +
  • Server software, which open source client software interacts with. For example: GitHub API clients.
  • +
+
+
+
+
+
+
+

Plagiarism vs promotion

Many researchers worry about people stealing their work if they open source their code. But often the biggest problem is not theft, but the fact no one is aware of your work.

+

Open source is a way to increase the probability that someone else on the planet will care enough about your work to cite you.

+

So when thinking about whether to open source your code, think about whether you're more worried about +anonymity or theft.

+
+
+
+
+
+
+

Your code is good enough

New coders worry that they'll be laughed at if they put their code online. Don't worry. Everyone, including people who've been coding for decades, +writes shoddy code that is full of bugs.

+

The only thing that will make your code better, is other people reading it.

+

For small scripts that no one but you will ever use, +my recommendation is to use an open repository anyway. +Find a buddy, and get them to comment on it.

+
+
+
+
+
+
+

Worry about licence compatibility and proliferation

Not all open source code can be used in all projects. Some licences are legally incompatible.

+

This is a huge and annoying problem. +As an author, you might not care, but you can't anticipate the exciting uses people might find by +mixing your code with someone else's.

+

Use a standard licence from the small list that are well-used. +Then people will understand. Don't make up your own.

+

When you're about to use a licence, see if there's a more common one which is recommended, e.g.: +using the opensource.org proliferation report.

+
+
+
+
+
+
+

Academic licence proliferation

Academics often write their own licence terms for their software.

+

For example:

+
+

XXXX NON-COMMERCIAL EDUCATIONAL LICENSE +Copyright (c) 2013 Prof. Foo. +All rights reserved.

+

You may use and modify this software for any non-commercial purpose within your educational +institution. Teaching, academic research, and personal experimentation are examples of purpose +which can be non-commercial.

+

You may redistribute the software and modifications to the software for non-commercial +purposes, but only to eligible users of the software (for example, to another university +student or faculty to support joint academic research).

+
+

Please don't do this. Your desire to slightly tweak the terms is harmful to the +future software ecosystem. Also, Unless you are a lawyer, you cannot do this safely!

+
+
+
+
+
+
+

Licences for code, content, and data.

Licences designed for code should not be used to license data or prose.

+

Don't use Creative Commons for software, or GPL for a book.

+
+
+
+
+
+
+

Licensing issues

    +
  • Permissive vs share-alike
  • +
  • Non-commercial and academic Use Only
  • +
  • Patents
  • +
  • Use as a web service
  • +
+
+
+
+
+
+
+

Permissive vs share-alike

Some licences require all derived software to be licensed under terms that are similarly free. +Such licences are called "Share Alike" or "Copyleft".

+
    +
  • Licences in this class include the GPL.
  • +
+

Those that don't are called "Permissive"

+ +

If you want your code to be maximally reusable, use a permissive licence +If you want to force other people using your code to make derivatives open source, use a copyleft licence.

+

If you want to use code that has a permissive licence, it's safe to use it and keep your code secret. +If you want to use code that has a copyleft licence, you'll have to release your code under such a licence.

+
+
+
+
+
+
+

Academic use only

Some researchers want to make their code free for 'academic use only'. +None of the standard licences state this, and this is a reason why academic bespoke licences proliferate.

+

However, there is no need for this, in our opinion.

+

Use of a standard Copyleft licence precludes derived software from being sold without also publishing the source

+

So use of a Copyleft licence precludes commercial use.

+

This is a very common way of making a business from open source code: offer the code under GPL for free +but offer the code under more permissive terms, allowing for commercial use, for a fee.

+
+
+
+
+
+
+

Patents

Intellectual property law distinguishes copyright from patents. +This is a complex field, which I am far from qualified to teach!

+

People who think carefully about intellectual property law distinguish software licences +based on how they address patents. Very roughly, if you want to ensure that contributors to your project +can't then go off and patent their contribution, some licences, such as the Apache licence, protect you from this.

+
+
+
+
+
+
+

Use as a web service

If I take copyleft code, and use it to host a web service, I have not sold the software.

+

Therefore, under some licences, I do not have to release any derivative software. +This "loophole" in the GPL is closed by the AGPL ("Affero GPL")

+
+
+
+
+
+
+

Library linking

If I use your code just as a library, without modifying it or including it directly in my own code, +does the copyleft term of the GPL apply?

+

Yes

+

If you don't want it to, use the LGPL. ("Lesser GPL"). This has an exception for linking libraries.

+
+
+
+
+
+
+

Citing software

Almost all software licences require people to credit you for what they used ("attribution").

+

In an academic context, it is useful to offer a statement as to how best to do this, +citing which paper to cite in all papers which use the software.

+

This is best done with a CITATION file in your repository.

+
+
+
+
+
+
+
+

To cite ggplot2 in publications, please use:

+

H. Wickham. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York, 2016.

+

A BibTeX entry for LaTeX users is

+

@Book{, +author = {Hadley Wickham}, +title = {ggplot2: Elegant Graphics for Data Analysis}, +publisher = {Springer-Verlag New York}, +year = {2016}, +isbn = {978-3-319-24277-4}, +url = {https://ggplot2.tidyverse.org}, +}

+
+
+
+
+
+
+
+

Referencing the licence in every file

Some licences require that you include licence information in every file. +Others do not.

+

Typically, every file should contain something like:

+
# (C) University College London 2010-2014
+# This software is licenced under the terms of the <foo licence>
+# See <somewhere> for the licence details.
+
+
+
+
+
+
+
+

Check your licence at +opensource.org for details of how to apply it to your software. For example, for the GPL.

+
+
+
+
+
+
+
+
+
+

Open source does not equal free maintenance

One common misunderstanding of open source software is that you'll automatically get loads of contributors from around the internets. +This is wrong. Most open source projects get no commits from anyone else.

+

Open source does not guarantee your software will live on with people adding to it after you stop working on it.

+

Learn more about these issues from the website of the Software Sustainability Institute.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/07Licensing.ipynb b/ch04packaging/07Licensing.ipynb new file mode 100644 index 000000000..3c428dcc3 --- /dev/null +++ b/ch04packaging/07Licensing.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "65917a0f", + "metadata": {}, + "source": [ + "## Software Licensing" + ] + }, + { + "cell_type": "markdown", + "id": "9dec5314", + "metadata": {}, + "source": [ + "### Reuse\n", + "\n", + "This course is distributed under the [Creative Commons By Attribution license](https://creativecommons.org/licenses/by/3.0/), which means you can modify and reuse the materials, so long as you credit [UCL Research IT Services](https://www.ucl.ac.uk/research-it-services/)." + ] + }, + { + "cell_type": "markdown", + "id": "0c3dc067", + "metadata": {}, + "source": [ + "### Disclaimer\n", + "\n", + "Here we attempt to give some basic advice on choosing a licence for your software. But:\n", + "\n", + "* we are NOT lawyers ([IANAL](https://en.wikipedia.org/wiki/IANAL)),\n", + "* opinions differ (and flamewars are boring),\n", + "* this training does NOT constitute legal advice. \n", + "\n", + "For an in-depth discussion of software licences, read the O'Reilly book [Understanding Open Source and Free Software Licensing](https://www.oreilly.com/library/view/understanding-open-source/0596005814/).\n", + "\n", + "Your department, or UCL, may have policies about applying licences to code you create while a UCL employee or student. This training doesn't address this issue, and does not represent UCL policy -- seek advice from your supervisor or manager if concerned." + ] + }, + { + "cell_type": "markdown", + "id": "7532af52", + "metadata": {}, + "source": [ + "### Choose a licence\n", + "\n", + "It is important to choose a licence and to create a *license file* to tell people what it is. \n", + "\n", + "The licence lets people know whether they can reuse your code and\n", + "under what terms. [This course has one](https://github.com/UCL/rsd-engineeringcourse/blob/master/LICENSE.md), for example.\n", + "\n", + "Your licence file should typically be called LICENSE.txt or similar. GitHub will offer to create a licence file automatically when you create a new repository." + ] + }, + { + "cell_type": "markdown", + "id": "3e47f16e", + "metadata": {}, + "source": [ + "### Open source doesn't stop you making money\n", + "\n", + "A common misconception about open source software is the thought that\n", + "open source means you can't make any money. This is *wrong*. \n", + "\n", + "Plenty of people open source their software and profit from:\n", + "\n", + "* The software under a different licence e.g. [Saxon](http://saxon.sourceforge.net/)\n", + "* Consulting. For example: [Anaconda](https://www.anaconda.com/consulting/) who help maintain NumPy.\n", + "* Manuals. For example: [VTK](http://www.vtk.org/).\n", + "* Add-ons. For example: [Puppet](http://puppetlabs.com/puppet/enterprise-vs-open-source).\n", + "* Server software, which open source client software interacts with. For example: [GitHub API clients](https://github.com/octokit/octokit.rb)." + ] + }, + { + "cell_type": "markdown", + "id": "e7f0f02b", + "metadata": {}, + "source": [ + "### Plagiarism vs promotion\n", + "\n", + "Many researchers worry about people stealing their work if they open source their code. But often the biggest problem is not theft, but the fact no one is aware of your work.\n", + "\n", + "Open source is a way to increase the probability that someone else on the planet will care enough about your work to cite you.\n", + "\n", + "So when thinking about whether to open source your code, think about whether you're more worried about\n", + "anonymity or theft." + ] + }, + { + "cell_type": "markdown", + "id": "ab35eb6e", + "metadata": {}, + "source": [ + "### Your code *is* good enough\n", + "\n", + "New coders worry that they'll be laughed at if they put their code online. Don't worry. Everyone, including people who've been coding for decades, \n", + "writes shoddy code that is full of bugs.\n", + "\n", + "The only thing that will make your code better, is *other people reading it*. \n", + "\n", + "For small scripts that no one but you will ever use,\n", + "my recommendation is to use an open repository anyway. \n", + "Find a buddy, and get them to comment on it." + ] + }, + { + "cell_type": "markdown", + "id": "0b54f69a", + "metadata": {}, + "source": [ + "### Worry about licence compatibility and proliferation\n", + "\n", + "Not all open source code can be used in all projects. Some licences are legally incompatible.\n", + "\n", + "This is a huge and annoying problem. \n", + "As an author, you might not care, but you can't anticipate the exciting uses people might find by\n", + "mixing your code with someone else's. \n", + "\n", + "Use a standard licence from the small list that are well-used.\n", + "Then people will understand. *Don't make up your own*.\n", + "\n", + "When you're about to use a licence, see if there's a more common one which is recommended, e.g.:\n", + "using the [opensource.org proliferation report](http://opensource.org/proliferation-report)." + ] + }, + { + "cell_type": "markdown", + "id": "bbae4c2d", + "metadata": {}, + "source": [ + "### Academic licence proliferation\n", + "\n", + "Academics often write their own licence terms for their software.\n", + "\n", + "For example:\n", + "\n", + ">XXXX NON-COMMERCIAL EDUCATIONAL LICENSE\n", + ">Copyright (c) 2013 Prof. Foo.\n", + ">All rights reserved.\n", + ">\n", + ">You may use and modify this software for any non-commercial purpose within your educational \n", + ">institution. Teaching, academic research, and personal experimentation are examples of purpose \n", + ">which can be non-commercial.\n", + ">\n", + ">You may redistribute the software and modifications to the software for non-commercial \n", + ">purposes, but only to eligible users of the software (for example, to another university\n", + ">student or faculty to support joint academic research).\n", + "\n", + "Please don't do this. Your desire to slightly tweak the terms is harmful to the\n", + "future software ecosystem. Also, *Unless you are a lawyer, you cannot do this safely!*" + ] + }, + { + "cell_type": "markdown", + "id": "21581b4a", + "metadata": {}, + "source": [ + "### Licences for code, content, and data.\n", + "\n", + "Licences designed for code should not be used to license data or prose.\n", + "\n", + "Don't use Creative Commons for software, or GPL for a book." + ] + }, + { + "cell_type": "markdown", + "id": "5498e474", + "metadata": {}, + "source": [ + "### Licensing issues\n", + "\n", + "* Permissive vs share-alike\n", + "* Non-commercial and academic Use Only\n", + "* Patents\n", + "* Use as a web service" + ] + }, + { + "cell_type": "markdown", + "id": "1a0eb988", + "metadata": {}, + "source": [ + "### Permissive vs share-alike\n", + "\n", + "Some licences require all derived software to be licensed under terms that are similarly free.\n", + "Such licences are called \"Share Alike\" or \"Copyleft\".\n", + "\n", + "* Licences in this class include the [GPL](https://opensource.org/licenses/GPL-3.0).\n", + "\n", + "Those that don't are called \"Permissive\"\n", + "\n", + "* These include [Apache](https://opensource.org/licenses/Apache-2.0), [BSD](https://opensource.org/licenses/BSD-2-Clause), and [MIT](https://opensource.org/licenses/MIT) licences.\n", + "\n", + "If you want your code to be maximally reusable, use a permissive licence\n", + "If you want to force other people using your code to make derivatives open source, use a copyleft licence.\n", + "\n", + "If you want to use code that has a permissive licence, it's safe to use it and keep your code secret.\n", + "If you want to use code that has a copyleft licence, you'll have to release your code under such a licence." + ] + }, + { + "cell_type": "markdown", + "id": "c011c48d", + "metadata": {}, + "source": [ + "### Academic use only\n", + "\n", + "Some researchers want to make their code free for 'academic use only'.\n", + "None of the standard licences state this, and this is a reason why academic bespoke licences proliferate.\n", + "\n", + "However, there is no need for this, in our opinion.\n", + "\n", + "*Use of a standard Copyleft licence precludes derived software from being sold without also publishing the source*\n", + "\n", + "So use of a Copyleft licence precludes commercial use.\n", + "\n", + "This is a very common way of making a business from open source code: offer the code under GPL for free\n", + "but offer the code under more permissive terms, allowing for commercial use, for a fee." + ] + }, + { + "cell_type": "markdown", + "id": "eea2bf11", + "metadata": {}, + "source": [ + "### Patents\n", + "\n", + "Intellectual property law distinguishes copyright from patents. \n", + "This is a complex field, which I am far from qualified to teach!\n", + "\n", + "People who think carefully about intellectual property law distinguish software licences\n", + "based on how they address patents. Very roughly, if you want to ensure that contributors to your project\n", + "can't then go off and patent their contribution, some licences, such as the Apache licence, protect you from this." + ] + }, + { + "cell_type": "markdown", + "id": "6b237bbd", + "metadata": {}, + "source": [ + "### Use as a web service\n", + "\n", + "If I take copyleft code, and use it to host a web service, I have not sold the software.\n", + "\n", + "Therefore, under some licences, I do not have to release any derivative software.\n", + "This \"loophole\" in the GPL is closed by the AGPL (\"Affero GPL\")" + ] + }, + { + "cell_type": "markdown", + "id": "b3e65ffd", + "metadata": {}, + "source": [ + "### Library linking\n", + "\n", + "If I use your code just as a library, without modifying it or including it directly in my own code, \n", + "does the copyleft term of the GPL apply?\n", + "\n", + "*Yes*\n", + "\n", + "If you don't want it to, use the LGPL. (\"Lesser GPL\"). This has an exception for linking libraries." + ] + }, + { + "cell_type": "markdown", + "id": "db77fbe5", + "metadata": {}, + "source": [ + "### Citing software\n", + "\n", + "Almost all software licences require people to credit you for what they used (\"attribution\").\n", + "\n", + "In an academic context, it is useful to offer a statement as to how best to do this,\n", + "citing *which paper to cite in all papers which use the software*.\n", + "\n", + "This is best done with a [CITATION](http://www.software.ac.uk/blog/2013-09-02-encouraging-citation-software-introducing-citation-files) file in your repository." + ] + }, + { + "cell_type": "markdown", + "id": "41393dce", + "metadata": {}, + "source": [ + "> To cite ggplot2 in publications, please use:\n", + ">\n", + "> H. Wickham. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York, 2016.\n", + ">\n", + "> A BibTeX entry for LaTeX users is\n", + ">\n", + "> @Book{,\n", + " author = {Hadley Wickham},\n", + " title = {ggplot2: Elegant Graphics for Data Analysis},\n", + " publisher = {Springer-Verlag New York},\n", + " year = {2016},\n", + " isbn = {978-3-319-24277-4},\n", + " url = {https://ggplot2.tidyverse.org},\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "45ba6456", + "metadata": {}, + "source": [ + "### Referencing the licence in every file\n", + "\n", + "Some licences require that you include licence information in every file.\n", + "Others do not. \n", + "\n", + "Typically, every file should contain something like:\n", + "\n", + "```python\n", + "# (C) University College London 2010-2014\n", + "# This software is licenced under the terms of the \n", + "# See for the licence details.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "9fd79431", + "metadata": {}, + "source": [ + "Check your licence at\n", + "[opensource.org](http://opensource.org/) for details of how to apply it to your software. For example, for the [GPL](http://opensource.org/licenses/GPL-3.0#howto)." + ] + }, + { + "cell_type": "markdown", + "id": "434c9767", + "metadata": {}, + "source": [ + "### Choose a licence\n", + "\n", + "See [GitHub's advice on how to choose a licence](http://choosealicense.com/)." + ] + }, + { + "cell_type": "markdown", + "id": "2cc87b70", + "metadata": {}, + "source": [ + "### Open source does not equal free maintenance\n", + "\n", + "One common misunderstanding of open source software is that you'll automatically get loads of contributors from around the internets.\n", + "This is wrong. Most open source projects get no commits from anyone else.\n", + "\n", + "Open source does *not* guarantee your software will live on with people adding to it after you stop working on it.\n", + "\n", + "Learn more about these issues from the website of the [Software Sustainability Institute](https://software.ac.uk/resources)." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Licensing" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch04packaging/07Licensing.ipynb.py b/ch04packaging/07Licensing.ipynb.py new file mode 100644 index 000000000..10660c2d6 --- /dev/null +++ b/ch04packaging/07Licensing.ipynb.py @@ -0,0 +1,251 @@ +# --- +# jupyter: +# jekyll: +# display_name: Licensing +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Software Licensing + +# %% [markdown] +# ### Reuse +# +# This course is distributed under the [Creative Commons By Attribution license](https://creativecommons.org/licenses/by/3.0/), which means you can modify and reuse the materials, so long as you credit [UCL Research IT Services](https://www.ucl.ac.uk/research-it-services/). + +# %% [markdown] +# ### Disclaimer +# +# Here we attempt to give some basic advice on choosing a licence for your software. But: +# +# * we are NOT lawyers ([IANAL](https://en.wikipedia.org/wiki/IANAL)), +# * opinions differ (and flamewars are boring), +# * this training does NOT constitute legal advice. +# +# For an in-depth discussion of software licences, read the O'Reilly book [Understanding Open Source and Free Software Licensing](https://www.oreilly.com/library/view/understanding-open-source/0596005814/). +# +# Your department, or UCL, may have policies about applying licences to code you create while a UCL employee or student. This training doesn't address this issue, and does not represent UCL policy -- seek advice from your supervisor or manager if concerned. + +# %% [markdown] +# ### Choose a licence +# +# It is important to choose a licence and to create a *license file* to tell people what it is. +# +# The licence lets people know whether they can reuse your code and +# under what terms. [This course has one](https://github.com/UCL/rsd-engineeringcourse/blob/master/LICENSE.md), for example. +# +# Your licence file should typically be called LICENSE.txt or similar. GitHub will offer to create a licence file automatically when you create a new repository. + +# %% [markdown] +# ### Open source doesn't stop you making money +# +# A common misconception about open source software is the thought that +# open source means you can't make any money. This is *wrong*. +# +# Plenty of people open source their software and profit from: +# +# * The software under a different licence e.g. [Saxon](http://saxon.sourceforge.net/) +# * Consulting. For example: [Anaconda](https://www.anaconda.com/consulting/) who help maintain NumPy. +# * Manuals. For example: [VTK](http://www.vtk.org/). +# * Add-ons. For example: [Puppet](http://puppetlabs.com/puppet/enterprise-vs-open-source). +# * Server software, which open source client software interacts with. For example: [GitHub API clients](https://github.com/octokit/octokit.rb). + +# %% [markdown] +# ### Plagiarism vs promotion +# +# Many researchers worry about people stealing their work if they open source their code. But often the biggest problem is not theft, but the fact no one is aware of your work. +# +# Open source is a way to increase the probability that someone else on the planet will care enough about your work to cite you. +# +# So when thinking about whether to open source your code, think about whether you're more worried about +# anonymity or theft. + +# %% [markdown] +# ### Your code *is* good enough +# +# New coders worry that they'll be laughed at if they put their code online. Don't worry. Everyone, including people who've been coding for decades, +# writes shoddy code that is full of bugs. +# +# The only thing that will make your code better, is *other people reading it*. +# +# For small scripts that no one but you will ever use, +# my recommendation is to use an open repository anyway. +# Find a buddy, and get them to comment on it. + +# %% [markdown] +# ### Worry about licence compatibility and proliferation +# +# Not all open source code can be used in all projects. Some licences are legally incompatible. +# +# This is a huge and annoying problem. +# As an author, you might not care, but you can't anticipate the exciting uses people might find by +# mixing your code with someone else's. +# +# Use a standard licence from the small list that are well-used. +# Then people will understand. *Don't make up your own*. +# +# When you're about to use a licence, see if there's a more common one which is recommended, e.g.: +# using the [opensource.org proliferation report](http://opensource.org/proliferation-report). + +# %% [markdown] +# ### Academic licence proliferation +# +# Academics often write their own licence terms for their software. +# +# For example: +# +# >XXXX NON-COMMERCIAL EDUCATIONAL LICENSE +# >Copyright (c) 2013 Prof. Foo. +# >All rights reserved. +# > +# >You may use and modify this software for any non-commercial purpose within your educational +# >institution. Teaching, academic research, and personal experimentation are examples of purpose +# >which can be non-commercial. +# > +# >You may redistribute the software and modifications to the software for non-commercial +# >purposes, but only to eligible users of the software (for example, to another university +# >student or faculty to support joint academic research). +# +# Please don't do this. Your desire to slightly tweak the terms is harmful to the +# future software ecosystem. Also, *Unless you are a lawyer, you cannot do this safely!* + +# %% [markdown] +# ### Licences for code, content, and data. +# +# Licences designed for code should not be used to license data or prose. +# +# Don't use Creative Commons for software, or GPL for a book. + +# %% [markdown] +# ### Licensing issues +# +# * Permissive vs share-alike +# * Non-commercial and academic Use Only +# * Patents +# * Use as a web service + +# %% [markdown] +# ### Permissive vs share-alike +# +# Some licences require all derived software to be licensed under terms that are similarly free. +# Such licences are called "Share Alike" or "Copyleft". +# +# * Licences in this class include the [GPL](https://opensource.org/licenses/GPL-3.0). +# +# Those that don't are called "Permissive" +# +# * These include [Apache](https://opensource.org/licenses/Apache-2.0), [BSD](https://opensource.org/licenses/BSD-2-Clause), and [MIT](https://opensource.org/licenses/MIT) licences. +# +# If you want your code to be maximally reusable, use a permissive licence +# If you want to force other people using your code to make derivatives open source, use a copyleft licence. +# +# If you want to use code that has a permissive licence, it's safe to use it and keep your code secret. +# If you want to use code that has a copyleft licence, you'll have to release your code under such a licence. + +# %% [markdown] +# ### Academic use only +# +# Some researchers want to make their code free for 'academic use only'. +# None of the standard licences state this, and this is a reason why academic bespoke licences proliferate. +# +# However, there is no need for this, in our opinion. +# +# *Use of a standard Copyleft licence precludes derived software from being sold without also publishing the source* +# +# So use of a Copyleft licence precludes commercial use. +# +# This is a very common way of making a business from open source code: offer the code under GPL for free +# but offer the code under more permissive terms, allowing for commercial use, for a fee. + +# %% [markdown] +# ### Patents +# +# Intellectual property law distinguishes copyright from patents. +# This is a complex field, which I am far from qualified to teach! +# +# People who think carefully about intellectual property law distinguish software licences +# based on how they address patents. Very roughly, if you want to ensure that contributors to your project +# can't then go off and patent their contribution, some licences, such as the Apache licence, protect you from this. + +# %% [markdown] +# ### Use as a web service +# +# If I take copyleft code, and use it to host a web service, I have not sold the software. +# +# Therefore, under some licences, I do not have to release any derivative software. +# This "loophole" in the GPL is closed by the AGPL ("Affero GPL") + +# %% [markdown] +# ### Library linking +# +# If I use your code just as a library, without modifying it or including it directly in my own code, +# does the copyleft term of the GPL apply? +# +# *Yes* +# +# If you don't want it to, use the LGPL. ("Lesser GPL"). This has an exception for linking libraries. + +# %% [markdown] +# ### Citing software +# +# Almost all software licences require people to credit you for what they used ("attribution"). +# +# In an academic context, it is useful to offer a statement as to how best to do this, +# citing *which paper to cite in all papers which use the software*. +# +# This is best done with a [CITATION](http://www.software.ac.uk/blog/2013-09-02-encouraging-citation-software-introducing-citation-files) file in your repository. + +# %% [markdown] +# > To cite ggplot2 in publications, please use: +# > +# > H. Wickham. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York, 2016. +# > +# > A BibTeX entry for LaTeX users is +# > +# > @Book{, +# author = {Hadley Wickham}, +# title = {ggplot2: Elegant Graphics for Data Analysis}, +# publisher = {Springer-Verlag New York}, +# year = {2016}, +# isbn = {978-3-319-24277-4}, +# url = {https://ggplot2.tidyverse.org}, +# } + +# %% [markdown] +# ### Referencing the licence in every file +# +# Some licences require that you include licence information in every file. +# Others do not. +# +# Typically, every file should contain something like: +# +# ```python +# # (C) University College London 2010-2014 +# # This software is licenced under the terms of the +# # See for the licence details. +# ``` + +# %% [markdown] +# Check your licence at +# [opensource.org](http://opensource.org/) for details of how to apply it to your software. For example, for the [GPL](http://opensource.org/licenses/GPL-3.0#howto). + +# %% [markdown] +# ### Choose a licence +# +# See [GitHub's advice on how to choose a licence](http://choosealicense.com/). + +# %% [markdown] +# ### Open source does not equal free maintenance +# +# One common misunderstanding of open source software is that you'll automatically get loads of contributors from around the internets. +# This is wrong. Most open source projects get no commits from anyone else. +# +# Open source does *not* guarantee your software will live on with people adding to it after you stop working on it. +# +# Learn more about these issues from the website of the [Software Sustainability Institute](https://software.ac.uk/resources). diff --git a/ch04packaging/greeter.py b/ch04packaging/greeter.py new file mode 100755 index 000000000..0953494b2 --- /dev/null +++ b/ch04packaging/greeter.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +def greet(personal, family, title="", polite=False): + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + print(greet(arguments.personal, arguments.family, + arguments.title, arguments.polite)) + +if __name__ == "__main__": + process() diff --git a/ch04packaging/greetings/CITATION.md b/ch04packaging/greetings/CITATION.md new file mode 100644 index 000000000..a59fcf213 --- /dev/null +++ b/ch04packaging/greetings/CITATION.md @@ -0,0 +1,5 @@ + +If you wish to refer to this course, please cite the URL +http://github-pages.ucl.ac.uk/rsd-engineeringcourse/ + +Portions of the material are taken from [Software Carpentry](http://software-carpentry.org/) \ No newline at end of file diff --git a/ch04packaging/greetings/LICENSE.md b/ch04packaging/greetings/LICENSE.md new file mode 100644 index 000000000..377f6781a --- /dev/null +++ b/ch04packaging/greetings/LICENSE.md @@ -0,0 +1,4 @@ + +(C) University College London 2014 + +This "greetings" example package is granted into the public domain. \ No newline at end of file diff --git a/ch04packaging/greetings/README.md b/ch04packaging/greetings/README.md new file mode 100644 index 000000000..562d40219 --- /dev/null +++ b/ch04packaging/greetings/README.md @@ -0,0 +1,10 @@ + +Greetings! +========== + +This is a very simple example package used as part of the UCL +[Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course. + +Usage: + +Invoke the tool with `greet ` \ No newline at end of file diff --git a/ch04packaging/greetings/conf.py b/ch04packaging/greetings/conf.py new file mode 100644 index 000000000..d5b081995 --- /dev/null +++ b/ch04packaging/greetings/conf.py @@ -0,0 +1,43 @@ + +import sys +import os + +# We need to tell Sphinx where to look for modules +sys.path.insert(0, os.path.abspath('.')) + +extensions = [ + 'sphinx.ext.autodoc', # Support automatic documentation + 'sphinx.ext.coverage', # Automatically check if functions are documented + 'sphinx.ext.mathjax', # Allow support for algebra + 'sphinx.ext.viewcode', # Include the source code in documentation + 'numpydoc' # Support NumPy style docstrings +] +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +project = 'Greetings' +copyright = '2014, James Hetherington' +version = '0.1' +release = '0.1' +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +html_theme = 'alabaster' +pygments_style = 'sphinx' +htmlhelp_basename = 'Greetingsdoc' +latex_elements = { +} + +latex_documents = [ + ('index', 'Greetings.tex', 'Greetings Documentation', + 'James Hetherington', 'manual'), +] + +man_pages = [ + ('index', 'greetings', 'Greetings Documentation', + ['James Hetherington'], 1) +] + +texinfo_documents = [ + ('index', 'Greetings', u'Greetings Documentation', + 'James Hetherington', 'Greetings', 'One line description of project.', + 'Miscellaneous'), +] diff --git a/ch04packaging/greetings/doc/_modules/greetings/greeter.html b/ch04packaging/greetings/doc/_modules/greetings/greeter.html new file mode 100644 index 000000000..9d31bf478 --- /dev/null +++ b/ch04packaging/greetings/doc/_modules/greetings/greeter.html @@ -0,0 +1,127 @@ + + + + + + + greetings.greeter — Greetings 0.1 documentation + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for greetings.greeter

+
[docs]def greet(personal, family, title="", polite=False): + """ Generate a greeting string for a person. + + Parameters + ---------- + personal: str + A given name, such as Will or Jean-Luc + family: str + A family name, such as Riker or Picard + title: str + An optional title, such as Captain or Reverend + polite: bool + True for a formal greeting, False for informal. + + Returns + ------- + string + An appropriate greeting + + Examples + -------- + >>> from greetings.greeter import greet + >>> greet("Terry", "Jones") + 'Hey, Terry Jones. + """ + + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting
+
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_modules/index.html b/ch04packaging/greetings/doc/_modules/index.html new file mode 100644 index 000000000..fb5afc2dc --- /dev/null +++ b/ch04packaging/greetings/doc/_modules/index.html @@ -0,0 +1,94 @@ + + + + + + + Overview: module code — Greetings 0.1 documentation + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

All modules for which code is available

+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_static/alabaster.css b/ch04packaging/greetings/doc/_static/alabaster.css new file mode 100644 index 000000000..517d0b29c --- /dev/null +++ b/ch04packaging/greetings/doc/_static/alabaster.css @@ -0,0 +1,703 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_static/basic.css b/ch04packaging/greetings/doc/_static/basic.css new file mode 100644 index 000000000..cfc60b86c --- /dev/null +++ b/ch04packaging/greetings/doc/_static/basic.css @@ -0,0 +1,921 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_static/custom.css b/ch04packaging/greetings/doc/_static/custom.css new file mode 100644 index 000000000..2a924f1d6 --- /dev/null +++ b/ch04packaging/greetings/doc/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/ch04packaging/greetings/doc/_static/doctools.js b/ch04packaging/greetings/doc/_static/doctools.js new file mode 100644 index 000000000..d06a71d75 --- /dev/null +++ b/ch04packaging/greetings/doc/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/ch04packaging/greetings/doc/_static/documentation_options.js b/ch04packaging/greetings/doc/_static/documentation_options.js new file mode 100644 index 000000000..cf359c0aa --- /dev/null +++ b/ch04packaging/greetings/doc/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.1', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_static/file.png b/ch04packaging/greetings/doc/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/ch04packaging/greetings/doc/_static/language_data.js b/ch04packaging/greetings/doc/_static/language_data.js new file mode 100644 index 000000000..250f5665f --- /dev/null +++ b/ch04packaging/greetings/doc/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/ch04packaging/greetings/doc/_static/minus.png b/ch04packaging/greetings/doc/_static/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..d96755fdaf8bb2214971e0db9c1fd3077d7c419d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu=nj kDsEF_5m^0CR;1wuP-*O&G^0G}KYk!hp00i_>zopr08q^qX#fBK literal 0 HcmV?d00001 diff --git a/ch04packaging/greetings/doc/_static/plus.png b/ch04packaging/greetings/doc/_static/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..7107cec93a979b9a5f64843235a16651d563ce2d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu>-2 m3q%Vub%g%s<8sJhVPMczOq}xhg9DJoz~JfX=d#Wzp$Pyb1r*Kz literal 0 HcmV?d00001 diff --git a/ch04packaging/greetings/doc/_static/pygments.css b/ch04packaging/greetings/doc/_static/pygments.css new file mode 100644 index 000000000..0d49244ed --- /dev/null +++ b/ch04packaging/greetings/doc/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #333333 } /* Generic.Output */ +.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070a0 } /* Literal.String */ +.highlight .na { color: #4070a0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60add5 } /* Name.Constant */ +.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287e } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #bb60d5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sa { color: #4070a0 } /* Literal.String.Affix */ +.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070a0 } /* Literal.String.Char */ +.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ +.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ +.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #c65d09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #06287e } /* Name.Function.Magic */ +.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ +.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ +.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/ch04packaging/greetings/doc/_static/searchtools.js b/ch04packaging/greetings/doc/_static/searchtools.js new file mode 100644 index 000000000..97d56a74d --- /dev/null +++ b/ch04packaging/greetings/doc/_static/searchtools.js @@ -0,0 +1,566 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = docUrlRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = docUrlRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms) + ); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + `Search finished, found ${resultCount} page(s) matching the search query.` + ); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() }); + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent !== undefined) return docContent.textContent; + console.warn( + "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + /** + * execute search (requires search index to be loaded) + */ + query: (query) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + // array of [docname, title, anchor, descr, score, filename] + let results = []; + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + let score = Math.round(100 * queryLower.length / title.length) + results.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id] of foundEntries) { + let score = Math.round(100 * queryLower.length / entry.length) + results.push([ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // lookup as object + objectTerms.forEach((term) => + results.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + results.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item))); + + // now sort the results by score (in opposite order of appearance, since the + // display function below uses pop() to retrieve items) and then + // alphabetically + results.sort((a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; + }); + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + results = results.reverse(); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord) && !terms[word]) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord) && !titleTerms[word]) + arr.push({ files: titleTerms[word], score: Scorer.partialTitle }); + }); + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); + else fileMap.set(file, [word]); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords) => { + const text = Search.htmlToText(htmlText); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/ch04packaging/greetings/doc/_static/sphinx_highlight.js b/ch04packaging/greetings/doc/_static/sphinx_highlight.js new file mode 100644 index 000000000..aae669d7e --- /dev/null +++ b/ch04packaging/greetings/doc/_static/sphinx_highlight.js @@ -0,0 +1,144 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + parent.insertBefore( + span, + parent.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(SphinxHighlight.highlightSearchWords); +_ready(SphinxHighlight.initEscapeListener); diff --git a/ch04packaging/greetings/doc/genindex.html b/ch04packaging/greetings/doc/genindex.html new file mode 100644 index 000000000..2555403ef --- /dev/null +++ b/ch04packaging/greetings/doc/genindex.html @@ -0,0 +1,107 @@ + + + + + + + Index — Greetings 0.1 documentation + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ G + +
+

G

+ + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/ch04packaging/greetings/doc/index.html b/ch04packaging/greetings/doc/index.html new file mode 100644 index 000000000..9ee7ad8b0 --- /dev/null +++ b/ch04packaging/greetings/doc/index.html @@ -0,0 +1,132 @@ + + + + + + + + Welcome to Greetings’s documentation! — Greetings 0.1 documentation + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Welcome to Greetings’s documentation!

+

Simple “Hello, James” module developed to teach research software engineering.

+
+
+greetings.greeter.greet(personal, family, title='', polite=False)[source]
+

Generate a greeting string for a person.

+
+
Parameters:
+
+
personal: str

A given name, such as Will or Jean-Luc

+
+
family: str

A family name, such as Riker or Picard

+
+
title: str

An optional title, such as Captain or Reverend

+
+
polite: bool

True for a formal greeting, False for informal.

+
+
+
+
Returns:
+
+
string

An appropriate greeting

+
+
+
+
+

Examples

+
>>> from greetings.greeter import greet
+>>> greet("Terry", "Jones")
+'Hey, Terry Jones.
+
+
+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/ch04packaging/greetings/doc/objects.inv b/ch04packaging/greetings/doc/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..6a1356165db844693525bbf03b3cf4ec2dca6d8b GIT binary patch literal 292 zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~Kj^N=+@v%u6o@ z3Wh-xSSc9j83MT>8L0|Iskw=nc`2zy3i)XYB^jB;3Tc@+sR}?kIX}0cD7CmaHASJc z7-)b(RZeD-9#_S!)_z~ELk0q^-z|@J#Cr`Qhw + + + + + + Search — Greetings 0.1 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + + +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/ch04packaging/greetings/doc/searchindex.js b/ch04packaging/greetings/doc/searchindex.js new file mode 100644 index 000000000..f7e034190 --- /dev/null +++ b/ch04packaging/greetings/doc/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["Welcome to Greetings\u2019s documentation!"], "terms": {"simpl": 0, "hello": 0, "jame": 0, "modul": 0, "develop": 0, "teach": 0, "research": 0, "softwar": 0, "engin": 0, "greeter": 0, "person": 0, "famili": 0, "titl": 0, "polit": 0, "fals": 0, "sourc": 0, "gener": 0, "string": 0, "paramet": 0, "str": 0, "A": 0, "given": 0, "name": 0, "Will": 0, "jean": 0, "luc": 0, "riker": 0, "picard": 0, "an": 0, "option": 0, "captain": 0, "reverend": 0, "bool": 0, "true": 0, "formal": 0, "inform": 0, "return": 0, "appropri": 0, "exampl": 0, "from": 0, "import": 0, "terri": 0, "jone": 0, "hei": 0}, "objects": {"greetings.greeter": [[0, 0, 1, "", "greet"]]}, "objtypes": {"0": "py:function"}, "objnames": {"0": ["py", "function", "Python function"]}, "titleterms": {"welcom": 0, "greet": 0, "": 0, "document": 0}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.viewcode": 1, "sphinx": 58}, "alltitles": {"Welcome to Greetings\u2019s documentation!": [[0, "welcome-to-greetings-s-documentation"]]}, "indexentries": {"greet() (in module greetings.greeter)": [[0, "greetings.greeter.greet"]]}}) \ No newline at end of file diff --git a/ch04packaging/greetings/greetings/command.py b/ch04packaging/greetings/greetings/command.py new file mode 100755 index 000000000..f50044f92 --- /dev/null +++ b/ch04packaging/greetings/greetings/command.py @@ -0,0 +1,18 @@ +from argparse import ArgumentParser +from .greeter import greet # Note python 3 relative import + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + print(greet(arguments.personal, arguments.family, + arguments.title, arguments.polite)) + +if __name__ == "__main__": + process() \ No newline at end of file diff --git a/ch04packaging/greetings/greetings/greeter.py b/ch04packaging/greetings/greetings/greeter.py new file mode 100644 index 000000000..3fb5ab84f --- /dev/null +++ b/ch04packaging/greetings/greetings/greeter.py @@ -0,0 +1,32 @@ +def greet(personal, family, title="", polite=False): + """ Generate a greeting string for a person. + + Parameters + ---------- + personal: str + A given name, such as Will or Jean-Luc + family: str + A family name, such as Riker or Picard + title: str + An optional title, such as Captain or Reverend + polite: bool + True for a formal greeting, False for informal. + + Returns + ------- + string + An appropriate greeting + + Examples + -------- + >>> from greetings.greeter import greet + >>> greet("Terry", "Jones") + 'Hey, Terry Jones. + """ + + greeting= "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting diff --git a/ch04packaging/greetings/greetings/test/fixtures/samples.yaml b/ch04packaging/greetings/greetings/test/fixtures/samples.yaml new file mode 100644 index 000000000..4b87d3f00 --- /dev/null +++ b/ch04packaging/greetings/greetings/test/fixtures/samples.yaml @@ -0,0 +1,11 @@ +- personal: Eric + family: Idle + answer: "Hey, Eric Idle." +- personal: Graham + family: Chapman + polite: True + answer: "How do you do, Graahm Chapman." +- personal: Michael + family: Palin + title: CBE + answer: "Hey, CBE Mike Palin." \ No newline at end of file diff --git a/ch04packaging/greetings/greetings/test/test_greeter.py b/ch04packaging/greetings/greetings/test/test_greeter.py new file mode 100644 index 000000000..c9e0d39ae --- /dev/null +++ b/ch04packaging/greetings/greetings/test/test_greeter.py @@ -0,0 +1,12 @@ +import yaml +import os +from ..greeter import greet + +def test_greeter(): + with open(os.path.join(os.path.dirname(__file__), + 'fixtures', + 'samples.yaml')) as fixtures_file: + fixtures = yaml.safe_load(fixtures_file) + for fixture in fixtures: + answer = fixture.pop('answer') + assert greet(**fixture) == answer diff --git a/ch04packaging/greetings/index.rst b/ch04packaging/greetings/index.rst new file mode 100644 index 000000000..a06c752c6 --- /dev/null +++ b/ch04packaging/greetings/index.rst @@ -0,0 +1,6 @@ +Welcome to Greetings's documentation! +===================================== + +Simple "Hello, James" module developed to teach research software engineering. + +.. autofunction:: greetings.greeter.greet diff --git a/ch04packaging/greetings/setup.py b/ch04packaging/greetings/setup.py new file mode 100644 index 000000000..53009f9bf --- /dev/null +++ b/ch04packaging/greetings/setup.py @@ -0,0 +1,11 @@ + +from setuptools import setup, find_packages + +setup( + name="Greetings", + version="0.1.0", + packages=find_packages(exclude=['*test']), + entry_points={ + 'console_scripts': [ + 'greet = greetings.command:process' + ]}) diff --git a/ch04packaging/greetings_repo/CITATION.md b/ch04packaging/greetings_repo/CITATION.md new file mode 100644 index 000000000..26eb0951c --- /dev/null +++ b/ch04packaging/greetings_repo/CITATION.md @@ -0,0 +1,5 @@ + +If you wish to refer to this course, please cite the URL +http://github-pages.ucl.ac.uk/rsd-engineeringcourse/ + +Portions of the material are taken from [Software Carpentry](http://software-carpentry.org/) diff --git a/ch04packaging/greetings_repo/LICENSE.md b/ch04packaging/greetings_repo/LICENSE.md new file mode 100644 index 000000000..0515c623f --- /dev/null +++ b/ch04packaging/greetings_repo/LICENSE.md @@ -0,0 +1,4 @@ + +(C) University College London 2014 + +This "greetings" example package is granted into the public domain. diff --git a/ch04packaging/greetings_repo/README.md b/ch04packaging/greetings_repo/README.md new file mode 100644 index 000000000..62d322d68 --- /dev/null +++ b/ch04packaging/greetings_repo/README.md @@ -0,0 +1,21 @@ + +# Greetings! + +This is a very simple example package used as part of the UCL +[Research Software Engineering with Python](development.rc.ucl.ac.uk/training/engineering) course. + +## Installation + +```bash +pip install git+git://github.com/UCL-ARC-RSEing-with-Python/greeter +``` + +## Usage + +Invoke the tool with `greet ` or use it on your own library: + +```python +from greeting import greeter + +greeter.greet(user.name, user.lastname) +``` diff --git a/ch04packaging/greetings_repo/greetings/command.py b/ch04packaging/greetings_repo/greetings/command.py new file mode 100644 index 000000000..f66081603 --- /dev/null +++ b/ch04packaging/greetings_repo/greetings/command.py @@ -0,0 +1,24 @@ + +from argparse import ArgumentParser + +from art import art + +from .greeter import greet + + +def process(): + parser = ArgumentParser(description="Generate appropriate greetings") + + parser.add_argument('--title', '-t') + parser.add_argument('--polite', '-p', action="store_true") + parser.add_argument('personal') + parser.add_argument('family') + + arguments = parser.parse_args() + + message = greet(arguments.personal, arguments.family, + arguments.title, arguments.polite) + print(art("cute face"), message) + +if __name__ == "__main__": + process() diff --git a/ch04packaging/greetings_repo/greetings/greeter.py b/ch04packaging/greetings_repo/greetings/greeter.py new file mode 100644 index 000000000..b49b5e72a --- /dev/null +++ b/ch04packaging/greetings_repo/greetings/greeter.py @@ -0,0 +1,30 @@ + +def greet(personal, family, title="", polite=False): + """ Generate a greeting string for a person. + Parameters + ---------- + personal: str + A given name, such as Will or Jean-Luc + family: str + A family name, such as Riker or Picard + title: str + An optional title, such as Captain or Reverend + polite: bool + True for a formal greeting, False for informal. + Returns + ------- + string + An appropriate greeting + Examples + -------- + >>> from greetings.greeter import greet + >>> greet("Terry", "Jones") + 'Hey, Terry Jones. + """ + + greeting = "How do you do, " if polite else "Hey, " + if title: + greeting += f"{title} " + + greeting += f"{personal} {family}." + return greeting diff --git a/ch04packaging/greetings_repo/greetings/test/fixtures/samples.yaml b/ch04packaging/greetings_repo/greetings/test/fixtures/samples.yaml new file mode 100644 index 000000000..be4caa356 --- /dev/null +++ b/ch04packaging/greetings_repo/greetings/test/fixtures/samples.yaml @@ -0,0 +1,12 @@ + +- personal: Eric + family: Idle + answer: "Hey, Eric Idle." +- personal: Graham + family: Chapman + polite: True + answer: "How do you do, Graahm Chapman." +- personal: Michael + family: Palin + title: CBE + answer: "Hey, CBE Mike Palin." diff --git a/ch04packaging/greetings_repo/greetings/test/test_greeter.py b/ch04packaging/greetings_repo/greetings/test/test_greeter.py new file mode 100644 index 000000000..5b64c2de7 --- /dev/null +++ b/ch04packaging/greetings_repo/greetings/test/test_greeter.py @@ -0,0 +1,19 @@ + +import os + +import pytest +import yaml + +from ..greeter import greet + +def read_fixture(): + with open(os.path.join(os.path.dirname(__file__), + 'fixtures', + 'samples.yaml')) as fixtures_file: + fixtures = yaml.safe_load(fixtures_file) + return fixtures + +@pytest.mark.parametrize("fixture", read_fixture()) +def test_greeter(fixture): + answer = fixture.pop('answer') + assert greet(**fixture) == answer diff --git a/ch04packaging/greetings_repo/pyproject.toml b/ch04packaging/greetings_repo/pyproject.toml new file mode 100644 index 000000000..ca13720a7 --- /dev/null +++ b/ch04packaging/greetings_repo/pyproject.toml @@ -0,0 +1,22 @@ + +[project] +name = "Greetings" +version = "0.1.0" +readme = "README.md" +license = { file = "LICENSE.md" } +dependencies = [ + "art", + "pyyaml", +] + +[project.scripts] +greet = "greetings.command:process" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]>=6.2", "wheel"] + +[tool.setuptools.packages.find] +include = ["greetings*"] +exclude = ["tests*"] + +[tool.setuptools_scm] diff --git a/ch04packaging/index.html b/ch04packaging/index.html new file mode 100644 index 000000000..f8bbd5837 --- /dev/null +++ b/ch04packaging/index.html @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Software Projects + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Turning your code into a package
  • +
  • Releasing code
  • +
  • Documentation
  • +
  • Software project management
  • +
  • Organising issues and tasks
  • +
  • Choosing an open-source license
  • +
+ + + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch04packaging/mazetool/exit.py b/ch04packaging/mazetool/exit.py new file mode 100644 index 000000000..0cf4b4a0b --- /dev/null +++ b/ch04packaging/mazetool/exit.py @@ -0,0 +1,8 @@ + +class Exit(object): + def __init__(self, name, target): + self.name = name + self.target = target + + def valid(self): + return self.target.has_space() diff --git a/ch04packaging/mazetool/maze.py b/ch04packaging/mazetool/maze.py new file mode 100644 index 000000000..128201623 --- /dev/null +++ b/ch04packaging/mazetool/maze.py @@ -0,0 +1,42 @@ + +from .room import Room +from .person import Person + +class Maze(object): + def __init__(self, name): + self.name = name + self.rooms = [] + self.occupants = [] + + def add_room(self, name, capacity): + result = Room(name, capacity) + self.rooms.append(result) + return result + + def add_exit(self, name, source, target, reverse= None): + source.add_exit(name, target) + if reverse: + target.add_exit(reverse, source) + + def add_occupant(self, name, room): + self.occupants.append(Person(name, room)) + room.occupancy += 1 + + def wander(self): + "Move all the people in a random direction" + for occupant in self.occupants: + occupant.wander() + + def describe(self): + for occupant in self.occupants: + occupant.describe() + + def step(self): + house.describe() + print() + house.wander() + print() + + def simulate(self, steps): + for _ in range(steps): + self.step() diff --git a/ch04packaging/mazetool/person.py b/ch04packaging/mazetool/person.py new file mode 100644 index 000000000..ebe43d7a9 --- /dev/null +++ b/ch04packaging/mazetool/person.py @@ -0,0 +1,20 @@ + +class Person(object): + def __init__(self, name, room = None): + self.name=name + self.room=room + + def use(self, exit): + self.room.occupancy -= 1 + destination=exit.target + destination.occupancy +=1 + self.room=destination + print(self.name, "goes", exit.name, "to the", destination.name) + + def wander(self): + exit = self.room.random_valid_exit() + if exit: + self.use(exit) + + def describe(self): + print(self.name, "is in the", self.room.name) diff --git a/ch04packaging/mazetool/room.py b/ch04packaging/mazetool/room.py new file mode 100644 index 000000000..20fbbcf79 --- /dev/null +++ b/ch04packaging/mazetool/room.py @@ -0,0 +1,25 @@ +from .exit import Exit + + +class Room(object): + def __init__(self, name, capacity): + self.name = name + self.capacity = capacity + self.occupancy = 0 + self.exits = [] + + def has_space(self): + return self.occupancy < self.capacity + + def available_exits(self): + return [exit for exit in self.exits if exit.valid() ] + + def random_valid_exit(self): + import random + if not self.available_exits(): + return None + return random.choice(self.available_exits()) + + def add_exit(self, name, target): + self.exits.append(Exit(name, target)) + diff --git a/ch05construction/01introduction.html b/ch05construction/01introduction.html new file mode 100644 index 000000000..9f90bc78d --- /dev/null +++ b/ch05construction/01introduction.html @@ -0,0 +1,549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Construction + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Construction

+
+
+
+
+
+
+

Construction

+
+
+
+
+
+
+

Software design gets a lot of press (Object orientation, UML, design patterns).

+

In this session we're going to look at advice on software construction.

+
+
+
+
+
+
+

Construction vs Design

+
+
+
+
+
+
+

For a given piece of code, there exist several different ways one could write it:

+
    +
  • Choice of variable names
  • +
  • Choice of comments
  • +
  • Choice of layout
  • +
+

The consideration of these questions is the area of Software Construction.

+
+
+
+
+
+
+

Low-level design decisions

+
+
+
+
+
+
+

We will also look at some of the lower-level software design decisions in the context of this section:

+
    +
  • Division of code into subroutines
  • +
  • Subroutine access signatures
  • +
  • Choice of data structures for readability
  • +
+
+
+
+
+
+
+

Algorithms and structures

+
+
+
+
+
+
+

We will not, in discussing construction, be looking at decisions as to how design questions impact performance:

+
    +
  • Choice of algorithms
  • +
  • Choice of data structures for performance
  • +
  • Choice of memory layout
  • +
+

We will consider these in a future discussion of performance programming.

+
+
+
+
+
+
+

Architectural design

+
+
+
+
+
+
+

We will not, in this session, be looking at the large-scale questions of how program components interact, +the stategic choices that govern how software behaves at the large scale:

+
    +
  • Where do objects get made?
  • +
  • Which objects own or access other objects?
  • +
  • How can I hide complexity in one part of the code from other parts of the code?
  • +
+

We will consider these in a future session.

+
+
+
+
+
+
+

Construction

+
+
+
+
+
+
+

So, we've excluded most of the exciting topics. What's left is the bricks and mortar of software: +how letters and symbols are used to build code which is readable.

+
+
+
+
+
+
+

Literate programming

+
+
+
+
+
+
+

In literature, books are enjoyable for different reasons:

+
    +
  • The beauty of stories
  • +
  • The beauty of plots
  • +
  • The beauty of characters
  • +
  • The beauty of paragraphs
  • +
  • The beauty of sentences
  • +
  • The beauty of words
  • +
+

Software has beauty at these levels too: stories and characters correspond to architecture and object design, +plots corresponds to algorithms, but the rhythm of sentences and the choice of words corresponds +to software construction.

+
+
+
+
+
+
+

Programming for humans

+
+
+
+
+
+
+
    +
  • Remember you're programming for humans as well as computers
  • +
  • A program is the best, most rigorous way to describe an algorithm
  • +
  • Code should be pleasant to read, a form of scholarly communication
  • +
+

Read Steve McConnell's Code Complete [UCL library].

+
+
+
+
+
+
+

Setup

+
+
+
+
+
+
+

This notebook is based on a number of fragments of code, with an implicit context. +We've made a library to set up the context so the examples work.

+
+
+
+
+
+
In [1]:
+
+
+
%%writefile context.py
+from unittest.mock import Mock, MagicMock
+class CompMock(Mock):
+    def __sub__(self, b):
+        return CompMock()
+    def __lt__(self,b):
+        return True
+    def __abs__(self):
+        return CompMock()
+array=[]
+agt=[]
+ws=[]
+agents=[]
+counter=0
+x=MagicMock()
+y=None
+agent=MagicMock()
+value=0
+bird_types=["Starling", "Hawk"]
+import numpy as np
+average=np.mean
+hawk=CompMock()
+starling=CompMock()
+sEntry="2.0"
+entry ="2.0"
+iOffset=1
+offset =1
+anothervariable=1
+flag1=True
+variable=1
+flag2=False
+def do_something(): pass
+chromosome=None
+start_codon=None
+subsequence=MagicMock()
+transcribe=MagicMock()
+ribe=MagicMock()
+find=MagicMock()
+can_see=MagicMock()
+my_name=""
+your_name=""
+flag1=False
+flag2=False
+start=0.0
+end=1.0
+step=0.1
+birds=[MagicMock()]*2
+resolution=100
+pi=3.141
+result= [0]*resolution
+import numpy as np
+import math
+data= [math.sin(y) for y in np.arange(0,pi,pi/resolution)]
+import yaml
+import os
+
+
+
+
+
+
+
+
+
+
Writing context.py
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/01introduction.ipynb b/ch05construction/01introduction.ipynb new file mode 100644 index 000000000..8b00b6ff0 --- /dev/null +++ b/ch05construction/01introduction.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a813ef3e", + "metadata": {}, + "source": [ + "# Construction" + ] + }, + { + "cell_type": "markdown", + "id": "f3d890fc", + "metadata": {}, + "source": [ + "## Construction" + ] + }, + { + "cell_type": "markdown", + "id": "034992a4", + "metadata": {}, + "source": [ + "\n", + "Software *design* gets a lot of press (Object orientation, UML, design patterns).\n", + "\n", + "In this session we're going to look at advice on software *construction*.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c205f31e", + "metadata": {}, + "source": [ + "### Construction vs Design" + ] + }, + { + "cell_type": "markdown", + "id": "5a4aea48", + "metadata": {}, + "source": [ + "\n", + "For a given piece of code, there exist several different ways one could write it:\n", + "\n", + "* Choice of variable names\n", + "* Choice of comments\n", + "* Choice of layout\n", + "\n", + "The consideration of these questions is the area of Software Construction.\n" + ] + }, + { + "cell_type": "markdown", + "id": "56017a46", + "metadata": {}, + "source": [ + "### Low-level design decisions" + ] + }, + { + "cell_type": "markdown", + "id": "671abba6", + "metadata": {}, + "source": [ + "\n", + "We will also look at some of the lower-level software design decisions in the context of this section:\n", + "\n", + "* Division of code into subroutines\n", + "* Subroutine access signatures\n", + "* Choice of data structures for readability\n" + ] + }, + { + "cell_type": "markdown", + "id": "48499aab", + "metadata": {}, + "source": [ + "### Algorithms and structures" + ] + }, + { + "cell_type": "markdown", + "id": "0dd9fb35", + "metadata": {}, + "source": [ + "\n", + "We will not, in discussing construction, be looking at decisions as to how design questions impact performance:\n", + "\n", + "* Choice of algorithms\n", + "* Choice of data structures for performance\n", + "* Choice of memory layout\n", + "\n", + "We will consider these in a future discussion of performance programming.\n" + ] + }, + { + "cell_type": "markdown", + "id": "96ae401d", + "metadata": {}, + "source": [ + "### Architectural design" + ] + }, + { + "cell_type": "markdown", + "id": "3d621143", + "metadata": {}, + "source": [ + "\n", + "We will not, in this session, be looking at the large-scale questions of how program components interact,\n", + "the stategic choices that govern how software behaves at the large scale:\n", + "\n", + "* Where do objects get made?\n", + "* Which objects own or access other objects?\n", + "* How can I hide complexity in one part of the code from other parts of the code?\n", + "\n", + "We will consider these in a future session.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b725f89b", + "metadata": {}, + "source": [ + "### Construction" + ] + }, + { + "cell_type": "markdown", + "id": "b65b9e80", + "metadata": {}, + "source": [ + "\n", + "So, we've excluded most of the exciting topics. What's left is the bricks and mortar of software:\n", + "how letters and symbols are used to build code which is readable.\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b840881", + "metadata": {}, + "source": [ + "### Literate programming" + ] + }, + { + "cell_type": "markdown", + "id": "0a90b571", + "metadata": {}, + "source": [ + "\n", + "In literature, books are enjoyable for different reasons:\n", + "\n", + "* The beauty of stories\n", + "* The beauty of plots\n", + "* The beauty of characters\n", + "* The beauty of paragraphs\n", + "* The beauty of sentences\n", + "* The beauty of words\n", + "\n", + "Software has beauty at these levels too: stories and characters correspond to architecture and object design,\n", + "plots corresponds to algorithms, but the rhythm of sentences and the choice of words corresponds\n", + "to software construction.\n" + ] + }, + { + "cell_type": "markdown", + "id": "fdb8b0a1", + "metadata": {}, + "source": [ + "### Programming for humans" + ] + }, + { + "cell_type": "markdown", + "id": "0b1ad90d", + "metadata": {}, + "source": [ + "\n", + "* Remember you're programming for humans as well as computers\n", + "* A program is the best, most rigorous way to describe an algorithm\n", + "* Code should be pleasant to read, a form of scholarly communication\n", + "\n", + "Read Steve McConnell's [Code Complete](https://en.wikipedia.org/wiki/Code_Complete) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21156385750004761&context=L)].\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "ad7d0614", + "metadata": {}, + "source": [ + "### Setup" + ] + }, + { + "cell_type": "markdown", + "id": "2c258222", + "metadata": {}, + "source": [ + "This notebook is based on a number of fragments of code, with an implicit context.\n", + "We've made a library to set up the context so the examples work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a7dddc7", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile context.py\n", + "from unittest.mock import Mock, MagicMock\n", + "class CompMock(Mock):\n", + " def __sub__(self, b):\n", + " return CompMock()\n", + " def __lt__(self,b):\n", + " return True\n", + " def __abs__(self):\n", + " return CompMock()\n", + "array=[]\n", + "agt=[]\n", + "ws=[]\n", + "agents=[]\n", + "counter=0\n", + "x=MagicMock()\n", + "y=None\n", + "agent=MagicMock()\n", + "value=0\n", + "bird_types=[\"Starling\", \"Hawk\"]\n", + "import numpy as np\n", + "average=np.mean\n", + "hawk=CompMock()\n", + "starling=CompMock()\n", + "sEntry=\"2.0\"\n", + "entry =\"2.0\"\n", + "iOffset=1\n", + "offset =1\n", + "anothervariable=1\n", + "flag1=True\n", + "variable=1\n", + "flag2=False\n", + "def do_something(): pass\n", + "chromosome=None\n", + "start_codon=None\n", + "subsequence=MagicMock()\n", + "transcribe=MagicMock()\n", + "ribe=MagicMock()\n", + "find=MagicMock()\n", + "can_see=MagicMock()\n", + "my_name=\"\"\n", + "your_name=\"\"\n", + "flag1=False\n", + "flag2=False\n", + "start=0.0\n", + "end=1.0\n", + "step=0.1\n", + "birds=[MagicMock()]*2\n", + "resolution=100\n", + "pi=3.141\n", + "result= [0]*resolution\n", + "import numpy as np\n", + "import math\n", + "data= [math.sin(y) for y in np.arange(0,pi,pi/resolution)]\n", + "import yaml\n", + "import os" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Construction" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/01introduction.ipynb.py b/ch05construction/01introduction.ipynb.py new file mode 100644 index 000000000..706ef950d --- /dev/null +++ b/ch05construction/01introduction.ipynb.py @@ -0,0 +1,188 @@ +# --- +# jupyter: +# jekyll: +# display_name: Construction +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Construction + +# %% [markdown] +# ## Construction + +# %% [markdown] +# +# Software *design* gets a lot of press (Object orientation, UML, design patterns). +# +# In this session we're going to look at advice on software *construction*. +# + +# %% [markdown] +# ### Construction vs Design + +# %% [markdown] +# +# For a given piece of code, there exist several different ways one could write it: +# +# * Choice of variable names +# * Choice of comments +# * Choice of layout +# +# The consideration of these questions is the area of Software Construction. +# + +# %% [markdown] +# ### Low-level design decisions + +# %% [markdown] +# +# We will also look at some of the lower-level software design decisions in the context of this section: +# +# * Division of code into subroutines +# * Subroutine access signatures +# * Choice of data structures for readability +# + +# %% [markdown] +# ### Algorithms and structures + +# %% [markdown] +# +# We will not, in discussing construction, be looking at decisions as to how design questions impact performance: +# +# * Choice of algorithms +# * Choice of data structures for performance +# * Choice of memory layout +# +# We will consider these in a future discussion of performance programming. +# + +# %% [markdown] +# ### Architectural design + +# %% [markdown] +# +# We will not, in this session, be looking at the large-scale questions of how program components interact, +# the stategic choices that govern how software behaves at the large scale: +# +# * Where do objects get made? +# * Which objects own or access other objects? +# * How can I hide complexity in one part of the code from other parts of the code? +# +# We will consider these in a future session. +# + +# %% [markdown] +# ### Construction + +# %% [markdown] +# +# So, we've excluded most of the exciting topics. What's left is the bricks and mortar of software: +# how letters and symbols are used to build code which is readable. +# + +# %% [markdown] +# ### Literate programming + +# %% [markdown] +# +# In literature, books are enjoyable for different reasons: +# +# * The beauty of stories +# * The beauty of plots +# * The beauty of characters +# * The beauty of paragraphs +# * The beauty of sentences +# * The beauty of words +# +# Software has beauty at these levels too: stories and characters correspond to architecture and object design, +# plots corresponds to algorithms, but the rhythm of sentences and the choice of words corresponds +# to software construction. +# + +# %% [markdown] +# ### Programming for humans + +# %% [markdown] +# +# * Remember you're programming for humans as well as computers +# * A program is the best, most rigorous way to describe an algorithm +# * Code should be pleasant to read, a form of scholarly communication +# +# Read Steve McConnell's [Code Complete](https://en.wikipedia.org/wiki/Code_Complete) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21156385750004761&context=L)]. +# +# +# +# +# + +# %% [markdown] +# ### Setup + +# %% [markdown] +# This notebook is based on a number of fragments of code, with an implicit context. +# We've made a library to set up the context so the examples work. + +# %% +# %%writefile context.py +from unittest.mock import Mock, MagicMock +class CompMock(Mock): + def __sub__(self, b): + return CompMock() + def __lt__(self,b): + return True + def __abs__(self): + return CompMock() +array=[] +agt=[] +ws=[] +agents=[] +counter=0 +x=MagicMock() +y=None +agent=MagicMock() +value=0 +bird_types=["Starling", "Hawk"] +import numpy as np +average=np.mean +hawk=CompMock() +starling=CompMock() +sEntry="2.0" +entry ="2.0" +iOffset=1 +offset =1 +anothervariable=1 +flag1=True +variable=1 +flag2=False +def do_something(): pass +chromosome=None +start_codon=None +subsequence=MagicMock() +transcribe=MagicMock() +ribe=MagicMock() +find=MagicMock() +can_see=MagicMock() +my_name="" +your_name="" +flag1=False +flag2=False +start=0.0 +end=1.0 +step=0.1 +birds=[MagicMock()]*2 +resolution=100 +pi=3.141 +result= [0]*resolution +import numpy as np +import math +data= [math.sin(y) for y in np.arange(0,pi,pi/resolution)] +import yaml +import os diff --git a/ch05construction/02conventions.html b/ch05construction/02conventions.html new file mode 100644 index 000000000..5448980f9 --- /dev/null +++ b/ch05construction/02conventions.html @@ -0,0 +1,769 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Coding Conventions + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Coding Conventions

+
+
+
+
+
+
+

Let's import first the context for this chapter.

+
+
+
+
+
+
In [1]:
+
+
+
from context import *
+
+
+
+
+
+
+
+
+

One code, many layouts:

+
+
+
+
+
+
+

Consider the following fragment of python:

+
+
+
+
+
+
In [2]:
+
+
+
import species
+def AddToReaction(name, reaction):
+    reaction.append(species.Species(name))
+
+
+
+
+
+
+
+
+

this could also have been written:

+
+
+
+
+
+
In [3]:
+
+
+
from species import Species
+
+def add_to_reaction(a_name,
+                    a_reaction):
+    l_species = Species(a_name)
+    a_reaction.append( l_species )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

So many choices

+
+
+
+
+
+
+
    +
  • Layout
  • +
  • Naming
  • +
  • Syntax choices
  • +
+
+
+
+
+
+
+

Layout

+
+
+
+
+
+
In [4]:
+
+
+
reaction = {
+    "reactants": ["H", "H", "O"],
+    "products": ["H2O"]
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
reaction2=(
+{
+  "reactants":
+  [
+    "H",
+    "H",
+    "O"
+  ],
+  "products":
+  [
+    "H2O"
+  ]
+}
+)
+
+
+
+
+
+
+
+
+

Layout choices

+
+
+
+
+
+
+
    +
  • Brace style
  • +
  • Line length
  • +
  • Indentation
  • +
  • Whitespace/Tabs
  • +
+

Inconsistency will produce a mess in your code! Some choices will make your code harder to read, whereas others may affect the code. For example, if you copy/paste code with tabs in a place that's using spaces, they may appear OK in your screen but it will fail when running it.

+
+
+
+
+
+
+

Naming Conventions

+
+
+
+
+
+
+

Camel case is used in the following example, where class name is in UpperCamel, functions in lowerCamel and underscore_separation for variables names. This convention is used broadly in the python community.

+
+
+
+
+
+
In [6]:
+
+
+
class ClassName:
+    def methodName(variable_name):
+        instance_variable = variable_name
+
+
+
+
+
+
+
+
+

This other example uses underscore_separation for all the names.

+
+
+
+
+
+
In [7]:
+
+
+
class class_name:
+    def method_name(a_variable):
+        m_instance_variable = a_variable
+
+
+
+
+
+
+
+
+

Hungarian Notation

+
+
+
+
+
+
+

Prefix denotes type:

+
+
+
+
+
+
In [8]:
+
+
+
fNumber = float(sEntry) + iOffset
+
+
+
+
+
+
+
+
+

So in the example above we know that we are creating a float number as a composition of a string entry and an integer offset.

+

People may find this useful in languages like Python where the type is intrisic in the variable.

+
+
+
+
+
+
In [9]:
+
+
+
number = float(entry) + offset
+
+
+
+
+
+
+
+
+

Newlines

+
+
+
+
+
+
+
    +
  • Newlines make code easier to read
  • +
  • Newlines make less code fit on a screen
  • +
+

Use newlines to describe your code's rhythm.

+
+
+
+
+
+
+

Syntax Choices

+
+
+
+
+
+
+

The following two snippets do the same, but the second is separated into more steps, making it more readable.

+
+
+
+
+
+
In [10]:
+
+
+
anothervariable += 1
+if ((variable == anothervariable) and flag1 or flag2): do_something()
+
+
+
+
+
+
+
+
In [11]:
+
+
+
anothervariable = anothervariable + 1
+variable_equality = (variable == anothervariable)
+if ((variable_equality and flag1) or flag2):
+    do_something()
+
+
+
+
+
+
+
+
+

We create extra variables as an intermediate step. Don't worry about the performance now, the compiler will do the right thing.

+

What about operator precedence? Being explicit helps to remind yourself what you are doing.

+
+
+
+
+
+
+

Syntax choices

+
+
+
+
+
+
+
    +
  • Explicit operator precedence
  • +
  • Compound expressions
  • +
  • Package import choices
  • +
+
+
+
+
+
+
+

Coding Conventions

+
+
+
+
+
+
+

You should try to have an agreed policy for your team for these matters.

+

If your language sponsor has a standard policy, use that. For example:

+ +
+
+
+
+
+
+

Lint

+
+
+
+
+
+
+

There are automated tools which enforce coding conventions and check for common mistakes.

+

These are called linters. A popular one is pycodestyle:

+

E.g. pip install pycodestyle

+
+
+
+
+
+
In [12]:
+
+
+
%%bash --no-raise-error
+pycodestyle species.py
+
+
+
+
+
+
+
+
+
+
species.py:2:6: E111 indentation is not a multiple of 4
+species.py:2:6: E117 over-indented
+
+
+
+
+
+
+
+
+
+

It is a good idea to run a linter before every commit, or include it in your CI tests.

+
+
+
+
+
+
+

There are other tools that help with linting that are worth mentioning. +With pylint you can also get other useful information about the quality of your code:

+

pip install pylint

+
+
+
+
+
+
In [13]:
+
+
+
%%bash --no-raise-error
+pylint species.py
+
+
+
+
+
+
+
+
+
+
************* Module species
+species.py:2:0: W0311: Bad indentation. Found 5 spaces, expected 4 (bad-indentation)
+species.py:1:0: C0114: Missing module docstring (missing-module-docstring)
+species.py:1:0: C0115: Missing class docstring (missing-class-docstring)
+species.py:1:0: R0205: Class 'Species' inherits from object, can be safely removed from bases in python3 (useless-object-inheritance)
+species.py:1:0: R0903: Too few public methods (0/2) (too-few-public-methods)
+
+-----------------------------------
+Your code has been rated at 0.00/10
+
+
+
+
+
+
+
+
+
+
+

and with black you can fix all the errors at once.

+
black species.py
+
+

These linters can be configured to choose which points to flag and which to ignore.

+

Do not blindly believe all these automated tools! Style guides are guides not rules.

+
+
+
+
+
+
+

Finally, there are tools like editorconfig to help sharing the conventions used within a project, where each contributor uses different IDEs and tools. There are also bots like pep8speaks that comments on contributors' pull requests suggesting what to change to follow the conventions for the project.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/02conventions.ipynb b/ch05construction/02conventions.ipynb new file mode 100644 index 000000000..7f8083492 --- /dev/null +++ b/ch05construction/02conventions.ipynb @@ -0,0 +1,504 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "beec1047", + "metadata": {}, + "source": [ + "## Coding Conventions" + ] + }, + { + "cell_type": "markdown", + "id": "d35ebf92", + "metadata": {}, + "source": [ + "Let's import first the context for this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dc27b20", + "metadata": {}, + "outputs": [], + "source": [ + "from context import *" + ] + }, + { + "cell_type": "markdown", + "id": "7b787420", + "metadata": {}, + "source": [ + "### One code, many layouts:" + ] + }, + { + "cell_type": "markdown", + "id": "53e59046", + "metadata": {}, + "source": [ + "\n", + "Consider the following fragment of python:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40e74fd1", + "metadata": {}, + "outputs": [], + "source": [ + "import species\n", + "def AddToReaction(name, reaction):\n", + " reaction.append(species.Species(name))" + ] + }, + { + "cell_type": "markdown", + "id": "3d016495", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "this could also have been written:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3971f751", + "metadata": {}, + "outputs": [], + "source": [ + "from species import Species\n", + "\n", + "def add_to_reaction(a_name,\n", + " a_reaction):\n", + " l_species = Species(a_name)\n", + " a_reaction.append( l_species )" + ] + }, + { + "cell_type": "markdown", + "id": "01547df9", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "fab5b567", + "metadata": {}, + "source": [ + "### So many choices" + ] + }, + { + "cell_type": "markdown", + "id": "79dd4a33", + "metadata": {}, + "source": [ + "\n", + "* Layout\n", + "* Naming\n", + "* Syntax choices\n" + ] + }, + { + "cell_type": "markdown", + "id": "9fd7d309", + "metadata": {}, + "source": [ + "### Layout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8661e330", + "metadata": {}, + "outputs": [], + "source": [ + "reaction = {\n", + " \"reactants\": [\"H\", \"H\", \"O\"],\n", + " \"products\": [\"H2O\"]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "1ff284ec", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "badcbc6c", + "metadata": {}, + "outputs": [], + "source": [ + "reaction2=(\n", + "{\n", + " \"reactants\":\n", + " [\n", + " \"H\",\n", + " \"H\",\n", + " \"O\"\n", + " ],\n", + " \"products\":\n", + " [\n", + " \"H2O\"\n", + " ]\n", + "}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ec3f4c86", + "metadata": {}, + "source": [ + "### Layout choices" + ] + }, + { + "cell_type": "markdown", + "id": "d58b07e0", + "metadata": {}, + "source": [ + "\n", + "* Brace style\n", + "* Line length\n", + "* Indentation\n", + "* Whitespace/Tabs\n", + "\n", + "Inconsistency will produce a mess in your code! Some choices will make your code harder to read, whereas others may affect the code. For example, if you copy/paste code with tabs in a place that's using spaces, they may appear OK in your screen but it will fail when running it." + ] + }, + { + "cell_type": "markdown", + "id": "d42b53b3", + "metadata": {}, + "source": [ + "### Naming Conventions" + ] + }, + { + "cell_type": "markdown", + "id": "4898a68c", + "metadata": {}, + "source": [ + "[Camel case](https://en.wikipedia.org/wiki/Camel_case) is used in the following example, where class name is in UpperCamel, functions in lowerCamel and underscore_separation for variables names. This convention is used broadly in the python community." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f262cb98", + "metadata": {}, + "outputs": [], + "source": [ + "class ClassName:\n", + " def methodName(variable_name):\n", + " instance_variable = variable_name" + ] + }, + { + "cell_type": "markdown", + "id": "4b94e2ae", + "metadata": {}, + "source": [ + "This other example uses underscore_separation for all the names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b88c37", + "metadata": {}, + "outputs": [], + "source": [ + "class class_name:\n", + " def method_name(a_variable):\n", + " m_instance_variable = a_variable" + ] + }, + { + "cell_type": "markdown", + "id": "1b17eba1", + "metadata": {}, + "source": [ + "### Hungarian Notation" + ] + }, + { + "cell_type": "markdown", + "id": "1db2832d", + "metadata": {}, + "source": [ + "\n", + "Prefix denotes *type*:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ddb0961", + "metadata": {}, + "outputs": [], + "source": [ + "fNumber = float(sEntry) + iOffset" + ] + }, + { + "cell_type": "markdown", + "id": "abbc5fcd", + "metadata": {}, + "source": [ + "So in the example above we know that we are creating a `f`loat number as a composition of a `s`tring entry and an `i`nteger offset.\n", + "\n", + "People may find this useful in languages like Python where the type is intrisic in the variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69837787", + "metadata": {}, + "outputs": [], + "source": [ + "number = float(entry) + offset" + ] + }, + { + "cell_type": "markdown", + "id": "2bfea115", + "metadata": {}, + "source": [ + "### Newlines" + ] + }, + { + "cell_type": "markdown", + "id": "5c243e53", + "metadata": {}, + "source": [ + "\n", + "* Newlines make code easier to read\n", + "* Newlines make less code fit on a screen\n", + "\n", + "Use newlines to describe your code's *rhythm*.\n" + ] + }, + { + "cell_type": "markdown", + "id": "cf27e6b7", + "metadata": {}, + "source": [ + "### Syntax Choices" + ] + }, + { + "cell_type": "markdown", + "id": "9e54b8fb", + "metadata": {}, + "source": [ + "The following two snippets do the same, but the second is separated into more steps, making it more readable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f92c8913", + "metadata": {}, + "outputs": [], + "source": [ + "anothervariable += 1\n", + "if ((variable == anothervariable) and flag1 or flag2): do_something()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba6b1f3d", + "metadata": {}, + "outputs": [], + "source": [ + "anothervariable = anothervariable + 1\n", + "variable_equality = (variable == anothervariable)\n", + "if ((variable_equality and flag1) or flag2):\n", + " do_something()" + ] + }, + { + "cell_type": "markdown", + "id": "4bc67350", + "metadata": {}, + "source": [ + "We create extra variables as an intermediate step. Don't worry about the performance now, the compiler will do the right thing.\n", + "\n", + "What about operator precedence? Being explicit helps to remind yourself what you are doing." + ] + }, + { + "cell_type": "markdown", + "id": "eec43372", + "metadata": {}, + "source": [ + "### Syntax choices" + ] + }, + { + "cell_type": "markdown", + "id": "cea4a776", + "metadata": {}, + "source": [ + "\n", + "* Explicit operator precedence\n", + "* Compound expressions\n", + "* Package import choices\n" + ] + }, + { + "cell_type": "markdown", + "id": "deacb196", + "metadata": {}, + "source": [ + "### Coding Conventions" + ] + }, + { + "cell_type": "markdown", + "id": "d42cafe1", + "metadata": {}, + "source": [ + "\n", + "You should try to have an agreed policy for your team for these matters.\n", + "\n", + "If your language sponsor has a standard policy, use that. For example:\n", + "\n", + "- **Python**: [PEP8](https://www.python.org/dev/peps/pep-0008/)\n", + "- **R**: [Google's guide for R](https://google.github.io/styleguide/Rguide.xml), [tidyverse style guide](https://style.tidyverse.org/)\n", + "- **C++**: [Google's style guide](https://google.github.io/styleguide/cppguide.html), [Mozilla's](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Coding_Style)\n", + "- **Julia**: [Official style guide](https://docs.julialang.org/en/v1/manual/style-guide/index.html)\n" + ] + }, + { + "cell_type": "markdown", + "id": "9a4205bb", + "metadata": {}, + "source": [ + "### Lint" + ] + }, + { + "cell_type": "markdown", + "id": "95568953", + "metadata": {}, + "source": [ + "\n", + "There are automated tools which enforce coding conventions and check for common mistakes.\n", + "\n", + "These are called **linters**. A popular one is [pycodestyle](https://pypi.org/project/pycodestyle/):\n", + "\n", + "E.g. `pip install pycodestyle` \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c197c7d4", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "pycodestyle species.py" + ] + }, + { + "cell_type": "markdown", + "id": "f8ee73e0", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "It is a good idea to run a linter before every commit, or include it in your CI tests.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5ea302e1", + "metadata": {}, + "source": [ + "There are other tools that help with linting that are worth mentioning.\n", + "With [pylint](https://www.pylint.org/) you can also get other useful information about the quality of your code:\n", + "\n", + "`pip install pylint` \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e636b37", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash --no-raise-error\n", + "pylint species.py" + ] + }, + { + "cell_type": "markdown", + "id": "eb628e30", + "metadata": {}, + "source": [ + "and with [black](https://black.readthedocs.io/) you can fix all the errors at once.\n", + "```bash\n", + "black species.py\n", + "```\n", + "These linters can be configured to choose which points to flag and which to ignore.\n", + "\n", + "Do not blindly believe all these automated tools! Style guides are **guides** not **rules**." + ] + }, + { + "cell_type": "markdown", + "id": "c4b75ade", + "metadata": {}, + "source": [ + "Finally, there are tools like [editorconfig](https://editorconfig.org/) to help sharing the conventions used within a project, where each contributor uses different IDEs and tools. There are also bots like [pep8speaks](https://pep8speaks.com/) that comments on contributors' pull requests suggesting what to change to follow the conventions for the project.\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Coding Conventions" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/02conventions.ipynb.py b/ch05construction/02conventions.ipynb.py new file mode 100644 index 000000000..41d78adf4 --- /dev/null +++ b/ch05construction/02conventions.ipynb.py @@ -0,0 +1,261 @@ +# --- +# jupyter: +# jekyll: +# display_name: Coding Conventions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Coding Conventions + +# %% [markdown] +# Let's import first the context for this chapter. + +# %% +from context import * + +# %% [markdown] +# ### One code, many layouts: + +# %% [markdown] +# +# Consider the following fragment of python: +# +# +# + +# %% +import species +def AddToReaction(name, reaction): + reaction.append(species.Species(name)) + + +# %% [markdown] +# +# +# +# this could also have been written: +# +# +# + +# %% +from species import Species + +def add_to_reaction(a_name, + a_reaction): + l_species = Species(a_name) + a_reaction.append( l_species ) + + +# %% [markdown] +# +# +# + +# %% [markdown] +# ### So many choices + +# %% [markdown] +# +# * Layout +# * Naming +# * Syntax choices +# + +# %% [markdown] +# ### Layout + +# %% +reaction = { + "reactants": ["H", "H", "O"], + "products": ["H2O"] +} + +# %% [markdown] +# +# +# +# + +# %% +reaction2=( +{ + "reactants": + [ + "H", + "H", + "O" + ], + "products": + [ + "H2O" + ] +} +) + + +# %% [markdown] +# ### Layout choices + +# %% [markdown] +# +# * Brace style +# * Line length +# * Indentation +# * Whitespace/Tabs +# +# Inconsistency will produce a mess in your code! Some choices will make your code harder to read, whereas others may affect the code. For example, if you copy/paste code with tabs in a place that's using spaces, they may appear OK in your screen but it will fail when running it. + +# %% [markdown] +# ### Naming Conventions + +# %% [markdown] +# [Camel case](https://en.wikipedia.org/wiki/Camel_case) is used in the following example, where class name is in UpperCamel, functions in lowerCamel and underscore_separation for variables names. This convention is used broadly in the python community. + +# %% +class ClassName: + def methodName(variable_name): + instance_variable = variable_name + + +# %% [markdown] +# This other example uses underscore_separation for all the names. + +# %% +class class_name: + def method_name(a_variable): + m_instance_variable = a_variable + + +# %% [markdown] +# ### Hungarian Notation + +# %% [markdown] +# +# Prefix denotes *type*: +# +# +# + +# %% +fNumber = float(sEntry) + iOffset + +# %% [markdown] +# So in the example above we know that we are creating a `f`loat number as a composition of a `s`tring entry and an `i`nteger offset. +# +# People may find this useful in languages like Python where the type is intrisic in the variable. + +# %% +number = float(entry) + offset + +# %% [markdown] +# ### Newlines + +# %% [markdown] +# +# * Newlines make code easier to read +# * Newlines make less code fit on a screen +# +# Use newlines to describe your code's *rhythm*. +# + +# %% [markdown] +# ### Syntax Choices + +# %% [markdown] +# The following two snippets do the same, but the second is separated into more steps, making it more readable. + +# %% +anothervariable += 1 +if ((variable == anothervariable) and flag1 or flag2): do_something() + +# %% +anothervariable = anothervariable + 1 +variable_equality = (variable == anothervariable) +if ((variable_equality and flag1) or flag2): + do_something() + +# %% [markdown] +# We create extra variables as an intermediate step. Don't worry about the performance now, the compiler will do the right thing. +# +# What about operator precedence? Being explicit helps to remind yourself what you are doing. + +# %% [markdown] +# ### Syntax choices + +# %% [markdown] +# +# * Explicit operator precedence +# * Compound expressions +# * Package import choices +# + +# %% [markdown] +# ### Coding Conventions + +# %% [markdown] +# +# You should try to have an agreed policy for your team for these matters. +# +# If your language sponsor has a standard policy, use that. For example: +# +# - **Python**: [PEP8](https://www.python.org/dev/peps/pep-0008/) +# - **R**: [Google's guide for R](https://google.github.io/styleguide/Rguide.xml), [tidyverse style guide](https://style.tidyverse.org/) +# - **C++**: [Google's style guide](https://google.github.io/styleguide/cppguide.html), [Mozilla's](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Coding_Style) +# - **Julia**: [Official style guide](https://docs.julialang.org/en/v1/manual/style-guide/index.html) +# + +# %% [markdown] +# ### Lint + +# %% [markdown] +# +# There are automated tools which enforce coding conventions and check for common mistakes. +# +# These are called **linters**. A popular one is [pycodestyle](https://pypi.org/project/pycodestyle/): +# +# E.g. `pip install pycodestyle` +# +# +# + +# %% magic_args="--no-raise-error" language="bash" +# pycodestyle species.py + +# %% [markdown] +# +# +# +# It is a good idea to run a linter before every commit, or include it in your CI tests. +# +# + +# %% [markdown] +# There are other tools that help with linting that are worth mentioning. +# With [pylint](https://www.pylint.org/) you can also get other useful information about the quality of your code: +# +# `pip install pylint` +# + +# %% magic_args="--no-raise-error" language="bash" +# pylint species.py + +# %% [markdown] +# and with [black](https://black.readthedocs.io/) you can fix all the errors at once. +# ```bash +# black species.py +# ``` +# These linters can be configured to choose which points to flag and which to ignore. +# +# Do not blindly believe all these automated tools! Style guides are **guides** not **rules**. + +# %% [markdown] +# Finally, there are tools like [editorconfig](https://editorconfig.org/) to help sharing the conventions used within a project, where each contributor uses different IDEs and tools. There are also bots like [pep8speaks](https://pep8speaks.com/) that comments on contributors' pull requests suggesting what to change to follow the conventions for the project. +# diff --git a/ch05construction/03comments.html b/ch05construction/03comments.html new file mode 100644 index 000000000..64ec63c7d --- /dev/null +++ b/ch05construction/03comments.html @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Comments + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Comments

+
+
+
+
+
+
+

Let's import first the context for this chapter.

+
+
+
+
+
+
In [1]:
+
+
+
from context import *
+
+
+
+
+
+
+
+
+

Why comment?

+
+
+
+
+
+
+
    +
  • You're writing code for people, as well as computers.
  • +
  • Comments can help you build code, by representing your design
  • +
  • Comments explain subtleties in the code which are not obvious from the syntax
  • +
  • Comments explain why you wrote the code the way you did
  • +
+
+
+
+
+
+
+

Bad Comments

+
+
+
+
+
+
+

"I write good code, you can tell by the number of comments."

+

This is wrong.

+
+
+
+
+
+
+

Comments which are obvious

+
+
+
+
+
+
In [2]:
+
+
+
counter = counter + 1 # Increment the counter
+for element in array: # Loop over elements
+    pass
+
+
+
+
+
+
+
+
+

Comments which could be replaced by better style

+
+
+
+
+
+
+

The following piece of code could be a part of a game to move a turtle in a certain direction, with a particular angular velocity and step size.

+
+
+
+
+
+
In [3]:
+
+
+
for i in range(len(agt)): #for each agent
+    agt[i].theta += ws[i]     # Increment the angle of each agent
+                              #by its angular velocity
+    agt[i].x += r * sin(agt[i].theta) #Move the agent by the step-size
+    agt[i].y += r * cos(agt[i].theta) #r in the direction indicated
+
+
+
+
+
+
+
+
+

we have used comments to make the code readable.

+

Why not make the code readable instead?

+
+
+
+
+
+
In [4]:
+
+
+
for agent in agents:
+    agent.turn()
+    agent.move()
+
+class Agent:
+    def turn(self):
+         self.direction += self.angular_velocity;
+    def move(self):
+        self.x += Agent.step_length * sin(self.direction)
+        self.y += Agent.step_length * cos(self.direction)
+
+
+
+
+
+
+
+
+

This is probably better. We are using the name of the functions (i.e., turn, move) instead of comments. Therefore, we've got self-documenting code.

+
+
+
+
+
+
+

Comments vs expressive code

+
+
+
+
+
+
+
+

The proper use of comments is to compensate for our failure to express yourself in code. +Note that I used the word failure. I meant it. Comments are always failures.

+
+

-- Robert Martin, Clean Code [UCL library].

+

I wouldn't disagree, but still, writing "self-documenting" code is very hard, so do comment if you're unsure!

+
+
+
+
+
+
+

Comments which belong in an issue tracker

+
+
+
+
+
+
In [5]:
+
+
+
x.clear() # Code crashes here sometimes
+class Agent(object):
+    pass
+    # TODO: Implement pretty-printer method
+
+
+
+
+
+
+
+
+

BUT comments that reference issues in the tracker can be good.

+

E.g.

+
+
+
+
+
+
In [6]:
+
+
+
if x.safe_to_clear(): # Guard added as temporary workaround for #32
+    x.clear()
+
+
+
+
+
+
+
+
+

is OK. And platforms like GitHub will create a link to it when browsing the code.

+
+
+
+
+
+
+

Comments which only make sense to the author today

+
+
+
+
+
+
In [7]:
+
+
+
agent.turn() # Turtle Power!
+agent.move()
+agents[:]=[]# Shredder!
+
+
+
+
+
+
+
+
+

Comments which are unpublishable

+
+
+
+
+
+
In [8]:
+
+
+
# Stupid supervisor made me write this code
+# So I did it while very very drunk.
+
+
+
+
+
+
+
+
+

Good commenting: pedagogical comments

+
+
+
+
+
+
+

Code that is good style, but you're not familiar with, or +that colleagues might not be familiar with

+
+
+
+
+
+
In [9]:
+
+
+
# This is how you define a decorator in python
+# See https://wiki.python.org/moin/PythonDecorators
+def double(decorated_function):
+    # Here, the result function forms a closure over 
+    # the decorated function
+    def result_function(entry):
+        return decorated_function(decorated_function(entry))
+    # The returned result is a function
+    return result_function
+
+@double
+def try_me_twice():
+    pass
+
+
+
+
+
+
+
+
+

Good commenting: reasons and definitions

+
+
+
+
+
+
+

Comments which explain coding definitions or reasons for programming choices.

+
+
+
+
+
+
In [10]:
+
+
+
def __init__(self):
+    self.angle = 0 # clockwise from +ve y-axis
+    nonzero_indices = [] # Use sparse model as memory constrained
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/03comments.ipynb b/ch05construction/03comments.ipynb new file mode 100644 index 000000000..82c2c249c --- /dev/null +++ b/ch05construction/03comments.ipynb @@ -0,0 +1,359 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3376d269", + "metadata": {}, + "source": [ + "## Comments" + ] + }, + { + "cell_type": "markdown", + "id": "ed80678a", + "metadata": {}, + "source": [ + "Let's import first the context for this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8320c2ab", + "metadata": {}, + "outputs": [], + "source": [ + "from context import *" + ] + }, + { + "cell_type": "markdown", + "id": "0a5b6872", + "metadata": {}, + "source": [ + "### Why comment?" + ] + }, + { + "cell_type": "markdown", + "id": "6943cb88", + "metadata": {}, + "source": [ + "\n", + "* You're writing code for people, as well as computers.\n", + "* Comments can help you build code, by representing your design\n", + "* Comments explain subtleties in the code which are not obvious from the syntax\n", + "* Comments explain *why* you wrote the code the way you did\n" + ] + }, + { + "cell_type": "markdown", + "id": "5f15f55d", + "metadata": {}, + "source": [ + "### Bad Comments" + ] + }, + { + "cell_type": "markdown", + "id": "fc17d3bb", + "metadata": {}, + "source": [ + "\n", + "\"I write good code, you can tell by the number of comments.\"\n", + "\n", + "This is wrong.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b10fe77b", + "metadata": {}, + "source": [ + "### Comments which are obvious" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef998f18", + "metadata": {}, + "outputs": [], + "source": [ + "counter = counter + 1 # Increment the counter\n", + "for element in array: # Loop over elements\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "e8db981e", + "metadata": {}, + "source": [ + "### Comments which could be replaced by better style" + ] + }, + { + "cell_type": "markdown", + "id": "e6e3260d", + "metadata": {}, + "source": [ + "The following piece of code could be a part of a game to move a turtle in a certain direction, with a particular angular velocity and step size." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07c92521", + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(len(agt)): #for each agent\n", + " agt[i].theta += ws[i] # Increment the angle of each agent\n", + " #by its angular velocity\n", + " agt[i].x += r * sin(agt[i].theta) #Move the agent by the step-size\n", + " agt[i].y += r * cos(agt[i].theta) #r in the direction indicated" + ] + }, + { + "cell_type": "markdown", + "id": "40ada570", + "metadata": {}, + "source": [ + "we have used comments to make the code readable.\n", + "\n", + "\n", + "Why not make the code readable instead?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff4b9e02", + "metadata": {}, + "outputs": [], + "source": [ + "for agent in agents:\n", + " agent.turn()\n", + " agent.move()\n", + "\n", + "class Agent:\n", + " def turn(self):\n", + " self.direction += self.angular_velocity;\n", + " def move(self):\n", + " self.x += Agent.step_length * sin(self.direction)\n", + " self.y += Agent.step_length * cos(self.direction)" + ] + }, + { + "cell_type": "markdown", + "id": "1c5686a6", + "metadata": {}, + "source": [ + "This is probably better. We are using the name of the functions (_i.e._, `turn`, `move`) instead of comments. Therefore, we've got _self-documenting_ code.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8531991b", + "metadata": {}, + "source": [ + "### Comments vs expressive code " + ] + }, + { + "cell_type": "markdown", + "id": "510e00c7", + "metadata": {}, + "source": [ + "\n", + "> The proper use of comments is to compensate for our failure to express yourself in code. \n", + "Note that I used the word failure. I meant it. Comments are always failures.\n", + "\n", + "-- Robert Martin, [Clean Code](https://www.worldcat.org/title/clean-code-a-handbook-of-agile-software-craftsmanship/oclc/1057907478&referer=brief_results) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21163090000004761)].\n", + "\n", + "I wouldn't disagree, but still, writing \"self-documenting\" code is very hard, so do comment if you're unsure!\n" + ] + }, + { + "cell_type": "markdown", + "id": "4c2ed06f", + "metadata": {}, + "source": [ + "### Comments which belong in an issue tracker" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a3becd9", + "metadata": {}, + "outputs": [], + "source": [ + "x.clear() # Code crashes here sometimes\n", + "class Agent(object):\n", + " pass\n", + " # TODO: Implement pretty-printer method" + ] + }, + { + "cell_type": "markdown", + "id": "b33fa764", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "BUT comments that reference issues in the tracker can be good.\n", + "\n", + "E.g.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d32e5757", + "metadata": {}, + "outputs": [], + "source": [ + "if x.safe_to_clear(): # Guard added as temporary workaround for #32\n", + " x.clear()" + ] + }, + { + "cell_type": "markdown", + "id": "fbc66334", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "is OK. And platforms like GitHub will create a link to it when browsing the code.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7bce708f", + "metadata": {}, + "source": [ + "### Comments which only make sense to the author today" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74153694", + "metadata": {}, + "outputs": [], + "source": [ + "agent.turn() # Turtle Power!\n", + "agent.move()\n", + "agents[:]=[]# Shredder!" + ] + }, + { + "cell_type": "markdown", + "id": "6b7d80e3", + "metadata": {}, + "source": [ + "### Comments which are unpublishable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f85f34ba", + "metadata": {}, + "outputs": [], + "source": [ + "# Stupid supervisor made me write this code\n", + "# So I did it while very very drunk." + ] + }, + { + "cell_type": "markdown", + "id": "570e2d58", + "metadata": {}, + "source": [ + "### Good commenting: pedagogical comments" + ] + }, + { + "cell_type": "markdown", + "id": "0bac8bf9", + "metadata": {}, + "source": [ + "\n", + "Code that *is* good style, but you're not familiar with, or \n", + "that colleagues might not be familiar with\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "297a3e51", + "metadata": {}, + "outputs": [], + "source": [ + "# This is how you define a decorator in python\n", + "# See https://wiki.python.org/moin/PythonDecorators\n", + "def double(decorated_function):\n", + " # Here, the result function forms a closure over \n", + " # the decorated function\n", + " def result_function(entry):\n", + " return decorated_function(decorated_function(entry))\n", + " # The returned result is a function\n", + " return result_function\n", + "\n", + "@double\n", + "def try_me_twice():\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "059b9abc", + "metadata": {}, + "source": [ + "### Good commenting: reasons and definitions" + ] + }, + { + "cell_type": "markdown", + "id": "342e34f9", + "metadata": {}, + "source": [ + "\n", + "Comments which explain coding definitions or reasons for programming choices.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8822dc4", + "metadata": {}, + "outputs": [], + "source": [ + "def __init__(self):\n", + " self.angle = 0 # clockwise from +ve y-axis\n", + " nonzero_indices = [] # Use sparse model as memory constrained" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Comments" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/03comments.ipynb.py b/ch05construction/03comments.ipynb.py new file mode 100644 index 000000000..82d8e8797 --- /dev/null +++ b/ch05construction/03comments.ipynb.py @@ -0,0 +1,188 @@ +# --- +# jupyter: +# jekyll: +# display_name: Comments +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Comments + +# %% [markdown] +# Let's import first the context for this chapter. + +# %% +from context import * + +# %% [markdown] +# ### Why comment? + +# %% [markdown] +# +# * You're writing code for people, as well as computers. +# * Comments can help you build code, by representing your design +# * Comments explain subtleties in the code which are not obvious from the syntax +# * Comments explain *why* you wrote the code the way you did +# + +# %% [markdown] +# ### Bad Comments + +# %% [markdown] +# +# "I write good code, you can tell by the number of comments." +# +# This is wrong. +# + +# %% [markdown] +# ### Comments which are obvious + +# %% +counter = counter + 1 # Increment the counter +for element in array: # Loop over elements + pass + +# %% [markdown] +# ### Comments which could be replaced by better style + +# %% [markdown] +# The following piece of code could be a part of a game to move a turtle in a certain direction, with a particular angular velocity and step size. + +# %% +for i in range(len(agt)): #for each agent + agt[i].theta += ws[i] # Increment the angle of each agent + #by its angular velocity + agt[i].x += r * sin(agt[i].theta) #Move the agent by the step-size + agt[i].y += r * cos(agt[i].theta) #r in the direction indicated + +# %% [markdown] +# we have used comments to make the code readable. +# +# +# Why not make the code readable instead? + +# %% +for agent in agents: + agent.turn() + agent.move() + +class Agent: + def turn(self): + self.direction += self.angular_velocity; + def move(self): + self.x += Agent.step_length * sin(self.direction) + self.y += Agent.step_length * cos(self.direction) + + +# %% [markdown] +# This is probably better. We are using the name of the functions (_i.e._, `turn`, `move`) instead of comments. Therefore, we've got _self-documenting_ code. +# + +# %% [markdown] +# ### Comments vs expressive code + +# %% [markdown] +# +# > The proper use of comments is to compensate for our failure to express yourself in code. +# Note that I used the word failure. I meant it. Comments are always failures. +# +# -- Robert Martin, [Clean Code](https://www.worldcat.org/title/clean-code-a-handbook-of-agile-software-craftsmanship/oclc/1057907478&referer=brief_results) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21163090000004761)]. +# +# I wouldn't disagree, but still, writing "self-documenting" code is very hard, so do comment if you're unsure! +# + +# %% [markdown] +# ### Comments which belong in an issue tracker + +# %% +x.clear() # Code crashes here sometimes +class Agent(object): + pass + # TODO: Implement pretty-printer method + + +# %% [markdown] +# +# +# +# BUT comments that reference issues in the tracker can be good. +# +# E.g. +# +# +# + +# %% +if x.safe_to_clear(): # Guard added as temporary workaround for #32 + x.clear() + +# %% [markdown] +# +# +# +# is OK. And platforms like GitHub will create a link to it when browsing the code. +# + +# %% [markdown] +# ### Comments which only make sense to the author today + +# %% +agent.turn() # Turtle Power! +agent.move() +agents[:]=[]# Shredder! + + +# %% [markdown] +# ### Comments which are unpublishable + +# %% +# Stupid supervisor made me write this code +# So I did it while very very drunk. + +# %% [markdown] +# ### Good commenting: pedagogical comments + +# %% [markdown] +# +# Code that *is* good style, but you're not familiar with, or +# that colleagues might not be familiar with +# +# +# + +# %% +# This is how you define a decorator in python +# See https://wiki.python.org/moin/PythonDecorators +def double(decorated_function): + # Here, the result function forms a closure over + # the decorated function + def result_function(entry): + return decorated_function(decorated_function(entry)) + # The returned result is a function + return result_function + +@double +def try_me_twice(): + pass + + +# %% [markdown] +# ### Good commenting: reasons and definitions + +# %% [markdown] +# +# Comments which explain coding definitions or reasons for programming choices. +# +# + +# %% +def __init__(self): + self.angle = 0 # clockwise from +ve y-axis + nonzero_indices = [] # Use sparse model as memory constrained diff --git a/ch05construction/05refactoring.html b/ch05construction/05refactoring.html new file mode 100644 index 000000000..70a7a3b22 --- /dev/null +++ b/ch05construction/05refactoring.html @@ -0,0 +1,1161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Refactoring + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Refactoring

+
+
+
+
+
+
+

Let's import first the context for this chapter.

+
+
+
+
+
+
In [1]:
+
+
+
from context import *
+
+
+
+
+
+
+
+
+

Let's put ourselves in an scenario - that you've probably been in before. Imagine you are changing a large piece of legacy code that's not well structured, introducing many changes at once, trying to keep in your head all the bits and pieces that need to be modified to make it all work again. And suddenly, your officemate comes and ask you to go for coffee... and you've lost all track of what you had in your head and need to start again.

+

Instead of doing so, we could use a more robust approach to go from nasty ugly code to clean code in a safer way.

+
+
+
+
+
+
+

Refactoring

+
+
+
+
+
+
+

To refactor is to:

+
    +
  • Make a change to the design of some software
  • +
  • Which improves the structure or readability
  • +
  • But which leaves the actual behaviour of the program completely unchanged.
  • +
+
+
+
+
+
+
+

A word from the Master

+
+
+
+
+
+
+
+

Refactoring is a controlled technique for improving the design of an existing code base. +Its essence is applying a series of small behavior-preserving transformations, each of which "too small to be worth doing". +However the cumulative effect of each of these transformations is quite significant. +By doing them in small steps you reduce the risk of introducing errors. +You also avoid having the system broken while you are carrying out the restructuring - +which allows you to gradually refactor a system over an extended period of time.

+
+

-- Martin Fowler Refactoring [UCL library].

+
+
+
+
+
+
+

List of known refactorings

+
+
+
+
+
+
+

The next few sections will present some known refactorings.

+

We'll show before and after code, present any new coding techniques needed to do the refactoring, and describe code smells: how you know you need to refactor.

+
+
+
+
+
+
+

Replace magic numbers with constants

+
+
+
+
+
+
+

Smell: Raw numbers appear in your code

+

Before:

+
+
+
+
+
+
In [2]:
+
+
+
data = [math.sin(x) for x in np.arange(0,3.141,3.141/100)]
+result = [0]*100
+for i in range(100):
+    for j in range(i+1, 100):
+        result[j] += data[i] * data[i-j] / 100
+
+
+
+
+
+
+
+
+

after:

+
+
+
+
+
+
In [3]:
+
+
+
resolution = 100
+pi = 3.141
+data = [math.sin(x) for x in np.arange(0, pi, pi/resolution)]
+result = [0] * resolution
+for i in range(resolution):
+    for j in range(i + 1, resolution):
+        result[j] += data[i] * data[i-j] / resolution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Replace repeated code with a function

+
+
+
+
+
+
+

Smell: Fragments of repeated code appear.

+

Fragment of model where some birds are chasing each other: if the angle of view of one can see the prey, then start hunting, and if the other see the predator, then start running away.

+

Before:

+
+
+
+
+
+
In [4]:
+
+
+
if abs(hawk.facing - starling.facing) < hawk.viewport:
+    hawk.hunting()
+
+if abs(starling.facing - hawk.facing) < starling.viewport:
+    starling.flee()
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [5]:
+
+
+
def can_see(source, target):
+    return (source.facing - target.facing) < source.viewport
+
+if can_see(hawk, starling):
+    hawk.hunting()
+
+if can_see(starling, hawk):
+    starling.flee()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Change of variable name

+
+
+
+
+
+
+

Smell: Code needs a comment to explain what it is for.

+

Before:

+
+
+
+
+
+
In [6]:
+
+
+
z = find(x,y)
+if z:
+    ribe(x)
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [7]:
+
+
+
gene = subsequence(chromosome, start_codon)
+if gene:
+    transcribe(gene)
+
+
+
+
+
+
+
+
+

Separate a complex expression into a local variable

+
+
+
+
+
+
+

Smell: An expression becomes long.

+
+
+
+
+
+
In [8]:
+
+
+
if ((my_name == your_name) and flag1 or flag2): do_something()
+
+
+
+
+
+
+
+
+

vs

+
+
+
+
+
+
In [9]:
+
+
+
same_names = (my_name == your_name)
+flags_OK = flag1 or flag2
+if same_names and flags_OK:
+    do_something()
+
+
+
+
+
+
+
+
+

Replace loop with iterator

+
+
+
+
+
+
+

Smell: Loop variable is an integer from 1 to something.

+

Before:

+
+
+
+
+
+
In [10]:
+
+
+
sum = 0
+for i in range(resolution):
+    sum += data[i]
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [11]:
+
+
+
sum = 0
+for value in data:
+    sum += value
+
+
+
+
+
+
+
+
+

Replace hand-written code with library code

+
+
+
+
+
+
+

Smell: It feels like surely someone else must have done this at some point.

+

Before:

+
+
+
+
+
+
In [12]:
+
+
+
xcoords = [start + i * step for i in range(int((end - start) / step))]
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [13]:
+
+
+
import numpy as np
+xcoords = np.arange(start, end, step)
+
+
+
+
+
+
+
+
+

See Numpy, +Pandas.

+
+
+
+
+
+
+

Replace set of arrays with array of structures

+
+
+
+
+
+
+

Smell: A function needs to work corresponding indices of several arrays:

+

Before:

+
+
+
+
+
+
In [14]:
+
+
+
def can_see(i, source_angles, target_angles, source_viewports):
+    return abs(source_angles[i] - target_angles[i]) < source_viewports[i]
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [15]:
+
+
+
def can_see(source, target):
+    return (source["facing"] - target["facing"]) < source["viewport"]
+
+
+
+
+
+
+
+
+

Warning: this refactoring greatly improves readability but can make code slower, +depending on memory layout. Be careful.

+
+
+
+
+
+
+

Replace constants with a configuration file

+
+
+
+
+
+
+

Smell: You need to change your code file to explore different research scenarios.

+

Before:

+
+
+
+
+
+
In [16]:
+
+
+
flight_speed = 2.0 # mph
+bounds = [0, 0, 100, 100]
+turning_circle = 3.0 # m
+bird_counts = {"hawk": 5, "starling": 500}
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [17]:
+
+
+
%%writefile config.yaml
+bounds: [0, 0, 100, 100]
+counts:
+    hawk: 5
+    starling: 500
+speed: 2.0
+turning_circle: 3.0
+
+
+
+
+
+
+
+
+
+
Writing config.yaml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [18]:
+
+
+
config = yaml.safe_load(open("config.yaml"))
+
+
+
+
+
+
+
+
+

See YAML and PyYaml, +and Python's os module.

+
+
+
+
+
+
+

Replace global variables with function arguments

+
+
+
+
+
+
+

Smell: A global variable is assigned and then used inside a called function:

+
+
+
+
+
+
In [19]:
+
+
+
viewport = pi/4
+
+if hawk.can_see(starling):
+    hawk.hunt(starling)
+
+class Hawk(object):
+    def can_see(self, target):
+        return (self.facing - target.facing) < viewport
+
+
+
+
+
+
+
+
+

Becomes:

+
+
+
+
+
+
In [20]:
+
+
+
viewport = pi/4
+if hawk.can_see(starling, viewport):
+    hawk.hunt(starling)
+
+class Hawk(object):
+    def can_see(self, target, viewport):
+        return (self.facing - target.facing) < viewport
+
+
+
+
+
+
+
+
+

Merge neighbouring loops

+
+
+
+
+
+
+

Smell: Two neighbouring loops have the same for statement

+
+
+
+
+
+
In [21]:
+
+
+
for bird in birds:
+    bird.build_nest()
+
+for bird in birds:
+    bird.lay_eggs()
+
+
+
+
+
+
+
+
+

Becomes:

+
+
+
+
+
+
In [22]:
+
+
+
for bird in birds:
+    bird.build_nest()
+    bird.lay_eggs()
+
+
+
+
+
+
+
+
+

Though there may be a case where all the nests need to be built before the birds can start laying eggs.

+
+
+
+
+
+
+

Break a large function into smaller units

+
+
+
+
+
+
+
    +
  • Smell: A function or subroutine no longer fits on a page in your editor.
  • +
  • Smell: A line of code is indented more than three levels.
  • +
  • Smell: A piece of code interacts with the surrounding code through just a few variables.
  • +
+

Before:

+
+
+
+
+
+
In [23]:
+
+
+
def do_calculation():
+    for predator in predators:
+        for prey in preys:
+            if predator.can_see(prey):
+                predator.hunt(prey)
+            if predator.can_reach(prey):
+                predator.eat(prey)
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [24]:
+
+
+
def do_calculation():
+    for predator in predators:
+        for prey in preys:
+            predate(predator, prey)
+
+def predate(predator, prey):
+    if predator.can_see(prey):
+        predator.hunt(prey)
+    if predator.can_reach(prey):
+        predator.eat(prey)
+
+
+
+
+
+
+
+
+

Separate code concepts into files or modules

+
+
+
+
+
+
+

Smell: You find it hard to locate a piece of code.

+

Smell: You get a lot of version control conflicts.

+

Before:

+
+
+
+
+
+
In [25]:
+
+
+
class One(object):
+    pass
+
+class Two(object):
+    def __init__():
+        self.child = One()
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [26]:
+
+
+
%%writefile anotherfile.py
+class One(object):
+    pass
+
+
+
+
+
+
+
+
+
+
Writing anotherfile.py
+
+
+
+
+
+
+
+
+
In [27]:
+
+
+
from anotherfile import One
+
+class Two(object):
+    def __init__():
+        self.child = One()
+
+
+
+
+
+
+
+
+

Refactoring is a safe way to improve code

+
+
+
+
+
+
+

You may think you can see how to rewrite a whole codebase to be better.

+

However, you may well get lost halfway through the exercise.

+

By making the changes as small, reversible, incremental steps, +you can reach your target design more reliably.

+
+
+
+
+
+
+

Tests and Refactoring

+
+
+
+
+
+
+

Badly structured code cannot be unit tested. There are no "units".

+

Before refactoring, ensure you have a robust regression test.

+

This will allow you to Refactor with confidence.

+

As you refactor, if you create any new units (functions, modules, classes), +add new tests for them.

+
+
+
+
+
+
+

Refactoring Summary

+
+
+
+
+
+
+
    +
  • Replace magic numbers with constants
  • +
  • Replace repeated code with a function
  • +
  • Change of variable/function/class name
  • +
  • Replace loop with iterator
  • +
  • Replace hand-written code with library code
  • +
  • Replace set of arrays with array of structures
  • +
  • Replace constants with a configuration file
  • +
  • Replace global variables with function arguments
  • +
  • Break a large function into smaller units
  • +
  • Separate code concepts into files or modules
  • +
+

And many more...

+

Read The Refactoring Book.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/05refactoring.ipynb b/ch05construction/05refactoring.ipynb new file mode 100644 index 000000000..323555a27 --- /dev/null +++ b/ch05construction/05refactoring.ipynb @@ -0,0 +1,1025 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a57f8a04", + "metadata": {}, + "source": [ + "## Refactoring" + ] + }, + { + "cell_type": "markdown", + "id": "4a1b0d2f", + "metadata": {}, + "source": [ + "Let's import first the context for this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1e9e1ae", + "metadata": {}, + "outputs": [], + "source": [ + "from context import *" + ] + }, + { + "cell_type": "markdown", + "id": "74c5f699", + "metadata": {}, + "source": [ + "Let's put ourselves in an scenario - that you've probably been in before. Imagine you are changing a large piece of legacy code that's not well structured, introducing many changes at once, trying to keep in your head all the bits and pieces that need to be modified to make it all work again. And suddenly, your officemate comes and ask you to go for coffee... and you've lost all track of what you had in your head and need to start again.\n", + "\n", + "Instead of doing so, we could use a more robust approach to go from nasty ugly code to clean code in a safer way." + ] + }, + { + "cell_type": "markdown", + "id": "344acddb", + "metadata": {}, + "source": [ + "### Refactoring" + ] + }, + { + "cell_type": "markdown", + "id": "20dd8de0", + "metadata": {}, + "source": [ + "\n", + "To refactor is to:\n", + "\n", + "* Make a change to the design of some software\n", + "* Which improves the structure or readability\n", + "* But which leaves the actual behaviour of the program completely unchanged.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "1fcc68f2", + "metadata": {}, + "source": [ + "### A word from the Master" + ] + }, + { + "cell_type": "markdown", + "id": "fa73d5e7", + "metadata": {}, + "source": [ + "\n", + "> Refactoring is a controlled technique for improving the design of an existing code base. \n", + "Its essence is applying a series of small behavior-preserving transformations, each of which \"too small to be worth doing\". \n", + "However the cumulative effect of each of these transformations is quite significant. \n", + "By doing them in small steps you reduce the risk of introducing errors. \n", + "You also avoid having the system broken while you are carrying out the restructuring - \n", + "which allows you to gradually refactor a system over an extended period of time.\n", + "\n", + "-- Martin Fowler [Refactoring](https://martinfowler.com/books/refactoring.html) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21146093980004761)].\n" + ] + }, + { + "cell_type": "markdown", + "id": "d8779ef0", + "metadata": {}, + "source": [ + "### List of known refactorings" + ] + }, + { + "cell_type": "markdown", + "id": "44854683", + "metadata": {}, + "source": [ + "\n", + "The next few sections will present some known refactorings.\n", + "\n", + "We'll show before and after code, present any new coding techniques needed to do the refactoring, and describe [*code smells*](https://en.wikipedia.org/wiki/Code_smell): how you know you need to refactor.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1a5b1750", + "metadata": {}, + "source": [ + "### Replace magic numbers with constants" + ] + }, + { + "cell_type": "markdown", + "id": "bdde558b", + "metadata": {}, + "source": [ + "\n", + "Smell: Raw numbers appear in your code\n", + "\n", + "Before: \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90a7e875", + "metadata": {}, + "outputs": [], + "source": [ + "data = [math.sin(x) for x in np.arange(0,3.141,3.141/100)]\n", + "result = [0]*100\n", + "for i in range(100):\n", + " for j in range(i+1, 100):\n", + " result[j] += data[i] * data[i-j] / 100" + ] + }, + { + "cell_type": "markdown", + "id": "8a0e27f1", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "after:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8b0bc78", + "metadata": {}, + "outputs": [], + "source": [ + "resolution = 100\n", + "pi = 3.141\n", + "data = [math.sin(x) for x in np.arange(0, pi, pi/resolution)]\n", + "result = [0] * resolution\n", + "for i in range(resolution):\n", + " for j in range(i + 1, resolution):\n", + " result[j] += data[i] * data[i-j] / resolution" + ] + }, + { + "cell_type": "markdown", + "id": "01aab287", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "31a218b2", + "metadata": {}, + "source": [ + "### Replace repeated code with a function" + ] + }, + { + "cell_type": "markdown", + "id": "27e8cd98", + "metadata": {}, + "source": [ + "\n", + "Smell: Fragments of repeated code appear.\n", + "\n", + "Fragment of model where some birds are chasing each other: if the angle of view of one can see the prey, then start hunting, and if the other see the predator, then start running away.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6cb393b", + "metadata": {}, + "outputs": [], + "source": [ + "if abs(hawk.facing - starling.facing) < hawk.viewport:\n", + " hawk.hunting()\n", + "\n", + "if abs(starling.facing - hawk.facing) < starling.viewport:\n", + " starling.flee()" + ] + }, + { + "cell_type": "markdown", + "id": "b906c6e0", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b4cc3f3", + "metadata": {}, + "outputs": [], + "source": [ + "def can_see(source, target):\n", + " return (source.facing - target.facing) < source.viewport\n", + "\n", + "if can_see(hawk, starling):\n", + " hawk.hunting()\n", + "\n", + "if can_see(starling, hawk):\n", + " starling.flee()" + ] + }, + { + "cell_type": "markdown", + "id": "368451ac", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "6c44289b", + "metadata": {}, + "source": [ + "### Change of variable name" + ] + }, + { + "cell_type": "markdown", + "id": "6f116545", + "metadata": {}, + "source": [ + "\n", + "Smell: Code needs a comment to explain what it is for.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22c2f5d0", + "metadata": {}, + "outputs": [], + "source": [ + "z = find(x,y)\n", + "if z:\n", + " ribe(x)" + ] + }, + { + "cell_type": "markdown", + "id": "3fcbb840", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2e429dc", + "metadata": {}, + "outputs": [], + "source": [ + "gene = subsequence(chromosome, start_codon)\n", + "if gene:\n", + " transcribe(gene)" + ] + }, + { + "cell_type": "markdown", + "id": "7f9c7d00", + "metadata": {}, + "source": [ + "### Separate a complex expression into a local variable" + ] + }, + { + "cell_type": "markdown", + "id": "fb552605", + "metadata": {}, + "source": [ + "\n", + "Smell: An expression becomes long.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "804db9be", + "metadata": {}, + "outputs": [], + "source": [ + "if ((my_name == your_name) and flag1 or flag2): do_something()" + ] + }, + { + "cell_type": "markdown", + "id": "93d46946", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "vs\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "108e6c23", + "metadata": {}, + "outputs": [], + "source": [ + "same_names = (my_name == your_name)\n", + "flags_OK = flag1 or flag2\n", + "if same_names and flags_OK:\n", + " do_something()" + ] + }, + { + "cell_type": "markdown", + "id": "22b9aaec", + "metadata": {}, + "source": [ + "### Replace loop with iterator" + ] + }, + { + "cell_type": "markdown", + "id": "77f7ac2b", + "metadata": {}, + "source": [ + "\n", + "Smell: Loop variable is an integer from 1 to something.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5262a52", + "metadata": {}, + "outputs": [], + "source": [ + "sum = 0\n", + "for i in range(resolution):\n", + " sum += data[i]" + ] + }, + { + "cell_type": "markdown", + "id": "f7c5b625", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4992b1d2", + "metadata": {}, + "outputs": [], + "source": [ + "sum = 0\n", + "for value in data:\n", + " sum += value" + ] + }, + { + "cell_type": "markdown", + "id": "f6115133", + "metadata": {}, + "source": [ + "### Replace hand-written code with library code" + ] + }, + { + "cell_type": "markdown", + "id": "ac179a09", + "metadata": {}, + "source": [ + "\n", + "Smell: It feels like surely someone else must have done this at some point.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5ef87cb", + "metadata": {}, + "outputs": [], + "source": [ + "xcoords = [start + i * step for i in range(int((end - start) / step))]" + ] + }, + { + "cell_type": "markdown", + "id": "4111ea43", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dfc2879", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "xcoords = np.arange(start, end, step)" + ] + }, + { + "cell_type": "markdown", + "id": "e08e3097", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "See [Numpy](http://docs.scipy.org/doc/numpy/reference/generated/numpy.arange.html),\n", + " [Pandas](http://pandas.pydata.org/).\n" + ] + }, + { + "cell_type": "markdown", + "id": "891133d5", + "metadata": {}, + "source": [ + "### Replace set of arrays with array of structures" + ] + }, + { + "cell_type": "markdown", + "id": "b7fdce22", + "metadata": {}, + "source": [ + "\n", + "Smell: A function needs to work corresponding indices of several arrays:\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdd42c87", + "metadata": {}, + "outputs": [], + "source": [ + "def can_see(i, source_angles, target_angles, source_viewports):\n", + " return abs(source_angles[i] - target_angles[i]) < source_viewports[i]" + ] + }, + { + "cell_type": "markdown", + "id": "ad68971f", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19233119", + "metadata": {}, + "outputs": [], + "source": [ + "def can_see(source, target):\n", + " return (source[\"facing\"] - target[\"facing\"]) < source[\"viewport\"]" + ] + }, + { + "cell_type": "markdown", + "id": "13547e62", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Warning: this refactoring greatly improves readability but can make code slower,\n", + "depending on memory layout. Be careful.\n" + ] + }, + { + "cell_type": "markdown", + "id": "10a28a98", + "metadata": {}, + "source": [ + "### Replace constants with a configuration file" + ] + }, + { + "cell_type": "markdown", + "id": "805cd8b2", + "metadata": {}, + "source": [ + "\n", + "Smell: You need to change your code file to explore different research scenarios.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1b48df0", + "metadata": {}, + "outputs": [], + "source": [ + "flight_speed = 2.0 # mph\n", + "bounds = [0, 0, 100, 100]\n", + "turning_circle = 3.0 # m\n", + "bird_counts = {\"hawk\": 5, \"starling\": 500}" + ] + }, + { + "cell_type": "markdown", + "id": "c32b41c1", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5c10400", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile config.yaml\n", + "bounds: [0, 0, 100, 100]\n", + "counts:\n", + " hawk: 5\n", + " starling: 500\n", + "speed: 2.0\n", + "turning_circle: 3.0" + ] + }, + { + "cell_type": "markdown", + "id": "b34d3859", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c10f9ed", + "metadata": {}, + "outputs": [], + "source": [ + "config = yaml.safe_load(open(\"config.yaml\"))" + ] + }, + { + "cell_type": "markdown", + "id": "11a16e66", + "metadata": {}, + "source": [ + "\n", + "See [YAML](http://www.yaml.org/) and [PyYaml](http://pyyaml.org/),\n", + "and [Python's os module](https://docs.python.org/3/library/os.html).\n" + ] + }, + { + "cell_type": "markdown", + "id": "0012a597", + "metadata": {}, + "source": [ + "### Replace global variables with function arguments" + ] + }, + { + "cell_type": "markdown", + "id": "58b3148b", + "metadata": {}, + "source": [ + "\n", + "Smell: A global variable is assigned and then used inside a called function:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18268278", + "metadata": {}, + "outputs": [], + "source": [ + "viewport = pi/4\n", + "\n", + "if hawk.can_see(starling):\n", + " hawk.hunt(starling)\n", + "\n", + "class Hawk(object):\n", + " def can_see(self, target):\n", + " return (self.facing - target.facing) < viewport" + ] + }, + { + "cell_type": "markdown", + "id": "f2ad9ab1", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Becomes:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dd59eed", + "metadata": {}, + "outputs": [], + "source": [ + "viewport = pi/4\n", + "if hawk.can_see(starling, viewport):\n", + " hawk.hunt(starling)\n", + "\n", + "class Hawk(object):\n", + " def can_see(self, target, viewport):\n", + " return (self.facing - target.facing) < viewport" + ] + }, + { + "cell_type": "markdown", + "id": "f20accf9", + "metadata": {}, + "source": [ + "### Merge neighbouring loops" + ] + }, + { + "cell_type": "markdown", + "id": "f7e4f149", + "metadata": {}, + "source": [ + "\n", + "Smell: Two neighbouring loops have the same for statement\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67676311", + "metadata": {}, + "outputs": [], + "source": [ + "for bird in birds:\n", + " bird.build_nest()\n", + "\n", + "for bird in birds:\n", + " bird.lay_eggs()" + ] + }, + { + "cell_type": "markdown", + "id": "4ba9dea6", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Becomes:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f2fa091", + "metadata": {}, + "outputs": [], + "source": [ + "for bird in birds:\n", + " bird.build_nest()\n", + " bird.lay_eggs()" + ] + }, + { + "cell_type": "markdown", + "id": "693fad4e", + "metadata": {}, + "source": [ + "Though there may be a case where all the nests need to be built before the birds can start laying eggs." + ] + }, + { + "cell_type": "markdown", + "id": "a52f3d27", + "metadata": {}, + "source": [ + "### Break a large function into smaller units" + ] + }, + { + "cell_type": "markdown", + "id": "e496be9f", + "metadata": {}, + "source": [ + "\n", + "* Smell: A function or subroutine no longer fits on a page in your editor.\n", + "* Smell: A line of code is indented more than three levels.\n", + "* Smell: A piece of code interacts with the surrounding code through just a few variables.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39f8c739", + "metadata": {}, + "outputs": [], + "source": [ + "def do_calculation():\n", + " for predator in predators:\n", + " for prey in preys:\n", + " if predator.can_see(prey):\n", + " predator.hunt(prey)\n", + " if predator.can_reach(prey):\n", + " predator.eat(prey)" + ] + }, + { + "cell_type": "markdown", + "id": "fba33105", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "335e0817", + "metadata": {}, + "outputs": [], + "source": [ + "def do_calculation():\n", + " for predator in predators:\n", + " for prey in preys:\n", + " predate(predator, prey)\n", + "\n", + "def predate(predator, prey):\n", + " if predator.can_see(prey):\n", + " predator.hunt(prey)\n", + " if predator.can_reach(prey):\n", + " predator.eat(prey)" + ] + }, + { + "cell_type": "markdown", + "id": "e99875ca", + "metadata": {}, + "source": [ + "### Separate code concepts into files or modules" + ] + }, + { + "cell_type": "markdown", + "id": "e31355df", + "metadata": {}, + "source": [ + "\n", + "Smell: You find it hard to locate a piece of code.\n", + "\n", + "Smell: You get a lot of version control conflicts.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2ae956a", + "metadata": {}, + "outputs": [], + "source": [ + "class One(object):\n", + " pass\n", + "\n", + "class Two(object):\n", + " def __init__():\n", + " self.child = One()" + ] + }, + { + "cell_type": "markdown", + "id": "dcb6d8a3", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5523ac52", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile anotherfile.py\n", + "class One(object):\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06e775ef", + "metadata": {}, + "outputs": [], + "source": [ + "from anotherfile import One\n", + "\n", + "class Two(object):\n", + " def __init__():\n", + " self.child = One()" + ] + }, + { + "cell_type": "markdown", + "id": "2076f142", + "metadata": {}, + "source": [ + "### Refactoring is a safe way to improve code" + ] + }, + { + "cell_type": "markdown", + "id": "8c3ddd62", + "metadata": {}, + "source": [ + "\n", + "You may think you can see how to rewrite a whole codebase to be better.\n", + "\n", + "However, you may well get lost halfway through the exercise.\n", + "\n", + "By making the changes as small, reversible, incremental steps,\n", + "you can reach your target design more reliably.\n" + ] + }, + { + "cell_type": "markdown", + "id": "0a1f1a8e", + "metadata": {}, + "source": [ + "### Tests and Refactoring" + ] + }, + { + "cell_type": "markdown", + "id": "6ba820cf", + "metadata": {}, + "source": [ + "\n", + "Badly structured code cannot be unit tested. There are no \"units\".\n", + "\n", + "Before refactoring, ensure you have a robust regression test.\n", + "\n", + "This will allow you to *Refactor with confidence*.\n", + "\n", + "As you refactor, if you create any new units (functions, modules, classes),\n", + "add new tests for them.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d0bef0c", + "metadata": {}, + "source": [ + "### Refactoring Summary" + ] + }, + { + "cell_type": "markdown", + "id": "2fe753eb", + "metadata": {}, + "source": [ + "\n", + "* Replace magic numbers with constants\n", + "* Replace repeated code with a function\n", + "* Change of variable/function/class name\n", + "* Replace loop with iterator\n", + "* Replace hand-written code with library code\n", + "* Replace set of arrays with array of structures\n", + "* Replace constants with a configuration file\n", + "* Replace global variables with function arguments\n", + "* Break a large function into smaller units\n", + "* Separate code concepts into files or modules\n", + "\n", + "And many more...\n", + "\n", + "Read [The Refactoring Book](https://martinfowler.com/books/refactoring.html).\n", + "\n", + "\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Refactoring" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/05refactoring.ipynb.py b/ch05construction/05refactoring.ipynb.py new file mode 100644 index 000000000..2e6b76221 --- /dev/null +++ b/ch05construction/05refactoring.ipynb.py @@ -0,0 +1,590 @@ +# --- +# jupyter: +# jekyll: +# display_name: Refactoring +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Refactoring + +# %% [markdown] +# Let's import first the context for this chapter. + +# %% +from context import * + +# %% [markdown] +# Let's put ourselves in an scenario - that you've probably been in before. Imagine you are changing a large piece of legacy code that's not well structured, introducing many changes at once, trying to keep in your head all the bits and pieces that need to be modified to make it all work again. And suddenly, your officemate comes and ask you to go for coffee... and you've lost all track of what you had in your head and need to start again. +# +# Instead of doing so, we could use a more robust approach to go from nasty ugly code to clean code in a safer way. + +# %% [markdown] +# ### Refactoring + +# %% [markdown] +# +# To refactor is to: +# +# * Make a change to the design of some software +# * Which improves the structure or readability +# * But which leaves the actual behaviour of the program completely unchanged. +# +# + +# %% [markdown] +# ### A word from the Master + +# %% [markdown] +# +# > Refactoring is a controlled technique for improving the design of an existing code base. +# Its essence is applying a series of small behavior-preserving transformations, each of which "too small to be worth doing". +# However the cumulative effect of each of these transformations is quite significant. +# By doing them in small steps you reduce the risk of introducing errors. +# You also avoid having the system broken while you are carrying out the restructuring - +# which allows you to gradually refactor a system over an extended period of time. +# +# -- Martin Fowler [Refactoring](https://martinfowler.com/books/refactoring.html) [[UCL library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21146093980004761)]. +# + +# %% [markdown] +# ### List of known refactorings + +# %% [markdown] +# +# The next few sections will present some known refactorings. +# +# We'll show before and after code, present any new coding techniques needed to do the refactoring, and describe [*code smells*](https://en.wikipedia.org/wiki/Code_smell): how you know you need to refactor. +# + +# %% [markdown] +# ### Replace magic numbers with constants + +# %% [markdown] +# +# Smell: Raw numbers appear in your code +# +# Before: +# +# +# + +# %% +data = [math.sin(x) for x in np.arange(0,3.141,3.141/100)] +result = [0]*100 +for i in range(100): + for j in range(i+1, 100): + result[j] += data[i] * data[i-j] / 100 + +# %% [markdown] +# +# +# +# after: +# +# +# + +# %% +resolution = 100 +pi = 3.141 +data = [math.sin(x) for x in np.arange(0, pi, pi/resolution)] +result = [0] * resolution +for i in range(resolution): + for j in range(i + 1, resolution): + result[j] += data[i] * data[i-j] / resolution + +# %% [markdown] +# +# +# + +# %% [markdown] +# ### Replace repeated code with a function + +# %% [markdown] +# +# Smell: Fragments of repeated code appear. +# +# Fragment of model where some birds are chasing each other: if the angle of view of one can see the prey, then start hunting, and if the other see the predator, then start running away. +# +# Before: +# +# +# + +# %% +if abs(hawk.facing - starling.facing) < hawk.viewport: + hawk.hunting() + +if abs(starling.facing - hawk.facing) < starling.viewport: + starling.flee() + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +def can_see(source, target): + return (source.facing - target.facing) < source.viewport + +if can_see(hawk, starling): + hawk.hunting() + +if can_see(starling, hawk): + starling.flee() + +# %% [markdown] +# +# +# + +# %% [markdown] +# ### Change of variable name + +# %% [markdown] +# +# Smell: Code needs a comment to explain what it is for. +# +# Before: +# +# +# + +# %% +z = find(x,y) +if z: + ribe(x) + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +gene = subsequence(chromosome, start_codon) +if gene: + transcribe(gene) + +# %% [markdown] +# ### Separate a complex expression into a local variable + +# %% [markdown] +# +# Smell: An expression becomes long. +# +# +# + +# %% +if ((my_name == your_name) and flag1 or flag2): do_something() + +# %% [markdown] +# +# +# +# vs +# +# +# + +# %% +same_names = (my_name == your_name) +flags_OK = flag1 or flag2 +if same_names and flags_OK: + do_something() + +# %% [markdown] +# ### Replace loop with iterator + +# %% [markdown] +# +# Smell: Loop variable is an integer from 1 to something. +# +# Before: +# +# +# + +# %% +sum = 0 +for i in range(resolution): + sum += data[i] + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +sum = 0 +for value in data: + sum += value + +# %% [markdown] +# ### Replace hand-written code with library code + +# %% [markdown] +# +# Smell: It feels like surely someone else must have done this at some point. +# +# Before: +# +# +# + +# %% +xcoords = [start + i * step for i in range(int((end - start) / step))] + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +import numpy as np +xcoords = np.arange(start, end, step) + + +# %% [markdown] +# +# +# +# See [Numpy](http://docs.scipy.org/doc/numpy/reference/generated/numpy.arange.html), +# [Pandas](http://pandas.pydata.org/). +# + +# %% [markdown] +# ### Replace set of arrays with array of structures + +# %% [markdown] +# +# Smell: A function needs to work corresponding indices of several arrays: +# +# Before: +# +# +# + +# %% +def can_see(i, source_angles, target_angles, source_viewports): + return abs(source_angles[i] - target_angles[i]) < source_viewports[i] + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +def can_see(source, target): + return (source["facing"] - target["facing"]) < source["viewport"] + + +# %% [markdown] +# +# +# +# Warning: this refactoring greatly improves readability but can make code slower, +# depending on memory layout. Be careful. +# + +# %% [markdown] +# ### Replace constants with a configuration file + +# %% [markdown] +# +# Smell: You need to change your code file to explore different research scenarios. +# +# Before: +# +# +# + +# %% +flight_speed = 2.0 # mph +bounds = [0, 0, 100, 100] +turning_circle = 3.0 # m +bird_counts = {"hawk": 5, "starling": 500} + +# %% [markdown] +# +# +# +# After: +# +# +# +# +# +# + +# %% +# %%writefile config.yaml +bounds: [0, 0, 100, 100] +counts: + hawk: 5 + starling: 500 +speed: 2.0 +turning_circle: 3.0 + +# %% [markdown] +# +# +# +# +# + +# %% +config = yaml.safe_load(open("config.yaml")) + +# %% [markdown] +# +# See [YAML](http://www.yaml.org/) and [PyYaml](http://pyyaml.org/), +# and [Python's os module](https://docs.python.org/3/library/os.html). +# + +# %% [markdown] +# ### Replace global variables with function arguments + +# %% [markdown] +# +# Smell: A global variable is assigned and then used inside a called function: +# +# +# + +# %% +viewport = pi/4 + +if hawk.can_see(starling): + hawk.hunt(starling) + +class Hawk(object): + def can_see(self, target): + return (self.facing - target.facing) < viewport + + +# %% [markdown] +# +# +# +# Becomes: +# +# +# + +# %% +viewport = pi/4 +if hawk.can_see(starling, viewport): + hawk.hunt(starling) + +class Hawk(object): + def can_see(self, target, viewport): + return (self.facing - target.facing) < viewport + + +# %% [markdown] +# ### Merge neighbouring loops + +# %% [markdown] +# +# Smell: Two neighbouring loops have the same for statement +# +# +# + +# %% +for bird in birds: + bird.build_nest() + +for bird in birds: + bird.lay_eggs() + +# %% [markdown] +# +# +# +# Becomes: +# +# +# + +# %% +for bird in birds: + bird.build_nest() + bird.lay_eggs() + + +# %% [markdown] +# Though there may be a case where all the nests need to be built before the birds can start laying eggs. + +# %% [markdown] +# ### Break a large function into smaller units + +# %% [markdown] +# +# * Smell: A function or subroutine no longer fits on a page in your editor. +# * Smell: A line of code is indented more than three levels. +# * Smell: A piece of code interacts with the surrounding code through just a few variables. +# +# Before: +# +# +# + +# %% +def do_calculation(): + for predator in predators: + for prey in preys: + if predator.can_see(prey): + predator.hunt(prey) + if predator.can_reach(prey): + predator.eat(prey) + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +def do_calculation(): + for predator in predators: + for prey in preys: + predate(predator, prey) + +def predate(predator, prey): + if predator.can_see(prey): + predator.hunt(prey) + if predator.can_reach(prey): + predator.eat(prey) + + +# %% [markdown] +# ### Separate code concepts into files or modules + +# %% [markdown] +# +# Smell: You find it hard to locate a piece of code. +# +# Smell: You get a lot of version control conflicts. +# +# Before: +# +# +# + +# %% +class One(object): + pass + +class Two(object): + def __init__(): + self.child = One() + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +# %%writefile anotherfile.py +class One(object): + pass + + +# %% +from anotherfile import One + +class Two(object): + def __init__(): + self.child = One() + +# %% [markdown] +# ### Refactoring is a safe way to improve code + +# %% [markdown] +# +# You may think you can see how to rewrite a whole codebase to be better. +# +# However, you may well get lost halfway through the exercise. +# +# By making the changes as small, reversible, incremental steps, +# you can reach your target design more reliably. +# + +# %% [markdown] +# ### Tests and Refactoring + +# %% [markdown] +# +# Badly structured code cannot be unit tested. There are no "units". +# +# Before refactoring, ensure you have a robust regression test. +# +# This will allow you to *Refactor with confidence*. +# +# As you refactor, if you create any new units (functions, modules, classes), +# add new tests for them. +# + +# %% [markdown] +# ### Refactoring Summary + +# %% [markdown] +# +# * Replace magic numbers with constants +# * Replace repeated code with a function +# * Change of variable/function/class name +# * Replace loop with iterator +# * Replace hand-written code with library code +# * Replace set of arrays with array of structures +# * Replace constants with a configuration file +# * Replace global variables with function arguments +# * Break a large function into smaller units +# * Separate code concepts into files or modules +# +# And many more... +# +# Read [The Refactoring Book](https://martinfowler.com/books/refactoring.html). +# +# +# diff --git a/ch05construction/06objects.html b/ch05construction/06objects.html new file mode 100644 index 000000000..eae898386 --- /dev/null +++ b/ch05construction/06objects.html @@ -0,0 +1,798 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Object Refactorings + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Design

+
+
+
+
+
+
+

Let's import first the context for this chapter.

+
+
+
+
+
+
In [1]:
+
+
+
from context import *
+
+
+
+
+
+
+
+
+

Object-Oriented Design

In this session, we will finally discuss the thing most people think of when they refer to "Software Engineering": the deliberate design of software. +We will discuss processes and methodologies for planned development of large-scale software projects: Software Architecture.

+

The software engineering community has, in large part, focused on an object-oriented approach to the design and development of large scale software systems. +The basic concepts of object orientation are necessary to follow much of the software engineering conversation.

+

Design processes

In addition to object-oriented architecture, software engineers have focused on the development of processes for robust, reliable software development. +These codified ways of working hope to enable organisations to repeatably and reliably complete complex software projects in a way that minimises both development +and maintainance costs, and meets user requirements.

+

Design and research

Software engineering theory has largely been developed in the context of commercial software companies.

+

The extent to which the practices and processes developed for commercial software are applicable in a research context is itself an active area of research.

+
+
+
+
+
+
+

Recap of Object-Orientation

+
+
+
+
+
+
+

Classes: User defined types

+
+
+
+
+
+
In [2]:
+
+
+
class Person:
+    def __init__(self, name, age):
+        self.name = name
+        self.age = age
+    def grow_up(self):
+        self.age += 1
+
+terry = Person("Terry", 76)
+terry.home = "Colwyn Bay"
+
+
+
+
+
+
+
+
+

Notice, that in Python, you can add properties to an object once it's been defined. Just because you can doesn't mean you should!

+
+
+
+
+
+
+

Declaring a class

+
+
+
+
+
+
+

Class: A user-defined type

+
+
+
+
+
+
In [3]:
+
+
+
class MyClass:
+    pass
+
+
+
+
+
+
+
+
+

Object instances

+
+
+
+
+
+
+

Instance: A particular object instantiated from a class.

+
+
+
+
+
+
In [4]:
+
+
+
my_object = MyClass()
+
+
+
+
+
+
+
+
+

Method

+
+
+
+
+
+
+

Method: A function which is "built in" to a class

+
+
+
+
+
+
In [5]:
+
+
+
class MyClass:
+    def someMethod(self, argument):
+        pass
+
+my_object = MyClass()
+my_object.someMethod(value)
+
+
+
+
+
+
+
+
+

Constructor

+
+
+
+
+
+
+

Constructor: A special method called when instantiating a new object

+
+
+
+
+
+
In [6]:
+
+
+
class MyClass:
+    def __init__(self, argument):
+        pass
+
+my_object = MyClass(value)
+
+
+
+
+
+
+
+
+

Member Variable

+
+
+
+
+
+
+

Member variable: a value stored inside an instance of a class.

+
+
+
+
+
+
In [7]:
+
+
+
class MyClass:
+    def __init__(self):
+        self.member = "Value"
+
+my_object = MyClass()
+assert(my_object.member == "Value")
+
+
+
+
+
+
+
+
+

Object refactorings

+
+
+
+
+
+
+

Replace add-hoc structure with user defined classes

+
+
+
+
+
+
+

Smell: A data structure made of nested arrays and dictionaries becomes unwieldy.

+

Before:

+
+
+
+
+
+
In [8]:
+
+
+
from random import random
+birds = [{"position": random(),
+          "velocity": random(),
+          "type": kind} for kind in bird_types]
+
+average_position = average([bird["position"] for bird in birds])
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [9]:
+
+
+
class Bird:
+    def __init__(self, kind):
+        from random import random
+        self.type = type
+        self.position = random()
+        self.velocity = random()
+
+birds = [Bird(kind) for kind in bird_types]
+average_position = average([bird.position for bird in birds])
+
+
+
+
+
+
+
+
+

Replace function with a method

+
+
+
+
+
+
+

Smell: A function is always called with the same kind of thing

+

Before:

+
+
+
+
+
+
In [10]:
+
+
+
def can_see(source, target):
+    return (source.facing - target.facing) < source.viewport
+
+if can_see(hawk, starling):
+    hawk.hunt()
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [11]:
+
+
+
class Bird:
+    def can_see(self, target):
+        return (self.facing - target.facing) < self.viewport
+
+if hawk.can_see(starling):
+    hawk.hunt()
+
+
+
+
+
+
+
+
+

Replace method arguments with class members

+
+
+
+
+
+
+

Smell: A variable is nearly always used in arguments to +a class.

+
+
+
+
+
+
In [12]:
+
+
+
class Person:
+    def __init__(self, genes):
+        self.genes = genes
+    def reproduce_probability(self, age): pass
+    def death_probability(self, age): pass
+    def emigrate_probability(self, age): pass
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [13]:
+
+
+
class Person:
+    def __init__(self, genes, age):
+        self.age = age
+        self.genes = genes
+    def reproduce_probability(self): pass
+    def death_probability(self): pass
+    def emigrate_probability(self): pass
+
+
+
+
+
+
+
+
+

Replace global variable with class and member

+
+
+
+
+
+
+

Smell: A global variable is referenced by a few functions

+
+
+
+
+
+
In [14]:
+
+
+
name = "Terry Jones"
+birthday = [1, 2, 1942]
+today = [22, 11]
+
+if today == birthday[0:2]:
+    print(f"Happy Birthday, {name}")
+else:
+    print("No birthday for you today.")
+
+
+
+
+
+
+
+
+
+
No birthday for you today.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [15]:
+
+
+
class Person(object):
+    def __init__(self, birthday, name):
+        self.birth_day = birthday[0]
+        self.birth_month = birthday[1]
+        self.birth_year = birthday[2]
+        self.name = name
+    def check_birthday(self, today_day, today_month):
+        if not self.birth_day == today_day:
+            return False
+        if not self.birth_month == today_month:
+            return False
+        return True
+    def greet_appropriately(self, today):
+        if self.check_birthday(*today):
+            print(f"Happy Birthday, {self.name}")
+        else:
+            print("No birthday for you.")
+
+john = Person([5, 5, 1943], "Michael Palin")
+john.greet_appropriately(today)
+
+
+
+
+
+
+
+
+
+
No birthday for you.
+
+
+
+
+
+
+
+
+
+

Object Oriented Refactoring Summary

+
+
+
+
+
+
+
    +
  • Replace ad-hoc structure with a class
  • +
  • Replace function with a method
  • +
  • Replace method argument with class member
  • +
  • Replace global variable with class data
  • +
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/06objects.ipynb b/ch05construction/06objects.ipynb new file mode 100644 index 000000000..e5ce6600e --- /dev/null +++ b/ch05construction/06objects.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2710fedb", + "metadata": {}, + "source": [ + "# Design" + ] + }, + { + "cell_type": "markdown", + "id": "c968ae14", + "metadata": {}, + "source": [ + "Let's import first the context for this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "915bcc1c", + "metadata": {}, + "outputs": [], + "source": [ + "from context import *" + ] + }, + { + "cell_type": "markdown", + "id": "7d254285", + "metadata": {}, + "source": [ + "\n", + "## Object-Oriented Design\n", + "\n", + "In this session, we will finally discuss the thing most people think of when they refer to \"Software Engineering\": the deliberate *design* of software.\n", + "We will discuss processes and methodologies for planned development of large-scale software projects: *Software Architecture*.\n", + "\n", + "The software engineering community has, in large part, focused on an object-oriented approach to the design and development of large scale software systems.\n", + "The basic concepts of object orientation are necessary to follow much of the software engineering conversation.\n", + "\n", + "\n", + "### Design processes\n", + "\n", + "\n", + "In addition to object-oriented architecture, software engineers have focused on the development of processes for robust, reliable software development. \n", + "These codified ways of working hope to enable organisations to repeatably and reliably complete complex software projects in a way that minimises both development \n", + "and maintainance costs, and meets user requirements.\n", + "\n", + "\n", + "### Design and research\n", + "\n", + "\n", + "Software engineering theory has largely been developed in the context of commercial software companies.\n", + "\n", + "The extent to which the practices and processes developed for commercial software are applicable in a research context is itself an active area of research.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "160115ff", + "metadata": {}, + "source": [ + "## Recap of Object-Orientation" + ] + }, + { + "cell_type": "markdown", + "id": "f47941ae", + "metadata": {}, + "source": [ + "### Classes: User defined types" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f18d9b0c", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age):\n", + " self.name = name\n", + " self.age = age\n", + " def grow_up(self):\n", + " self.age += 1\n", + "\n", + "terry = Person(\"Terry\", 76)\n", + "terry.home = \"Colwyn Bay\"" + ] + }, + { + "cell_type": "markdown", + "id": "e242fd10", + "metadata": {}, + "source": [ + "Notice, that in Python, you can add properties to an object once it's been defined. Just because you can doesn't mean you should!\n" + ] + }, + { + "cell_type": "markdown", + "id": "0e900c0e", + "metadata": {}, + "source": [ + "### Declaring a class " + ] + }, + { + "cell_type": "markdown", + "id": "d44a435e", + "metadata": {}, + "source": [ + "\n", + "Class: A user-defined type\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "299c1985", + "metadata": {}, + "outputs": [], + "source": [ + "class MyClass:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "0435ef3b", + "metadata": {}, + "source": [ + "### Object instances" + ] + }, + { + "cell_type": "markdown", + "id": "068f5e0d", + "metadata": {}, + "source": [ + "\n", + "Instance: A particular object *instantiated* from a class.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "887e3f01", + "metadata": {}, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "markdown", + "id": "10a63c12", + "metadata": {}, + "source": [ + "### Method" + ] + }, + { + "cell_type": "markdown", + "id": "4c784121", + "metadata": {}, + "source": [ + "\n", + "Method: A function which is \"built in\" to a class\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c825fd18", + "metadata": {}, + "outputs": [], + "source": [ + "class MyClass:\n", + " def someMethod(self, argument):\n", + " pass\n", + "\n", + "my_object = MyClass()\n", + "my_object.someMethod(value)" + ] + }, + { + "cell_type": "markdown", + "id": "c66a45f1", + "metadata": {}, + "source": [ + "### Constructor" + ] + }, + { + "cell_type": "markdown", + "id": "86d33dd1", + "metadata": {}, + "source": [ + "\n", + "Constructor: A special method called when instantiating a new object\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ece92b9", + "metadata": {}, + "outputs": [], + "source": [ + "class MyClass:\n", + " def __init__(self, argument):\n", + " pass\n", + "\n", + "my_object = MyClass(value)" + ] + }, + { + "cell_type": "markdown", + "id": "9ca0f286", + "metadata": {}, + "source": [ + "### Member Variable" + ] + }, + { + "cell_type": "markdown", + "id": "b3bf5090", + "metadata": {}, + "source": [ + "\n", + "Member variable: a value stored inside an instance of a class.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb11981a", + "metadata": {}, + "outputs": [], + "source": [ + "class MyClass:\n", + " def __init__(self):\n", + " self.member = \"Value\"\n", + "\n", + "my_object = MyClass()\n", + "assert(my_object.member == \"Value\")" + ] + }, + { + "cell_type": "markdown", + "id": "c7d4aee8", + "metadata": {}, + "source": [ + "## Object refactorings" + ] + }, + { + "cell_type": "markdown", + "id": "2f83c9c1", + "metadata": {}, + "source": [ + "### Replace add-hoc structure with user defined classes" + ] + }, + { + "cell_type": "markdown", + "id": "e9273931", + "metadata": {}, + "source": [ + "\n", + "Smell: A data structure made of nested arrays and dictionaries becomes unwieldy.\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "864840c6", + "metadata": {}, + "outputs": [], + "source": [ + "from random import random\n", + "birds = [{\"position\": random(),\n", + " \"velocity\": random(),\n", + " \"type\": kind} for kind in bird_types]\n", + "\n", + "average_position = average([bird[\"position\"] for bird in birds])" + ] + }, + { + "cell_type": "markdown", + "id": "9ccec3c9", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e0adaff", + "metadata": {}, + "outputs": [], + "source": [ + "class Bird:\n", + " def __init__(self, kind):\n", + " from random import random\n", + " self.type = type\n", + " self.position = random()\n", + " self.velocity = random()\n", + "\n", + "birds = [Bird(kind) for kind in bird_types]\n", + "average_position = average([bird.position for bird in birds])" + ] + }, + { + "cell_type": "markdown", + "id": "0021c37e", + "metadata": {}, + "source": [ + "### Replace function with a method" + ] + }, + { + "cell_type": "markdown", + "id": "4212f3a7", + "metadata": {}, + "source": [ + "\n", + "Smell: A function is always called with the same kind of thing\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d4ff05c", + "metadata": {}, + "outputs": [], + "source": [ + "def can_see(source, target):\n", + " return (source.facing - target.facing) < source.viewport\n", + "\n", + "if can_see(hawk, starling):\n", + " hawk.hunt()" + ] + }, + { + "cell_type": "markdown", + "id": "e723cc95", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65e31969", + "metadata": {}, + "outputs": [], + "source": [ + "class Bird:\n", + " def can_see(self, target):\n", + " return (self.facing - target.facing) < self.viewport\n", + "\n", + "if hawk.can_see(starling):\n", + " hawk.hunt()" + ] + }, + { + "cell_type": "markdown", + "id": "ab2cffa0", + "metadata": {}, + "source": [ + "### Replace method arguments with class members" + ] + }, + { + "cell_type": "markdown", + "id": "30574946", + "metadata": {}, + "source": [ + "\n", + "Smell: A variable is nearly always used in arguments to \n", + "a class.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb3ff4f0", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, genes):\n", + " self.genes = genes\n", + " def reproduce_probability(self, age): pass\n", + " def death_probability(self, age): pass\n", + " def emigrate_probability(self, age): pass" + ] + }, + { + "cell_type": "markdown", + "id": "14631071", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa561a56", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, genes, age):\n", + " self.age = age\n", + " self.genes = genes\n", + " def reproduce_probability(self): pass\n", + " def death_probability(self): pass\n", + " def emigrate_probability(self): pass" + ] + }, + { + "cell_type": "markdown", + "id": "a059104d", + "metadata": {}, + "source": [ + "### Replace global variable with class and member" + ] + }, + { + "cell_type": "markdown", + "id": "a44b9e2c", + "metadata": {}, + "source": [ + "\n", + "Smell: A global variable is referenced by a few functions\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57887498", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Terry Jones\"\n", + "birthday = [1, 2, 1942]\n", + "today = [22, 11]\n", + "\n", + "if today == birthday[0:2]:\n", + " print(f\"Happy Birthday, {name}\")\n", + "else:\n", + " print(\"No birthday for you today.\")" + ] + }, + { + "cell_type": "markdown", + "id": "ffc9dfe3", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1c32b85", + "metadata": {}, + "outputs": [], + "source": [ + "class Person(object):\n", + " def __init__(self, birthday, name):\n", + " self.birth_day = birthday[0]\n", + " self.birth_month = birthday[1]\n", + " self.birth_year = birthday[2]\n", + " self.name = name\n", + " def check_birthday(self, today_day, today_month):\n", + " if not self.birth_day == today_day:\n", + " return False\n", + " if not self.birth_month == today_month:\n", + " return False\n", + " return True\n", + " def greet_appropriately(self, today):\n", + " if self.check_birthday(*today):\n", + " print(f\"Happy Birthday, {self.name}\")\n", + " else:\n", + " print(\"No birthday for you.\")\n", + "\n", + "john = Person([5, 5, 1943], \"Michael Palin\")\n", + "john.greet_appropriately(today)" + ] + }, + { + "cell_type": "markdown", + "id": "2057f9f9", + "metadata": {}, + "source": [ + "### Object Oriented Refactoring Summary" + ] + }, + { + "cell_type": "markdown", + "id": "8be6c0d4", + "metadata": {}, + "source": [ + "\n", + "* Replace ad-hoc structure with a class\n", + "* Replace function with a method\n", + "* Replace method argument with class member\n", + "* Replace global variable with class data\n", + "\n", + "\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Object Refactorings" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/06objects.ipynb.py b/ch05construction/06objects.ipynb.py new file mode 100644 index 000000000..f98d4f26b --- /dev/null +++ b/ch05construction/06objects.ipynb.py @@ -0,0 +1,342 @@ +# --- +# jupyter: +# jekyll: +# display_name: Object Refactorings +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Design + +# %% [markdown] +# Let's import first the context for this chapter. + +# %% +from context import * + + +# %% [markdown] +# +# ## Object-Oriented Design +# +# In this session, we will finally discuss the thing most people think of when they refer to "Software Engineering": the deliberate *design* of software. +# We will discuss processes and methodologies for planned development of large-scale software projects: *Software Architecture*. +# +# The software engineering community has, in large part, focused on an object-oriented approach to the design and development of large scale software systems. +# The basic concepts of object orientation are necessary to follow much of the software engineering conversation. +# +# +# ### Design processes +# +# +# In addition to object-oriented architecture, software engineers have focused on the development of processes for robust, reliable software development. +# These codified ways of working hope to enable organisations to repeatably and reliably complete complex software projects in a way that minimises both development +# and maintainance costs, and meets user requirements. +# +# +# ### Design and research +# +# +# Software engineering theory has largely been developed in the context of commercial software companies. +# +# The extent to which the practices and processes developed for commercial software are applicable in a research context is itself an active area of research. +# +# +# + +# %% [markdown] +# ## Recap of Object-Orientation + +# %% [markdown] +# ### Classes: User defined types + +# %% +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + def grow_up(self): + self.age += 1 + +terry = Person("Terry", 76) +terry.home = "Colwyn Bay" + + +# %% [markdown] +# Notice, that in Python, you can add properties to an object once it's been defined. Just because you can doesn't mean you should! +# + +# %% [markdown] +# ### Declaring a class + +# %% [markdown] +# +# Class: A user-defined type +# +# +# + +# %% +class MyClass: + pass + + +# %% [markdown] +# ### Object instances + +# %% [markdown] +# +# Instance: A particular object *instantiated* from a class. +# +# +# + +# %% +my_object = MyClass() + + +# %% [markdown] +# ### Method + +# %% [markdown] +# +# Method: A function which is "built in" to a class +# +# +# + +# %% +class MyClass: + def someMethod(self, argument): + pass + +my_object = MyClass() +my_object.someMethod(value) + + +# %% [markdown] +# ### Constructor + +# %% [markdown] +# +# Constructor: A special method called when instantiating a new object +# +# +# + +# %% +class MyClass: + def __init__(self, argument): + pass + +my_object = MyClass(value) + + +# %% [markdown] +# ### Member Variable + +# %% [markdown] +# +# Member variable: a value stored inside an instance of a class. +# +# +# + +# %% +class MyClass: + def __init__(self): + self.member = "Value" + +my_object = MyClass() +assert(my_object.member == "Value") + +# %% [markdown] +# ## Object refactorings + +# %% [markdown] +# ### Replace add-hoc structure with user defined classes + +# %% [markdown] +# +# Smell: A data structure made of nested arrays and dictionaries becomes unwieldy. +# +# Before: +# +# +# + +# %% +from random import random +birds = [{"position": random(), + "velocity": random(), + "type": kind} for kind in bird_types] + +average_position = average([bird["position"] for bird in birds]) + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +class Bird: + def __init__(self, kind): + from random import random + self.type = type + self.position = random() + self.velocity = random() + +birds = [Bird(kind) for kind in bird_types] +average_position = average([bird.position for bird in birds]) + + +# %% [markdown] +# ### Replace function with a method + +# %% [markdown] +# +# Smell: A function is always called with the same kind of thing +# +# Before: +# +# +# + +# %% +def can_see(source, target): + return (source.facing - target.facing) < source.viewport + +if can_see(hawk, starling): + hawk.hunt() + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +class Bird: + def can_see(self, target): + return (self.facing - target.facing) < self.viewport + +if hawk.can_see(starling): + hawk.hunt() + + +# %% [markdown] +# ### Replace method arguments with class members + +# %% [markdown] +# +# Smell: A variable is nearly always used in arguments to +# a class. +# +# +# + +# %% +class Person: + def __init__(self, genes): + self.genes = genes + def reproduce_probability(self, age): pass + def death_probability(self, age): pass + def emigrate_probability(self, age): pass + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +class Person: + def __init__(self, genes, age): + self.age = age + self.genes = genes + def reproduce_probability(self): pass + def death_probability(self): pass + def emigrate_probability(self): pass + + +# %% [markdown] +# ### Replace global variable with class and member + +# %% [markdown] +# +# Smell: A global variable is referenced by a few functions +# +# +# + +# %% +name = "Terry Jones" +birthday = [1, 2, 1942] +today = [22, 11] + +if today == birthday[0:2]: + print(f"Happy Birthday, {name}") +else: + print("No birthday for you today.") + + +# %% [markdown] +# +# +# +# +# + +# %% +class Person(object): + def __init__(self, birthday, name): + self.birth_day = birthday[0] + self.birth_month = birthday[1] + self.birth_year = birthday[2] + self.name = name + def check_birthday(self, today_day, today_month): + if not self.birth_day == today_day: + return False + if not self.birth_month == today_month: + return False + return True + def greet_appropriately(self, today): + if self.check_birthday(*today): + print(f"Happy Birthday, {self.name}") + else: + print("No birthday for you.") + +john = Person([5, 5, 1943], "Michael Palin") +john.greet_appropriately(today) + +# %% [markdown] +# ### Object Oriented Refactoring Summary + +# %% [markdown] +# +# * Replace ad-hoc structure with a class +# * Replace function with a method +# * Replace method argument with class member +# * Replace global variable with class data +# +# +# diff --git a/ch05construction/08objects.html b/ch05construction/08objects.html new file mode 100644 index 000000000..1bf8cb3b9 --- /dev/null +++ b/ch05construction/08objects.html @@ -0,0 +1,1503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Object Oriented Design + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Class design

+
+
+
+
+
+
+

The concepts we have introduced are common between different object oriented languages. +Thus, when we design our program using these concepts, we can think at an architectural level, +independent of language syntax.

+

In Python:

+
+
+
+
+
+
In [1]:
+
+
+
class Particle:
+    def __init__(self, position, velocity):
+        self.position = position
+        self.velocity = velocity
+    def move(self, delta_t):
+        self.position += self.velocity * delta_t
+
+
+
+
+
+
+
+
+

In C++:

+
class Particle {
+    std::vector<double> position;
+    std::vector<double> velocity;
+    Particle(std::vector<double> position, std::vector<double> velocity);
+    void move(double delta_t);
+}
+
+
+
+
+
+
+
+

In Fortran:

+
type particle
+    real :: position
+    real :: velocity
+  contains
+    procedure :: init
+    procedure :: move
+end type particle
+
+
+
+
+
+
+
+

UML

+
+
+
+
+
+
+

UML is a conventional diagrammatic notation used to describe "class structures" and other higher level +aspects of software design.

+

Computer scientists get worked up about formal correctness of UML diagrams and learning the conventions precisely. +Working programmers can still benefit from using UML to describe their designs.

+
+
+
+
+
+
+

YUML

+
+
+
+
+
+
+

We can see a YUML model for a Particle class with position and velocity data and a move() method using +the YUML online UML drawing tool (example).

+
    http://yuml.me/diagram/boring/class/[Particle|position;velocity|move%28%29]
+
+
+
+
+
+
+
+

Here's how we can use Python code to get an image back from YUML:

+
+
+
+
+
+
In [2]:
+
+
+
import requests
+from IPython.display import Image
+
+def yuml(model):
+    result = requests.get("http://yuml.me/diagram/boring/class/" + model)
+    return Image(result.content)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
yuml("[Particle|position;velocity|move()]")
+
+
+
+
+
+
+
+
Out[3]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The representation of the Particle class defined above in UML is done with a box with three sections. The name of the class goes on the top, then the name of the member variables in the middle, and the name of the methods on the bottom. We will see later why this is useful.

+
+
+
+
+
+
+

Information Hiding

+
+
+
+
+
+
+

Sometimes, our design for a program would be broken if users start messing around with variables we don't want them to change.

+

Robust class design requires consideration of which subroutines are intended for users to use, and which are internal. +Languages provide features to implement this: access control.

+

In python, we use leading underscores to control whether member variables and methods can be accessed from outside the class:

+
    +
  • single leading underscore (_) is used to document it's private but people could use it if wanted (thought they shouldn't);
  • +
  • double leading underscore (__) raises errors if called.
  • +
+
+
+
+
+
+
In [4]:
+
+
+
class MyClass:
+    def __init__(self):
+        self.__private_data = 0
+        self._private_data = 0
+        self.public_data = 0
+    
+    def __private_method(self): pass
+    
+    def _private_method(self): pass
+    
+    def public_method(self): pass
+    
+    def called_inside(self):
+        self.__private_method()
+        self._private_method()
+        self.__private_data = 1
+        self._private_data = 1
+        
+
+MyClass().called_inside()
+
+
+
+
+
+
+
+
In [5]:
+
+
+
MyClass()._private_method() # Works, but forbidden by convention
+
+
+
+
+
+
+
+
In [6]:
+
+
+
MyClass().public_method() # OK
+
+print(MyClass()._private_data)
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
print(MyClass().public_data)
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
In [8]:
+
+
+
MyClass().__private_method() # Generates error
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[8], line 1
+----> 1 MyClass().__private_method() # Generates error
+
+AttributeError: 'MyClass' object has no attribute '__private_method'
+
+
+
+
+
+
+
+
In [9]:
+
+
+
print(MyClass().__private_data) # Generates error
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[9], line 1
+----> 1 print(MyClass().__private_data) # Generates error
+
+AttributeError: 'MyClass' object has no attribute '__private_data'
+
+
+
+
+
+
+
+
+

Property accessors

+
+
+
+
+
+
+

Python provides a mechanism to make functions appear to be variables. This can be used if you want to +change the way a class is implemented without changing the interface:

+
+
+
+
+
+
In [10]:
+
+
+
class Person:
+    def __init__(self):
+        self.name = "Graham Chapman"
+
+assert(Person().name == "Graham Chapman")
+
+
+
+
+
+
+
+
+

becomes:

+
+
+
+
+
+
In [11]:
+
+
+
class Person(object):
+    def __init__(self):
+        self._first = "Graham"
+        self._second = "Chapman"
+        
+
+    @property
+    def name(self):
+        return f"{self._first} {self._second}"
+
+assert(Person().name == "Graham Chapman")
+
+
+
+
+
+
+
+
+

Making the same external code work as before.

+
+
+
+
+
+
+

Note that the code behaves the same way to the outside user. +The implementation detail is hidden by private variables. +In languages without this feature, such as C++, it is best to always +make data private, and always +access data through functions:

+
+
+
+
+
+
In [12]:
+
+
+
class Person(object):
+    def __init__(self):
+        self._name = "Graham Chapman"
+        
+    def name(self):  # an access function
+        return self._name
+
+assert(Person().name() == "Graham Chapman")
+
+
+
+
+
+
+
+
+

But in Python this is unnecessary because the @property capability.

+
+
+
+
+
+
+

Another way could be to create a member variable name which holds the full name. However, this could lead to inconsistent data. If we create a get_married function, then the name of the person won't change!

+
+
+
+
+
+
In [13]:
+
+
+
class Person(object):
+    def __init__(self, first, second):
+        self._first = first
+        self._second = second
+        self.name = f"{self._first} {self._second}"
+        
+    def get_married(self, to):
+        self._second = to._second
+
+graham = Person("Graham", "Chapman")
+david = Person("David", "Sherlock")
+assert(graham.name == "Graham Chapman")
+graham.get_married(david)
+assert(graham.name == "Graham Sherlock")
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AssertionError                            Traceback (most recent call last)
+Cell In[13], line 14
+     12 assert(graham.name == "Graham Chapman")
+     13 graham.get_married(david)
+---> 14 assert(graham.name == "Graham Sherlock")
+
+AssertionError: 
+
+
+
+
+
+
+
+
+

This type of situation could makes that the object data structure gets inconsistent with itself. Making variables being out of sync with other variables. Each piece of information should only be stored in once place! In this case, name should be calculated each time it's required as previously shown. +In database design, this is called Normalisation.

+
+
+
+
+
+
+

UML for private/public

+
+
+
+
+
+
+

We prepend a +/- on public/private member variables and methods:

+
+
+
+
+
+
In [14]:
+
+
+
yuml("[Particle|+public;-private|+publicmethod();-privatemethod]")
+
+
+
+
+
+
+
+
Out[14]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Class Members

+
+
+
+
+
+
+

Class, or static members, belong to the class as a whole, and are shared between instances.

+

This is an object that keeps a count on how many have been created of it.

+
+
+
+
+
+
In [15]:
+
+
+
class Counted:
+    number_created = 0
+    
+    def __init__(self):
+        Counted.number_created += 1
+        
+    @classmethod
+    def howMany(cls):
+        return cls.number_created
+
+Counted.howMany()  # 0
+x = Counted()
+Counted.howMany()  # 1
+z = [Counted() for x in range(5)]
+Counted.howMany()  # 6 
+
+
+
+
+
+
+
+
Out[15]:
+
+
6
+
+
+
+
+
+
+
+
+

The data is shared among all the objects instantiated from that class. Note that in __init__ we are not using self.number_created but the name of the class. The howMany function is not a method of a particular object. It's called on the class, not on the object. This is possible by using the @classmethod decorator.

+
+
+
+
+
+
+

Inheritance and Polymorphism

+
+
+
+
+
+
+

Object-based vs Object-Oriented

+
+
+
+
+
+
+

So far we have seen only object-based programming, not object-oriented programming.

+

Using Objects doesn't mean your code is object-oriented.

+

To understand object-oriented programming, we need to introduce polymorphism and inheritance.

+
+
+
+
+
+
+

Inheritance

+
+
+
+
+
+
+
    +
  • Inheritance is a mechanism that allows related classes to share code.
  • +
  • Inheritance allows a program to reflect the ontology) of kinds of thing in a program.
  • +
+
+
+
+
+
+
+

Ontology and inheritance

+
+
+
+
+
+
+
    +
  • A bird is a kind of animal
  • +
  • An eagle is a kind of bird
  • +
  • A starling is also a kind of bird
  • +
  • All animals can be born and die
  • +
  • Only birds can fly (Ish.)
  • +
  • Only eagles hunt
  • +
  • Only starlings flock
  • +
+
+
+
+
+
+
+

Inheritance in python

+
+
+
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
class Animal:
+    def beBorn(self):
+        print("I exist")
+    def die(self): 
+        print("Argh!")
+
+class Bird(Animal):
+    def fly(self): 
+        print("Whee!")
+
+class Eagle(Bird):
+    def hunt(self): 
+        print("I'm gonna eatcha!")
+
+class Starling(Bird):
+    def flew(self): 
+        print("I'm flying away!")
+
+Eagle().beBorn()
+Eagle().hunt()
+
+
+
+
+
+
+
+
+
+
I exist
+I'm gonna eatcha!
+
+
+
+
+
+
+
+
+
+

Inheritance terminology

+
+
+
+
+
+
+

Here are two equivalents definition, one coming from C++ and another from Java:

+
    +
  • A derived class derives from a base class.
  • +
  • A subclass inherits from a superclass.
  • +
+

These are different terms for the same thing. +So, we can say:

+
    +
  • Eagle is a subclass of the Animal superclass.
  • +
  • Animal is the base class of the Eagle derived class.
  • +
+

Another equivalent definition is using the synonym child / parent for derived / base class:

+
    +
  • A child class extends a parent class.
  • +
+
+
+
+
+
+
+

Inheritance and constructors

+
+
+
+
+
+
+

To use implicitly constructors from a superclass, we can use super as shown below.

+
+
+
+
+
+
In [17]:
+
+
+
class Animal:
+    def __init__(self, age):
+        self.age = age
+
+class Person(Animal):
+    def __init__(self, age, name):
+        super().__init__(age)
+        self.name = name
+
+
+
+
+
+
+
+
+

Read Raymond Hettinger's article about super to see various real examples.

+
+
+
+
+
+
+

Inheritance UML diagrams

+
+
+
+
+
+
+

UML shows inheritance with an open triangular arrow pointing from subclass to superclass.

+
+
+
+
+
+
In [18]:
+
+
+
yuml("[Animal]^-[Bird],[Bird]^-[Eagle],[Bird]^-[Starling]%")
+
+
+
+
+
+
+
+
Out[18]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Aggregation vs Inheritance

+
+
+
+
+
+
+

If one object has or owns one or more objects, this is not inheritance.

+

For example, the boids example we saw few weeks ago, could be organised as an overall Model, which it owns several Boids, +and each Boid owns two 2-vectors, one for position and one for velocity.

+
+
+
+
+
+
+

Aggregation in UML

+
+
+
+
+
+
+

The Boids situation can be represented thus:

+
+
+
+
+
+
In [19]:
+
+
+
yuml("[Model]<>-*>[Boid],[Boid]position++->[Vector],[Boid]velocity++->[Vector]%")
+
+
+
+
+
+
+
+
Out[19]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The open diamond indicates Aggregation, the closed diamond composition. +(A given boid might belong to multiple models, a given position vector is forever part of the corresponding Boid.)

+

The asterisk represents cardinality, a model may contain multiple Boids. This is a one to many relationship). Many to many relationship) is shown with * on both sides.

+
+
+
+
+
+
+

Refactoring to inheritance

+
+
+
+
+
+
+

Smell: Repeated code between two classes which are both ontologically subtypes of something

+

Before:

+
+
+
+
+
+
In [20]:
+
+
+
class Person:
+    def __init__(self, age, job): 
+        self.age = age
+        self.job = job
+    def birthday(self): 
+        self.age += 1
+
+class Pet:
+    def __init__(self, age, owner): 
+        self.age = age
+        self.owner = owner
+    def birthday(self): 
+        self.age += 1
+
+
+
+
+
+
+
+
+

After:

+
+
+
+
+
+
In [21]:
+
+
+
class Animal:
+    def __init__(self, age): 
+        self.age = age
+    def birthday(self): 
+        self.age += 1
+
+class Person(Animal):
+    def __init__(self, age, job):
+        self.job = job
+        super().__init__(age)
+        
+class Pet(Animal):
+    def __init__(self, age, owner):
+        self.owner = owner
+        super().__init__(age)
+
+
+
+
+
+
+
+
+

Polymorphism

+
+
+
+
+
+
In [22]:
+
+
+
class Dog:
+    def noise(self):
+        return "Bark"
+
+class Cat:
+    def noise(self):
+        return "Miaow"
+
+class Pig:
+    def noise(self):
+        return "Oink"
+
+class Cow:
+    def noise(self):
+        return "Moo"
+
+animals = [Dog(), Dog(), Cat(), Pig(), Cow(), Cat()]
+for animal in animals:
+    print(animal.noise())
+
+
+
+
+
+
+
+
+
+
Bark
+Bark
+Miaow
+Oink
+Moo
+Miaow
+
+
+
+
+
+
+
+
+
+

This will print "Bark Bark Miaow Oink Moo Miaow"

+

If two classes support the same method, but it does different things for the two classes, +then if an object is of an unknown class, calling the method will invoke the version for +whatever class the instance is an instance of.

+
+
+
+
+
+
+

Polymorphism and Inheritance

+
+
+
+
+
+
+

Often, polymorphism uses multiple derived classes with a common base class. +However, duck typing in Python means that all that is required is that the +types support a common Concept (Such as iterable, or container, or, in this case, the +Noisy concept.)

+

A common base class is used where there is a likely default that you want several +of the derived classes to have.

+
+
+
+
+
+
In [23]:
+
+
+
class Animal:
+    def noise(self):
+        return "I don't make a noise."
+
+class Dog(Animal):
+    def noise(self):
+        return "Bark"
+
+class Worm(Animal):
+    pass
+
+class Poodle(Dog):
+    pass
+
+animals = [Dog(), Worm(), Pig(), Cow(), Poodle()]
+for animal in animals:
+    print(animal.noise())
+
+
+
+
+
+
+
+
+
+
Bark
+I don't make a noise.
+Oink
+Moo
+Bark
+
+
+
+
+
+
+
+
+
+

Undefined Functions and Polymorphism

+
+
+
+
+
+
+

In the above example, we put in a dummy noise for Animals that don't know what type they are.

+

Instead, we can explicitly deliberately leave this undefined, and we get a crash if we access an undefined method.

+
+
+
+
+
+
In [24]:
+
+
+
class Animal:
+    pass
+
+class Worm(Animal):
+    pass
+
+
+
+
+
+
+
+
In [25]:
+
+
+
Worm().noise() # Generates error
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AttributeError                            Traceback (most recent call last)
+Cell In[25], line 1
+----> 1 Worm().noise() # Generates error
+
+AttributeError: 'Worm' object has no attribute 'noise'
+
+
+
+
+
+
+
+
+

Refactoring to Polymorphism

+
+
+
+
+
+
+

Smell: a function uses a big set of if statements or a case statement to decide what to do:

+

Before:

+
+
+
+
+
+
In [26]:
+
+
+
class Animal:
+    def __init__(self, animal_kind): 
+        self.animal_kind = animal_kind
+        
+    def noise(self): 
+        if self.animal_kind == "Dog":
+            return "Bark"
+        elif self.animal_kind == "Cat":
+            return "Miaow"
+        elif self.animal_kind == "Cow":
+            return "Moo"
+
+
+
+
+
+
+
+
+

which is better replaced by the code above.

+
+
+
+
+
+
+

Interfaces and concepts

+
+
+
+
+
+
+

In C++, it is common to define classes which declare dummy methods, called "virtual" methods, which specify +the methods which derived classes must implement. Classes which define these methods, but which cannot be instantiated +into actual objects, are called "abstract base" classes or "interfaces".

+

Python's Duck Typing approach means explicitly declaring these is unnesssary: any class concept which implements +appropriately named methods will do. These as user-defined concepts, just as "iterable" or "container" are +built-in Python concepts. A class is said to "implement an interface" or "satisfy a concept".

+
+
+
+
+
+
+

Interfaces in UML

+
+
+
+
+
+
+

Interfaces implementation (a common ancestor that doesn't do anything but defines methods to share) in UML is indicated thus:

+
+
+
+
+
+
In [27]:
+
+
+
yuml("[<<Animal>>]^-.-[Dog]")
+
+
+
+
+
+
+
+
Out[27]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Further UML

+
+
+
+
+
+
+

UML is a much larger diagram language than the aspects we've shown here.

+
    +
  • Message sequence charts show signals passing back and forth between objects (Web Sequence Diagrams).

    +
  • +
  • Entity Relationship Diagrams can be used to show more general relationships between things in a system.

    +
  • +
+

Read more about UML on Martin Fowler's book about the topic.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/08objects.ipynb b/ch05construction/08objects.ipynb new file mode 100644 index 000000000..ebb545dfe --- /dev/null +++ b/ch05construction/08objects.ipynb @@ -0,0 +1,1128 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b2a17933", + "metadata": {}, + "source": [ + "## Class design" + ] + }, + { + "cell_type": "markdown", + "id": "5889eaae", + "metadata": {}, + "source": [ + "\n", + "The concepts we have introduced are common between different object oriented languages.\n", + "Thus, when we design our program using these concepts, we can think at an architectural level,\n", + "independent of language syntax.\n", + "\n", + "In Python:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06485ccf", + "metadata": {}, + "outputs": [], + "source": [ + "class Particle:\n", + " def __init__(self, position, velocity):\n", + " self.position = position\n", + " self.velocity = velocity\n", + " def move(self, delta_t):\n", + " self.position += self.velocity * delta_t" + ] + }, + { + "cell_type": "markdown", + "id": "01e7d5d0", + "metadata": {}, + "source": [ + "In C++:\n", + "\n", + "``` cpp\n", + "class Particle {\n", + " std::vector position;\n", + " std::vector velocity;\n", + " Particle(std::vector position, std::vector velocity);\n", + " void move(double delta_t);\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "ad3436ed", + "metadata": {}, + "source": [ + "In Fortran:\n", + "\n", + "``` fortran\n", + "type particle\n", + " real :: position\n", + " real :: velocity\n", + " contains\n", + " procedure :: init\n", + " procedure :: move\n", + "end type particle\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8558e116", + "metadata": {}, + "source": [ + "### UML" + ] + }, + { + "cell_type": "markdown", + "id": "1439b102", + "metadata": {}, + "source": [ + "\n", + "UML is a conventional diagrammatic notation used to describe \"class structures\" and other higher level\n", + "aspects of software design.\n", + "\n", + "Computer scientists get worked up about formal correctness of UML diagrams and learning the conventions precisely.\n", + "Working programmers can still benefit from using UML to describe their designs.\n" + ] + }, + { + "cell_type": "markdown", + "id": "170fb437", + "metadata": {}, + "source": [ + "### YUML" + ] + }, + { + "cell_type": "markdown", + "id": "4c6633eb", + "metadata": {}, + "source": [ + "We can see a YUML model for a Particle class with `position` and `velocity` data and a `move()` method using\n", + "the [YUML](http://yuml.me/) online UML drawing tool ([example](http://yuml.me/diagram/boring/class/[Particle|position;velocity|move%28%29])).\n", + "\n", + "```\n", + " http://yuml.me/diagram/boring/class/[Particle|position;velocity|move%28%29]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "30ca8397", + "metadata": {}, + "source": [ + "Here's how we can use Python code to get an image back from YUML:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1bc5f6bd", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from IPython.display import Image\n", + "\n", + "def yuml(model):\n", + " result = requests.get(\"http://yuml.me/diagram/boring/class/\" + model)\n", + " return Image(result.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f0d8b3d", + "metadata": {}, + "outputs": [], + "source": [ + "yuml(\"[Particle|position;velocity|move()]\")" + ] + }, + { + "cell_type": "markdown", + "id": "75552c83", + "metadata": {}, + "source": [ + "The representation of the `Particle` class defined above in UML is done with a box with three sections. The name of the class goes on the top, then the name of the member variables in the middle, and the name of the methods on the bottom. We will see later why this is useful." + ] + }, + { + "cell_type": "markdown", + "id": "14426576", + "metadata": {}, + "source": [ + "### Information Hiding" + ] + }, + { + "cell_type": "markdown", + "id": "0c59e62c", + "metadata": {}, + "source": [ + "\n", + "Sometimes, our design for a program would be broken if users start messing around with variables we don't want them to change.\n", + "\n", + "Robust class design requires consideration of which subroutines are intended for users to use, and which are internal.\n", + "Languages provide features to implement this: access control. \n", + "\n", + "In python, we use leading underscores to control whether member variables and methods can be accessed from outside the class:\n", + " - single leading underscore (`_`) is used to document it's private but people could use it if wanted (thought they shouldn't);\n", + " - double leading underscore (`__`) raises errors if called.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "153c0f3a", + "metadata": {}, + "outputs": [], + "source": [ + "class MyClass:\n", + " def __init__(self):\n", + " self.__private_data = 0\n", + " self._private_data = 0\n", + " self.public_data = 0\n", + " \n", + " def __private_method(self): pass\n", + " \n", + " def _private_method(self): pass\n", + " \n", + " def public_method(self): pass\n", + " \n", + " def called_inside(self):\n", + " self.__private_method()\n", + " self._private_method()\n", + " self.__private_data = 1\n", + " self._private_data = 1\n", + " \n", + "\n", + "MyClass().called_inside()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88f9569c", + "metadata": {}, + "outputs": [], + "source": [ + "MyClass()._private_method() # Works, but forbidden by convention" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bfd4f8a", + "metadata": {}, + "outputs": [], + "source": [ + "MyClass().public_method() # OK\n", + "\n", + "print(MyClass()._private_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e7aad4d", + "metadata": {}, + "outputs": [], + "source": [ + "print(MyClass().public_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c3a146d", + "metadata": {}, + "outputs": [], + "source": [ + "MyClass().__private_method() # Generates error" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efd496ec", + "metadata": {}, + "outputs": [], + "source": [ + "print(MyClass().__private_data) # Generates error" + ] + }, + { + "cell_type": "markdown", + "id": "178b77b3", + "metadata": {}, + "source": [ + "### Property accessors" + ] + }, + { + "cell_type": "markdown", + "id": "06643830", + "metadata": {}, + "source": [ + "\n", + "Python provides a mechanism to make functions appear to be variables. This can be used if you want to\n", + "change the way a class is implemented without changing the interface:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4eea9d9", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self):\n", + " self.name = \"Graham Chapman\"\n", + "\n", + "assert(Person().name == \"Graham Chapman\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b43686f", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "becomes:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f2634f7", + "metadata": {}, + "outputs": [], + "source": [ + "class Person(object):\n", + " def __init__(self):\n", + " self._first = \"Graham\"\n", + " self._second = \"Chapman\"\n", + " \n", + "\n", + " @property\n", + " def name(self):\n", + " return f\"{self._first} {self._second}\"\n", + "\n", + "assert(Person().name == \"Graham Chapman\")" + ] + }, + { + "cell_type": "markdown", + "id": "ef60ce38", + "metadata": {}, + "source": [ + "Making the same external code work as before." + ] + }, + { + "cell_type": "markdown", + "id": "98cb563b", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Note that the code behaves the same way to the outside user.\n", + "The implementation detail is hidden by private variables.\n", + "In languages without this feature, such as C++, it is best to always\n", + "make data private, and always\n", + "access data through functions:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b1f23e4", + "metadata": {}, + "outputs": [], + "source": [ + "class Person(object):\n", + " def __init__(self):\n", + " self._name = \"Graham Chapman\"\n", + " \n", + " def name(self): # an access function\n", + " return self._name\n", + "\n", + "assert(Person().name() == \"Graham Chapman\")" + ] + }, + { + "cell_type": "markdown", + "id": "adccf0db", + "metadata": {}, + "source": [ + "But in Python this is unnecessary because the `@property` capability.\n" + ] + }, + { + "cell_type": "markdown", + "id": "088e639c", + "metadata": {}, + "source": [ + "Another way could be to create a member variable `name` which holds the full name. However, this could lead to inconsistent data. If we create a `get_married` function, then the name of the person won't change!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "615c8c55", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "class Person(object):\n", + " def __init__(self, first, second):\n", + " self._first = first\n", + " self._second = second\n", + " self.name = f\"{self._first} {self._second}\"\n", + " \n", + " def get_married(self, to):\n", + " self._second = to._second\n", + "\n", + "graham = Person(\"Graham\", \"Chapman\")\n", + "david = Person(\"David\", \"Sherlock\")\n", + "assert(graham.name == \"Graham Chapman\")\n", + "graham.get_married(david)\n", + "assert(graham.name == \"Graham Sherlock\")" + ] + }, + { + "cell_type": "markdown", + "id": "a7263161", + "metadata": {}, + "source": [ + "This type of situation could makes that the object data structure gets inconsistent with itself. Making variables being out of sync with other variables. Each piece of information should only be stored in once place! In this case, `name` should be calculated each time it's required as previously shown.\n", + "In database design, this is called [Normalisation](https://en.wikipedia.org/wiki/Database_normalization)." + ] + }, + { + "cell_type": "markdown", + "id": "f97eb58f", + "metadata": {}, + "source": [ + "#### UML for private/public" + ] + }, + { + "cell_type": "markdown", + "id": "f208681b", + "metadata": {}, + "source": [ + "We prepend a `+`/`-` on public/private member variables and methods:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "739b8d54", + "metadata": {}, + "outputs": [], + "source": [ + "yuml(\"[Particle|+public;-private|+publicmethod();-privatemethod]\")" + ] + }, + { + "cell_type": "markdown", + "id": "f776baee", + "metadata": {}, + "source": [ + "### Class Members" + ] + }, + { + "cell_type": "markdown", + "id": "2ce16d8e", + "metadata": {}, + "source": [ + "\n", + "*Class*, or *static* members, belong to the class as a whole, and are shared between instances.\n", + "\n", + "This is an object that keeps a count on how many have been created of it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "848ad2f8", + "metadata": {}, + "outputs": [], + "source": [ + "class Counted:\n", + " number_created = 0\n", + " \n", + " def __init__(self):\n", + " Counted.number_created += 1\n", + " \n", + " @classmethod\n", + " def howMany(cls):\n", + " return cls.number_created\n", + "\n", + "Counted.howMany() # 0\n", + "x = Counted()\n", + "Counted.howMany() # 1\n", + "z = [Counted() for x in range(5)]\n", + "Counted.howMany() # 6 " + ] + }, + { + "cell_type": "markdown", + "id": "0c675aa1", + "metadata": {}, + "source": [ + "The data is shared among all the objects instantiated from that class. Note that in `__init__` we are not using `self.number_created` but the name of the class. The `howMany` function is not a method of a particular object. It's called on the class, not on the object. This is possible by using the `@classmethod` decorator." + ] + }, + { + "cell_type": "markdown", + "id": "6c4565f7", + "metadata": {}, + "source": [ + "## Inheritance and Polymorphism" + ] + }, + { + "cell_type": "markdown", + "id": "ca69e60c", + "metadata": {}, + "source": [ + "### Object-based vs Object-Oriented" + ] + }, + { + "cell_type": "markdown", + "id": "3af3b69c", + "metadata": {}, + "source": [ + "\n", + "So far we have seen only object-based programming, not object-oriented programming.\n", + "\n", + "Using Objects doesn't mean your code is object-oriented.\n", + "\n", + "To understand object-oriented programming, we need to introduce **polymorphism** and **inheritance**.\n" + ] + }, + { + "cell_type": "markdown", + "id": "e35105c6", + "metadata": {}, + "source": [ + "### Inheritance" + ] + }, + { + "cell_type": "markdown", + "id": "36632a33", + "metadata": {}, + "source": [ + "\n", + "* Inheritance is a mechanism that allows related classes to share code.\n", + "* Inheritance allows a program to reflect the *[ontology](https://en.wikipedia.org/wiki/Ontology_(information_science))* of kinds of thing in a program.\n" + ] + }, + { + "cell_type": "markdown", + "id": "85db8833", + "metadata": {}, + "source": [ + "### Ontology and inheritance" + ] + }, + { + "cell_type": "markdown", + "id": "6e496fbe", + "metadata": {}, + "source": [ + "\n", + "* A bird is a kind of animal\n", + "* An eagle is a kind of bird\n", + "* A starling is also a kind of bird\n", + "* All animals can be born and die\n", + "* Only birds can fly (Ish.)\n", + "* Only eagles hunt\n", + "* Only starlings flock\n" + ] + }, + { + "cell_type": "markdown", + "id": "529aef0a", + "metadata": {}, + "source": [ + "### Inheritance in python" + ] + }, + { + "cell_type": "markdown", + "id": "a1830a04", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bc118da", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " def beBorn(self):\n", + " print(\"I exist\")\n", + " def die(self): \n", + " print(\"Argh!\")\n", + "\n", + "class Bird(Animal):\n", + " def fly(self): \n", + " print(\"Whee!\")\n", + "\n", + "class Eagle(Bird):\n", + " def hunt(self): \n", + " print(\"I'm gonna eatcha!\")\n", + "\n", + "class Starling(Bird):\n", + " def flew(self): \n", + " print(\"I'm flying away!\")\n", + "\n", + "Eagle().beBorn()\n", + "Eagle().hunt()" + ] + }, + { + "cell_type": "markdown", + "id": "a94279b6", + "metadata": {}, + "source": [ + "### Inheritance terminology" + ] + }, + { + "cell_type": "markdown", + "id": "8a8ffe1e", + "metadata": {}, + "source": [ + "Here are two equivalents definition, one coming from C++ and another from Java:\n", + "* A *derived class* _derives_ from a *base class*.\n", + "* A *subclass* _inherits_ from a *superclass*.\n", + "\n", + "These are different terms for the same thing.\n", + "So, we can say:\n", + "\n", + "* Eagle is a subclass of the Animal superclass.\n", + "* Animal is the base class of the Eagle derived class.\n", + "\n", + "Another equivalent definition is using the synonym *child* / *parent* for *derived* / *base* class:\n", + "* A *child class* extends a *parent class*.\n" + ] + }, + { + "cell_type": "markdown", + "id": "6bdffe46", + "metadata": {}, + "source": [ + "### Inheritance and constructors" + ] + }, + { + "cell_type": "markdown", + "id": "a566cfaa", + "metadata": {}, + "source": [ + "To use implicitly constructors from a *superclass*, we can use `super` as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3ed1ccb", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " def __init__(self, age):\n", + " self.age = age\n", + "\n", + "class Person(Animal):\n", + " def __init__(self, age, name):\n", + " super().__init__(age)\n", + " self.name = name" + ] + }, + { + "cell_type": "markdown", + "id": "b39e6fc2", + "metadata": {}, + "source": [ + "Read [Raymond Hettinger](https://twitter.com/raymondh)'s [article about `super`](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/) to see various real examples." + ] + }, + { + "cell_type": "markdown", + "id": "3f7072ac", + "metadata": {}, + "source": [ + "### Inheritance UML diagrams" + ] + }, + { + "cell_type": "markdown", + "id": "853dbac6", + "metadata": {}, + "source": [ + "UML shows inheritance with an open triangular arrow pointing from subclass to superclass." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9145cc8a", + "metadata": {}, + "outputs": [], + "source": [ + "yuml(\"[Animal]^-[Bird],[Bird]^-[Eagle],[Bird]^-[Starling]%\")" + ] + }, + { + "cell_type": "markdown", + "id": "caa55205", + "metadata": {}, + "source": [ + "### Aggregation vs Inheritance" + ] + }, + { + "cell_type": "markdown", + "id": "335408aa", + "metadata": {}, + "source": [ + "\n", + "If one object *has* or *owns* one or more objects, this is *not* inheritance.\n", + "\n", + "For example, the boids example we saw few weeks ago, could be organised as an overall Model, which it owns several Boids,\n", + "and each Boid owns two 2-vectors, one for position and one for velocity.\n" + ] + }, + { + "cell_type": "markdown", + "id": "74c90d19", + "metadata": {}, + "source": [ + "#### Aggregation in UML" + ] + }, + { + "cell_type": "markdown", + "id": "26a77259", + "metadata": {}, + "source": [ + "The Boids situation can be represented thus:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08842de4", + "metadata": {}, + "outputs": [], + "source": [ + "yuml(\"[Model]<>-*>[Boid],[Boid]position++->[Vector],[Boid]velocity++->[Vector]%\")" + ] + }, + { + "cell_type": "markdown", + "id": "622ff8d7", + "metadata": {}, + "source": [ + "The open diamond indicates **Aggregation**, the closed diamond **composition**.\n", + "(A given boid might belong to multiple models, a given position vector is forever part of the corresponding Boid.)\n", + "\n", + "The asterisk represents cardinality, a model may contain multiple Boids. This is a [one to many relationship](https://en.wikipedia.org/wiki/One-to-many_(data_model)). [Many to many relationship](https://en.wikipedia.org/wiki/Many-to-many_(data_model)) is shown with `*` on both sides." + ] + }, + { + "cell_type": "markdown", + "id": "4d011221", + "metadata": {}, + "source": [ + "#### Refactoring to inheritance" + ] + }, + { + "cell_type": "markdown", + "id": "8fbe2c4d", + "metadata": {}, + "source": [ + "\n", + "Smell: Repeated code between two classes which are both ontologically subtypes of something\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5366cb2", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, age, job): \n", + " self.age = age\n", + " self.job = job\n", + " def birthday(self): \n", + " self.age += 1\n", + "\n", + "class Pet:\n", + " def __init__(self, age, owner): \n", + " self.age = age\n", + " self.owner = owner\n", + " def birthday(self): \n", + " self.age += 1" + ] + }, + { + "cell_type": "markdown", + "id": "38e2036d", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "After:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2696ba24", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " def __init__(self, age): \n", + " self.age = age\n", + " def birthday(self): \n", + " self.age += 1\n", + "\n", + "class Person(Animal):\n", + " def __init__(self, age, job):\n", + " self.job = job\n", + " super().__init__(age)\n", + " \n", + "class Pet(Animal):\n", + " def __init__(self, age, owner):\n", + " self.owner = owner\n", + " super().__init__(age)" + ] + }, + { + "cell_type": "markdown", + "id": "4daa4059", + "metadata": {}, + "source": [ + "### Polymorphism" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b7a30a7", + "metadata": {}, + "outputs": [], + "source": [ + "class Dog:\n", + " def noise(self):\n", + " return \"Bark\"\n", + "\n", + "class Cat:\n", + " def noise(self):\n", + " return \"Miaow\"\n", + "\n", + "class Pig:\n", + " def noise(self):\n", + " return \"Oink\"\n", + "\n", + "class Cow:\n", + " def noise(self):\n", + " return \"Moo\"\n", + "\n", + "animals = [Dog(), Dog(), Cat(), Pig(), Cow(), Cat()]\n", + "for animal in animals:\n", + " print(animal.noise())" + ] + }, + { + "cell_type": "markdown", + "id": "298a8170", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "This will print \"Bark Bark Miaow Oink Moo Miaow\"\n", + "\n", + "If two classes support the same method, but it does different things for the two classes, \n", + "then if an object is of an unknown class, calling the method will invoke the version for\n", + "whatever class the instance is an instance of.\n" + ] + }, + { + "cell_type": "markdown", + "id": "2c517074", + "metadata": {}, + "source": [ + "### Polymorphism and Inheritance" + ] + }, + { + "cell_type": "markdown", + "id": "cd203768", + "metadata": {}, + "source": [ + "\n", + "Often, polymorphism uses multiple derived classes with a common base class.\n", + "However, [duck typing](https://en.wikipedia.org/wiki/Duck_typing) in Python means that all that is required is that the \n", + "types support a common **Concept** (Such as iterable, or container, or, in this case, the\n", + "Noisy concept.)\n", + "\n", + "A common base class is used where there is a likely **default** that you want several\n", + "of the derived classes to have.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd169480", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " def noise(self):\n", + " return \"I don't make a noise.\"\n", + "\n", + "class Dog(Animal):\n", + " def noise(self):\n", + " return \"Bark\"\n", + "\n", + "class Worm(Animal):\n", + " pass\n", + "\n", + "class Poodle(Dog):\n", + " pass\n", + "\n", + "animals = [Dog(), Worm(), Pig(), Cow(), Poodle()]\n", + "for animal in animals:\n", + " print(animal.noise())" + ] + }, + { + "cell_type": "markdown", + "id": "dc56ca52", + "metadata": {}, + "source": [ + "### Undefined Functions and Polymorphism" + ] + }, + { + "cell_type": "markdown", + "id": "9f51c0d6", + "metadata": {}, + "source": [ + "\n", + "In the above example, we put in a dummy noise for Animals that don't know what type they are.\n", + "\n", + "Instead, we can explicitly deliberately leave this undefined, and we get a crash if we access an undefined method.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a6627ad", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " pass\n", + "\n", + "class Worm(Animal):\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdd5b7dd", + "metadata": {}, + "outputs": [], + "source": [ + "Worm().noise() # Generates error" + ] + }, + { + "cell_type": "markdown", + "id": "e93458c3", + "metadata": {}, + "source": [ + "### Refactoring to Polymorphism" + ] + }, + { + "cell_type": "markdown", + "id": "b5dd27d4", + "metadata": {}, + "source": [ + "\n", + "Smell: a function uses a big set of `if` statements or a `case` statement to decide what to do:\n", + "\n", + "Before:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00343491", + "metadata": {}, + "outputs": [], + "source": [ + "class Animal:\n", + " def __init__(self, animal_kind): \n", + " self.animal_kind = animal_kind\n", + " \n", + " def noise(self): \n", + " if self.animal_kind == \"Dog\":\n", + " return \"Bark\"\n", + " elif self.animal_kind == \"Cat\":\n", + " return \"Miaow\"\n", + " elif self.animal_kind == \"Cow\":\n", + " return \"Moo\"" + ] + }, + { + "cell_type": "markdown", + "id": "5ec10da5", + "metadata": {}, + "source": [ + "which is better replaced by the code above." + ] + }, + { + "cell_type": "markdown", + "id": "72d1ebbc", + "metadata": {}, + "source": [ + "### Interfaces and concepts" + ] + }, + { + "cell_type": "markdown", + "id": "cb83e336", + "metadata": {}, + "source": [ + "\n", + "In C++, it is common to define classes which declare dummy methods, called \"virtual\" methods, which specify\n", + "the methods which derived classes must implement. Classes which define these methods, but which cannot be instantiated\n", + "into actual objects, are called \"abstract base\" classes or \"interfaces\".\n", + "\n", + "Python's Duck Typing approach means explicitly declaring these is unnesssary: any class concept which implements\n", + "appropriately named methods will do. These as user-defined **concepts**, just as \"iterable\" or \"container\" are \n", + "built-in Python concepts. A class is said to \"implement an interface\" or \"satisfy a concept\".\n" + ] + }, + { + "cell_type": "markdown", + "id": "b146156b", + "metadata": {}, + "source": [ + "### Interfaces in UML" + ] + }, + { + "cell_type": "markdown", + "id": "3d8455bd", + "metadata": {}, + "source": [ + "Interfaces implementation (a common ancestor that doesn't do anything but defines methods to share) in UML is indicated thus:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "308e4ee3", + "metadata": {}, + "outputs": [], + "source": [ + "yuml(\"[<>]^-.-[Dog]\")" + ] + }, + { + "cell_type": "markdown", + "id": "44caac61", + "metadata": {}, + "source": [ + "### Further UML" + ] + }, + { + "cell_type": "markdown", + "id": "5c95cb89", + "metadata": {}, + "source": [ + "\n", + "UML is a much larger diagram language than the aspects we've shown here.\n", + "\n", + "* Message sequence charts show signals passing back and forth between objects ([Web Sequence Diagrams](https://www.websequencediagrams.com/)).\n", + "\n", + "* Entity Relationship Diagrams can be used to show more general relationships between things in a system.\n", + "\n", + "\n", + "Read more about UML on Martin Fowler's [book about the topic](https://martinfowler.com/books/uml.html).\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Object Oriented Design" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/08objects.ipynb.py b/ch05construction/08objects.ipynb.py new file mode 100644 index 000000000..5c200c836 --- /dev/null +++ b/ch05construction/08objects.ipynb.py @@ -0,0 +1,656 @@ +# --- +# jupyter: +# jekyll: +# display_name: Object Oriented Design +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Class design + +# %% [markdown] +# +# The concepts we have introduced are common between different object oriented languages. +# Thus, when we design our program using these concepts, we can think at an architectural level, +# independent of language syntax. +# +# In Python: +# + +# %% +class Particle: + def __init__(self, position, velocity): + self.position = position + self.velocity = velocity + def move(self, delta_t): + self.position += self.velocity * delta_t + + +# %% [markdown] +# In C++: +# +# ``` cpp +# class Particle { +# std::vector position; +# std::vector velocity; +# Particle(std::vector position, std::vector velocity); +# void move(double delta_t); +# } +# ``` + +# %% [markdown] +# In Fortran: +# +# ``` fortran +# type particle +# real :: position +# real :: velocity +# contains +# procedure :: init +# procedure :: move +# end type particle +# ``` + +# %% [markdown] +# ### UML + +# %% [markdown] +# +# UML is a conventional diagrammatic notation used to describe "class structures" and other higher level +# aspects of software design. +# +# Computer scientists get worked up about formal correctness of UML diagrams and learning the conventions precisely. +# Working programmers can still benefit from using UML to describe their designs. +# + +# %% [markdown] +# ### YUML + +# %% [markdown] +# We can see a YUML model for a Particle class with `position` and `velocity` data and a `move()` method using +# the [YUML](http://yuml.me/) online UML drawing tool ([example](http://yuml.me/diagram/boring/class/[Particle|position;velocity|move%28%29])). +# +# ``` +# http://yuml.me/diagram/boring/class/[Particle|position;velocity|move%28%29] +# ``` + +# %% [markdown] +# Here's how we can use Python code to get an image back from YUML: + +# %% +import requests +from IPython.display import Image + +def yuml(model): + result = requests.get("http://yuml.me/diagram/boring/class/" + model) + return Image(result.content) + + +# %% +yuml("[Particle|position;velocity|move()]") + + +# %% [markdown] +# The representation of the `Particle` class defined above in UML is done with a box with three sections. The name of the class goes on the top, then the name of the member variables in the middle, and the name of the methods on the bottom. We will see later why this is useful. + +# %% [markdown] +# ### Information Hiding + +# %% [markdown] +# +# Sometimes, our design for a program would be broken if users start messing around with variables we don't want them to change. +# +# Robust class design requires consideration of which subroutines are intended for users to use, and which are internal. +# Languages provide features to implement this: access control. +# +# In python, we use leading underscores to control whether member variables and methods can be accessed from outside the class: +# - single leading underscore (`_`) is used to document it's private but people could use it if wanted (thought they shouldn't); +# - double leading underscore (`__`) raises errors if called. +# +# +# + +# %% +class MyClass: + def __init__(self): + self.__private_data = 0 + self._private_data = 0 + self.public_data = 0 + + def __private_method(self): pass + + def _private_method(self): pass + + def public_method(self): pass + + def called_inside(self): + self.__private_method() + self._private_method() + self.__private_data = 1 + self._private_data = 1 + + +MyClass().called_inside() + +# %% +MyClass()._private_method() # Works, but forbidden by convention + +# %% +MyClass().public_method() # OK + +print(MyClass()._private_data) + +# %% +print(MyClass().public_data) + +# %% +MyClass().__private_method() # Generates error + +# %% +print(MyClass().__private_data) # Generates error + + +# %% [markdown] +# ### Property accessors + +# %% [markdown] +# +# Python provides a mechanism to make functions appear to be variables. This can be used if you want to +# change the way a class is implemented without changing the interface: +# +# +# + +# %% +class Person: + def __init__(self): + self.name = "Graham Chapman" + +assert(Person().name == "Graham Chapman") + + +# %% [markdown] +# +# +# +# becomes: +# +# +# + +# %% +class Person(object): + def __init__(self): + self._first = "Graham" + self._second = "Chapman" + + + @property + def name(self): + return f"{self._first} {self._second}" + +assert(Person().name == "Graham Chapman") + + +# %% [markdown] +# Making the same external code work as before. + +# %% [markdown] +# +# +# +# Note that the code behaves the same way to the outside user. +# The implementation detail is hidden by private variables. +# In languages without this feature, such as C++, it is best to always +# make data private, and always +# access data through functions: +# +# +# + +# %% +class Person(object): + def __init__(self): + self._name = "Graham Chapman" + + def name(self): # an access function + return self._name + +assert(Person().name() == "Graham Chapman") + + +# %% [markdown] +# But in Python this is unnecessary because the `@property` capability. +# + +# %% [markdown] +# Another way could be to create a member variable `name` which holds the full name. However, this could lead to inconsistent data. If we create a `get_married` function, then the name of the person won't change! + +# %% +class Person(object): + def __init__(self, first, second): + self._first = first + self._second = second + self.name = f"{self._first} {self._second}" + + def get_married(self, to): + self._second = to._second + +graham = Person("Graham", "Chapman") +david = Person("David", "Sherlock") +assert(graham.name == "Graham Chapman") +graham.get_married(david) +assert(graham.name == "Graham Sherlock") + + +# %% [markdown] +# This type of situation could makes that the object data structure gets inconsistent with itself. Making variables being out of sync with other variables. Each piece of information should only be stored in once place! In this case, `name` should be calculated each time it's required as previously shown. +# In database design, this is called [Normalisation](https://en.wikipedia.org/wiki/Database_normalization). + +# %% [markdown] +# #### UML for private/public + +# %% [markdown] +# We prepend a `+`/`-` on public/private member variables and methods: + +# %% +yuml("[Particle|+public;-private|+publicmethod();-privatemethod]") + + +# %% [markdown] +# ### Class Members + +# %% [markdown] +# +# *Class*, or *static* members, belong to the class as a whole, and are shared between instances. +# +# This is an object that keeps a count on how many have been created of it. +# + +# %% +class Counted: + number_created = 0 + + def __init__(self): + Counted.number_created += 1 + + @classmethod + def howMany(cls): + return cls.number_created + +Counted.howMany() # 0 +x = Counted() +Counted.howMany() # 1 +z = [Counted() for x in range(5)] +Counted.howMany() # 6 + + +# %% [markdown] +# The data is shared among all the objects instantiated from that class. Note that in `__init__` we are not using `self.number_created` but the name of the class. The `howMany` function is not a method of a particular object. It's called on the class, not on the object. This is possible by using the `@classmethod` decorator. + +# %% [markdown] +# ## Inheritance and Polymorphism + +# %% [markdown] +# ### Object-based vs Object-Oriented + +# %% [markdown] +# +# So far we have seen only object-based programming, not object-oriented programming. +# +# Using Objects doesn't mean your code is object-oriented. +# +# To understand object-oriented programming, we need to introduce **polymorphism** and **inheritance**. +# + +# %% [markdown] +# ### Inheritance + +# %% [markdown] +# +# * Inheritance is a mechanism that allows related classes to share code. +# * Inheritance allows a program to reflect the *[ontology](https://en.wikipedia.org/wiki/Ontology_(information_science))* of kinds of thing in a program. +# + +# %% [markdown] +# ### Ontology and inheritance + +# %% [markdown] +# +# * A bird is a kind of animal +# * An eagle is a kind of bird +# * A starling is also a kind of bird +# * All animals can be born and die +# * Only birds can fly (Ish.) +# * Only eagles hunt +# * Only starlings flock +# + +# %% [markdown] +# ### Inheritance in python + +# %% [markdown] +# +# +# + +# %% +class Animal: + def beBorn(self): + print("I exist") + def die(self): + print("Argh!") + +class Bird(Animal): + def fly(self): + print("Whee!") + +class Eagle(Bird): + def hunt(self): + print("I'm gonna eatcha!") + +class Starling(Bird): + def flew(self): + print("I'm flying away!") + +Eagle().beBorn() +Eagle().hunt() + + +# %% [markdown] +# ### Inheritance terminology + +# %% [markdown] +# Here are two equivalents definition, one coming from C++ and another from Java: +# * A *derived class* _derives_ from a *base class*. +# * A *subclass* _inherits_ from a *superclass*. +# +# These are different terms for the same thing. +# So, we can say: +# +# * Eagle is a subclass of the Animal superclass. +# * Animal is the base class of the Eagle derived class. +# +# Another equivalent definition is using the synonym *child* / *parent* for *derived* / *base* class: +# * A *child class* extends a *parent class*. +# + +# %% [markdown] +# ### Inheritance and constructors + +# %% [markdown] +# To use implicitly constructors from a *superclass*, we can use `super` as shown below. + +# %% +class Animal: + def __init__(self, age): + self.age = age + +class Person(Animal): + def __init__(self, age, name): + super().__init__(age) + self.name = name + + +# %% [markdown] +# Read [Raymond Hettinger](https://twitter.com/raymondh)'s [article about `super`](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/) to see various real examples. + +# %% [markdown] +# ### Inheritance UML diagrams + +# %% [markdown] +# UML shows inheritance with an open triangular arrow pointing from subclass to superclass. + +# %% +yuml("[Animal]^-[Bird],[Bird]^-[Eagle],[Bird]^-[Starling]%") + +# %% [markdown] +# ### Aggregation vs Inheritance + +# %% [markdown] +# +# If one object *has* or *owns* one or more objects, this is *not* inheritance. +# +# For example, the boids example we saw few weeks ago, could be organised as an overall Model, which it owns several Boids, +# and each Boid owns two 2-vectors, one for position and one for velocity. +# + +# %% [markdown] +# #### Aggregation in UML + +# %% [markdown] +# The Boids situation can be represented thus: + +# %% +yuml("[Model]<>-*>[Boid],[Boid]position++->[Vector],[Boid]velocity++->[Vector]%") + + +# %% [markdown] +# The open diamond indicates **Aggregation**, the closed diamond **composition**. +# (A given boid might belong to multiple models, a given position vector is forever part of the corresponding Boid.) +# +# The asterisk represents cardinality, a model may contain multiple Boids. This is a [one to many relationship](https://en.wikipedia.org/wiki/One-to-many_(data_model)). [Many to many relationship](https://en.wikipedia.org/wiki/Many-to-many_(data_model)) is shown with `*` on both sides. + +# %% [markdown] +# #### Refactoring to inheritance + +# %% [markdown] +# +# Smell: Repeated code between two classes which are both ontologically subtypes of something +# +# Before: +# +# +# + +# %% +class Person: + def __init__(self, age, job): + self.age = age + self.job = job + def birthday(self): + self.age += 1 + +class Pet: + def __init__(self, age, owner): + self.age = age + self.owner = owner + def birthday(self): + self.age += 1 + + +# %% [markdown] +# +# +# +# After: +# +# +# + +# %% +class Animal: + def __init__(self, age): + self.age = age + def birthday(self): + self.age += 1 + +class Person(Animal): + def __init__(self, age, job): + self.job = job + super().__init__(age) + +class Pet(Animal): + def __init__(self, age, owner): + self.owner = owner + super().__init__(age) + + +# %% [markdown] +# ### Polymorphism + +# %% +class Dog: + def noise(self): + return "Bark" + +class Cat: + def noise(self): + return "Miaow" + +class Pig: + def noise(self): + return "Oink" + +class Cow: + def noise(self): + return "Moo" + +animals = [Dog(), Dog(), Cat(), Pig(), Cow(), Cat()] +for animal in animals: + print(animal.noise()) + + +# %% [markdown] +# +# +# +# This will print "Bark Bark Miaow Oink Moo Miaow" +# +# If two classes support the same method, but it does different things for the two classes, +# then if an object is of an unknown class, calling the method will invoke the version for +# whatever class the instance is an instance of. +# + +# %% [markdown] +# ### Polymorphism and Inheritance + +# %% [markdown] +# +# Often, polymorphism uses multiple derived classes with a common base class. +# However, [duck typing](https://en.wikipedia.org/wiki/Duck_typing) in Python means that all that is required is that the +# types support a common **Concept** (Such as iterable, or container, or, in this case, the +# Noisy concept.) +# +# A common base class is used where there is a likely **default** that you want several +# of the derived classes to have. +# +# +# + +# %% +class Animal: + def noise(self): + return "I don't make a noise." + +class Dog(Animal): + def noise(self): + return "Bark" + +class Worm(Animal): + pass + +class Poodle(Dog): + pass + +animals = [Dog(), Worm(), Pig(), Cow(), Poodle()] +for animal in animals: + print(animal.noise()) + + +# %% [markdown] +# ### Undefined Functions and Polymorphism + +# %% [markdown] +# +# In the above example, we put in a dummy noise for Animals that don't know what type they are. +# +# Instead, we can explicitly deliberately leave this undefined, and we get a crash if we access an undefined method. +# +# +# + +# %% +class Animal: + pass + +class Worm(Animal): + pass + + +# %% +Worm().noise() # Generates error + + +# %% [markdown] +# ### Refactoring to Polymorphism + +# %% [markdown] +# +# Smell: a function uses a big set of `if` statements or a `case` statement to decide what to do: +# +# Before: +# +# +# + +# %% +class Animal: + def __init__(self, animal_kind): + self.animal_kind = animal_kind + + def noise(self): + if self.animal_kind == "Dog": + return "Bark" + elif self.animal_kind == "Cat": + return "Miaow" + elif self.animal_kind == "Cow": + return "Moo" + + +# %% [markdown] +# which is better replaced by the code above. + +# %% [markdown] +# ### Interfaces and concepts + +# %% [markdown] +# +# In C++, it is common to define classes which declare dummy methods, called "virtual" methods, which specify +# the methods which derived classes must implement. Classes which define these methods, but which cannot be instantiated +# into actual objects, are called "abstract base" classes or "interfaces". +# +# Python's Duck Typing approach means explicitly declaring these is unnesssary: any class concept which implements +# appropriately named methods will do. These as user-defined **concepts**, just as "iterable" or "container" are +# built-in Python concepts. A class is said to "implement an interface" or "satisfy a concept". +# + +# %% [markdown] +# ### Interfaces in UML + +# %% [markdown] +# Interfaces implementation (a common ancestor that doesn't do anything but defines methods to share) in UML is indicated thus: + +# %% +yuml("[<>]^-.-[Dog]") + +# %% [markdown] +# ### Further UML + +# %% [markdown] +# +# UML is a much larger diagram language than the aspects we've shown here. +# +# * Message sequence charts show signals passing back and forth between objects ([Web Sequence Diagrams](https://www.websequencediagrams.com/)). +# +# * Entity Relationship Diagrams can be used to show more general relationships between things in a system. +# +# +# Read more about UML on Martin Fowler's [book about the topic](https://martinfowler.com/books/uml.html). +# diff --git a/ch05construction/09patterns.html b/ch05construction/09patterns.html new file mode 100644 index 000000000..f3d167479 --- /dev/null +++ b/ch05construction/09patterns.html @@ -0,0 +1,1494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Design Patterns + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Patterns

+
+
+
+
+
+
+

Class Complexity

We've seen that using object orientation can produce quite complex class structures, with classes owning each other, instantiating each other, +and inheriting from each other.

+

There are lots of different ways to design things, and decisions to make.

+
+
    +
  • How much flexibility should I allow in this class's inner workings?
  • +
  • Should I split this related functionality into multiple classes or keep it in one?
  • +
  • To reuse functionality: should I use inheritance, or add class variable which it is delegated to?
  • +
+
+
+
+
+
+
+
+

Inheritance vs composition

The last point is known as is-a vs has-a or inheritance vs composition.

+

Both options allow us to define a way for classes to collaborate while also having a single responsibility. +As a rule of thumb, we suggest choosing composition over inheritance unless you have a strong reason. +As we go into below, composition introduces fewer dependencies and assumptions about how your code will be used in the future.

+

Inheritance (is-a):

+
    +
  • Used if you want to have the same functionality across multiple instances
  • +
  • Abstracting commonly used methods or data, so all children can use the generic functionality
      +
    • This has a drawback because we make large assumptions about how the code will be used in the future, +which is often hard or impossible to do the first time
    • +
    +
  • +
  • If we find a bug in the functionality, this can be fixed in one place
      +
    • This is good because one change can be cascaded
    • +
    • This can be bad because every subclass relies on its parent and changing one may need us to change the other +(known as tight coupling)
    • +
    +
  • +
+

Composition (has-a):

+
    +
  • Create separate classes (component) which carry out the shared functionality.
  • +
  • Instead of inheriting a method, instantiate the class with the components as class variables, +when that functionality is required then we call that method on from the component.
  • +
  • Unlike inheritance, you can design each component's interface independently +so that it knows how to interact with other parts of your code
  • +
  • Potential to have some code duplication
      +
    • Doesn't have the problem of side effects cascading when you alter one component.
    • +
    +
  • +
+

We've linked to an article which carries out a deep dive on this topic in the other resources section

+
+
+
+
+
+
+

Design Patterns

+
+
+
+
+
+
+

Programmers have noticed that there are certain ways of arranging classes that work better than others.

+

These are called "design patterns".

+

They were first collected on one of the world's first Wikis, +as the Portland Pattern Repository.

+
+
+
+
+
+
+

Reading a pattern

+
+
+
+
+
+
+

A description of a pattern in a book such as the Gang Of Four +book (UCL Library) usually includes:

+
    +
  • Intent - what's the purpose
  • +
  • Motivation - why you want to use it
  • +
  • Applicability - when do you want to use it
  • +
  • Structure - what does it look like (e.g., UML diagram)
  • +
  • Participants - What are the different classes in it
  • +
  • Collaborations - how they work together
  • +
  • Consequences - What are the results and trade-offs
  • +
  • Implementation - How is it implemented
  • +
  • Sample Code - In practice.
  • +
+
+
+
+
+
+
+

Introducing Some Patterns

+
+
+
+
+
+
+

There are lots and lots of design patterns, and it's a great literature to get into to +read about design questions in programming and learn from other people's experience.

+

We'll just show a few in this session:

+ +

Some explanations won't click for some people even though we've tried. +So if you're stuck on wrapping your head around a pattern, +check out another explanation from the other resources section

+
+
+
+
+
+
+

Supporting code

+
+
+
+
+
+
In [1]:
+
+
+
%matplotlib inline
+from unittest.mock import Mock
+
+from IPython.display import SVG
+
+def yuml(model):
+    result=requests.get("http://yuml.me/diagram/boring/class/" + model)
+    return SVG(result.content)
+
+
+
+
+
+
+
+
+

Strategy Pattern

+
+
+
+
+
+
+

Define a family of algorithms, encapsulate each one +(e.g. use composition, or a has-a relationship instead of inheritance), and make them interchangeable. +Strategy lets the algorithm vary independently, without requiring any class that uses it to change.

+
+
+
+
+
+
+

Strategy pattern example: sunspots

+
+
+
+
+
+
In [2]:
+
+
+
import csv
+from datetime import datetime
+import math
+
+import matplotlib.pyplot as plt
+from numpy import linspace, log, sqrt, array, delete
+from numpy.fft import rfft,fft,fftfreq
+from scipy.interpolate import UnivariateSpline
+from scipy.signal import lombscargle
+import requests
+
+
+
+
+
+
+
+
+

Consider the sequence of sunspot observations:

+
    +
  • We want to analyse the variation in sunspot activity
  • +
  • Sunspot activity is cyclical, we expect to find this cycle to be about 11 years
  • +
  • We can use the Fast Fourier Transform (FFT) to process the sunspot signal
  • +
+
+
+
+
+
+
In [3]:
+
+
+
def load_sunspots():
+    with open("SIDC-SUNSPOTS_A.csv") as header:
+        data = csv.reader(header)
+
+        next(data) # Skip header row
+        # The numbers we want are in the 2nd column
+        return [float(row[1]) for row in data]
+
+
+
+
+
+
+
+
In [4]:
+
+
+
spots = load_sunspots()
+plt.plot(spots)
+plt.title("Yearly mean sunspot number")
+plt.xlabel("Years since 1700")
+plt.ylabel("Sunspot number")
+
+
+
+
+
+
+
+
Out[4]:
+
+
Text(0, 0.5, 'Sunspot number')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Sunspot cycle has periodicity

+
+
+
+
+
+
In [5]:
+
+
+
# Use Fast Fourier Transform
+spectrum = rfft(spots)
+
+# the first entry the sum of the data so let's remove it 
+clean_spectrum = delete(spectrum, 0)
+
+plt.figure()
+plt.plot(abs(clean_spectrum.real))
+plt.title("Fourier Coefficients")
+plt.xlabel("Real coefficients")
+
+
+
+
+
+
+
+
Out[5]:
+
+
Text(0.5, 0, 'Real coefficients')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Years are not constant length

+
+
+
+
+
+
+

After we've started out analysis we realise there's a potential problem with this analysis:

+
    +
  • Years are not constant length
  • +
  • Leap years exist
  • +
  • But, the Fast Fourier Transform assumes evenly spaced intervals
  • +
+

We could:

+
    +
  • Ignore this problem, and assume the effect is small;
  • +
  • Interpolate and resample to even times;
  • +
  • Use a method which is robust to unevenly sampled series, such as LSSA;
  • +
+

We also want to find the period of the strongest periodic signal in the data, there are +various different methods we could use for this also, such as integrating the fourier series +by quadrature to find the mean frequency, or choosing the largest single value.

+
+
+
+
+
+
+

Number of child-classes can increase quickly

+
+
+
+
+
+
+

We could implement a base class for our common code between the different approaches, +and define derived classes for each different algorithmic approach. However, this has drawbacks:

+
    +
  • The constructors for each derived class will need arguments for all the numerical method's control parameters, +such as the degree of spline for the interpolation method, the order of quadrature for integrators, and so on.
  • +
  • Where we have multiple algorithmic choices to make (interpolator, periodogram, peak finder...) the number +of derived classes would explode: class SunspotAnalyzerSplineFFTTrapeziumNearMode is a bit unwieldy.
  • +
  • The algorithmic choices are not then available for other projects (so we may have to reinvent the wheel next time)
  • +
  • This design doesn't fit with a clean Ontology of "kinds of things": there's no Abstract Base for spectrogram generators...
  • +
+
+
+
+
+
+
+

Apply the strategy pattern:

+
+
+
+
+
+
+
    +
  • We implement each algorithm for generating a spectrum as its own Strategy class.
  • +
  • They all implement a common interface
  • +
  • Arguments to strategy constructor specify parameters of algorithms, such as spline degree
  • +
  • One strategy instance for each algorithm is passed to the constructor for the overall analysis
  • +
+
+
+
+
+
+
+

First, we'll define a helper class for our time series.

+
+
+
+
+
+
In [6]:
+
+
+
class Series:
+    """Enhance NumPy N-d array with some helper functions for clarity"""
+    def __init__(self, data):
+        self.data = array(data)
+        self.count = self.data.shape[0]
+        self.start = self.data[0, 0]
+        self.end = self.data[-1, 0]
+        self.range = self.end - self.start
+        self.step = self.range / self.count
+        # create separate arrays as some algorithms require an array
+        # as an argument and will throw an exception 
+        # if a view of an array is passed as an argument
+        self.times = self.data[:, 0].copy()
+        self.values = self.data[:, 1].copy()
+        self.plot_data = [self.times, self.values]
+        self.inverse_plot_data = [1 / self.times[20:], self.values[20:]]
+
+
+
+
+
+
+
+
+

Then, our analysis class which contains all methods except the numerical methods

+
+
+
+
+
+
In [7]:
+
+
+
class SunspotDataAnalyser(object):
+    def __init__(self, frequency_strategy):
+        self.secs_per_year = (
+                             datetime(2014, 1, 1) - datetime(2013, 1, 1)
+                     ).total_seconds()
+        self.load_data()
+        self.frequency_strategy = frequency_strategy
+
+    def format_date(self, date):
+        date_format="%Y-%m-%d"
+        return datetime.strptime(date, date_format)
+
+    def date_to_years(self, date_string):
+        return (self.format_date(date_string) - self.start_date
+                ).total_seconds() / self.secs_per_year
+
+    def load_data(self):
+        start_date_str = '1700-12-31'
+        self.start_date = self.format_date(start_date_str)
+
+
+        with open("SIDC-SUNSPOTS_A.csv") as header:
+            data = csv.reader(header)
+
+            next(data) # Skip header row
+            self.series = Series([[
+                self.date_to_years(row[0]), float(row[1])]
+                for row in data])
+
+    def frequency_data(self):
+        return self.frequency_strategy.transform(self.series)
+
+
+
+
+
+
+
+
+

Here is our existing simple fourier method, implemented as a strategy

+
+
+
+
+
+
In [8]:
+
+
+
class FourierNearestFrequencyStrategy:
+    def transform(self, series):
+        transformed = fft(series.values)[0:series.count//2]
+        frequencies = fftfreq(series.count, series.step
+                              )[0:series.count//2]
+        return Series(list(
+            zip(frequencies, abs(transformed)/series.count))
+        )
+
+
+
+
+
+
+
+
+

A strategy based on interpolation to a spline

+
+
+
+
+
+
In [9]:
+
+
+
class FourierSplineFrequencyStrategy:
+    def next_power_of_two(self, value):
+        """Return the next power of 2 above value"""
+        return 2**(1 + int(log(value) / log(2)))
+
+    def transform(self, series):
+        spline = UnivariateSpline(series.times, series.values)
+        # Linspace will give us *evenly* spaced points in the series
+        fft_count = self.next_power_of_two(series.count)
+        points = linspace(series.start,series.end,fft_count)
+        regular_xs = [spline(point) for point in points]
+        transformed = fft(regular_xs)[0:fft_count//2]
+        frequencies = fftfreq(fft_count,
+                              series.range/fft_count)[0:fft_count//2]
+        return Series(list(zip(frequencies, abs(transformed)/fft_count)))
+
+
+
+
+
+
+
+
+

A strategy using the Lomb-Scargle Periodogram

+
+
+
+
+
+
In [10]:
+
+
+
class LombFrequencyStrategy:
+    def transform(self, series):
+        frequencies = array(linspace(1.0 / series.range,
+                                     0.5 / series.step,
+                                     series.count))
+        result = lombscargle(series.times,
+                             series.values,
+                             2.0 * math.pi * frequencies)
+        return Series(list(
+            zip(frequencies, sqrt(result / series.count)))
+        )
+
+
+
+
+
+
+
+
+

Define our concrete solutions with particular strategies, +now it's more straightforward to interchange which numerical method we want the object to use.

+
+
+
+
+
+
In [11]:
+
+
+
fourier_model = SunspotDataAnalyser(FourierSplineFrequencyStrategy())
+lomb_model = SunspotDataAnalyser(LombFrequencyStrategy())
+nearest_model = SunspotDataAnalyser(FourierNearestFrequencyStrategy())
+
+
+
+
+
+
+
+
+

Use these new tools to compare solutions

+
+
+
+
+
+
In [12]:
+
+
+
comparison = fourier_model.frequency_data().inverse_plot_data + ['r']
+comparison += lomb_model.frequency_data().inverse_plot_data + ['g']
+comparison += nearest_model.frequency_data().inverse_plot_data + ['b']
+
+
+
+
+
+
+
+
In [13]:
+
+
+
plt.plot(*comparison)
+plt.xlim(0, 20)
+plt.title("Cycle length")
+plt.xlabel("Years per cycle")
+plt.ylabel("Power")
+
+
+
+
+
+
+
+
Out[13]:
+
+
Text(0, 0.5, 'Power')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Here we get the expected cycle length of around 11 years 🎉

+
+
+
+
+
+
+

Factory Method

+
+
+
+
+
+
+

Here's what the Gang of Four Book says about Factory Method:

+

Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate. +Factory Method lets a class defer instantiation to subclasses.

+

Applicability: Use the Factory method pattern when:

+
    +
  • A class can't anticipate the class of objects it must create
  • +
  • A class wants its subclasses to specify the objects it creates
  • +
+
+
+
+
+
+
+

Factory UML

+
+
+
+
+
+
In [14]:
+
+
+
yuml("[Product]^-[ConcreteProduct], "
+     "[Creator| (v) FactoryMethod()]^-[ConcreteCreator| FactoryMethod()], "
+     "[ConcreteCreator]-.->[ConcreteProduct]")
+
+
+
+
+
+
+
+
Out[14]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

This is all very abstract, so let's get a clearer idea of what that means with an example.

+
+
+
+
+
+
+

Initial Example

+
+
+
+
+
+
+

We have created code that can analyse imaging data from different types of instrument. +However we still want to be able to interact with the imaging data in the same way, +independent of how each instrument stores its data.

+

To do this we have created a GenericImage class which implements default methods for interacting +with imaging data.

+
+
+
+
+
+
In [15]:
+
+
+
import matplotlib.pyplot as plt
+import matplotlib.colors as colors
+
+
+# Create some mocked helper functions so example runs
+ImageNormalize = Mock()
+source_stretch = Mock()
+Image = Mock()
+
+
+class GenericImage:
+    def __init__(self, data, header):
+        """Read image and populate image metadata"""
+        self.data = data
+        ...
+
+    def text_summary(self):
+        """Outputs table summary table of image metadata"""
+        return "\n".join(f"Instrument: {self.instrument}",
+                         f"Observation Date: {self.observation_date}",
+                         f"Scale: {self.scale}",
+                         )
+
+    def plot(self):
+        """Plot normalised and coloured image"""
+        normalised_data = self._pre_process()
+        colour_data = self._get_colour_image(normalised_data)
+        # plot the image data the image data
+        colour_data.plot()
+        plt.colorbar()
+        plt.show()
+
+    def _pre_process(self):
+        """
+        Default preprocessing for an image.
+        This can be normalisation or conversion from raw data into a
+        2d image
+        """
+        # image normaliser not shown here, but used as an example
+        normaliser = ImageNormalize(stretch=source_stretch(0.01), clip=False)
+        return normaliser.normalise(self.data)
+
+    def _get_colour_image(self, normalised_data):
+        """
+        Converts image data into coloured image
+        based on colourmap for the instrument
+        """
+        normalised_data.plot_settings['cmap'] = plt.get_cmap('inferno')
+        normalised_data.plot_settings['norm'] = colors.Normalize(
+            0, normalised_data.max())
+        return normalised_data
+
+
+
+
+
+
+
+
+

Implemented classes

Here are four example child classes that have implemented their own internal methods for normalising +the image data and creating a coloured image. We can use these in exactly the same way as our +GenericImage classes because of polymorphism, we're just using the same methods that exist in +the parent class.

+
+
+
+
+
+
In [16]:
+
+
+
# Imaging for astronomical data
+
+
+class AIAImage(GenericImage):
+    """Atmospheric Imaging Assembly image reader"""
+    def __init__(self, data, header):
+        super().__init__(data, header)
+        self.instrument = "AIA"
+        ...
+
+    def _pre_process(self):
+        # image normaliser not shown here, but used as an example
+        normaliser = ImageNormalize(stretch=source_stretch(0.01),
+                                    clip=False)
+        return normaliser.normalise(self.data)
+
+    def _get_colour_image(self, normalised_data):
+        normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')
+        normalised_data.plot_settings['norm'] = colors.LogNorm(
+            100, normalised_data.max())
+        return normalised_data
+
+
+class HIImage(GenericImage):
+        """STEREO-SECCHI Heliospheric Imager (HI) reader"""
+        def __init__(self, data, header):
+            super().__init__(data, header)
+            self.instrument = "HI"
+            ...
+
+        def _pre_process(self):
+            # image normaliser not shown here, but used as an example
+            normaliser = ImageNormalize(stretch=source_stretch(0.25),
+                                        clip=False)
+            return normaliser.normalise(self.data)
+
+        def _get_colour_image(self, normalised_data):
+            normalised_data.plot_settings['cmap'] = (
+                f'stereocor{self.detector[-1]!s}')
+            normalised_data.plot_settings['norm'] = colors.Normalize(
+                0, normalised_data.max())
+            return normalised_data
+
+
+# Imaging from microscopes
+
+
+class MRC2014Image(GenericImage):
+    """MRC/CCR4 2014 format Transmission Electron Microscopy (LM)
+     Image reader"""
+    def __init__(self, data, header):
+        super().__init__(data, header)
+        self.instrument = self._determine_instrument(header)
+        ...
+
+    def _determine_instrument(self, header):
+        ...
+
+    def _convert_to_2d(self, data):
+        ...
+
+    def _pre_process(self):
+        return  self._convert_to_2d(self.data)
+
+    def _get_colour_image(self, normalised_data):
+        normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')
+        normalised_data.plot_settings['norm'] = colors.LogNorm(
+            100, normalised_data.max())
+        return normalised_data
+
+
+class LeicaImage(GenericImage):
+    """Leica Confocal Microscopy image reader"""
+    def __init__(self, data, header):
+        super().__init__(data, header)
+        self.instrument = self._determine_instrument(header)
+        ...
+
+    def _determine_instrument(self, header):
+        ...
+
+    def _convert_to_2d(self, data):
+        ...
+
+    def _pre_process(self):
+        return  self._convert_to_2d(self.data)
+
+    def _get_colour_image(self, normalised_data):
+        normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')
+        normalised_data.plot_settings['norm'] = colors.Normalize(
+            0, normalised_data.max())
+        return normalised_data
+
+
+
+
+
+
+
+
+

Let's imagine that we've done the hard work and implemented another 10 different image sources each for +astronomy images and microscopy images.

+

Whenever we load an image we want to use the correct image class, falling back to the GenericImage +if we can't fine a match from our known entities.

+

A naive implementation of this would to have an if else block where we use the image metadata +to determine what the right image class is.

+
+
+
+
+
+
In [17]:
+
+
+
class ImageFactory:
+    """Base class that defines the factory interface"""
+    
+    def read_image(self, path):
+        """Reads image from path, using the appropriate image class"""
+        raise NotImplementedError(
+            "Child classes must implement this method")
+
+
+class AstronomyFactory(ImageFactory):
+    def read_image(self, path):
+        # reads in filepath and returns image data and metadata,
+        data, header = Image.read(path)
+
+        if str(header.get('detector', '')).startswith('HI'):
+            return HIImage(data, header)
+        elif str(header.get('instrume', '')).startswith('AIA'):
+            return AIAImage(data, header)
+        # ...this would continue for all 10 other image sources after
+        # we've gone through all possible matches to known data types
+        else:
+            return GenericImage(data, header)
+        
+class MicroscopyFactory(ImageFactory):
+    def read_image(self, path):
+        # reads in filepath and returns image data and metadata,
+        header = Image.get_header(path)
+
+        if str(header.get('nversion', '')) == '20140':
+            return MRC2014Image(Image.parse(path), header)
+        elif path.suffix == '.lif':
+            return LeicaImage(Image.parse(path), header)
+        # ...this would continue for all 10 other image sources
+        # after we've gone through all possible matches to known data types
+        else:
+            raise ValueError(
+                f"File was not a recognised microscopy image: {path}")
+
+
+
+
+
+
+
+
+

Now users can use either factory to read in the right file types, using the same ImageFactory public interface.

+
+
+
+
+
+
+
astro_factory = AstronomyFactory()
+micro_factory = MicroscopyFactory()
+
+image_1 = astro_factory.read_image("testing/reading_AIA_AIA_193.jp2")
+image_1.text_summary()
+image_1.plot()
+
+image_2 = micro_factory.read_image("testing/20110910_114721_s7h2A.lif")
+image_2.plot()
+
+
+
+
+
+
+
+

This is the factory method pattern: +a common design solution to the need to defer the construction of child-objects to a derived class. +With polymorphism we can use any image object returned form the read_image function without +knowing the underlying image source.

+

Having this in a class can allow for more complex logic to determine which child +class should be returned from the factory method, including leveraging the statefullness of the +class to help with the logic.

+
+
+
+
+
+
+

Builder Pattern

+
+
+
+
+
+
+

Intent: Separate the steps for constructing a complex object from its final representation.

+
+
+
+
+
+
In [18]:
+
+
+
yuml("[Director|Construct()]<>->[Builder| (a) BuildPart()],"+
+     " [Builder]^-[ConcreteBuilder| BuildPart();GetResult() ],"+
+     "[ConcreteBuilder]-.->[Product]")
+
+
+
+
+
+
+
+
Out[18]:
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Builder example

+
+
+
+
+
+
+

Imagine that we have a large model with many parameters that we want to run.

+

There's a lot more to defining a model than just adding agents of different kinds: +we need to define boundary conditions, specify wind speed or light conditions.

+

We could define all of this for an imagined advanced Model with a +very very long constructor, with lots of optional arguments:

+
+
+
+
+
+
In [19]:
+
+
+
class Model:
+    def __init__(self, xsize, ysize,
+                 agent_count, wind_speed,
+                 agent_sight_range, eagle_start_location):
+        pass
+
+
+
+
+
+
+
+
+

Builder preferred to complex constructor

+
+
+
+
+
+
+

However, long constructors easily become very complicated. +Instead, it can be cleaner to define a Builder for models. +A builder is like a deferred factory: +each step of the construction process is implemented as an individual method call, +and the completed object is returned when the model is ready.

+
+
+
+
+
+
In [20]:
+
+
+
# Create a bare bones Model so that we can use our builder
+
+class Model:
+    def __init__(self,):
+        ...
+
+    def simulate(self):
+        print("Starting simulation")
+        ...
+
+
+
+
+
+
+
+
In [21]:
+
+
+
class ModelBuilder:
+    def start_model(self):
+        self.model = Model()
+        self.model.xlim = None
+        self.model.ylim = None
+        
+    def set_bounds(self, xlim, ylim):
+        self.model.xlim = xlim
+        self.model.ylim = ylim
+    
+    def add_agent(self, xpos, ypos):
+        pass # Implementation here
+    
+    def finish(self):
+        self.validate()
+        return self.model
+    
+    def validate(self):
+        assert(self.model.xlim is not None)
+        # Check that the all the
+        # parameters that need to be set
+        # have indeed been set.
+
+
+
+
+
+
+
+
+

Inheritance of an Abstract Builder for multiple concrete builders could be used +where there might be multiple ways to build models with the same set of calls to the builder: +for example a version of the model builder yielding models which can be executed +in parallel on a remote cluster.

+
+
+
+
+
+
+

Using a builder

+
+
+
+
+
+
In [22]:
+
+
+
builder = ModelBuilder()
+builder.start_model()
+
+builder.set_bounds(500, 500)
+builder.add_agent(40, 40)
+builder.add_agent(400, 100)
+
+model = builder.finish()
+model.simulate()
+
+
+
+
+
+
+
+
+
+
Starting simulation
+
+
+
+
+
+
+
+
+
+

Avoid staged construction without a builder.

+
+
+
+
+
+
+

We could, of course, just add all the building methods to the model itself, +rather than having the model be yielded from a separate builder.

+

This is an antipattern that is often seen: a class whose __init__ constructor alone is insufficient +for it to be ready to use. A series of methods must be called, in the right order, +in order for it to be ready to use.

+

This results in very fragile code: its hard to keep track of whether an object instance is "ready" or not. +Use the builder pattern to keep deferred construction in control.

+
+
+
+
+
+
+

We might ask why we couldn't just use a validator in all of the methods that must follow the deferred constructors; +to check they have been called. +But we'd need to put these in every method of the class, +whereas with a builder, we can validate only in the finish method.

+
+
+
+
+
+
+

Other resources

There are a lot of design patterns and one explanation might not work well for all people so here are some extra +sources of information about them. Spending some time to understand them can pay off in the future so you don't +reinvent the wheel!

+ +
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/09patterns.ipynb b/ch05construction/09patterns.ipynb new file mode 100644 index 000000000..d447950a0 --- /dev/null +++ b/ch05construction/09patterns.ipynb @@ -0,0 +1,1275 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "36cda014", + "metadata": {}, + "source": [ + "## Patterns" + ] + }, + { + "cell_type": "markdown", + "id": "b166165d", + "metadata": {}, + "source": [ + "### Class Complexity\n", + "\n", + "\n", + "We've seen that using object orientation can produce quite complex class structures, with classes owning each other, instantiating each other,\n", + "and inheriting from each other.\n", + "\n", + "There are lots of different ways to design things, and decisions to make.\n", + "\n", + "> - How much flexibility should I allow in this class's inner workings?\n", + "> - Should I split this related functionality into multiple classes or keep it in one?\n", + "> - To reuse functionality: should I use inheritance, or add class variable which it is delegated to?" + ] + }, + { + "cell_type": "markdown", + "id": "ef1c34f0", + "metadata": {}, + "source": [ + "#### Inheritance vs composition\n", + "\n", + "The last point is known as `is-a` vs `has-a` or inheritance vs composition.\n", + "\n", + "Both options allow us to define a way for classes to collaborate while also having a single responsibility.\n", + "As a rule of thumb, we suggest choosing composition over inheritance unless you have a strong reason.\n", + "As we go into below, composition introduces fewer dependencies and assumptions about how your code will be used in the future.\n", + "\n", + "**Inheritance** *(is-a)*:\n", + "\n", + "- Used if you want to have the same functionality across multiple instances\n", + "- Abstracting commonly used methods or data, so all children can use the generic functionality\n", + " - This has a drawback because we make large assumptions about how the code will be used in the future,\n", + " which is often hard or impossible to do the first time\n", + "- If we find a bug in the functionality, this can be fixed in one place\n", + " - This is good because one change can be cascaded\n", + " - This can be bad because every subclass relies on its parent and changing one may need us to change the other\n", + " (known as tight coupling)\n", + "\n", + "\n", + "**Composition** *(has-a)*:\n", + "\n", + "- Create separate classes (component) which carry out the shared functionality.\n", + "- Instead of inheriting a method, instantiate the class with the components as class variables,\n", + " when that functionality is required then we call that method on from the component.\n", + "- Unlike inheritance, you can design each component's interface independently\n", + " so that it knows how to interact with other parts of your code\n", + "- Potential to have some code duplication\n", + " - Doesn't have the problem of side effects cascading when you alter one component.\n", + "\n", + "We've linked to an article which carries out a deep dive on this topic in the [other resources section](#Other-resources)" + ] + }, + { + "cell_type": "markdown", + "id": "80f9ade0", + "metadata": {}, + "source": [ + "### Design Patterns" + ] + }, + { + "cell_type": "markdown", + "id": "f425416d", + "metadata": {}, + "source": [ + "\n", + "Programmers have noticed that there are certain ways of arranging classes that work better than others.\n", + "\n", + "These are called \"design patterns\".\n", + "\n", + "They were first collected on one of the [world's first Wikis](http://c2.com/cgi/wiki?WelcomeVisitors), \n", + "as the [Portland Pattern Repository](http://c2.com/cgi-bin/wiki?PatternIndex).\n" + ] + }, + { + "cell_type": "markdown", + "id": "98be3fce", + "metadata": {}, + "source": [ + "### Reading a pattern" + ] + }, + { + "cell_type": "markdown", + "id": "83ae502d", + "metadata": {}, + "source": [ + "\n", + "A description of a pattern in a book such as the [Gang Of Four](https://www.worldcat.org/title/design-patterns-elements-of-reusable-object-oriented-software/oclc/31171684)\n", + "book ([UCL Library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21146030410004761&context=L&vid=UCL_VU2&search_scope=CSCOP_UCL&tab=local&lang=en_US)) usually includes:\n", + "\n", + "* **Intent** - what's the purpose\n", + "* **Motivation** - why you want to use it\n", + "* **Applicability** - when do you want to use it\n", + "* **Structure** - what does it look like (e.g., UML diagram)\n", + "* **Participants** - What are the different classes in it\n", + "* **Collaborations** - how they work together\n", + "* **Consequences** - What are the results and trade-offs\n", + "* **Implementation** - How is it implemented\n", + "* **Sample Code** - In practice.\n" + ] + }, + { + "cell_type": "markdown", + "id": "2fe857f7", + "metadata": {}, + "source": [ + "### Introducing Some Patterns" + ] + }, + { + "cell_type": "markdown", + "id": "26731f32", + "metadata": {}, + "source": [ + "\n", + "There are lots and lots of design patterns, and it's a great literature to get into to\n", + "read about design questions in programming and learn from other people's experience.\n", + "\n", + "We'll just show a few in this session:\n", + "\n", + "* [Strategy](#Strategy-Pattern)\n", + "* [Factory Method](#Factory-Method)\n", + "* [Builder](#Builder-Pattern)\n", + "\n", + "Some explanations won't click for some people even though we've tried.\n", + "So if you're stuck on wrapping your head around a pattern,\n", + "check out another explanation from the [other resources section](#Other-resources)" + ] + }, + { + "cell_type": "markdown", + "id": "95b27061", + "metadata": {}, + "source": [ + "### Supporting code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0b2d6fa", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from unittest.mock import Mock\n", + "\n", + "from IPython.display import SVG\n", + "\n", + "def yuml(model):\n", + " result=requests.get(\"http://yuml.me/diagram/boring/class/\" + model)\n", + " return SVG(result.content)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8a94a1b7", + "metadata": {}, + "source": [ + "## Strategy Pattern" + ] + }, + { + "cell_type": "markdown", + "id": "6aace666", + "metadata": {}, + "source": [ + "\n", + "Define a family of algorithms, encapsulate each one\n", + "(e.g. use composition, or a `has-a` relationship instead of inheritance), and make them interchangeable.\n", + "Strategy lets the algorithm vary independently, without requiring any class that uses it to change.\n" + ] + }, + { + "cell_type": "markdown", + "id": "38536be5", + "metadata": {}, + "source": [ + "### Strategy pattern example: sunspots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0784b1a", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import csv\n", + "from datetime import datetime\n", + "import math\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from numpy import linspace, log, sqrt, array, delete\n", + "from numpy.fft import rfft,fft,fftfreq\n", + "from scipy.interpolate import UnivariateSpline\n", + "from scipy.signal import lombscargle\n", + "import requests" + ] + }, + { + "cell_type": "markdown", + "id": "0c1ab8a3", + "metadata": {}, + "source": [ + "Consider the sequence of sunspot observations:\n", + "\n", + "- We want to analyse the variation in sunspot activity\n", + "- Sunspot activity is cyclical, we expect to find this cycle to be about 11 years\n", + "- We can use the Fast Fourier Transform (FFT) to process the sunspot signal\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f335951", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "def load_sunspots():\n", + " with open(\"SIDC-SUNSPOTS_A.csv\") as header:\n", + " data = csv.reader(header)\n", + "\n", + " next(data) # Skip header row\n", + " # The numbers we want are in the 2nd column\n", + " return [float(row[1]) for row in data]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2f2fcbb", + "metadata": { + "lines_to_next_cell": 2, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "spots = load_sunspots()\n", + "plt.plot(spots)\n", + "plt.title(\"Yearly mean sunspot number\")\n", + "plt.xlabel(\"Years since 1700\")\n", + "plt.ylabel(\"Sunspot number\")" + ] + }, + { + "cell_type": "markdown", + "id": "2501e2d3", + "metadata": {}, + "source": [ + "### Sunspot cycle has periodicity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "565a6221", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# Use Fast Fourier Transform\n", + "spectrum = rfft(spots)\n", + "\n", + "# the first entry the sum of the data so let's remove it \n", + "clean_spectrum = delete(spectrum, 0)\n", + "\n", + "plt.figure()\n", + "plt.plot(abs(clean_spectrum.real))\n", + "plt.title(\"Fourier Coefficients\")\n", + "plt.xlabel(\"Real coefficients\")" + ] + }, + { + "cell_type": "markdown", + "id": "39d455e3", + "metadata": {}, + "source": [ + "### Years are not constant length" + ] + }, + { + "cell_type": "markdown", + "id": "4d15bb10", + "metadata": {}, + "source": [ + "After we've started out analysis we realise there's a potential problem with this analysis:\n", + "\n", + "* Years are not constant length\n", + "* Leap years exist\n", + "* But, the Fast Fourier Transform assumes evenly spaced intervals\n", + "\n", + "We could:\n", + "\n", + "* Ignore this problem, and assume the effect is small;\n", + "* Interpolate and resample to even times;\n", + "* Use a method which is robust to unevenly sampled series, such as [LSSA](https://en.wikipedia.org/wiki/Least-squares_spectral_analysis);\n", + "\n", + "We also want to find the period of the strongest periodic signal in the data, there are\n", + "various different methods we could use for this also, such as integrating the fourier series\n", + "by quadrature to find the mean frequency, or choosing the largest single value." + ] + }, + { + "cell_type": "markdown", + "id": "5b96edc1", + "metadata": {}, + "source": [ + "### Number of child-classes can increase quickly" + ] + }, + { + "cell_type": "markdown", + "id": "fadcd311", + "metadata": {}, + "source": [ + "We could implement a base class for our common code between the different approaches,\n", + "and define derived classes for each different algorithmic approach. However, this has drawbacks:\n", + "\n", + "* The constructors for each derived class will need arguments for all the numerical method's control parameters,\n", + "such as the degree of spline for the interpolation method, the order of quadrature for integrators, and so on.\n", + "* Where we have multiple algorithmic choices to make (interpolator, periodogram, peak finder...) the number\n", + "of derived classes would explode: `class SunspotAnalyzerSplineFFTTrapeziumNearMode` is a bit unwieldy.\n", + "* The algorithmic choices are not then available for other projects (so we may have to reinvent the wheel next time)\n", + "* This design doesn't fit with a clean Ontology of \"kinds of things\": there's no Abstract Base for spectrogram generators..." + ] + }, + { + "cell_type": "markdown", + "id": "49dc1d5d", + "metadata": {}, + "source": [ + "### Apply the strategy pattern:" + ] + }, + { + "cell_type": "markdown", + "id": "a14c0f9e", + "metadata": {}, + "source": [ + "* We implement each algorithm for generating a spectrum as its own Strategy class.\n", + "* They all implement a common interface\n", + "* Arguments to strategy constructor specify parameters of algorithms, such as spline degree\n", + "* One strategy instance for each algorithm is passed to the constructor for the overall analysis" + ] + }, + { + "cell_type": "markdown", + "id": "e16b8124", + "metadata": {}, + "source": [ + "First, we'll define a helper class for our time series." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfc89ca0", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class Series:\n", + " \"\"\"Enhance NumPy N-d array with some helper functions for clarity\"\"\"\n", + " def __init__(self, data):\n", + " self.data = array(data)\n", + " self.count = self.data.shape[0]\n", + " self.start = self.data[0, 0]\n", + " self.end = self.data[-1, 0]\n", + " self.range = self.end - self.start\n", + " self.step = self.range / self.count\n", + " # create separate arrays as some algorithms require an array\n", + " # as an argument and will throw an exception \n", + " # if a view of an array is passed as an argument\n", + " self.times = self.data[:, 0].copy()\n", + " self.values = self.data[:, 1].copy()\n", + " self.plot_data = [self.times, self.values]\n", + " self.inverse_plot_data = [1 / self.times[20:], self.values[20:]]" + ] + }, + { + "cell_type": "markdown", + "id": "8a30f33d", + "metadata": {}, + "source": [ + "Then, our analysis class which contains all methods *except* the numerical methods" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77288c93", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class SunspotDataAnalyser(object):\n", + " def __init__(self, frequency_strategy):\n", + " self.secs_per_year = (\n", + " datetime(2014, 1, 1) - datetime(2013, 1, 1)\n", + " ).total_seconds()\n", + " self.load_data()\n", + " self.frequency_strategy = frequency_strategy\n", + "\n", + " def format_date(self, date):\n", + " date_format=\"%Y-%m-%d\"\n", + " return datetime.strptime(date, date_format)\n", + "\n", + " def date_to_years(self, date_string):\n", + " return (self.format_date(date_string) - self.start_date\n", + " ).total_seconds() / self.secs_per_year\n", + "\n", + " def load_data(self):\n", + " start_date_str = '1700-12-31'\n", + " self.start_date = self.format_date(start_date_str)\n", + "\n", + "\n", + " with open(\"SIDC-SUNSPOTS_A.csv\") as header:\n", + " data = csv.reader(header)\n", + "\n", + " next(data) # Skip header row\n", + " self.series = Series([[\n", + " self.date_to_years(row[0]), float(row[1])]\n", + " for row in data])\n", + "\n", + " def frequency_data(self):\n", + " return self.frequency_strategy.transform(self.series)" + ] + }, + { + "cell_type": "markdown", + "id": "06d8692a", + "metadata": {}, + "source": [ + "Here is our existing simple fourier method, implemented as a strategy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa1a8693", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class FourierNearestFrequencyStrategy:\n", + " def transform(self, series):\n", + " transformed = fft(series.values)[0:series.count//2]\n", + " frequencies = fftfreq(series.count, series.step\n", + " )[0:series.count//2]\n", + " return Series(list(\n", + " zip(frequencies, abs(transformed)/series.count))\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "4a18e464", + "metadata": {}, + "source": [ + "A strategy based on interpolation to a spline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86d21e80", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class FourierSplineFrequencyStrategy:\n", + " def next_power_of_two(self, value):\n", + " \"\"\"Return the next power of 2 above value\"\"\"\n", + " return 2**(1 + int(log(value) / log(2)))\n", + "\n", + " def transform(self, series):\n", + " spline = UnivariateSpline(series.times, series.values)\n", + " # Linspace will give us *evenly* spaced points in the series\n", + " fft_count = self.next_power_of_two(series.count)\n", + " points = linspace(series.start,series.end,fft_count)\n", + " regular_xs = [spline(point) for point in points]\n", + " transformed = fft(regular_xs)[0:fft_count//2]\n", + " frequencies = fftfreq(fft_count,\n", + " series.range/fft_count)[0:fft_count//2]\n", + " return Series(list(zip(frequencies, abs(transformed)/fft_count)))" + ] + }, + { + "cell_type": "markdown", + "id": "422b40fc", + "metadata": {}, + "source": [ + "A strategy using the Lomb-Scargle Periodogram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "857d52a3", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class LombFrequencyStrategy:\n", + " def transform(self, series):\n", + " frequencies = array(linspace(1.0 / series.range,\n", + " 0.5 / series.step,\n", + " series.count))\n", + " result = lombscargle(series.times,\n", + " series.values,\n", + " 2.0 * math.pi * frequencies)\n", + " return Series(list(\n", + " zip(frequencies, sqrt(result / series.count)))\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f317d0fc", + "metadata": {}, + "source": [ + "Define our concrete solutions with particular strategies,\n", + "now it's more straightforward to interchange which numerical method we want the object to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91659232", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "fourier_model = SunspotDataAnalyser(FourierSplineFrequencyStrategy())\n", + "lomb_model = SunspotDataAnalyser(LombFrequencyStrategy())\n", + "nearest_model = SunspotDataAnalyser(FourierNearestFrequencyStrategy())" + ] + }, + { + "cell_type": "markdown", + "id": "ee2e6d00", + "metadata": {}, + "source": [ + "Use these new tools to compare solutions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2595df7c", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "comparison = fourier_model.frequency_data().inverse_plot_data + ['r']\n", + "comparison += lomb_model.frequency_data().inverse_plot_data + ['g']\n", + "comparison += nearest_model.frequency_data().inverse_plot_data + ['b']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd47bb47", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "plt.plot(*comparison)\n", + "plt.xlim(0, 20)\n", + "plt.title(\"Cycle length\")\n", + "plt.xlabel(\"Years per cycle\")\n", + "plt.ylabel(\"Power\")" + ] + }, + { + "cell_type": "markdown", + "id": "4aedad58", + "metadata": {}, + "source": [ + "Here we get the expected cycle length of around 11 years 🎉" + ] + }, + { + "cell_type": "markdown", + "id": "3c6f12b2", + "metadata": {}, + "source": [ + "## Factory Method" + ] + }, + { + "cell_type": "markdown", + "id": "434d050e", + "metadata": {}, + "source": [ + "\n", + "Here's what the Gang of Four Book says about Factory Method:\n", + "\n", + "**Intent**: Define an interface for creating an object, but let subclasses decide which class to instantiate.\n", + "Factory Method lets a class defer instantiation to subclasses.\n", + "\n", + "**Applicability**: Use the Factory method pattern when:\n", + "\n", + "* A class can't anticipate the class of objects it must create\n", + "* A class wants its subclasses to specify the objects it creates" + ] + }, + { + "cell_type": "markdown", + "id": "c77bbf65", + "metadata": {}, + "source": [ + "### Factory UML\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f08d45f", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "yuml(\"[Product]^-[ConcreteProduct], \"\n", + " \"[Creator| (v) FactoryMethod()]^-[ConcreteCreator| FactoryMethod()], \"\n", + " \"[ConcreteCreator]-.->[ConcreteProduct]\")" + ] + }, + { + "cell_type": "markdown", + "id": "1e869bd1", + "metadata": {}, + "source": [ + "This is all very abstract, so let's get a clearer idea of what that means with an example." + ] + }, + { + "cell_type": "markdown", + "id": "a501ca5b", + "metadata": {}, + "source": [ + "### Initial Example" + ] + }, + { + "cell_type": "markdown", + "id": "8807ef88", + "metadata": {}, + "source": [ + "We have created code that can analyse imaging data from different types of instrument.\n", + "However we still want to be able to interact with the imaging data in the same way,\n", + "independent of how each instrument stores its data.\n", + "\n", + "To do this we have created a `GenericImage` class which implements default methods for interacting\n", + "with imaging data.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e7a113b", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import matplotlib.colors as colors\n", + "\n", + "\n", + "# Create some mocked helper functions so example runs\n", + "ImageNormalize = Mock()\n", + "source_stretch = Mock()\n", + "Image = Mock()\n", + "\n", + "\n", + "class GenericImage:\n", + " def __init__(self, data, header):\n", + " \"\"\"Read image and populate image metadata\"\"\"\n", + " self.data = data\n", + " ...\n", + "\n", + " def text_summary(self):\n", + " \"\"\"Outputs table summary table of image metadata\"\"\"\n", + " return \"\\n\".join(f\"Instrument: {self.instrument}\",\n", + " f\"Observation Date: {self.observation_date}\",\n", + " f\"Scale: {self.scale}\",\n", + " )\n", + "\n", + " def plot(self):\n", + " \"\"\"Plot normalised and coloured image\"\"\"\n", + " normalised_data = self._pre_process()\n", + " colour_data = self._get_colour_image(normalised_data)\n", + " # plot the image data the image data\n", + " colour_data.plot()\n", + " plt.colorbar()\n", + " plt.show()\n", + "\n", + " def _pre_process(self):\n", + " \"\"\"\n", + " Default preprocessing for an image.\n", + " This can be normalisation or conversion from raw data into a\n", + " 2d image\n", + " \"\"\"\n", + " # image normaliser not shown here, but used as an example\n", + " normaliser = ImageNormalize(stretch=source_stretch(0.01), clip=False)\n", + " return normaliser.normalise(self.data)\n", + "\n", + " def _get_colour_image(self, normalised_data):\n", + " \"\"\"\n", + " Converts image data into coloured image\n", + " based on colourmap for the instrument\n", + " \"\"\"\n", + " normalised_data.plot_settings['cmap'] = plt.get_cmap('inferno')\n", + " normalised_data.plot_settings['norm'] = colors.Normalize(\n", + " 0, normalised_data.max())\n", + " return normalised_data" + ] + }, + { + "cell_type": "markdown", + "id": "043ebd9c", + "metadata": {}, + "source": [ + "### Implemented classes\n", + "\n", + "Here are four example child classes that have implemented their own internal methods for normalising\n", + "the image data and creating a coloured image. We can use these in exactly the same way as our\n", + "GenericImage classes because of polymorphism, we're just using the same methods that exist in\n", + "the parent class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4570a068", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# Imaging for astronomical data\n", + "\n", + "\n", + "class AIAImage(GenericImage):\n", + " \"\"\"Atmospheric Imaging Assembly image reader\"\"\"\n", + " def __init__(self, data, header):\n", + " super().__init__(data, header)\n", + " self.instrument = \"AIA\"\n", + " ...\n", + "\n", + " def _pre_process(self):\n", + " # image normaliser not shown here, but used as an example\n", + " normaliser = ImageNormalize(stretch=source_stretch(0.01),\n", + " clip=False)\n", + " return normaliser.normalise(self.data)\n", + "\n", + " def _get_colour_image(self, normalised_data):\n", + " normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')\n", + " normalised_data.plot_settings['norm'] = colors.LogNorm(\n", + " 100, normalised_data.max())\n", + " return normalised_data\n", + "\n", + "\n", + "class HIImage(GenericImage):\n", + " \"\"\"STEREO-SECCHI Heliospheric Imager (HI) reader\"\"\"\n", + " def __init__(self, data, header):\n", + " super().__init__(data, header)\n", + " self.instrument = \"HI\"\n", + " ...\n", + "\n", + " def _pre_process(self):\n", + " # image normaliser not shown here, but used as an example\n", + " normaliser = ImageNormalize(stretch=source_stretch(0.25),\n", + " clip=False)\n", + " return normaliser.normalise(self.data)\n", + "\n", + " def _get_colour_image(self, normalised_data):\n", + " normalised_data.plot_settings['cmap'] = (\n", + " f'stereocor{self.detector[-1]!s}')\n", + " normalised_data.plot_settings['norm'] = colors.Normalize(\n", + " 0, normalised_data.max())\n", + " return normalised_data\n", + "\n", + "\n", + "# Imaging from microscopes\n", + "\n", + "\n", + "class MRC2014Image(GenericImage):\n", + " \"\"\"MRC/CCR4 2014 format Transmission Electron Microscopy (LM)\n", + " Image reader\"\"\"\n", + " def __init__(self, data, header):\n", + " super().__init__(data, header)\n", + " self.instrument = self._determine_instrument(header)\n", + " ...\n", + "\n", + " def _determine_instrument(self, header):\n", + " ...\n", + "\n", + " def _convert_to_2d(self, data):\n", + " ...\n", + "\n", + " def _pre_process(self):\n", + " return self._convert_to_2d(self.data)\n", + "\n", + " def _get_colour_image(self, normalised_data):\n", + " normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')\n", + " normalised_data.plot_settings['norm'] = colors.LogNorm(\n", + " 100, normalised_data.max())\n", + " return normalised_data\n", + "\n", + "\n", + "class LeicaImage(GenericImage):\n", + " \"\"\"Leica Confocal Microscopy image reader\"\"\"\n", + " def __init__(self, data, header):\n", + " super().__init__(data, header)\n", + " self.instrument = self._determine_instrument(header)\n", + " ...\n", + "\n", + " def _determine_instrument(self, header):\n", + " ...\n", + "\n", + " def _convert_to_2d(self, data):\n", + " ...\n", + "\n", + " def _pre_process(self):\n", + " return self._convert_to_2d(self.data)\n", + "\n", + " def _get_colour_image(self, normalised_data):\n", + " normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r')\n", + " normalised_data.plot_settings['norm'] = colors.Normalize(\n", + " 0, normalised_data.max())\n", + " return normalised_data\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "b57f1e0c", + "metadata": {}, + "source": [ + "Let's imagine that we've done the hard work and implemented another 10 different image sources each for\n", + "astronomy images and microscopy images.\n", + "\n", + "Whenever we load an image we want to use the correct image class, falling back to the GenericImage\n", + "if we can't fine a match from our known entities.\n", + "\n", + "\n", + "A naive implementation of this would to have an `if else` block where we use the image metadata\n", + "to determine what the right image class is." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66c59a83", + "metadata": {}, + "outputs": [], + "source": [ + "class ImageFactory:\n", + " \"\"\"Base class that defines the factory interface\"\"\"\n", + " \n", + " def read_image(self, path):\n", + " \"\"\"Reads image from path, using the appropriate image class\"\"\"\n", + " raise NotImplementedError(\n", + " \"Child classes must implement this method\")\n", + "\n", + "\n", + "class AstronomyFactory(ImageFactory):\n", + " def read_image(self, path):\n", + " # reads in filepath and returns image data and metadata,\n", + " data, header = Image.read(path)\n", + "\n", + " if str(header.get('detector', '')).startswith('HI'):\n", + " return HIImage(data, header)\n", + " elif str(header.get('instrume', '')).startswith('AIA'):\n", + " return AIAImage(data, header)\n", + " # ...this would continue for all 10 other image sources after\n", + " # we've gone through all possible matches to known data types\n", + " else:\n", + " return GenericImage(data, header)\n", + " \n", + "class MicroscopyFactory(ImageFactory):\n", + " def read_image(self, path):\n", + " # reads in filepath and returns image data and metadata,\n", + " header = Image.get_header(path)\n", + "\n", + " if str(header.get('nversion', '')) == '20140':\n", + " return MRC2014Image(Image.parse(path), header)\n", + " elif path.suffix == '.lif':\n", + " return LeicaImage(Image.parse(path), header)\n", + " # ...this would continue for all 10 other image sources\n", + " # after we've gone through all possible matches to known data types\n", + " else:\n", + " raise ValueError(\n", + " f\"File was not a recognised microscopy image: {path}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ec6a1645", + "metadata": {}, + "source": [ + "Now users can use either factory to read in the right file types, using the same ImageFactory public interface. " + ] + }, + { + "cell_type": "markdown", + "id": "2a6d32d9", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "```python\n", + "astro_factory = AstronomyFactory()\n", + "micro_factory = MicroscopyFactory()\n", + "\n", + "image_1 = astro_factory.read_image(\"testing/reading_AIA_AIA_193.jp2\")\n", + "image_1.text_summary()\n", + "image_1.plot()\n", + "\n", + "image_2 = micro_factory.read_image(\"testing/20110910_114721_s7h2A.lif\")\n", + "image_2.plot()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "045fc183", + "metadata": {}, + "source": [ + "This is the *factory method* pattern:\n", + "a common design solution to the need to defer the construction of child-objects to a derived class.\n", + "With polymorphism we can use any image object returned form the `read_image` function without\n", + "knowing the underlying image source.\n", + "\n", + "Having this in a class can allow for more complex logic to determine which child\n", + "class should be returned from the factory method, including leveraging the statefullness of the\n", + "class to help with the logic.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8b9bfddd", + "metadata": {}, + "source": [ + "## Builder Pattern" + ] + }, + { + "cell_type": "markdown", + "id": "f2cd6d79", + "metadata": {}, + "source": [ + "**Intent**: Separate the steps for constructing a complex object from its final representation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a18107cc", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "yuml(\"[Director|Construct()]<>->[Builder| (a) BuildPart()],\"+\n", + " \" [Builder]^-[ConcreteBuilder| BuildPart();GetResult() ],\"+\n", + " \"[ConcreteBuilder]-.->[Product]\")" + ] + }, + { + "cell_type": "markdown", + "id": "f5841d96", + "metadata": {}, + "source": [ + "### Builder example" + ] + }, + { + "cell_type": "markdown", + "id": "6a58589f", + "metadata": {}, + "source": [ + "Imagine that we have a large model with many parameters that we want to run.\n", + "\n", + "There's a lot more to defining a model than just adding agents of different kinds:\n", + "we need to define boundary conditions, specify wind speed or light conditions.\n", + "\n", + "We could define all of this for an imagined advanced Model with a\n", + "very very long constructor, with lots of optional arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ef70545", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class Model:\n", + " def __init__(self, xsize, ysize,\n", + " agent_count, wind_speed,\n", + " agent_sight_range, eagle_start_location):\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "fd24a2a0", + "metadata": {}, + "source": [ + "#### Builder preferred to complex constructor" + ] + }, + { + "cell_type": "markdown", + "id": "1545d65e", + "metadata": {}, + "source": [ + "However, long constructors easily become very complicated.\n", + "Instead, it can be cleaner to define a Builder for models.\n", + "A builder is like a deferred factory:\n", + "each step of the construction process is implemented as an individual method call,\n", + "and the completed object is returned when the model is ready.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c45c4faa", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# Create a bare bones Model so that we can use our builder\n", + "\n", + "class Model:\n", + " def __init__(self,):\n", + " ...\n", + "\n", + " def simulate(self):\n", + " print(\"Starting simulation\")\n", + " ...\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15f1db1e", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "class ModelBuilder:\n", + " def start_model(self):\n", + " self.model = Model()\n", + " self.model.xlim = None\n", + " self.model.ylim = None\n", + " \n", + " def set_bounds(self, xlim, ylim):\n", + " self.model.xlim = xlim\n", + " self.model.ylim = ylim\n", + " \n", + " def add_agent(self, xpos, ypos):\n", + " pass # Implementation here\n", + " \n", + " def finish(self):\n", + " self.validate()\n", + " return self.model\n", + " \n", + " def validate(self):\n", + " assert(self.model.xlim is not None)\n", + " # Check that the all the\n", + " # parameters that need to be set\n", + " # have indeed been set." + ] + }, + { + "cell_type": "markdown", + "id": "5892eb2a", + "metadata": {}, + "source": [ + "\n", + "Inheritance of an Abstract Builder for multiple concrete builders could be used\n", + "where there might be multiple ways to build models with the same set of calls to the builder:\n", + "for example a version of the model builder yielding models which can be executed\n", + "in parallel on a remote cluster.\n" + ] + }, + { + "cell_type": "markdown", + "id": "43ba19b4", + "metadata": {}, + "source": [ + "### Using a builder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6d36647", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "builder = ModelBuilder()\n", + "builder.start_model()\n", + "\n", + "builder.set_bounds(500, 500)\n", + "builder.add_agent(40, 40)\n", + "builder.add_agent(400, 100)\n", + "\n", + "model = builder.finish()\n", + "model.simulate()" + ] + }, + { + "cell_type": "markdown", + "id": "9e188320", + "metadata": {}, + "source": [ + "### Avoid staged construction without a builder." + ] + }, + { + "cell_type": "markdown", + "id": "51e90e4a", + "metadata": {}, + "source": [ + "\n", + "We could, of course, just add all the building methods to the model itself,\n", + "rather than having the model be yielded from a separate builder.\n", + "\n", + "This is an antipattern that is often seen: a class whose `__init__` constructor alone is insufficient\n", + "for it to be ready to use. A series of methods must be called, in the right order,\n", + "in order for it to be ready to use.\n", + "\n", + "This results in very fragile code: its hard to keep track of whether an object instance is \"ready\" or not.\n", + "Use the builder pattern to keep deferred construction in control." + ] + }, + { + "cell_type": "markdown", + "id": "f6f232c1", + "metadata": {}, + "source": [ + "We might ask why we couldn't just use a validator in all of the methods that must follow the deferred constructors;\n", + "to check they have been called.\n", + "But we'd need to put these in *every* method of the class,\n", + "whereas with a builder, we can validate only in the `finish` method.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d73f34c2", + "metadata": {}, + "source": [ + "### Other resources\n", + "\n", + "There are a lot of design patterns and one explanation might not work well for all people so here are some extra\n", + "sources of information about them. Spending some time to understand them can pay off in the future so you don't\n", + "reinvent the wheel!\n", + "\n", + "- [Article on inheritance and composition in Python](https://realpython.com/inheritance-composition-python/)\n", + "- [Course on design patterns](https://www.linkedin.com/learning/python-design-patterns-2015/welcome)\n", + " and [Advanced design patterns](https://www.linkedin.com/learning/python-advanced-design-patterns/welcome)\n", + " with Python at [linkedin learning](https://www.ucl.ac.uk/isd/linkedin-learning).\n", + "- [A collection of design patterns and idioms in Python](https://github.com/faif/python-patterns).\n", + "- [Head First Design Patterns](http://www.worldcat.org/title/head-first-design-patterns/oclc/893944765)\n", + " (Available [online at UCL](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS51233633660004761&context=L&vid=UCL_VU2&search_scope=CSCOP_UCL&tab=local&lang=en_US)\n", + " or [O'Reilly](https://learning.oreilly.com/library/view/head-first-design/0596007124/?sso_link=yes&sso_link_from=university-college-london)\n", + " : sign in using your UCL email) - based on Java (with [online course at linkedin learning](https://www.linkedin.com/learning/programming-foundations-design-patterns-2/don-t-reinvent-the-wheel)).\n", + "- [Design Pattern for Dummies](http://www.worldcat.org/title/design-patterns-for-dummies/oclc/69537420&referer=brief_results)\n", + " (Available on [O'Reilly](https://learning.oreilly.com/library/view/design-patterns-for/9780471798545/?sso_link=yes&sso_link_from=university-college-london)\n", + " : sign in using your UCL email).\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Design Patterns" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/09patterns.ipynb.py b/ch05construction/09patterns.ipynb.py new file mode 100644 index 000000000..2e17ed502 --- /dev/null +++ b/ch05construction/09patterns.ipynb.py @@ -0,0 +1,797 @@ +# --- +# jupyter: +# jekyll: +# display_name: Design Patterns +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Patterns + +# %% [markdown] +# ### Class Complexity +# +# +# We've seen that using object orientation can produce quite complex class structures, with classes owning each other, instantiating each other, +# and inheriting from each other. +# +# There are lots of different ways to design things, and decisions to make. +# +# > - How much flexibility should I allow in this class's inner workings? +# > - Should I split this related functionality into multiple classes or keep it in one? +# > - To reuse functionality: should I use inheritance, or add class variable which it is delegated to? + +# %% [markdown] +# #### Inheritance vs composition +# +# The last point is known as `is-a` vs `has-a` or inheritance vs composition. +# +# Both options allow us to define a way for classes to collaborate while also having a single responsibility. +# As a rule of thumb, we suggest choosing composition over inheritance unless you have a strong reason. +# As we go into below, composition introduces fewer dependencies and assumptions about how your code will be used in the future. +# +# **Inheritance** *(is-a)*: +# +# - Used if you want to have the same functionality across multiple instances +# - Abstracting commonly used methods or data, so all children can use the generic functionality +# - This has a drawback because we make large assumptions about how the code will be used in the future, +# which is often hard or impossible to do the first time +# - If we find a bug in the functionality, this can be fixed in one place +# - This is good because one change can be cascaded +# - This can be bad because every subclass relies on its parent and changing one may need us to change the other +# (known as tight coupling) +# +# +# **Composition** *(has-a)*: +# +# - Create separate classes (component) which carry out the shared functionality. +# - Instead of inheriting a method, instantiate the class with the components as class variables, +# when that functionality is required then we call that method on from the component. +# - Unlike inheritance, you can design each component's interface independently +# so that it knows how to interact with other parts of your code +# - Potential to have some code duplication +# - Doesn't have the problem of side effects cascading when you alter one component. +# +# We've linked to an article which carries out a deep dive on this topic in the [other resources section](#Other-resources) + +# %% [markdown] +# ### Design Patterns + +# %% [markdown] +# +# Programmers have noticed that there are certain ways of arranging classes that work better than others. +# +# These are called "design patterns". +# +# They were first collected on one of the [world's first Wikis](http://c2.com/cgi/wiki?WelcomeVisitors), +# as the [Portland Pattern Repository](http://c2.com/cgi-bin/wiki?PatternIndex). +# + +# %% [markdown] +# ### Reading a pattern + +# %% [markdown] +# +# A description of a pattern in a book such as the [Gang Of Four](https://www.worldcat.org/title/design-patterns-elements-of-reusable-object-oriented-software/oclc/31171684) +# book ([UCL Library](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS21146030410004761&context=L&vid=UCL_VU2&search_scope=CSCOP_UCL&tab=local&lang=en_US)) usually includes: +# +# * **Intent** - what's the purpose +# * **Motivation** - why you want to use it +# * **Applicability** - when do you want to use it +# * **Structure** - what does it look like (e.g., UML diagram) +# * **Participants** - What are the different classes in it +# * **Collaborations** - how they work together +# * **Consequences** - What are the results and trade-offs +# * **Implementation** - How is it implemented +# * **Sample Code** - In practice. +# + +# %% [markdown] +# ### Introducing Some Patterns + +# %% [markdown] +# +# There are lots and lots of design patterns, and it's a great literature to get into to +# read about design questions in programming and learn from other people's experience. +# +# We'll just show a few in this session: +# +# * [Strategy](#Strategy-Pattern) +# * [Factory Method](#Factory-Method) +# * [Builder](#Builder-Pattern) +# +# Some explanations won't click for some people even though we've tried. +# So if you're stuck on wrapping your head around a pattern, +# check out another explanation from the [other resources section](#Other-resources) + +# %% [markdown] +# ### Supporting code + +# %% pycharm={"name": "#%%\n"} +# %matplotlib inline +from unittest.mock import Mock + +from IPython.display import SVG + +def yuml(model): + result=requests.get("http://yuml.me/diagram/boring/class/" + model) + return SVG(result.content) + + + +# %% [markdown] +# ## Strategy Pattern + +# %% [markdown] +# +# Define a family of algorithms, encapsulate each one +# (e.g. use composition, or a `has-a` relationship instead of inheritance), and make them interchangeable. +# Strategy lets the algorithm vary independently, without requiring any class that uses it to change. +# + +# %% [markdown] +# ### Strategy pattern example: sunspots + +# %% pycharm={"name": "#%%\n"} +import csv +from datetime import datetime +import math + +import matplotlib.pyplot as plt +from numpy import linspace, log, sqrt, array, delete +from numpy.fft import rfft,fft,fftfreq +from scipy.interpolate import UnivariateSpline +from scipy.signal import lombscargle +import requests + + +# %% [markdown] +# Consider the sequence of sunspot observations: +# +# - We want to analyse the variation in sunspot activity +# - Sunspot activity is cyclical, we expect to find this cycle to be about 11 years +# - We can use the Fast Fourier Transform (FFT) to process the sunspot signal +# + +# %% pycharm={"name": "#%%\n"} +def load_sunspots(): + with open("SIDC-SUNSPOTS_A.csv") as header: + data = csv.reader(header) + + next(data) # Skip header row + # The numbers we want are in the 2nd column + return [float(row[1]) for row in data] + + +# %% pycharm={"name": "#%%\n"} +spots = load_sunspots() +plt.plot(spots) +plt.title("Yearly mean sunspot number") +plt.xlabel("Years since 1700") +plt.ylabel("Sunspot number") + + +# %% [markdown] +# ### Sunspot cycle has periodicity + +# %% pycharm={"name": "#%%\n"} +# Use Fast Fourier Transform +spectrum = rfft(spots) + +# the first entry the sum of the data so let's remove it +clean_spectrum = delete(spectrum, 0) + +plt.figure() +plt.plot(abs(clean_spectrum.real)) +plt.title("Fourier Coefficients") +plt.xlabel("Real coefficients") + + +# %% [markdown] +# ### Years are not constant length + +# %% [markdown] +# After we've started out analysis we realise there's a potential problem with this analysis: +# +# * Years are not constant length +# * Leap years exist +# * But, the Fast Fourier Transform assumes evenly spaced intervals +# +# We could: +# +# * Ignore this problem, and assume the effect is small; +# * Interpolate and resample to even times; +# * Use a method which is robust to unevenly sampled series, such as [LSSA](https://en.wikipedia.org/wiki/Least-squares_spectral_analysis); +# +# We also want to find the period of the strongest periodic signal in the data, there are +# various different methods we could use for this also, such as integrating the fourier series +# by quadrature to find the mean frequency, or choosing the largest single value. + +# %% [markdown] +# ### Number of child-classes can increase quickly + +# %% [markdown] +# We could implement a base class for our common code between the different approaches, +# and define derived classes for each different algorithmic approach. However, this has drawbacks: +# +# * The constructors for each derived class will need arguments for all the numerical method's control parameters, +# such as the degree of spline for the interpolation method, the order of quadrature for integrators, and so on. +# * Where we have multiple algorithmic choices to make (interpolator, periodogram, peak finder...) the number +# of derived classes would explode: `class SunspotAnalyzerSplineFFTTrapeziumNearMode` is a bit unwieldy. +# * The algorithmic choices are not then available for other projects (so we may have to reinvent the wheel next time) +# * This design doesn't fit with a clean Ontology of "kinds of things": there's no Abstract Base for spectrogram generators... + +# %% [markdown] +# ### Apply the strategy pattern: + +# %% [markdown] +# * We implement each algorithm for generating a spectrum as its own Strategy class. +# * They all implement a common interface +# * Arguments to strategy constructor specify parameters of algorithms, such as spline degree +# * One strategy instance for each algorithm is passed to the constructor for the overall analysis + +# %% [markdown] +# First, we'll define a helper class for our time series. + +# %% pycharm={"name": "#%%\n"} +class Series: + """Enhance NumPy N-d array with some helper functions for clarity""" + def __init__(self, data): + self.data = array(data) + self.count = self.data.shape[0] + self.start = self.data[0, 0] + self.end = self.data[-1, 0] + self.range = self.end - self.start + self.step = self.range / self.count + # create separate arrays as some algorithms require an array + # as an argument and will throw an exception + # if a view of an array is passed as an argument + self.times = self.data[:, 0].copy() + self.values = self.data[:, 1].copy() + self.plot_data = [self.times, self.values] + self.inverse_plot_data = [1 / self.times[20:], self.values[20:]] + + +# %% [markdown] +# Then, our analysis class which contains all methods *except* the numerical methods + +# %% pycharm={"name": "#%%\n"} +class SunspotDataAnalyser(object): + def __init__(self, frequency_strategy): + self.secs_per_year = ( + datetime(2014, 1, 1) - datetime(2013, 1, 1) + ).total_seconds() + self.load_data() + self.frequency_strategy = frequency_strategy + + def format_date(self, date): + date_format="%Y-%m-%d" + return datetime.strptime(date, date_format) + + def date_to_years(self, date_string): + return (self.format_date(date_string) - self.start_date + ).total_seconds() / self.secs_per_year + + def load_data(self): + start_date_str = '1700-12-31' + self.start_date = self.format_date(start_date_str) + + + with open("SIDC-SUNSPOTS_A.csv") as header: + data = csv.reader(header) + + next(data) # Skip header row + self.series = Series([[ + self.date_to_years(row[0]), float(row[1])] + for row in data]) + + def frequency_data(self): + return self.frequency_strategy.transform(self.series) + + +# %% [markdown] +# Here is our existing simple fourier method, implemented as a strategy + +# %% pycharm={"name": "#%%\n"} +class FourierNearestFrequencyStrategy: + def transform(self, series): + transformed = fft(series.values)[0:series.count//2] + frequencies = fftfreq(series.count, series.step + )[0:series.count//2] + return Series(list( + zip(frequencies, abs(transformed)/series.count)) + ) + + +# %% [markdown] +# A strategy based on interpolation to a spline + +# %% pycharm={"name": "#%%\n"} +class FourierSplineFrequencyStrategy: + def next_power_of_two(self, value): + """Return the next power of 2 above value""" + return 2**(1 + int(log(value) / log(2))) + + def transform(self, series): + spline = UnivariateSpline(series.times, series.values) + # Linspace will give us *evenly* spaced points in the series + fft_count = self.next_power_of_two(series.count) + points = linspace(series.start,series.end,fft_count) + regular_xs = [spline(point) for point in points] + transformed = fft(regular_xs)[0:fft_count//2] + frequencies = fftfreq(fft_count, + series.range/fft_count)[0:fft_count//2] + return Series(list(zip(frequencies, abs(transformed)/fft_count))) + + +# %% [markdown] +# A strategy using the Lomb-Scargle Periodogram + +# %% pycharm={"name": "#%%\n"} +class LombFrequencyStrategy: + def transform(self, series): + frequencies = array(linspace(1.0 / series.range, + 0.5 / series.step, + series.count)) + result = lombscargle(series.times, + series.values, + 2.0 * math.pi * frequencies) + return Series(list( + zip(frequencies, sqrt(result / series.count))) + ) + + +# %% [markdown] +# Define our concrete solutions with particular strategies, +# now it's more straightforward to interchange which numerical method we want the object to use. + +# %% pycharm={"name": "#%%\n"} +fourier_model = SunspotDataAnalyser(FourierSplineFrequencyStrategy()) +lomb_model = SunspotDataAnalyser(LombFrequencyStrategy()) +nearest_model = SunspotDataAnalyser(FourierNearestFrequencyStrategy()) + +# %% [markdown] +# Use these new tools to compare solutions + +# %% pycharm={"name": "#%%\n"} +comparison = fourier_model.frequency_data().inverse_plot_data + ['r'] +comparison += lomb_model.frequency_data().inverse_plot_data + ['g'] +comparison += nearest_model.frequency_data().inverse_plot_data + ['b'] + +# %% pycharm={"name": "#%%\n"} +plt.plot(*comparison) +plt.xlim(0, 20) +plt.title("Cycle length") +plt.xlabel("Years per cycle") +plt.ylabel("Power") + +# %% [markdown] +# Here we get the expected cycle length of around 11 years 🎉 + +# %% [markdown] +# ## Factory Method + +# %% [markdown] +# +# Here's what the Gang of Four Book says about Factory Method: +# +# **Intent**: Define an interface for creating an object, but let subclasses decide which class to instantiate. +# Factory Method lets a class defer instantiation to subclasses. +# +# **Applicability**: Use the Factory method pattern when: +# +# * A class can't anticipate the class of objects it must create +# * A class wants its subclasses to specify the objects it creates + +# %% [markdown] +# ### Factory UML +# + +# %% pycharm={"name": "#%%\n"} +yuml("[Product]^-[ConcreteProduct], " + "[Creator| (v) FactoryMethod()]^-[ConcreteCreator| FactoryMethod()], " + "[ConcreteCreator]-.->[ConcreteProduct]") + +# %% [markdown] +# This is all very abstract, so let's get a clearer idea of what that means with an example. + +# %% [markdown] +# ### Initial Example + +# %% [markdown] +# We have created code that can analyse imaging data from different types of instrument. +# However we still want to be able to interact with the imaging data in the same way, +# independent of how each instrument stores its data. +# +# To do this we have created a `GenericImage` class which implements default methods for interacting +# with imaging data. +# +# + +# %% pycharm={"name": "#%%\n"} +import matplotlib.pyplot as plt +import matplotlib.colors as colors + + +# Create some mocked helper functions so example runs +ImageNormalize = Mock() +source_stretch = Mock() +Image = Mock() + + +class GenericImage: + def __init__(self, data, header): + """Read image and populate image metadata""" + self.data = data + ... + + def text_summary(self): + """Outputs table summary table of image metadata""" + return "\n".join(f"Instrument: {self.instrument}", + f"Observation Date: {self.observation_date}", + f"Scale: {self.scale}", + ) + + def plot(self): + """Plot normalised and coloured image""" + normalised_data = self._pre_process() + colour_data = self._get_colour_image(normalised_data) + # plot the image data the image data + colour_data.plot() + plt.colorbar() + plt.show() + + def _pre_process(self): + """ + Default preprocessing for an image. + This can be normalisation or conversion from raw data into a + 2d image + """ + # image normaliser not shown here, but used as an example + normaliser = ImageNormalize(stretch=source_stretch(0.01), clip=False) + return normaliser.normalise(self.data) + + def _get_colour_image(self, normalised_data): + """ + Converts image data into coloured image + based on colourmap for the instrument + """ + normalised_data.plot_settings['cmap'] = plt.get_cmap('inferno') + normalised_data.plot_settings['norm'] = colors.Normalize( + 0, normalised_data.max()) + return normalised_data + +# %% [markdown] +# ### Implemented classes +# +# Here are four example child classes that have implemented their own internal methods for normalising +# the image data and creating a coloured image. We can use these in exactly the same way as our +# GenericImage classes because of polymorphism, we're just using the same methods that exist in +# the parent class. +# + +# %% pycharm={"name": "#%%\n"} +# Imaging for astronomical data + + +class AIAImage(GenericImage): + """Atmospheric Imaging Assembly image reader""" + def __init__(self, data, header): + super().__init__(data, header) + self.instrument = "AIA" + ... + + def _pre_process(self): + # image normaliser not shown here, but used as an example + normaliser = ImageNormalize(stretch=source_stretch(0.01), + clip=False) + return normaliser.normalise(self.data) + + def _get_colour_image(self, normalised_data): + normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r') + normalised_data.plot_settings['norm'] = colors.LogNorm( + 100, normalised_data.max()) + return normalised_data + + +class HIImage(GenericImage): + """STEREO-SECCHI Heliospheric Imager (HI) reader""" + def __init__(self, data, header): + super().__init__(data, header) + self.instrument = "HI" + ... + + def _pre_process(self): + # image normaliser not shown here, but used as an example + normaliser = ImageNormalize(stretch=source_stretch(0.25), + clip=False) + return normaliser.normalise(self.data) + + def _get_colour_image(self, normalised_data): + normalised_data.plot_settings['cmap'] = ( + f'stereocor{self.detector[-1]!s}') + normalised_data.plot_settings['norm'] = colors.Normalize( + 0, normalised_data.max()) + return normalised_data + + +# Imaging from microscopes + + +class MRC2014Image(GenericImage): + """MRC/CCR4 2014 format Transmission Electron Microscopy (LM) + Image reader""" + def __init__(self, data, header): + super().__init__(data, header) + self.instrument = self._determine_instrument(header) + ... + + def _determine_instrument(self, header): + ... + + def _convert_to_2d(self, data): + ... + + def _pre_process(self): + return self._convert_to_2d(self.data) + + def _get_colour_image(self, normalised_data): + normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r') + normalised_data.plot_settings['norm'] = colors.LogNorm( + 100, normalised_data.max()) + return normalised_data + + +class LeicaImage(GenericImage): + """Leica Confocal Microscopy image reader""" + def __init__(self, data, header): + super().__init__(data, header) + self.instrument = self._determine_instrument(header) + ... + + def _determine_instrument(self, header): + ... + + def _convert_to_2d(self, data): + ... + + def _pre_process(self): + return self._convert_to_2d(self.data) + + def _get_colour_image(self, normalised_data): + normalised_data.plot_settings['cmap'] = plt.get_cmap('Greys_r') + normalised_data.plot_settings['norm'] = colors.Normalize( + 0, normalised_data.max()) + return normalised_data + + + +# %% [markdown] +# Let's imagine that we've done the hard work and implemented another 10 different image sources each for +# astronomy images and microscopy images. +# +# Whenever we load an image we want to use the correct image class, falling back to the GenericImage +# if we can't fine a match from our known entities. +# +# +# A naive implementation of this would to have an `if else` block where we use the image metadata +# to determine what the right image class is. + +# %% +class ImageFactory: + """Base class that defines the factory interface""" + + def read_image(self, path): + """Reads image from path, using the appropriate image class""" + raise NotImplementedError( + "Child classes must implement this method") + + +class AstronomyFactory(ImageFactory): + def read_image(self, path): + # reads in filepath and returns image data and metadata, + data, header = Image.read(path) + + if str(header.get('detector', '')).startswith('HI'): + return HIImage(data, header) + elif str(header.get('instrume', '')).startswith('AIA'): + return AIAImage(data, header) + # ...this would continue for all 10 other image sources after + # we've gone through all possible matches to known data types + else: + return GenericImage(data, header) + +class MicroscopyFactory(ImageFactory): + def read_image(self, path): + # reads in filepath and returns image data and metadata, + header = Image.get_header(path) + + if str(header.get('nversion', '')) == '20140': + return MRC2014Image(Image.parse(path), header) + elif path.suffix == '.lif': + return LeicaImage(Image.parse(path), header) + # ...this would continue for all 10 other image sources + # after we've gone through all possible matches to known data types + else: + raise ValueError( + f"File was not a recognised microscopy image: {path}") + + +# %% [markdown] +# Now users can use either factory to read in the right file types, using the same ImageFactory public interface. + +# %% [markdown] pycharm={"name": "#%% md\n"} +# ```python +# astro_factory = AstronomyFactory() +# micro_factory = MicroscopyFactory() +# +# image_1 = astro_factory.read_image("testing/reading_AIA_AIA_193.jp2") +# image_1.text_summary() +# image_1.plot() +# +# image_2 = micro_factory.read_image("testing/20110910_114721_s7h2A.lif") +# image_2.plot() +# ``` + +# %% [markdown] +# This is the *factory method* pattern: +# a common design solution to the need to defer the construction of child-objects to a derived class. +# With polymorphism we can use any image object returned form the `read_image` function without +# knowing the underlying image source. +# +# Having this in a class can allow for more complex logic to determine which child +# class should be returned from the factory method, including leveraging the statefullness of the +# class to help with the logic. +# + +# %% [markdown] +# ## Builder Pattern + +# %% [markdown] +# **Intent**: Separate the steps for constructing a complex object from its final representation. + +# %% pycharm={"name": "#%%\n"} +yuml("[Director|Construct()]<>->[Builder| (a) BuildPart()],"+ + " [Builder]^-[ConcreteBuilder| BuildPart();GetResult() ],"+ + "[ConcreteBuilder]-.->[Product]") + + +# %% [markdown] +# ### Builder example + +# %% [markdown] +# Imagine that we have a large model with many parameters that we want to run. +# +# There's a lot more to defining a model than just adding agents of different kinds: +# we need to define boundary conditions, specify wind speed or light conditions. +# +# We could define all of this for an imagined advanced Model with a +# very very long constructor, with lots of optional arguments: + +# %% pycharm={"name": "#%%\n"} +class Model: + def __init__(self, xsize, ysize, + agent_count, wind_speed, + agent_sight_range, eagle_start_location): + pass + + +# %% [markdown] +# #### Builder preferred to complex constructor + +# %% [markdown] +# However, long constructors easily become very complicated. +# Instead, it can be cleaner to define a Builder for models. +# A builder is like a deferred factory: +# each step of the construction process is implemented as an individual method call, +# and the completed object is returned when the model is ready. +# + +# %% pycharm={"name": "#%%\n"} +# Create a bare bones Model so that we can use our builder + +class Model: + def __init__(self,): + ... + + def simulate(self): + print("Starting simulation") + ... + + + +# %% pycharm={"name": "#%%\n"} +class ModelBuilder: + def start_model(self): + self.model = Model() + self.model.xlim = None + self.model.ylim = None + + def set_bounds(self, xlim, ylim): + self.model.xlim = xlim + self.model.ylim = ylim + + def add_agent(self, xpos, ypos): + pass # Implementation here + + def finish(self): + self.validate() + return self.model + + def validate(self): + assert(self.model.xlim is not None) + # Check that the all the + # parameters that need to be set + # have indeed been set. + + +# %% [markdown] +# +# Inheritance of an Abstract Builder for multiple concrete builders could be used +# where there might be multiple ways to build models with the same set of calls to the builder: +# for example a version of the model builder yielding models which can be executed +# in parallel on a remote cluster. +# + +# %% [markdown] +# ### Using a builder + +# %% pycharm={"name": "#%%\n"} +builder = ModelBuilder() +builder.start_model() + +builder.set_bounds(500, 500) +builder.add_agent(40, 40) +builder.add_agent(400, 100) + +model = builder.finish() +model.simulate() + +# %% [markdown] +# ### Avoid staged construction without a builder. + +# %% [markdown] +# +# We could, of course, just add all the building methods to the model itself, +# rather than having the model be yielded from a separate builder. +# +# This is an antipattern that is often seen: a class whose `__init__` constructor alone is insufficient +# for it to be ready to use. A series of methods must be called, in the right order, +# in order for it to be ready to use. +# +# This results in very fragile code: its hard to keep track of whether an object instance is "ready" or not. +# Use the builder pattern to keep deferred construction in control. + +# %% [markdown] +# We might ask why we couldn't just use a validator in all of the methods that must follow the deferred constructors; +# to check they have been called. +# But we'd need to put these in *every* method of the class, +# whereas with a builder, we can validate only in the `finish` method. +# + +# %% [markdown] +# ### Other resources +# +# There are a lot of design patterns and one explanation might not work well for all people so here are some extra +# sources of information about them. Spending some time to understand them can pay off in the future so you don't +# reinvent the wheel! +# +# - [Article on inheritance and composition in Python](https://realpython.com/inheritance-composition-python/) +# - [Course on design patterns](https://www.linkedin.com/learning/python-design-patterns-2015/welcome) +# and [Advanced design patterns](https://www.linkedin.com/learning/python-advanced-design-patterns/welcome) +# with Python at [linkedin learning](https://www.ucl.ac.uk/isd/linkedin-learning). +# - [A collection of design patterns and idioms in Python](https://github.com/faif/python-patterns). +# - [Head First Design Patterns](http://www.worldcat.org/title/head-first-design-patterns/oclc/893944765) +# (Available [online at UCL](https://ucl-new-primo.hosted.exlibrisgroup.com/primo-explore/fulldisplay?docid=UCL_LMS_DS51233633660004761&context=L&vid=UCL_VU2&search_scope=CSCOP_UCL&tab=local&lang=en_US) +# or [O'Reilly](https://learning.oreilly.com/library/view/head-first-design/0596007124/?sso_link=yes&sso_link_from=university-college-london) +# : sign in using your UCL email) - based on Java (with [online course at linkedin learning](https://www.linkedin.com/learning/programming-foundations-design-patterns-2/don-t-reinvent-the-wheel)). +# - [Design Pattern for Dummies](http://www.worldcat.org/title/design-patterns-for-dummies/oclc/69537420&referer=brief_results) +# (Available on [O'Reilly](https://learning.oreilly.com/library/view/design-patterns-for/9780471798545/?sso_link=yes&sso_link_from=university-college-london) +# : sign in using your UCL email). +# diff --git a/ch05construction/10boids.html b/ch05construction/10boids.html new file mode 100644 index 000000000..65572c9d3 --- /dev/null +++ b/ch05construction/10boids.html @@ -0,0 +1,54909 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exercise - Boids + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Exercise: Refactoring The Bad Boids

+
+
+
+
+
+
+

Bad_Boids

+
+
+
+
+
+
+

We have written some very bad code implementing our Boids flocking example.

+

Here's the Github link.

+

Please fork it on GitHub, and clone your fork.

+
+
+
+
+
+
+
git clone      git@github.com:yourname/bad-boids.git 
+# OR git clone https://github.com/yourname/bad-boids.git
+
+
+
+
+
+
+
+

For the Exercise, you should start from the GitHub repository, but here's our terrible code:

+
+
+
+
+
+
In [1]:
+
+
+
"""
+A deliberately bad implementation of 
+[Boids](http://dl.acm.org/citation.cfm?doid=37401.37406)
+for use as an exercise on refactoring.
+"""
+
+from matplotlib import pyplot as plt
+from matplotlib import animation
+
+import random
+
+# Deliberately terrible code for teaching purposes
+
+boids_x=[random.uniform(-450,50.0) for x in range(50)]
+boids_y=[random.uniform(300.0,600.0) for x in range(50)]
+boid_x_velocities=[random.uniform(0,10.0) for x in range(50)]
+boid_y_velocities=[random.uniform(-20.0,20.0) for x in range(50)]
+boids=(boids_x,boids_y,boid_x_velocities,boid_y_velocities)
+
+def update_boids(boids):
+    xs,ys,xvs,yvs=boids
+    # Fly towards the middle
+    for i in range(len(xs)):
+        for j in range(len(xs)):
+            xvs[i]=xvs[i]+(xs[j]-xs[i])*0.01/len(xs)
+    for i in range(len(xs)):
+        for j in range(len(xs)):
+            yvs[i]=yvs[i]+(ys[j]-ys[i])*0.01/len(xs)
+    # Fly away from nearby boids
+    for i in range(len(xs)):
+        for j in range(len(xs)):
+            if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 100:
+                xvs[i]=xvs[i]+(xs[i]-xs[j])
+                yvs[i]=yvs[i]+(ys[i]-ys[j])
+    # Try to match speed with nearby boids
+    for i in range(len(xs)):
+        for j in range(len(xs)):
+            if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 10000:
+                xvs[i]=xvs[i]+(xvs[j]-xvs[i])*0.125/len(xs)
+                yvs[i]=yvs[i]+(yvs[j]-yvs[i])*0.125/len(xs)
+    # Move according to velocities
+    for i in range(len(xs)):
+        xs[i]=xs[i]+xvs[i]
+        ys[i]=ys[i]+yvs[i]
+
+
+figure=plt.figure()
+axes=plt.axes(xlim=(-500,1500), ylim=(-500,1500))
+scatter=axes.scatter(boids[0],boids[1])
+
+def animate(frame):
+    update_boids(boids)
+    scatter.set_offsets(list(zip(boids[0],boids[1])))
+
+
+anim = animation.FuncAnimation(figure, animate,
+                               frames=200, interval=50)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

If you go into your folder and run the code:

+
+
+
+
+
+
+
cd bad_boids
+python boids.py
+
+
+
+
+
+
+
+

You should be able to see some birds flying around, and then disappearing as they leave the window.

+
+
+
+
+
+
In [2]:
+
+
+
from IPython.display import HTML
+HTML(anim.to_jshtml())
+
+
+
+
+
+
+
+
Out[2]:
+
+ + + +
+No description has been provided for this image +
+ +
+ + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+

Your Task

+
+
+
+
+
+
+

Transform bad_boids gradually into better code, while making sure it still works, using a Refactoring approach.

+
+
+
+
+
+
+

A regression test

+
+
+
+
+
+
+

First, have a look at the regression test we made.

+

To create it, we saved out the before and after state +for one iteration of some boids, using ipython:

+
+
+
+
+
+
+
import yaml
+import boids
+from copy import deepcopy
+
+before = deepcopy(boids.boids)
+boids.update_boids(boids.boids)
+after = boids.boids
+fixture = {"before": before, "after": after}
+fixture_file = open("fixture.yml", 'w')
+fixture_file.write(yaml.dump(fixture))
+fixture_file.close()
+
+
+
+
+
+
+
+

Invoking the test

+
+
+
+
+
+
+

Then, I used the fixture file to define the test:

+
+
+
+
+
+
+
from boids import update_boids
+from nose.tools import assert_almost_equal
+import os
+import yaml
+
+def test_bad_boids_regression():
+    regression_data = yaml.safe_load(open(os.path.join(os.path.dirname(__file__),'fixture.yml')))
+    boid_data = regression_data["before"]
+    update_boids(boid_data)
+    for after, before in zip(regression_data["after"], boid_data):
+        for after_value, before_value in zip(after, before): 
+            assert_almost_equal(after_value, before_value, delta=0.01)
+
+
+
+
+
+
+
+

Make the regression test fail

+
+
+
+
+
+
+

Check the tests pass:

+
+
+
+
+
+
+
pytest
+
+
+
+
+
+
+
+

Edit the file to make the test fail, see the fail, then reset it:

+
+
+
+
+
+
+
git checkout boids.py
+
+
+
+
+
+
+
+

Start Refactoring

+
+
+
+
+
+
+

Look at the code, consider the list of refactorings, and make changes.

+

Each time, do a git commit on your fork, and write a commit message explaining the +refactoring you did.

+

Try to keep the changes as small as possible.

+

If your refactoring creates any units, (functions, modules, or classes) +write a unit test for the unit: it is a good idea to get away from regression testing as soon as you can.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/10boids.ipynb b/ch05construction/10boids.ipynb new file mode 100644 index 000000000..d9b8dc122 --- /dev/null +++ b/ch05construction/10boids.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "223bacac", + "metadata": {}, + "source": [ + "## Exercise: Refactoring The Bad Boids" + ] + }, + { + "cell_type": "markdown", + "id": "f92b16c4", + "metadata": {}, + "source": [ + "### Bad_Boids" + ] + }, + { + "cell_type": "markdown", + "id": "dba9286c", + "metadata": {}, + "source": [ + "\n", + "We have written some _very bad_ code implementing our Boids flocking example.\n", + "\n", + "Here's the [Github link](https://github.com/UCL-ARC-RSEing-with-Python/bad-boids).\n", + "\n", + "Please fork it on GitHub, and clone your fork.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c46bc6f8", + "metadata": {}, + "source": [ + "``` bash\n", + "git clone git@github.com:yourname/bad-boids.git \n", + "# OR git clone https://github.com/yourname/bad-boids.git\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "34e47e93", + "metadata": {}, + "source": [ + "For the Exercise, you should start from the GitHub repository, but here's our terrible code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b383ed1", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "A deliberately bad implementation of \n", + "[Boids](http://dl.acm.org/citation.cfm?doid=37401.37406)\n", + "for use as an exercise on refactoring.\n", + "\"\"\"\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import animation\n", + "\n", + "import random\n", + "\n", + "# Deliberately terrible code for teaching purposes\n", + "\n", + "boids_x=[random.uniform(-450,50.0) for x in range(50)]\n", + "boids_y=[random.uniform(300.0,600.0) for x in range(50)]\n", + "boid_x_velocities=[random.uniform(0,10.0) for x in range(50)]\n", + "boid_y_velocities=[random.uniform(-20.0,20.0) for x in range(50)]\n", + "boids=(boids_x,boids_y,boid_x_velocities,boid_y_velocities)\n", + "\n", + "def update_boids(boids):\n", + " xs,ys,xvs,yvs=boids\n", + " # Fly towards the middle\n", + " for i in range(len(xs)):\n", + " for j in range(len(xs)):\n", + " xvs[i]=xvs[i]+(xs[j]-xs[i])*0.01/len(xs)\n", + " for i in range(len(xs)):\n", + " for j in range(len(xs)):\n", + " yvs[i]=yvs[i]+(ys[j]-ys[i])*0.01/len(xs)\n", + " # Fly away from nearby boids\n", + " for i in range(len(xs)):\n", + " for j in range(len(xs)):\n", + " if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 100:\n", + " xvs[i]=xvs[i]+(xs[i]-xs[j])\n", + " yvs[i]=yvs[i]+(ys[i]-ys[j])\n", + " # Try to match speed with nearby boids\n", + " for i in range(len(xs)):\n", + " for j in range(len(xs)):\n", + " if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 10000:\n", + " xvs[i]=xvs[i]+(xvs[j]-xvs[i])*0.125/len(xs)\n", + " yvs[i]=yvs[i]+(yvs[j]-yvs[i])*0.125/len(xs)\n", + " # Move according to velocities\n", + " for i in range(len(xs)):\n", + " xs[i]=xs[i]+xvs[i]\n", + " ys[i]=ys[i]+yvs[i]\n", + "\n", + "\n", + "figure=plt.figure()\n", + "axes=plt.axes(xlim=(-500,1500), ylim=(-500,1500))\n", + "scatter=axes.scatter(boids[0],boids[1])\n", + "\n", + "def animate(frame):\n", + " update_boids(boids)\n", + " scatter.set_offsets(list(zip(boids[0],boids[1])))\n", + "\n", + "\n", + "anim = animation.FuncAnimation(figure, animate,\n", + " frames=200, interval=50)" + ] + }, + { + "cell_type": "markdown", + "id": "8d44c07c", + "metadata": {}, + "source": [ + "If you go into your folder and run the code:" + ] + }, + { + "cell_type": "markdown", + "id": "23df0cc3", + "metadata": {}, + "source": [ + "``` bash\n", + "cd bad_boids\n", + "python boids.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "95486794", + "metadata": {}, + "source": [ + "\n", + "You should be able to see some birds flying around, and then disappearing as they leave the window.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22076180", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import HTML\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "markdown", + "id": "ddf85e64", + "metadata": {}, + "source": [ + "### Your Task" + ] + }, + { + "cell_type": "markdown", + "id": "8edd1645", + "metadata": {}, + "source": [ + "\n", + "Transform bad_boids **gradually** into better code, while making sure it still works, using a Refactoring approach.\n" + ] + }, + { + "cell_type": "markdown", + "id": "907d78b8", + "metadata": {}, + "source": [ + "### A regression test" + ] + }, + { + "cell_type": "markdown", + "id": "6c922864", + "metadata": {}, + "source": [ + "\n", + "First, have a look at the regression test we made.\n", + "\n", + "To create it, we saved out the before and after state\n", + "for one iteration of some boids, using ipython:\n" + ] + }, + { + "cell_type": "markdown", + "id": "00686056", + "metadata": {}, + "source": [ + "``` python\n", + "import yaml\n", + "import boids\n", + "from copy import deepcopy\n", + "\n", + "before = deepcopy(boids.boids)\n", + "boids.update_boids(boids.boids)\n", + "after = boids.boids\n", + "fixture = {\"before\": before, \"after\": after}\n", + "fixture_file = open(\"fixture.yml\", 'w')\n", + "fixture_file.write(yaml.dump(fixture))\n", + "fixture_file.close()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "2b641b81", + "metadata": {}, + "source": [ + "### Invoking the test" + ] + }, + { + "cell_type": "markdown", + "id": "d839f891", + "metadata": {}, + "source": [ + "\n", + "Then, I used the fixture file to define the test:\n" + ] + }, + { + "cell_type": "markdown", + "id": "95add223", + "metadata": {}, + "source": [ + "``` python\n", + "from boids import update_boids\n", + "from nose.tools import assert_almost_equal\n", + "import os\n", + "import yaml\n", + "\n", + "def test_bad_boids_regression():\n", + " regression_data = yaml.safe_load(open(os.path.join(os.path.dirname(__file__),'fixture.yml')))\n", + " boid_data = regression_data[\"before\"]\n", + " update_boids(boid_data)\n", + " for after, before in zip(regression_data[\"after\"], boid_data):\n", + " for after_value, before_value in zip(after, before): \n", + " assert_almost_equal(after_value, before_value, delta=0.01)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "f6ba14ca", + "metadata": {}, + "source": [ + "### Make the regression test fail" + ] + }, + { + "cell_type": "markdown", + "id": "06855d99", + "metadata": {}, + "source": [ + "Check the tests pass:" + ] + }, + { + "cell_type": "markdown", + "id": "7977befb", + "metadata": {}, + "source": [ + "``` bash\n", + "pytest\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3e396c1a", + "metadata": {}, + "source": [ + "\n", + "Edit the file to make the test fail, see the fail, then reset it:\n" + ] + }, + { + "cell_type": "markdown", + "id": "b6f1b945", + "metadata": {}, + "source": [ + "```\n", + "git checkout boids.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8317ffa2", + "metadata": {}, + "source": [ + "### Start Refactoring" + ] + }, + { + "cell_type": "markdown", + "id": "e5f06e29", + "metadata": {}, + "source": [ + "\n", + "Look at the code, consider the [list of refactorings](./05refactoring.html#refactoring-summary), and make changes.\n", + "\n", + "Each time, do a git commit on your fork, and write a commit message explaining the \n", + "refactoring you did.\n", + "\n", + "Try to keep the changes as small as possible.\n", + "\n", + "If your refactoring creates any units, (functions, modules, or classes)\n", + "**write a unit test** for the unit: it is a good idea to get away from regression testing as soon as you can." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Exercise - Boids" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch05construction/10boids.ipynb.py b/ch05construction/10boids.ipynb.py new file mode 100644 index 000000000..4a747ded4 --- /dev/null +++ b/ch05construction/10boids.ipynb.py @@ -0,0 +1,207 @@ +# --- +# jupyter: +# jekyll: +# display_name: Exercise - Boids +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Exercise: Refactoring The Bad Boids + +# %% [markdown] +# ### Bad_Boids + +# %% [markdown] +# +# We have written some _very bad_ code implementing our Boids flocking example. +# +# Here's the [Github link](https://github.com/UCL-ARC-RSEing-with-Python/bad-boids). +# +# Please fork it on GitHub, and clone your fork. +# + +# %% [markdown] +# ``` bash +# git clone git@github.com:yourname/bad-boids.git +# # OR git clone https://github.com/yourname/bad-boids.git +# ``` + +# %% [markdown] +# For the Exercise, you should start from the GitHub repository, but here's our terrible code: + +# %% +""" +A deliberately bad implementation of +[Boids](http://dl.acm.org/citation.cfm?doid=37401.37406) +for use as an exercise on refactoring. +""" + +from matplotlib import pyplot as plt +from matplotlib import animation + +import random + +# Deliberately terrible code for teaching purposes + +boids_x=[random.uniform(-450,50.0) for x in range(50)] +boids_y=[random.uniform(300.0,600.0) for x in range(50)] +boid_x_velocities=[random.uniform(0,10.0) for x in range(50)] +boid_y_velocities=[random.uniform(-20.0,20.0) for x in range(50)] +boids=(boids_x,boids_y,boid_x_velocities,boid_y_velocities) + +def update_boids(boids): + xs,ys,xvs,yvs=boids + # Fly towards the middle + for i in range(len(xs)): + for j in range(len(xs)): + xvs[i]=xvs[i]+(xs[j]-xs[i])*0.01/len(xs) + for i in range(len(xs)): + for j in range(len(xs)): + yvs[i]=yvs[i]+(ys[j]-ys[i])*0.01/len(xs) + # Fly away from nearby boids + for i in range(len(xs)): + for j in range(len(xs)): + if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 100: + xvs[i]=xvs[i]+(xs[i]-xs[j]) + yvs[i]=yvs[i]+(ys[i]-ys[j]) + # Try to match speed with nearby boids + for i in range(len(xs)): + for j in range(len(xs)): + if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 10000: + xvs[i]=xvs[i]+(xvs[j]-xvs[i])*0.125/len(xs) + yvs[i]=yvs[i]+(yvs[j]-yvs[i])*0.125/len(xs) + # Move according to velocities + for i in range(len(xs)): + xs[i]=xs[i]+xvs[i] + ys[i]=ys[i]+yvs[i] + + +figure=plt.figure() +axes=plt.axes(xlim=(-500,1500), ylim=(-500,1500)) +scatter=axes.scatter(boids[0],boids[1]) + +def animate(frame): + update_boids(boids) + scatter.set_offsets(list(zip(boids[0],boids[1]))) + + +anim = animation.FuncAnimation(figure, animate, + frames=200, interval=50) + +# %% [markdown] +# If you go into your folder and run the code: + +# %% [markdown] +# ``` bash +# cd bad_boids +# python boids.py +# ``` + +# %% [markdown] +# +# You should be able to see some birds flying around, and then disappearing as they leave the window. +# + +# %% +from IPython.display import HTML +HTML(anim.to_jshtml()) + +# %% [markdown] +# ### Your Task + +# %% [markdown] +# +# Transform bad_boids **gradually** into better code, while making sure it still works, using a Refactoring approach. +# + +# %% [markdown] +# ### A regression test + +# %% [markdown] +# +# First, have a look at the regression test we made. +# +# To create it, we saved out the before and after state +# for one iteration of some boids, using ipython: +# + +# %% [markdown] +# ``` python +# import yaml +# import boids +# from copy import deepcopy +# +# before = deepcopy(boids.boids) +# boids.update_boids(boids.boids) +# after = boids.boids +# fixture = {"before": before, "after": after} +# fixture_file = open("fixture.yml", 'w') +# fixture_file.write(yaml.dump(fixture)) +# fixture_file.close() +# ``` + +# %% [markdown] +# ### Invoking the test + +# %% [markdown] +# +# Then, I used the fixture file to define the test: +# + +# %% [markdown] +# ``` python +# from boids import update_boids +# from nose.tools import assert_almost_equal +# import os +# import yaml +# +# def test_bad_boids_regression(): +# regression_data = yaml.safe_load(open(os.path.join(os.path.dirname(__file__),'fixture.yml'))) +# boid_data = regression_data["before"] +# update_boids(boid_data) +# for after, before in zip(regression_data["after"], boid_data): +# for after_value, before_value in zip(after, before): +# assert_almost_equal(after_value, before_value, delta=0.01) +# ``` + +# %% [markdown] +# ### Make the regression test fail + +# %% [markdown] +# Check the tests pass: + +# %% [markdown] +# ``` bash +# pytest +# ``` + +# %% [markdown] +# +# Edit the file to make the test fail, see the fail, then reset it: +# + +# %% [markdown] +# ``` +# git checkout boids.py +# ``` + +# %% [markdown] +# ### Start Refactoring + +# %% [markdown] +# +# Look at the code, consider the [list of refactorings](./05refactoring.html#refactoring-summary), and make changes. +# +# Each time, do a git commit on your fork, and write a commit message explaining the +# refactoring you did. +# +# Try to keep the changes as small as possible. +# +# If your refactoring creates any units, (functions, modules, or classes) +# **write a unit test** for the unit: it is a good idea to get away from regression testing as soon as you can. diff --git a/ch05construction/SIDC-SUNSPOTS_A.csv b/ch05construction/SIDC-SUNSPOTS_A.csv new file mode 100644 index 000000000..16e59b7c0 --- /dev/null +++ b/ch05construction/SIDC-SUNSPOTS_A.csv @@ -0,0 +1,322 @@ +Date,Yearly Mean Total Sunspot Number,Yearly Mean Standard Deviation,Number of Observations,Definitive/Provisional Indicator +1700-12-31,8.3,,,1.0 +1701-12-31,18.3,,,1.0 +1702-12-31,26.7,,,1.0 +1703-12-31,38.3,,,1.0 +1704-12-31,60.0,,,1.0 +1705-12-31,96.7,,,1.0 +1706-12-31,48.3,,,1.0 +1707-12-31,33.3,,,1.0 +1708-12-31,16.7,,,1.0 +1709-12-31,13.3,,,1.0 +1710-12-31,5.0,,,1.0 +1711-12-31,0.0,,,1.0 +1712-12-31,0.0,,,1.0 +1713-12-31,3.3,,,1.0 +1714-12-31,18.3,,,1.0 +1715-12-31,45.0,,,1.0 +1716-12-31,78.3,,,1.0 +1717-12-31,105.0,,,1.0 +1718-12-31,100.0,,,1.0 +1719-12-31,65.0,,,1.0 +1720-12-31,46.7,,,1.0 +1721-12-31,43.3,,,1.0 +1722-12-31,36.7,,,1.0 +1723-12-31,18.3,,,1.0 +1724-12-31,35.0,,,1.0 +1725-12-31,66.7,,,1.0 +1726-12-31,130.0,,,1.0 +1727-12-31,203.3,,,1.0 +1728-12-31,171.7,,,1.0 +1729-12-31,121.7,,,1.0 +1730-12-31,78.3,,,1.0 +1731-12-31,58.3,,,1.0 +1732-12-31,18.3,,,1.0 +1733-12-31,8.3,,,1.0 +1734-12-31,26.7,,,1.0 +1735-12-31,56.7,,,1.0 +1736-12-31,116.7,,,1.0 +1737-12-31,135.0,,,1.0 +1738-12-31,185.0,,,1.0 +1739-12-31,168.3,,,1.0 +1740-12-31,121.7,,,1.0 +1741-12-31,66.7,,,1.0 +1742-12-31,33.3,,,1.0 +1743-12-31,26.7,,,1.0 +1744-12-31,8.3,,,1.0 +1745-12-31,18.3,,,1.0 +1746-12-31,36.7,,,1.0 +1747-12-31,66.7,,,1.0 +1748-12-31,100.0,,,1.0 +1749-12-31,134.8,,,1.0 +1750-12-31,139.0,,,1.0 +1751-12-31,79.5,,,1.0 +1752-12-31,79.7,,,1.0 +1753-12-31,51.2,,,1.0 +1754-12-31,20.3,,,1.0 +1755-12-31,16.0,,,1.0 +1756-12-31,17.0,,,1.0 +1757-12-31,54.0,,,1.0 +1758-12-31,79.3,,,1.0 +1759-12-31,90.0,,,1.0 +1760-12-31,104.8,,,1.0 +1761-12-31,143.2,,,1.0 +1762-12-31,102.0,,,1.0 +1763-12-31,75.2,,,1.0 +1764-12-31,60.7,,,1.0 +1765-12-31,34.8,,,1.0 +1766-12-31,19.0,,,1.0 +1767-12-31,63.0,,,1.0 +1768-12-31,116.3,,,1.0 +1769-12-31,176.8,,,1.0 +1770-12-31,168.0,,,1.0 +1771-12-31,136.0,,,1.0 +1772-12-31,110.8,,,1.0 +1773-12-31,58.0,,,1.0 +1774-12-31,51.0,,,1.0 +1775-12-31,11.7,,,1.0 +1776-12-31,33.0,,,1.0 +1777-12-31,154.2,,,1.0 +1778-12-31,257.3,,,1.0 +1779-12-31,209.8,,,1.0 +1780-12-31,141.3,,,1.0 +1781-12-31,113.5,,,1.0 +1782-12-31,64.2,,,1.0 +1783-12-31,38.0,,,1.0 +1784-12-31,17.0,,,1.0 +1785-12-31,40.2,,,1.0 +1786-12-31,138.2,,,1.0 +1787-12-31,220.0,,,1.0 +1788-12-31,218.2,,,1.0 +1789-12-31,196.8,,,1.0 +1790-12-31,149.8,,,1.0 +1791-12-31,111.0,,,1.0 +1792-12-31,100.0,,,1.0 +1793-12-31,78.2,,,1.0 +1794-12-31,68.3,,,1.0 +1795-12-31,35.5,,,1.0 +1796-12-31,26.7,,,1.0 +1797-12-31,10.7,,,1.0 +1798-12-31,6.8,,,1.0 +1799-12-31,11.3,,,1.0 +1800-12-31,24.2,,,1.0 +1801-12-31,56.7,,,1.0 +1802-12-31,75.0,,,1.0 +1803-12-31,71.8,,,1.0 +1804-12-31,79.2,,,1.0 +1805-12-31,70.3,,,1.0 +1806-12-31,46.8,,,1.0 +1807-12-31,16.8,,,1.0 +1808-12-31,13.5,,,1.0 +1809-12-31,4.2,,,1.0 +1810-12-31,0.0,,,1.0 +1811-12-31,2.3,,,1.0 +1812-12-31,8.3,,,1.0 +1813-12-31,20.3,,,1.0 +1814-12-31,23.2,,,1.0 +1815-12-31,59.0,,,1.0 +1816-12-31,76.3,,,1.0 +1817-12-31,68.3,,,1.0 +1818-12-31,52.9,9.2,213.0,1.0 +1819-12-31,38.5,7.9,249.0,1.0 +1820-12-31,24.2,6.4,224.0,1.0 +1821-12-31,9.2,4.2,304.0,1.0 +1822-12-31,6.3,3.7,353.0,1.0 +1823-12-31,2.2,2.7,302.0,1.0 +1824-12-31,11.4,4.6,194.0,1.0 +1825-12-31,28.2,6.8,310.0,1.0 +1826-12-31,59.9,9.8,320.0,1.0 +1827-12-31,83.0,11.6,321.0,1.0 +1828-12-31,108.5,13.2,301.0,1.0 +1829-12-31,115.2,13.6,291.0,1.0 +1830-12-31,117.4,13.7,268.0,1.0 +1831-12-31,80.8,11.4,285.0,1.0 +1832-12-31,44.3,8.5,277.0,1.0 +1833-12-31,13.4,4.9,292.0,1.0 +1834-12-31,19.5,5.8,260.0,1.0 +1835-12-31,85.8,11.8,173.0,1.0 +1836-12-31,192.7,17.6,166.0,1.0 +1837-12-31,227.3,19.1,150.0,1.0 +1838-12-31,168.7,16.5,201.0,1.0 +1839-12-31,143.0,15.1,194.0,1.0 +1840-12-31,105.5,13.0,248.0,1.0 +1841-12-31,63.3,10.1,277.0,1.0 +1842-12-31,40.3,8.1,311.0,1.0 +1843-12-31,18.1,5.6,320.0,1.0 +1844-12-31,25.1,6.5,294.0,1.0 +1845-12-31,65.8,10.3,265.0,1.0 +1846-12-31,102.7,12.8,247.0,1.0 +1847-12-31,166.3,16.3,232.0,1.0 +1848-12-31,208.3,18.3,234.0,1.0 +1849-12-31,182.5,16.6,365.0,1.0 +1850-12-31,126.3,13.7,365.0,1.0 +1851-12-31,122.0,13.4,365.0,1.0 +1852-12-31,102.7,12.3,366.0,1.0 +1853-12-31,74.1,10.4,365.0,1.0 +1854-12-31,39.0,7.5,365.0,1.0 +1855-12-31,12.7,4.3,365.0,1.0 +1856-12-31,8.2,3.4,366.0,1.0 +1857-12-31,43.4,8.0,365.0,1.0 +1858-12-31,104.4,12.4,365.0,1.0 +1859-12-31,178.3,16.3,365.0,1.0 +1860-12-31,182.2,16.5,366.0,1.0 +1861-12-31,146.6,14.8,365.0,1.0 +1862-12-31,112.1,12.9,365.0,1.0 +1863-12-31,83.5,11.1,365.0,1.0 +1864-12-31,89.2,11.5,366.0,1.0 +1865-12-31,57.8,9.2,365.0,1.0 +1866-12-31,30.7,6.7,365.0,1.0 +1867-12-31,13.9,4.5,365.0,1.0 +1868-12-31,62.8,8.9,366.0,1.0 +1869-12-31,123.6,12.5,365.0,1.0 +1870-12-31,232.0,17.1,365.0,1.0 +1871-12-31,185.3,15.3,365.0,1.0 +1872-12-31,169.2,14.6,366.0,1.0 +1873-12-31,110.1,11.8,365.0,1.0 +1874-12-31,74.5,9.7,365.0,1.0 +1875-12-31,28.3,6.1,365.0,1.0 +1876-12-31,18.9,5.1,366.0,1.0 +1877-12-31,20.7,5.3,365.0,1.0 +1878-12-31,5.7,3.2,365.0,1.0 +1879-12-31,10.0,3.9,365.0,1.0 +1880-12-31,53.7,8.3,366.0,1.0 +1881-12-31,90.5,10.7,365.0,1.0 +1882-12-31,99.0,11.2,365.0,1.0 +1883-12-31,106.1,11.6,365.0,1.0 +1884-12-31,105.8,11.6,366.0,1.0 +1885-12-31,86.3,10.5,365.0,1.0 +1886-12-31,42.4,7.4,365.0,1.0 +1887-12-31,21.8,5.4,365.0,1.0 +1888-12-31,11.2,4.0,366.0,1.0 +1889-12-31,10.4,3.9,365.0,1.0 +1890-12-31,11.8,4.1,365.0,1.0 +1891-12-31,59.5,8.7,365.0,1.0 +1892-12-31,121.7,12.4,366.0,1.0 +1893-12-31,142.0,10.6,365.0,1.0 +1894-12-31,130.0,10.2,365.0,1.0 +1895-12-31,106.6,9.2,365.0,1.0 +1896-12-31,69.4,7.4,366.0,1.0 +1897-12-31,43.8,5.9,365.0,1.0 +1898-12-31,44.4,6.0,365.0,1.0 +1899-12-31,20.2,4.1,365.0,1.0 +1900-12-31,15.7,3.8,365.0,1.0 +1901-12-31,4.6,2.6,365.0,1.0 +1902-12-31,8.5,3.1,365.0,1.0 +1903-12-31,40.8,5.7,365.0,1.0 +1904-12-31,70.1,7.5,366.0,1.0 +1905-12-31,105.5,9.2,365.0,1.0 +1906-12-31,90.1,8.5,365.0,1.0 +1907-12-31,102.8,9.0,365.0,1.0 +1908-12-31,80.9,8.0,366.0,1.0 +1909-12-31,73.2,7.6,365.0,1.0 +1910-12-31,30.9,5.0,365.0,1.0 +1911-12-31,9.5,3.1,365.0,1.0 +1912-12-31,6.0,2.7,366.0,1.0 +1913-12-31,2.4,2.3,365.0,1.0 +1914-12-31,16.1,3.8,365.0,1.0 +1915-12-31,79.0,7.9,365.0,1.0 +1916-12-31,95.0,8.7,366.0,1.0 +1917-12-31,173.6,11.8,365.0,1.0 +1918-12-31,134.6,10.3,365.0,1.0 +1919-12-31,105.7,9.2,365.0,1.0 +1920-12-31,62.7,7.1,366.0,1.0 +1921-12-31,43.5,5.9,365.0,1.0 +1922-12-31,23.7,4.5,365.0,1.0 +1923-12-31,9.7,3.1,365.0,1.0 +1924-12-31,27.9,4.8,366.0,1.0 +1925-12-31,74.0,7.7,365.0,1.0 +1926-12-31,106.5,9.2,365.0,1.0 +1927-12-31,114.7,9.5,365.0,1.0 +1928-12-31,129.7,10.2,366.0,1.0 +1929-12-31,108.2,9.3,365.0,1.0 +1930-12-31,59.4,6.9,365.0,1.0 +1931-12-31,35.1,5.3,365.0,1.0 +1932-12-31,18.6,4.0,366.0,1.0 +1933-12-31,9.2,3.2,365.0,1.0 +1934-12-31,14.6,3.6,365.0,1.0 +1935-12-31,60.2,6.9,365.0,1.0 +1936-12-31,132.8,10.3,366.0,1.0 +1937-12-31,190.6,12.3,365.0,1.0 +1938-12-31,182.6,12.1,365.0,1.0 +1939-12-31,148.0,10.8,365.0,1.0 +1940-12-31,113.0,9.5,366.0,1.0 +1941-12-31,79.2,7.9,365.0,1.0 +1942-12-31,50.8,6.4,365.0,1.0 +1943-12-31,27.1,4.7,365.0,1.0 +1944-12-31,16.1,3.8,366.0,1.0 +1945-12-31,55.3,6.6,365.0,1.0 +1946-12-31,154.3,11.1,365.0,1.0 +1947-12-31,214.7,9.8,365.0,1.0 +1948-12-31,193.0,9.3,366.0,1.0 +1949-12-31,190.7,9.2,365.0,1.0 +1950-12-31,118.9,7.3,365.0,1.0 +1951-12-31,98.3,6.6,365.0,1.0 +1952-12-31,45.0,4.5,366.0,1.0 +1953-12-31,20.1,3.0,365.0,1.0 +1954-12-31,6.6,1.7,365.0,1.0 +1955-12-31,54.2,4.9,365.0,1.0 +1956-12-31,200.7,9.5,366.0,1.0 +1957-12-31,269.3,11.0,365.0,1.0 +1958-12-31,261.7,10.8,365.0,1.0 +1959-12-31,225.1,10.0,365.0,1.0 +1960-12-31,159.0,8.4,366.0,1.0 +1961-12-31,76.4,5.8,365.0,1.0 +1962-12-31,53.4,4.9,365.0,1.0 +1963-12-31,39.9,4.2,365.0,1.0 +1964-12-31,15.0,2.6,366.0,1.0 +1965-12-31,22.0,3.2,365.0,1.0 +1966-12-31,66.8,5.4,365.0,1.0 +1967-12-31,132.9,7.7,365.0,1.0 +1968-12-31,150.0,8.2,366.0,1.0 +1969-12-31,149.4,8.2,365.0,1.0 +1970-12-31,148.0,8.1,365.0,1.0 +1971-12-31,94.4,6.5,365.0,1.0 +1972-12-31,97.6,6.6,366.0,1.0 +1973-12-31,54.1,4.9,365.0,1.0 +1974-12-31,49.2,4.7,365.0,1.0 +1975-12-31,22.5,3.2,365.0,1.0 +1976-12-31,18.4,2.9,366.0,1.0 +1977-12-31,39.3,4.2,365.0,1.0 +1978-12-31,131.0,7.6,365.0,1.0 +1979-12-31,220.1,9.9,365.0,1.0 +1980-12-31,218.9,9.9,366.0,1.0 +1981-12-31,198.9,13.1,3049.0,1.0 +1982-12-31,162.4,12.1,3436.0,1.0 +1983-12-31,91.0,7.6,4216.0,1.0 +1984-12-31,60.5,5.9,5103.0,1.0 +1985-12-31,20.6,3.7,5543.0,1.0 +1986-12-31,14.8,3.5,5934.0,1.0 +1987-12-31,33.9,3.7,6396.0,1.0 +1988-12-31,123.0,8.4,6556.0,1.0 +1989-12-31,211.1,12.8,6932.0,1.0 +1990-12-31,191.8,11.2,7108.0,1.0 +1991-12-31,203.3,12.7,6932.0,1.0 +1992-12-31,133.0,8.9,7845.0,1.0 +1993-12-31,76.1,5.8,8010.0,1.0 +1994-12-31,44.9,4.4,8524.0,1.0 +1995-12-31,25.1,3.7,8429.0,1.0 +1996-12-31,11.6,3.1,7614.0,1.0 +1997-12-31,28.9,3.6,7294.0,1.0 +1998-12-31,88.3,6.6,6353.0,1.0 +1999-12-31,136.3,9.3,6413.0,1.0 +2000-12-31,173.9,10.1,5953.0,1.0 +2001-12-31,170.4,10.5,6558.0,1.0 +2002-12-31,163.6,9.8,6588.0,1.0 +2003-12-31,99.3,7.1,7087.0,1.0 +2004-12-31,65.3,5.9,6882.0,1.0 +2005-12-31,45.8,4.7,7084.0,1.0 +2006-12-31,24.7,3.5,6370.0,1.0 +2007-12-31,12.6,2.7,6841.0,1.0 +2008-12-31,4.2,2.5,6644.0,1.0 +2009-12-31,4.8,2.5,6465.0,1.0 +2010-12-31,24.9,3.4,6328.0,1.0 +2011-12-31,80.8,6.7,6077.0,1.0 +2012-12-31,84.5,6.7,5753.0,1.0 +2013-12-31,94.0,6.9,5347.0,1.0 +2014-12-31,113.3,8.0,5273.0,1.0 +2015-12-31,69.8,6.4,8903.0,1.0 +2016-12-31,39.8,3.9,9940.0,1.0 +2017-12-31,21.7,2.5,11444.0,1.0 +2018-12-31,7.0,1.1,12611.0,1.0 +2019-12-31,3.6,0.5,12884.0,1.0 +2020-12-31,8.8,4.1,14440.0,1.0 diff --git a/ch05construction/anotherfile.py b/ch05construction/anotherfile.py new file mode 100644 index 000000000..eaaedb322 --- /dev/null +++ b/ch05construction/anotherfile.py @@ -0,0 +1,2 @@ +class One(object): + pass diff --git a/ch05construction/config.yaml b/ch05construction/config.yaml new file mode 100644 index 000000000..10e44e42b --- /dev/null +++ b/ch05construction/config.yaml @@ -0,0 +1,6 @@ +bounds: [0, 0, 100, 100] +counts: + hawk: 5 + starling: 500 +speed: 2.0 +turning_circle: 3.0 diff --git a/ch05construction/context.py b/ch05construction/context.py new file mode 100644 index 000000000..0e8d772f2 --- /dev/null +++ b/ch05construction/context.py @@ -0,0 +1,54 @@ +from unittest.mock import Mock, MagicMock +class CompMock(Mock): + def __sub__(self, b): + return CompMock() + def __lt__(self,b): + return True + def __abs__(self): + return CompMock() +array=[] +agt=[] +ws=[] +agents=[] +counter=0 +x=MagicMock() +y=None +agent=MagicMock() +value=0 +bird_types=["Starling", "Hawk"] +import numpy as np +average=np.mean +hawk=CompMock() +starling=CompMock() +sEntry="2.0" +entry ="2.0" +iOffset=1 +offset =1 +anothervariable=1 +flag1=True +variable=1 +flag2=False +def do_something(): pass +chromosome=None +start_codon=None +subsequence=MagicMock() +transcribe=MagicMock() +ribe=MagicMock() +find=MagicMock() +can_see=MagicMock() +my_name="" +your_name="" +flag1=False +flag2=False +start=0.0 +end=1.0 +step=0.1 +birds=[MagicMock()]*2 +resolution=100 +pi=3.141 +result= [0]*resolution +import numpy as np +import math +data= [math.sin(y) for y in np.arange(0,pi,pi/resolution)] +import yaml +import os diff --git a/ch05construction/conventions.py b/ch05construction/conventions.py new file mode 100644 index 000000000..e45c57a19 --- /dev/null +++ b/ch05construction/conventions.py @@ -0,0 +1,81 @@ +### "dense" + +import species +def AddToReaction(name, reaction): + reaction.append(species.Species(name)) + +### "sparse" + +from species import Species + +def add_to_reaction(a_name, + a_reaction): + l_species = Species(a_name) + a_reaction.append( l_species ) + +### "layout1" + +reaction= { + "reactants": ["H","H","O"], + "products": ["H2O"] +} + +### "layout2" + +reaction2=( +{ + "reactants": + [ + "H", + "H", + "O" + ], + "products": + [ + "H2O" + ] +} +) + +### "naming1" + +class ClassName(object): + def methodName(variable_name): + instance_variable=variable_name + +### "naming2" + +class class_name(object): + def method_name(a_variable): + m_instance_variable=a_variable + +### "setup" + +# Define some variables to make following fragments work +sInput="2.0" +input ="2.0" +iOffset=1 +offset =1 +anothervariable=1 +flag1=True +variable=1 +flag2=False +def do_something(): pass + +### "naming3" + +fNumber= float(sInput) + iOffset +number = float(input) + offset + +### "syntax1" + +anothervariable+=1 +if ((variable==anothervariable) and flag1 or flag2): do_something() + +### "syntax2" + +anothervariable = anothervariable + 1 +variable_equality = (variable == anothervariable); +if ((variable_equality and flag1) or flag2): + do_something() + diff --git a/ch05construction/index.html b/ch05construction/index.html new file mode 100644 index 000000000..ce6434315 --- /dev/null +++ b/ch05construction/index.html @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Construction and Design + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Coding conventions
  • +
  • Comments
  • +
  • Refactoring
  • +
  • Object Orientation
  • +
  • Design Patterns
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch05construction/species.py b/ch05construction/species.py new file mode 100644 index 000000000..4a12d4cdd --- /dev/null +++ b/ch05construction/species.py @@ -0,0 +1,2 @@ +class Species(object): + pass diff --git a/ch07dry/01intro.html b/ch07dry/01intro.html new file mode 100644 index 000000000..44f15863b --- /dev/null +++ b/ch07dry/01intro.html @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Don't Repeat Yourself + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Advanced Python Programming

+
+
+
+
+
+
+

... or, how to avoid repeating yourself.

+
+
+
+
+
+
+

Avoid Boiler-Plate

+
+
+
+
+
+
+

Code can often be annoyingly full of "boiler-plate" code: characters you don't really want to have to type.

+

Not only is this tedious, it's also time-consuming and dangerous: unnecessary code is an unnecessary potential place for mistakes.

+

There are two important phrases in software design that we've spoken of before in this context:

+
+

Once And Only Once

+

Don't Repeat Yourself (DRY)

+
+

All concepts, ideas, or instructions should be in the program in just one place. +Every line in the program should say something useful and important.

+

We refer to code that respects this principle as DRY code.

+

In this chapter, we'll look at some techniques that can enable us to refactor away repetitive code.

+

Since in many of these places, the techniques will involve working with +functions as if they were variables, we'll learn some functional +programming. We'll also learn more about the innards of how Python implements +classes.

+

We'll also think about how to write programs that generate the more verbose, repetitive program we could otherwise write. +We call this metaprogramming.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/01intro.ipynb b/ch07dry/01intro.ipynb new file mode 100644 index 000000000..93f0d79bc --- /dev/null +++ b/ch07dry/01intro.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a4c68c9c", + "metadata": {}, + "source": [ + "# Advanced Python Programming" + ] + }, + { + "cell_type": "markdown", + "id": "330bc98b", + "metadata": {}, + "source": [ + "... or, how to avoid repeating yourself." + ] + }, + { + "cell_type": "markdown", + "id": "e69b5470", + "metadata": {}, + "source": [ + "## Avoid Boiler-Plate" + ] + }, + { + "cell_type": "markdown", + "id": "b4309527", + "metadata": {}, + "source": [ + "Code can often be annoyingly full of \"boiler-plate\" code: characters you don't really want to have to type.\n", + "\n", + "Not only is this tedious, it's also time-consuming and dangerous: unnecessary code is an unnecessary potential place for mistakes.\n", + "\n", + "There are two important phrases in software design that we've spoken of before in this context:\n", + "\n", + "> Once And Only Once\n", + ">\n", + "> Don't Repeat Yourself (DRY)\n", + "\n", + "All concepts, ideas, or instructions should be in the program in just one place.\n", + "Every line in the program should say something useful and important.\n", + "\n", + "We refer to code that respects this principle as DRY code.\n", + "\n", + "In this chapter, we'll look at some techniques that can enable us to refactor away repetitive code.\n", + "\n", + "Since in many of these places, the techniques will involve working with\n", + "functions as if they were variables, we'll learn some **functional**\n", + "programming. We'll also learn more about the innards of how Python implements\n", + "classes.\n", + "\n", + "We'll also think about how to write programs that *generate* the more verbose, repetitive program we could otherwise write.\n", + "We call this **metaprogramming**.\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Don't Repeat Yourself" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/01intro.ipynb.py b/ch07dry/01intro.ipynb.py new file mode 100644 index 000000000..2e90e19bb --- /dev/null +++ b/ch07dry/01intro.ipynb.py @@ -0,0 +1,48 @@ +# --- +# jupyter: +# jekyll: +# display_name: Don't Repeat Yourself +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Advanced Python Programming + +# %% [markdown] +# ... or, how to avoid repeating yourself. + +# %% [markdown] +# ## Avoid Boiler-Plate + +# %% [markdown] +# Code can often be annoyingly full of "boiler-plate" code: characters you don't really want to have to type. +# +# Not only is this tedious, it's also time-consuming and dangerous: unnecessary code is an unnecessary potential place for mistakes. +# +# There are two important phrases in software design that we've spoken of before in this context: +# +# > Once And Only Once +# > +# > Don't Repeat Yourself (DRY) +# +# All concepts, ideas, or instructions should be in the program in just one place. +# Every line in the program should say something useful and important. +# +# We refer to code that respects this principle as DRY code. +# +# In this chapter, we'll look at some techniques that can enable us to refactor away repetitive code. +# +# Since in many of these places, the techniques will involve working with +# functions as if they were variables, we'll learn some **functional** +# programming. We'll also learn more about the innards of how Python implements +# classes. +# +# We'll also think about how to write programs that *generate* the more verbose, repetitive program we could otherwise write. +# We call this **metaprogramming**. +# diff --git a/ch07dry/020Functional.html b/ch07dry/020Functional.html new file mode 100644 index 000000000..3e925b951 --- /dev/null +++ b/ch07dry/020Functional.html @@ -0,0 +1,1545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Functional Programming + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Functional programming

+
+
+
+
+
+
+

We have previously seen the object-oriented style of programming, and how to organise our code according to it using objects, classes and inheritance. While widely-adopted and very useful, this is not the only way of writing code. The functional paradigm, as the name suggests, emphasises functions as building blocks of programs.

+

Understanding to think in a functional programming style is almost as +important as object orientation for building DRY, clear scientific software, +and is just as conceptually difficult. +However, being aware of different paradigms and styles gives you access to more techniques that you can use to write, structure and reason about your code.

+
+
+
+
+
+
+

Functions within functions

+
+
+
+
+
+
+

Programs are composed of functions: they take data in (which we call +parameters or arguments) and send data out (through return statements).

+

A conceptual trick which is often used by computer scientists to teach the core +idea of functional programming is this: to write a program, +in theory, you only ever need functions with one argument, even when you think you need two or more. Why?

+

Let's define a program to add two numbers:

+
+
+
+
+
+
In [1]:
+
+
+
def add(a, b):
+    return a + b
+
+add(5, 6)
+
+
+
+
+
+
+
+
Out[1]:
+
+
11
+
+
+
+
+
+
+
+
+

How could we do this, in a fictional version of Python which only defined functions of one argument? +In order to understand this, we'll have to understand several of the concepts +of functional programming. Let's start with a program which just adds five to +something:

+
+
+
+
+
+
In [2]:
+
+
+
def add_five(a):
+    return a + 5
+
+add_five(6)
+
+
+
+
+
+
+
+
Out[2]:
+
+
11
+
+
+
+
+
+
+
+
+

OK, we could define lots of these, one for each number we want to add. But that +would be infinitely repetitive. So, let's try to metaprogram that: we want a +function which returns these add_N() functions.

+

Let's start with the easy case: a function which returns a function which adds 5 to something:

+
+
+
+
+
+
In [3]:
+
+
+
def generate_five_adder():
+    def _five_adder(a):
+        return a + 5
+    return _five_adder
+
+coolfunction = generate_five_adder()
+coolfunction(7)
+
+
+
+
+
+
+
+
Out[3]:
+
+
12
+
+
+
+
+
+
+
+
+

OK, so what happened there? Well, we defined a function inside the other function. We can always do that:

+
+
+
+
+
+
In [4]:
+
+
+
def thirty_function():
+    def times_three(a):
+        return a * 3
+    def add_seven(a):
+        return a + 7
+    return times_three(add_seven(3))
+
+thirty_function()
+
+
+
+
+
+
+
+
Out[4]:
+
+
30
+
+
+
+
+
+
+
+
+

When we do this, the functions enclosed inside the outer function are local functions, and can't be seen outside:

+
+
+
+
+
+
In [5]:
+
+
+
add_seven
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+NameError                                 Traceback (most recent call last)
+Cell In[5], line 1
+----> 1 add_seven
+
+NameError: name 'add_seven' is not defined
+
+
+
+
+
+
+
+
+

There's not really much of a difference between functions and other variables +in python. A function is just a variable which can have () put after it to call +the code!

+
+
+
+
+
+
In [6]:
+
+
+
print(thirty_function)
+
+
+
+
+
+
+
+
+
+
<function thirty_function at 0x7fa1f490db80>
+
+
+
+
+
+
+
+
+
In [7]:
+
+
+
x = [thirty_function, add_five, add]
+
+
+
+
+
+
+
+
In [8]:
+
+
+
for fun in x:
+    print(fun)
+
+
+
+
+
+
+
+
+
+
<function thirty_function at 0x7fa1f490db80>
+<function add_five at 0x7fa1f490dd30>
+<function add at 0x7fa1f490d0d0>
+
+
+
+
+
+
+
+
+
+

And we know that one of the things we can do with a variable is return it. So we can return a function, and then call it outside:

+
+
+
+
+
+
In [9]:
+
+
+
def deferred_greeting():
+    def greet():
+        print("Hello")
+    return greet
+
+friendlyfunction = deferred_greeting()
+
+
+
+
+
+
+
+
In [10]:
+
+
+
# Do something else
+print("Just passing the time...")
+
+
+
+
+
+
+
+
+
+
Just passing the time...
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
# OK, Go!
+friendlyfunction()
+
+
+
+
+
+
+
+
+
+
Hello
+
+
+
+
+
+
+
+
+
+

So now, to finish this, we just need to return a function to add an arbitrary amount:

+
+
+
+
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
def generate_adder(increment):
+    def _adder(a):
+        return a + increment
+    return _adder
+
+add_3 = generate_adder(3)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
add_3(9)
+
+
+
+
+
+
+
+
Out[13]:
+
+
12
+
+
+
+
+
+
+
+
+

We can make this even prettier: let's make another variable pointing to our define_adder() function:

+
+
+
+
+
+
In [14]:
+
+
+
add = generate_adder
+
+
+
+
+
+
+
+
+

And now we can do the real magic:

+
+
+
+
+
+
In [15]:
+
+
+
add(8)(5)
+
+
+
+
+
+
+
+
Out[15]:
+
+
13
+
+
+
+
+
+
+
+
+

In summary, we have started with a function that takes two arguments (add(a, b)) and replaced it with a new function (add(a)(b)). This new function takes a single argument, and returns a function that itself takes the second argument.

+

This may seem like an overly complicated process - and, in some cases, it is! However, this pattern of functions that return functions (or even take them as arguments!) can be very useful. In fact, it is the basis of decorators, a Python feature that we will discuss more in this chapter [notebook].

+
+
+
+
+
+
+

Closures

+
+
+
+
+
+
+

You may have noticed something a bit weird:

+

In the definition of generate_adder, increment is a local variable. It should have gone out of scope and died at the end of the definition. How can the amount the returned adder function is adding still be kept?

+

This is called a closure. In Python, whenever a function definition references a variable in the surrounding scope, it is preserved within the function definition.

+

You can close over global module variables as well:

+
+
+
+
+
+
In [16]:
+
+
+
name = "Eric"
+
+def greet():
+    print("Hello, ", name)
+
+greet()
+
+
+
+
+
+
+
+
+
+
Hello,  Eric
+
+
+
+
+
+
+
+
+
+

And note that the closure stores a reference to the variable in the surrounding scope: ("Late Binding")

+
+
+
+
+
+
In [17]:
+
+
+
name = "John"
+
+greet()
+
+
+
+
+
+
+
+
+
+
Hello,  John
+
+
+
+
+
+
+
+
+
+

Map and Reduce

+
+
+
+
+
+
+

We often want to apply a function to each variable in an array, to return a new array. We can do this with a list comprehension:

+
+
+
+
+
+
In [18]:
+
+
+
numbers = range(10)
+
+[add_five(i) for i in numbers]
+
+
+
+
+
+
+
+
Out[18]:
+
+
[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
+
+
+
+
+
+
+
+
+

But this is sufficiently common that there's a quick built-in:

+
+
+
+
+
+
In [19]:
+
+
+
list(map(add_five, numbers))
+
+
+
+
+
+
+
+
Out[19]:
+
+
[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
+
+
+
+
+
+
+
+
+

This map operation is really important conceptually when understanding +efficient parallel programming: different computers can apply the mapped +function to their input at the same time. We call this Single Program, Multiple +Data (SPMD). map is half of the map-reduce functional programming +paradigm which is key to the efficient operation of much of today's "data +science" explosion.

+

Let's continue our functional programming mind-stretch by looking at reduce operations.

+

We very often want to loop with some kind of accumulator (an intermediate result that we update), such as when finding a mean:

+
+
+
+
+
+
In [20]:
+
+
+
def summer(data):
+    total = 0.0
+
+    for x in data:
+        total += x
+
+    return total
+
+
+
+
+
+
+
+
In [21]:
+
+
+
summer(range(10))
+
+
+
+
+
+
+
+
Out[21]:
+
+
45.0
+
+
+
+
+
+
+
+
+

or finding a maximum:

+
+
+
+
+
+
In [22]:
+
+
+
import sys
+
+def my_max(data):
+    # Start with the smallest possible number
+    highest = sys.float_info.min
+
+    for x in data:
+        if x > highest:
+            highest = x
+
+    return highest
+
+
+
+
+
+
+
+
In [23]:
+
+
+
my_max([2, 5, 10, -11, -5])
+
+
+
+
+
+
+
+
Out[23]:
+
+
10
+
+
+
+
+
+
+
+
+

These operations, where we have some variable which is building up a result, +and the result is updated with some operation, can be gathered together as a +functional program, taking in (as an argument) the operation to be used to combine results:

+
+
+
+
+
+
In [24]:
+
+
+
def accumulate(initial, operation, data):
+    accumulator = initial
+    for x in data:
+        accumulator = operation(accumulator, x)
+    return accumulator
+
+def my_sum(data):
+    def _add(a, b):
+        return a + b
+    return accumulate(0, _add, data)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
my_sum(range(5))
+
+
+
+
+
+
+
+
Out[25]:
+
+
10
+
+
+
+
+
+
+
+
In [26]:
+
+
+
def bigger(a, b):
+    if b > a:
+        return b
+    return a
+
+def my_max(data):
+    return accumulate(sys.float_info.min, bigger, data)
+
+my_max([2, 5, 10, -11, -5])
+
+
+
+
+
+
+
+
Out[26]:
+
+
10
+
+
+
+
+
+
+
+
+

Anyway, this accumulate-under-an-operation process is so fundamental to +computing that it's usually in standard libraries for languages which allow +functional programming:

+
+
+
+
+
+
In [27]:
+
+
+
from functools import reduce
+
+def my_max(data):
+    return reduce(bigger, data, sys.float_info.min)
+
+my_max([2, 5, 10, -11, -5])
+
+
+
+
+
+
+
+
Out[27]:
+
+
10
+
+
+
+
+
+
+
+
+

Efficient map-reduce

Now, because these operations, bigger and _add, are such that e.g. (a+b)+c = a+(b+c) , i.e. they are associative, we could apply our accumulation +to the left half and the right half of the array, each on a different computer, and then combine the two halves:

+

1 + 2 + 3 + 4 = (1 + 2) + (3 + 4)

+

Indeed, with a bigger array, we can divide-and-conquer more times:

+

1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = ((1 + 2) + (3 + 4)) + ((5 + 6) + (7 + 8))

+

So with enough parallel computers, we could do this operation on eight numbers +in three steps: first, we use four computers to do one each of the pairwise +adds.

+

Then, we use two computers to add the four totals.

+

Then, we use one of the computers to do the final add of the two last numbers.

+

You might be able to do the maths to see that with an N element list, the +number of such steps is proportional to the logarithm of N.

+

We say that with enough computers, reduction operations are O(ln N)

+

This course isn't an introduction to algorithms, but we'll talk more about this +O() notation when we think about programming for performance.

+
+
+
+
+
+
+

Lambda Functions

+
+
+
+
+
+
+

When doing functional programming, we often want to be able to define a function on the fly:

+
+
+
+
+
+
In [28]:
+
+
+
def most_Cs_in_any_sequence(sequences):
+
+    def count_Cs(sequence):
+        return sequence.count('C')
+
+    counts = map(count_Cs, sequences)
+    return max(counts)
+
+
+def most_Gs_in_any_sequence(sequences):
+    return max(map(lambda sequence: sequence.count('G'), sequences))
+
+
+data = [
+    "CGTA",
+    "CGGGTAAACG",
+    "GATTACA"
+]
+
+most_Gs_in_any_sequence(data)
+
+
+
+
+
+
+
+
Out[28]:
+
+
4
+
+
+
+
+
+
+
+
+

The syntax here means that these two definitions are identical:

+
+
+
+
+
+
In [29]:
+
+
+
func_name = lambda a, b, c: a + b + c
+
+def func_name(a, b, c):
+    return a + b + c
+
+
+
+
+
+
+
+
+

The lambda keyword defines an "anonymous" function.

+
+
+
+
+
+
In [30]:
+
+
+
def most_of_given_base_in_any_sequence(sequences, base):
+    return max(map(lambda sequence: sequence.count(base), sequences))
+
+most_of_given_base_in_any_sequence(data, 'A')
+
+
+
+
+
+
+
+
Out[30]:
+
+
3
+
+
+
+
+
+
+
+
+

The above fragment defined a lambda function as a closure over base. If you understood that, you've got it!

+
+
+
+
+
+
+

To double all elements in an array:

+
+
+
+
+
+
In [31]:
+
+
+
data = range(10)
+list(map(lambda x: 2*x, data))
+
+
+
+
+
+
+
+
Out[31]:
+
+
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
+
+
+
+
+
+
+
+
In [32]:
+
+
+
[2*x for x in data]
+
+
+
+
+
+
+
+
Out[32]:
+
+
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
+
+
+
+
+
+
+
+
+

Similarly, to find the maximum value in a sequence:

+
+
+
+
+
+
In [33]:
+
+
+
def my_max(data): 
+    return reduce(lambda a, b: a if a > b else b, data, sys.float_info.min)
+
+my_max([2, 5, 10, -11, -5])
+
+
+
+
+
+
+
+
Out[33]:
+
+
10
+
+
+
+
+
+
+
+
+

Using functional programming for numerical methods

+
+
+
+
+
+
+

Probably the most common use in research computing for functional programming +is the application of a numerical method to a function.

+

Consider this example which uses the newton function from SciPy, a root-finding function implementing the Newton-Raphson method. The arguments we pass to newton are the function whose roots we want to find, and a starting point to search from.

+

We will be using this to find the roots of the function $f(x) = x^2 - x$.

+
+
+
+
+
+
In [34]:
+
+
+
%matplotlib inline
+
+
+
+
+
+
+
+
In [35]:
+
+
+
from scipy.optimize import newton
+from numpy import linspace, zeros
+from matplotlib import pyplot as plt
+
+solve_me = lambda x: x**2 - x
+
+for x0 in [2, 0.2]:
+    answer = newton(solve_me, x0)
+    print("Starting from {}, the root I found is {}".format(x0, answer))
+
+xs = linspace(-1, 2, 50)
+solved = [xs, list(map(solve_me, xs)), xs, zeros(len(xs))]
+
+plt.plot(*solved)
+
+
+
+
+
+
+
+
+
+
Starting from 2, the root I found is 1.0
+Starting from 0.2, the root I found is -3.441905100203782e-21
+
+
+
+
+
Out[35]:
+
+
[<matplotlib.lines.Line2D at 0x7fa1b15238e0>,
+ <matplotlib.lines.Line2D at 0x7fa1b1523940>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Sometimes such tools return another function, for example the derivative of their input function. This is what a naive implementation of that could look like:

+
+
+
+
+
+
In [36]:
+
+
+
def derivative_simple(func, eps, at):
+    return (func(at + eps) - func(at)) / eps
+
+
+
+
+
+
+
+
In [37]:
+
+
+
def derivative(func, eps):
+
+    def _func_derived(x):
+        return (func(x + eps) - func(x)) / eps
+
+    return _func_derived
+
+straight = derivative(solve_me, 0.01)
+
+
+
+
+
+
+
+
+

The derivative of solve_me is $f'(x) = 2x - 1$, which represents a straight line. +We can verify that our computations are correct, i.e. that the returned function straight matches $f'(x)$, by checking the value of straight at some $x$:

+
+
+
+
+
+
In [38]:
+
+
+
straight(3)
+
+
+
+
+
+
+
+
Out[38]:
+
+
5.00999999999987
+
+
+
+
+
+
+
+
+

or by plotting it:

+
+
+
+
+
+
In [39]:
+
+
+
derived = (
+    xs, list(map(solve_me, xs)),
+    xs, list(map(derivative(solve_me, 0.01), xs))
+)
+plt.plot(*derived)
+print(newton(derivative(solve_me, 0.01), 0))
+
+
+
+
+
+
+
+
+
+
0.495000000000001
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Of course, coding your own numerical methods is bad, because the implementations you develop are likely to be less efficient, less accurate and more error-prone than what you can find in existing established libraries.

+

For example, the above definition could be replaced by:

+
+
+
+
+
+
In [40]:
+
+
+
import scipy.misc
+
+def derivative(func):
+    def _func_derived(x):
+        return scipy.misc.derivative(func, x)
+    return _func_derived
+
+newton(derivative(solve_me), 0)
+
+
+
+
+
+
+
+
+
+
/tmp/ipykernel_13308/592417789.py:5: DeprecationWarning: scipy.misc.derivative is deprecated in SciPy v1.10.0; and will be completely removed in SciPy v1.12.0. You may consider using findiff: https://github.com/maroba/findiff or numdifftools: https://github.com/pbrod/numdifftools
+  return scipy.misc.derivative(func, x)
+
+
+
+
+
Out[40]:
+
+
0.5
+
+
+
+
+
+
+
+
+

If you've done a moderate amount of calculus, then you'll find similarities +between functional programming in computer science and Functionals in the +calculus of variations.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/020Functional.ipynb b/ch07dry/020Functional.ipynb new file mode 100644 index 000000000..1b3a7bee4 --- /dev/null +++ b/ch07dry/020Functional.ipynb @@ -0,0 +1,1035 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4e265ed7", + "metadata": {}, + "source": [ + "## Functional programming" + ] + }, + { + "cell_type": "markdown", + "id": "9adc4784", + "metadata": {}, + "source": [ + "We have previously seen the object-oriented style of programming, and how to organise our code according to it using objects, classes and inheritance. While widely-adopted and very useful, this is not the only way of writing code. The [*functional paradigm*](https://en.wikipedia.org/wiki/Functional_programming), as the name suggests, emphasises functions as building blocks of programs.\n", + "\n", + "Understanding to think in a functional programming style is almost as\n", + "important as object orientation for building DRY, clear scientific software,\n", + "and is just as conceptually difficult.\n", + "However, being aware of different paradigms and styles gives you access to more techniques that you can use to write, structure and reason about your code." + ] + }, + { + "cell_type": "markdown", + "id": "cd307149", + "metadata": {}, + "source": [ + "### Functions within functions" + ] + }, + { + "cell_type": "markdown", + "id": "64753ea6", + "metadata": {}, + "source": [ + "Programs are composed of functions: they take data in (which we call\n", + "*parameters* or *arguments*) and send data out (through `return` statements).\n", + "\n", + "A conceptual trick which is often used by computer scientists to teach the core\n", + "idea of functional programming is this: to write a program,\n", + "in theory, you only ever need functions with **one** argument, even when you think you need two or more. Why?\n", + "\n", + "Let's define a program to add two numbers:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee954d75", + "metadata": {}, + "outputs": [], + "source": [ + "def add(a, b):\n", + " return a + b\n", + "\n", + "add(5, 6)" + ] + }, + { + "cell_type": "markdown", + "id": "aefd7c79", + "metadata": {}, + "source": [ + "\n", + "\n", + "How could we do this, in a fictional version of Python which only defined functions of one argument?\n", + "In order to understand this, we'll have to understand several of the concepts\n", + "of functional programming. Let's start with a program which just adds five to\n", + "something:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfc595cd", + "metadata": {}, + "outputs": [], + "source": [ + "def add_five(a):\n", + " return a + 5\n", + "\n", + "add_five(6)" + ] + }, + { + "cell_type": "markdown", + "id": "f220c7c6", + "metadata": {}, + "source": [ + "\n", + "\n", + "OK, we could define lots of these, one for each number we want to add. But that\n", + "would be infinitely repetitive. So, let's try to metaprogram that: we want a\n", + "function which returns these add_N() functions.\n", + "\n", + "Let's start with the easy case: a function which returns a function which adds 5 to something:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13a63288", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_five_adder():\n", + " def _five_adder(a):\n", + " return a + 5\n", + " return _five_adder\n", + "\n", + "coolfunction = generate_five_adder()\n", + "coolfunction(7)" + ] + }, + { + "cell_type": "markdown", + "id": "0e65b0ae", + "metadata": {}, + "source": [ + "\n", + "\n", + "OK, so what happened there? Well, we defined a function **inside** the other function. We can always do that:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6fd3309", + "metadata": {}, + "outputs": [], + "source": [ + "def thirty_function():\n", + " def times_three(a):\n", + " return a * 3\n", + " def add_seven(a):\n", + " return a + 7\n", + " return times_three(add_seven(3))\n", + "\n", + "thirty_function()" + ] + }, + { + "cell_type": "markdown", + "id": "c8f92da0", + "metadata": {}, + "source": [ + "\n", + "\n", + "When we do this, the functions enclosed inside the outer function are **local** functions, and can't be seen outside:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36b35bc3", + "metadata": {}, + "outputs": [], + "source": [ + "add_seven" + ] + }, + { + "cell_type": "markdown", + "id": "f4b37998", + "metadata": {}, + "source": [ + "\n", + "\n", + "There's not really much of a difference between functions and other variables\n", + "in python. A function is just a variable which can have () put after it to call\n", + "the code!\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e788c304", + "metadata": {}, + "outputs": [], + "source": [ + "print(thirty_function)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46774854", + "metadata": {}, + "outputs": [], + "source": [ + "x = [thirty_function, add_five, add]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a987736", + "metadata": {}, + "outputs": [], + "source": [ + "for fun in x:\n", + " print(fun)" + ] + }, + { + "cell_type": "markdown", + "id": "432dd3e3", + "metadata": {}, + "source": [ + "\n", + "\n", + "And we know that one of the things we can do with a variable is `return` it. So we can return a function, and then call it outside:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddd0fdf2", + "metadata": {}, + "outputs": [], + "source": [ + "def deferred_greeting():\n", + " def greet():\n", + " print(\"Hello\")\n", + " return greet\n", + "\n", + "friendlyfunction = deferred_greeting()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a0e36a7", + "metadata": {}, + "outputs": [], + "source": [ + "# Do something else\n", + "print(\"Just passing the time...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9765edd", + "metadata": {}, + "outputs": [], + "source": [ + "# OK, Go!\n", + "friendlyfunction()" + ] + }, + { + "cell_type": "markdown", + "id": "70f49a5b", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "So now, to finish this, we just need to return a function to add an arbitrary amount:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5de2b6ca", + "metadata": {}, + "source": [ + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f59e2c3", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_adder(increment):\n", + " def _adder(a):\n", + " return a + increment\n", + " return _adder\n", + "\n", + "add_3 = generate_adder(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fb4ff57", + "metadata": {}, + "outputs": [], + "source": [ + "add_3(9)" + ] + }, + { + "cell_type": "markdown", + "id": "0ce8ed92", + "metadata": {}, + "source": [ + "\n", + "\n", + "We can make this even prettier: let's make another variable pointing to our define_adder() function:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0e08051", + "metadata": {}, + "outputs": [], + "source": [ + "add = generate_adder" + ] + }, + { + "cell_type": "markdown", + "id": "6aafc0c3", + "metadata": {}, + "source": [ + "\n", + "\n", + "And now we can do the real magic:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e407d66f", + "metadata": {}, + "outputs": [], + "source": [ + "add(8)(5)" + ] + }, + { + "cell_type": "markdown", + "id": "7d06980e", + "metadata": {}, + "source": [ + "In summary, we have started with a function that takes two arguments (`add(a, b)`) and replaced it with a new function (`add(a)(b)`). This new function takes a single argument, and returns a function that itself takes the second argument.\n", + "\n", + "This may seem like an overly complicated process - and, in some cases, it is! However, this pattern of functions that return functions (or even take them as arguments!) can be very useful. In fact, it is the basis of decorators, a Python feature that we will discuss more [in this chapter](./025Iterators.html#Decorators) [[notebook](./025Iterators.ipynb#Decorators)]." + ] + }, + { + "cell_type": "markdown", + "id": "86dc7a6c", + "metadata": {}, + "source": [ + "### Closures" + ] + }, + { + "cell_type": "markdown", + "id": "bc0a2c85", + "metadata": {}, + "source": [ + "You may have noticed something a bit weird:\n", + "\n", + "In the definition of [`generate_adder`](#generate_adder), `increment` is a local variable. It should have gone out of scope and died at the end of the definition. How can the amount the returned adder function is adding still be kept?\n", + "\n", + "This is called a **closure**. In Python, whenever a function definition references a variable in the surrounding scope, it is preserved within the function definition.\n", + "\n", + "You can close over global module variables as well:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "477c2cef", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"Eric\"\n", + "\n", + "def greet():\n", + " print(\"Hello, \", name)\n", + "\n", + "greet()" + ] + }, + { + "cell_type": "markdown", + "id": "1d9fd64e", + "metadata": {}, + "source": [ + "\n", + "\n", + "And note that the closure stores a reference to the variable in the surrounding scope: (\"Late Binding\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0711e8e", + "metadata": {}, + "outputs": [], + "source": [ + "name = \"John\"\n", + "\n", + "greet()" + ] + }, + { + "cell_type": "markdown", + "id": "57e47d02", + "metadata": {}, + "source": [ + "### Map and Reduce" + ] + }, + { + "cell_type": "markdown", + "id": "b096f33e", + "metadata": {}, + "source": [ + "We often want to apply a function to each variable in an array, to return a new array. We can do this with a list comprehension:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "423e4ac7", + "metadata": {}, + "outputs": [], + "source": [ + "numbers = range(10)\n", + "\n", + "[add_five(i) for i in numbers]" + ] + }, + { + "cell_type": "markdown", + "id": "88151741", + "metadata": {}, + "source": [ + "\n", + "\n", + "But this is sufficiently common that there's a quick built-in:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3718c95b", + "metadata": {}, + "outputs": [], + "source": [ + "list(map(add_five, numbers))" + ] + }, + { + "cell_type": "markdown", + "id": "decedf25", + "metadata": {}, + "source": [ + "\n", + "\n", + "This **map** operation is really important conceptually when understanding\n", + "efficient parallel programming: different computers can apply the *mapped*\n", + "function to their input at the same time. We call this Single Program, Multiple\n", + "Data (SPMD). **map** is half of the [**map-reduce**](https://en.wikipedia.org/wiki/MapReduce) functional programming\n", + "paradigm which is key to the efficient operation of much of today's \"data\n", + "science\" explosion. \n", + "\n", + "Let's continue our functional programming mind-stretch by looking at **reduce** operations.\n", + "\n", + "We very often want to loop with some kind of accumulator (an intermediate result that we update), such as when finding a mean:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28f25a26", + "metadata": {}, + "outputs": [], + "source": [ + "def summer(data):\n", + " total = 0.0\n", + "\n", + " for x in data:\n", + " total += x\n", + "\n", + " return total" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42947c06", + "metadata": {}, + "outputs": [], + "source": [ + "summer(range(10))" + ] + }, + { + "cell_type": "markdown", + "id": "7edc57e7", + "metadata": {}, + "source": [ + " or finding a maximum:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "034062b2", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "def my_max(data):\n", + " # Start with the smallest possible number\n", + " highest = sys.float_info.min\n", + "\n", + " for x in data:\n", + " if x > highest:\n", + " highest = x\n", + "\n", + " return highest" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8656ceb", + "metadata": {}, + "outputs": [], + "source": [ + "my_max([2, 5, 10, -11, -5])" + ] + }, + { + "cell_type": "markdown", + "id": "14af9024", + "metadata": {}, + "source": [ + "These operations, where we have some variable which is building up a result,\n", + "and the result is updated with some operation, can be gathered together as a\n", + "functional program, taking in (as an argument) the operation to be used to combine results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e02acfc", + "metadata": {}, + "outputs": [], + "source": [ + "def accumulate(initial, operation, data):\n", + " accumulator = initial\n", + " for x in data:\n", + " accumulator = operation(accumulator, x)\n", + " return accumulator\n", + "\n", + "def my_sum(data):\n", + " def _add(a, b):\n", + " return a + b\n", + " return accumulate(0, _add, data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "773668ea", + "metadata": {}, + "outputs": [], + "source": [ + "my_sum(range(5))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5f01a06", + "metadata": {}, + "outputs": [], + "source": [ + "def bigger(a, b):\n", + " if b > a:\n", + " return b\n", + " return a\n", + "\n", + "def my_max(data):\n", + " return accumulate(sys.float_info.min, bigger, data)\n", + "\n", + "my_max([2, 5, 10, -11, -5])" + ] + }, + { + "cell_type": "markdown", + "id": "6f617a81", + "metadata": {}, + "source": [ + "Anyway, this accumulate-under-an-operation process is so fundamental to\n", + "computing that it's usually in standard libraries for languages which allow\n", + "functional programming:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18b6cbe3", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import reduce\n", + "\n", + "def my_max(data):\n", + " return reduce(bigger, data, sys.float_info.min)\n", + "\n", + "my_max([2, 5, 10, -11, -5])" + ] + }, + { + "cell_type": "markdown", + "id": "d95af4dd", + "metadata": {}, + "source": [ + "#### Efficient map-reduce\n", + "\n", + "Now, because these operations, `bigger` and `_add`, are such that e.g. (a+b)+c = a+(b+c) , i.e. they are **associative**, we could apply our accumulation\n", + "to the left half and the right half of the array, each on a different computer, and then combine the two halves:\n", + "\n", + "1 + 2 + 3 + 4 = (1 + 2) + (3 + 4)\n", + "\n", + "Indeed, with a bigger array, we can divide-and-conquer more times:\n", + "\n", + "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = ((1 + 2) + (3 + 4)) + ((5 + 6) + (7 + 8))\n", + "\n", + "So with enough parallel computers, we could do this operation on eight numbers\n", + "in three steps: first, we use four computers to do one each of the pairwise\n", + "adds.\n", + "\n", + "Then, we use two computers to add the four totals.\n", + "\n", + "Then, we use one of the computers to do the final add of the two last numbers.\n", + "\n", + "You might be able to do the maths to see that with an N element list, the\n", + "number of such steps is proportional to the logarithm of N.\n", + "\n", + "We say that with enough computers, reduction operations are O(ln N)\n", + "\n", + "This course isn't an introduction to algorithms, but we'll talk more about this\n", + "O() notation when we think about programming for performance.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "b39b20fc", + "metadata": {}, + "source": [ + "### Lambda Functions" + ] + }, + { + "cell_type": "markdown", + "id": "f9d3847d", + "metadata": {}, + "source": [ + "\n", + "\n", + "When doing functional programming, we often want to be able to define a function on the fly:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d6bd575", + "metadata": {}, + "outputs": [], + "source": [ + "def most_Cs_in_any_sequence(sequences):\n", + "\n", + " def count_Cs(sequence):\n", + " return sequence.count('C')\n", + "\n", + " counts = map(count_Cs, sequences)\n", + " return max(counts)\n", + "\n", + "\n", + "def most_Gs_in_any_sequence(sequences):\n", + " return max(map(lambda sequence: sequence.count('G'), sequences))\n", + "\n", + "\n", + "data = [\n", + " \"CGTA\",\n", + " \"CGGGTAAACG\",\n", + " \"GATTACA\"\n", + "]\n", + "\n", + "most_Gs_in_any_sequence(data)" + ] + }, + { + "cell_type": "markdown", + "id": "ed2900c5", + "metadata": {}, + "source": [ + "\n", + "\n", + "The syntax here means that these two definitions are identical:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "241a6852", + "metadata": {}, + "outputs": [], + "source": [ + "func_name = lambda a, b, c: a + b + c\n", + "\n", + "def func_name(a, b, c):\n", + " return a + b + c" + ] + }, + { + "cell_type": "markdown", + "id": "7e861ab8", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "The **lambda** keyword defines an \"anonymous\" function.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f831f316", + "metadata": {}, + "outputs": [], + "source": [ + "def most_of_given_base_in_any_sequence(sequences, base):\n", + " return max(map(lambda sequence: sequence.count(base), sequences))\n", + "\n", + "most_of_given_base_in_any_sequence(data, 'A')" + ] + }, + { + "cell_type": "markdown", + "id": "e7263fce", + "metadata": {}, + "source": [ + "\n", + "\n", + "The above fragment defined a lambda function as a **closure** over `base`. If you understood that, you've got it! \n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "09a119ed", + "metadata": {}, + "source": [ + "To double all elements in an array:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33403c2d", + "metadata": {}, + "outputs": [], + "source": [ + "data = range(10)\n", + "list(map(lambda x: 2*x, data))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf654796", + "metadata": {}, + "outputs": [], + "source": [ + "[2*x for x in data]" + ] + }, + { + "cell_type": "markdown", + "id": "8c809252", + "metadata": {}, + "source": [ + "Similarly, to find the maximum value in a sequence:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7ee3ea6", + "metadata": {}, + "outputs": [], + "source": [ + "def my_max(data): \n", + " return reduce(lambda a, b: a if a > b else b, data, sys.float_info.min)\n", + "\n", + "my_max([2, 5, 10, -11, -5])" + ] + }, + { + "cell_type": "markdown", + "id": "175cfda7", + "metadata": {}, + "source": [ + "### Using functional programming for numerical methods" + ] + }, + { + "cell_type": "markdown", + "id": "08ad6a81", + "metadata": {}, + "source": [ + "\n", + "Probably the most common use in research computing for functional programming\n", + "is the application of a numerical method to a function.\n", + "\n", + "Consider this example which uses the [`newton` function from SciPy](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html), a root-finding function implementing the [Newton-Raphson method](http://mathworld.wolfram.com/NewtonsMethod.html). The arguments we pass to `newton` are the function whose roots we want to find, and a starting point to search from.\n", + "\n", + "We will be using this to find the roots of the function $f(x) = x^2 - x$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a39e059", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "610a0a1b", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.optimize import newton\n", + "from numpy import linspace, zeros\n", + "from matplotlib import pyplot as plt\n", + "\n", + "solve_me = lambda x: x**2 - x\n", + "\n", + "for x0 in [2, 0.2]:\n", + " answer = newton(solve_me, x0)\n", + " print(\"Starting from {}, the root I found is {}\".format(x0, answer))\n", + "\n", + "xs = linspace(-1, 2, 50)\n", + "solved = [xs, list(map(solve_me, xs)), xs, zeros(len(xs))]\n", + "\n", + "plt.plot(*solved)" + ] + }, + { + "cell_type": "markdown", + "id": "d13270a7", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Sometimes such tools return another function, for example the derivative of their input function. This is what a naive implementation of that could look like:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1eac0aa", + "metadata": {}, + "outputs": [], + "source": [ + "def derivative_simple(func, eps, at):\n", + " return (func(at + eps) - func(at)) / eps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1e94f4a", + "metadata": {}, + "outputs": [], + "source": [ + "def derivative(func, eps):\n", + "\n", + " def _func_derived(x):\n", + " return (func(x + eps) - func(x)) / eps\n", + "\n", + " return _func_derived\n", + "\n", + "straight = derivative(solve_me, 0.01)" + ] + }, + { + "cell_type": "markdown", + "id": "94161f91", + "metadata": {}, + "source": [ + "The derivative of `solve_me` is $f'(x) = 2x - 1$, which represents a straight line.\n", + "We can verify that our computations are correct, i.e. that the returned function `straight` matches $f'(x)$, by checking the value of `straight` at some $x$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a116e55", + "metadata": {}, + "outputs": [], + "source": [ + "straight(3)" + ] + }, + { + "cell_type": "markdown", + "id": "dd8ddbd9", + "metadata": {}, + "source": [ + "or by plotting it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2af453c5", + "metadata": {}, + "outputs": [], + "source": [ + "derived = (\n", + " xs, list(map(solve_me, xs)),\n", + " xs, list(map(derivative(solve_me, 0.01), xs))\n", + ")\n", + "plt.plot(*derived)\n", + "print(newton(derivative(solve_me, 0.01), 0))" + ] + }, + { + "cell_type": "markdown", + "id": "bcd63c25", + "metadata": {}, + "source": [ + "Of course, coding your own numerical methods is bad, because the implementations you develop are likely to be less efficient, less accurate and more error-prone than what you can find in existing established libraries.\n", + "\n", + "For example, the above definition could be replaced by:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f774506", + "metadata": {}, + "outputs": [], + "source": [ + "import scipy.misc\n", + "\n", + "def derivative(func):\n", + " def _func_derived(x):\n", + " return scipy.misc.derivative(func, x)\n", + " return _func_derived\n", + "\n", + "newton(derivative(solve_me), 0)" + ] + }, + { + "cell_type": "markdown", + "id": "061ba4c8", + "metadata": {}, + "source": [ + "\n", + "\n", + "If you've done a moderate amount of calculus, then you'll find similarities\n", + "between functional programming in computer science and Functionals in the\n", + "calculus of variations.\n", + "\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Functional Programming" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/020Functional.ipynb.py b/ch07dry/020Functional.ipynb.py new file mode 100644 index 000000000..072592e6c --- /dev/null +++ b/ch07dry/020Functional.ipynb.py @@ -0,0 +1,590 @@ +# --- +# jupyter: +# jekyll: +# display_name: Functional Programming +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Functional programming + +# %% [markdown] +# We have previously seen the object-oriented style of programming, and how to organise our code according to it using objects, classes and inheritance. While widely-adopted and very useful, this is not the only way of writing code. The [*functional paradigm*](https://en.wikipedia.org/wiki/Functional_programming), as the name suggests, emphasises functions as building blocks of programs. +# +# Understanding to think in a functional programming style is almost as +# important as object orientation for building DRY, clear scientific software, +# and is just as conceptually difficult. +# However, being aware of different paradigms and styles gives you access to more techniques that you can use to write, structure and reason about your code. + +# %% [markdown] +# ### Functions within functions + +# %% [markdown] +# Programs are composed of functions: they take data in (which we call +# *parameters* or *arguments*) and send data out (through `return` statements). +# +# A conceptual trick which is often used by computer scientists to teach the core +# idea of functional programming is this: to write a program, +# in theory, you only ever need functions with **one** argument, even when you think you need two or more. Why? +# +# Let's define a program to add two numbers: +# +# +# + +# %% +def add(a, b): + return a + b + +add(5, 6) + + +# %% [markdown] +# +# +# How could we do this, in a fictional version of Python which only defined functions of one argument? +# In order to understand this, we'll have to understand several of the concepts +# of functional programming. Let's start with a program which just adds five to +# something: +# +# +# + +# %% +def add_five(a): + return a + 5 + +add_five(6) + + +# %% [markdown] +# +# +# OK, we could define lots of these, one for each number we want to add. But that +# would be infinitely repetitive. So, let's try to metaprogram that: we want a +# function which returns these add_N() functions. +# +# Let's start with the easy case: a function which returns a function which adds 5 to something: +# +# +# + +# %% +def generate_five_adder(): + def _five_adder(a): + return a + 5 + return _five_adder + +coolfunction = generate_five_adder() +coolfunction(7) + + +# %% [markdown] +# +# +# OK, so what happened there? Well, we defined a function **inside** the other function. We can always do that: +# +# +# + +# %% +def thirty_function(): + def times_three(a): + return a * 3 + def add_seven(a): + return a + 7 + return times_three(add_seven(3)) + +thirty_function() + +# %% [markdown] +# +# +# When we do this, the functions enclosed inside the outer function are **local** functions, and can't be seen outside: +# +# +# + +# %% +add_seven + +# %% [markdown] +# +# +# There's not really much of a difference between functions and other variables +# in python. A function is just a variable which can have () put after it to call +# the code! +# +# +# + +# %% +print(thirty_function) + +# %% +x = [thirty_function, add_five, add] + +# %% +for fun in x: + print(fun) + + +# %% [markdown] +# +# +# And we know that one of the things we can do with a variable is `return` it. So we can return a function, and then call it outside: +# +# +# + +# %% +def deferred_greeting(): + def greet(): + print("Hello") + return greet + +friendlyfunction = deferred_greeting() + +# %% +# Do something else +print("Just passing the time...") + +# %% +# OK, Go! +friendlyfunction() + + +# %% [markdown] +# +# +# +# So now, to finish this, we just need to return a function to add an arbitrary amount: +# +# +# + +# %% [markdown] +#
+ +# %% +def generate_adder(increment): + def _adder(a): + return a + increment + return _adder + +add_3 = generate_adder(3) + +# %% +add_3(9) + +# %% [markdown] +# +# +# We can make this even prettier: let's make another variable pointing to our define_adder() function: +# +# +# + +# %% +add = generate_adder + +# %% [markdown] +# +# +# And now we can do the real magic: +# +# +# + +# %% +add(8)(5) + +# %% [markdown] +# In summary, we have started with a function that takes two arguments (`add(a, b)`) and replaced it with a new function (`add(a)(b)`). This new function takes a single argument, and returns a function that itself takes the second argument. +# +# This may seem like an overly complicated process - and, in some cases, it is! However, this pattern of functions that return functions (or even take them as arguments!) can be very useful. In fact, it is the basis of decorators, a Python feature that we will discuss more [in this chapter](./025Iterators.html#Decorators) [[notebook](./025Iterators.ipynb#Decorators)]. + +# %% [markdown] +# ### Closures + +# %% [markdown] +# You may have noticed something a bit weird: +# +# In the definition of [`generate_adder`](#generate_adder), `increment` is a local variable. It should have gone out of scope and died at the end of the definition. How can the amount the returned adder function is adding still be kept? +# +# This is called a **closure**. In Python, whenever a function definition references a variable in the surrounding scope, it is preserved within the function definition. +# +# You can close over global module variables as well: +# +# +# + +# %% +name = "Eric" + +def greet(): + print("Hello, ", name) + +greet() + +# %% [markdown] +# +# +# And note that the closure stores a reference to the variable in the surrounding scope: ("Late Binding") +# +# +# + +# %% +name = "John" + +greet() + +# %% [markdown] +# ### Map and Reduce + +# %% [markdown] +# We often want to apply a function to each variable in an array, to return a new array. We can do this with a list comprehension: +# +# +# + +# %% +numbers = range(10) + +[add_five(i) for i in numbers] + +# %% [markdown] +# +# +# But this is sufficiently common that there's a quick built-in: +# +# +# + +# %% +list(map(add_five, numbers)) + + +# %% [markdown] +# +# +# This **map** operation is really important conceptually when understanding +# efficient parallel programming: different computers can apply the *mapped* +# function to their input at the same time. We call this Single Program, Multiple +# Data (SPMD). **map** is half of the [**map-reduce**](https://en.wikipedia.org/wiki/MapReduce) functional programming +# paradigm which is key to the efficient operation of much of today's "data +# science" explosion. +# +# Let's continue our functional programming mind-stretch by looking at **reduce** operations. +# +# We very often want to loop with some kind of accumulator (an intermediate result that we update), such as when finding a mean: +# +# +# + +# %% +def summer(data): + total = 0.0 + + for x in data: + total += x + + return total + + +# %% +summer(range(10)) + +# %% [markdown] +# or finding a maximum: + +# %% +import sys + +def my_max(data): + # Start with the smallest possible number + highest = sys.float_info.min + + for x in data: + if x > highest: + highest = x + + return highest + + +# %% +my_max([2, 5, 10, -11, -5]) + + +# %% [markdown] +# These operations, where we have some variable which is building up a result, +# and the result is updated with some operation, can be gathered together as a +# functional program, taking in (as an argument) the operation to be used to combine results: + +# %% +def accumulate(initial, operation, data): + accumulator = initial + for x in data: + accumulator = operation(accumulator, x) + return accumulator + +def my_sum(data): + def _add(a, b): + return a + b + return accumulate(0, _add, data) + + +# %% +my_sum(range(5)) + + +# %% +def bigger(a, b): + if b > a: + return b + return a + +def my_max(data): + return accumulate(sys.float_info.min, bigger, data) + +my_max([2, 5, 10, -11, -5]) + +# %% [markdown] +# Anyway, this accumulate-under-an-operation process is so fundamental to +# computing that it's usually in standard libraries for languages which allow +# functional programming: + +# %% +from functools import reduce + +def my_max(data): + return reduce(bigger, data, sys.float_info.min) + +my_max([2, 5, 10, -11, -5]) + + +# %% [markdown] +# #### Efficient map-reduce +# +# Now, because these operations, `bigger` and `_add`, are such that e.g. (a+b)+c = a+(b+c) , i.e. they are **associative**, we could apply our accumulation +# to the left half and the right half of the array, each on a different computer, and then combine the two halves: +# +# 1 + 2 + 3 + 4 = (1 + 2) + (3 + 4) +# +# Indeed, with a bigger array, we can divide-and-conquer more times: +# +# 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = ((1 + 2) + (3 + 4)) + ((5 + 6) + (7 + 8)) +# +# So with enough parallel computers, we could do this operation on eight numbers +# in three steps: first, we use four computers to do one each of the pairwise +# adds. +# +# Then, we use two computers to add the four totals. +# +# Then, we use one of the computers to do the final add of the two last numbers. +# +# You might be able to do the maths to see that with an N element list, the +# number of such steps is proportional to the logarithm of N. +# +# We say that with enough computers, reduction operations are O(ln N) +# +# This course isn't an introduction to algorithms, but we'll talk more about this +# O() notation when we think about programming for performance. +# +# + +# %% [markdown] +# ### Lambda Functions + +# %% [markdown] +# +# +# When doing functional programming, we often want to be able to define a function on the fly: +# +# +# + +# %% +def most_Cs_in_any_sequence(sequences): + + def count_Cs(sequence): + return sequence.count('C') + + counts = map(count_Cs, sequences) + return max(counts) + + +def most_Gs_in_any_sequence(sequences): + return max(map(lambda sequence: sequence.count('G'), sequences)) + + +data = [ + "CGTA", + "CGGGTAAACG", + "GATTACA" +] + +most_Gs_in_any_sequence(data) + +# %% [markdown] +# +# +# The syntax here means that these two definitions are identical: +# +# +# + +# %% +func_name = lambda a, b, c: a + b + c + +def func_name(a, b, c): + return a + b + c + + +# %% [markdown] +# +# +# +# The **lambda** keyword defines an "anonymous" function. +# +# +# + +# %% +def most_of_given_base_in_any_sequence(sequences, base): + return max(map(lambda sequence: sequence.count(base), sequences)) + +most_of_given_base_in_any_sequence(data, 'A') + +# %% [markdown] +# +# +# The above fragment defined a lambda function as a **closure** over `base`. If you understood that, you've got it! +# +# +# + +# %% [markdown] +# To double all elements in an array: + +# %% +data = range(10) +list(map(lambda x: 2*x, data)) + +# %% +[2*x for x in data] + + +# %% [markdown] +# Similarly, to find the maximum value in a sequence: + +# %% +def my_max(data): + return reduce(lambda a, b: a if a > b else b, data, sys.float_info.min) + +my_max([2, 5, 10, -11, -5]) + +# %% [markdown] +# ### Using functional programming for numerical methods + +# %% [markdown] +# +# Probably the most common use in research computing for functional programming +# is the application of a numerical method to a function. +# +# Consider this example which uses the [`newton` function from SciPy](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html), a root-finding function implementing the [Newton-Raphson method](http://mathworld.wolfram.com/NewtonsMethod.html). The arguments we pass to `newton` are the function whose roots we want to find, and a starting point to search from. +# +# We will be using this to find the roots of the function $f(x) = x^2 - x$. + +# %% +# %matplotlib inline + +# %% +from scipy.optimize import newton +from numpy import linspace, zeros +from matplotlib import pyplot as plt + +solve_me = lambda x: x**2 - x + +for x0 in [2, 0.2]: + answer = newton(solve_me, x0) + print("Starting from {}, the root I found is {}".format(x0, answer)) + +xs = linspace(-1, 2, 50) +solved = [xs, list(map(solve_me, xs)), xs, zeros(len(xs))] + +plt.plot(*solved) + + +# %% [markdown] +# +# +# +# Sometimes such tools return another function, for example the derivative of their input function. This is what a naive implementation of that could look like: +# +# +# + +# %% +def derivative_simple(func, eps, at): + return (func(at + eps) - func(at)) / eps + + +# %% +def derivative(func, eps): + + def _func_derived(x): + return (func(x + eps) - func(x)) / eps + + return _func_derived + +straight = derivative(solve_me, 0.01) + +# %% [markdown] +# The derivative of `solve_me` is $f'(x) = 2x - 1$, which represents a straight line. +# We can verify that our computations are correct, i.e. that the returned function `straight` matches $f'(x)$, by checking the value of `straight` at some $x$: + +# %% +straight(3) + +# %% [markdown] +# or by plotting it: + +# %% +derived = ( + xs, list(map(solve_me, xs)), + xs, list(map(derivative(solve_me, 0.01), xs)) +) +plt.plot(*derived) +print(newton(derivative(solve_me, 0.01), 0)) + +# %% [markdown] +# Of course, coding your own numerical methods is bad, because the implementations you develop are likely to be less efficient, less accurate and more error-prone than what you can find in existing established libraries. +# +# For example, the above definition could be replaced by: + +# %% +import scipy.misc + +def derivative(func): + def _func_derived(x): + return scipy.misc.derivative(func, x) + return _func_derived + +newton(derivative(solve_me), 0) + +# %% [markdown] +# +# +# If you've done a moderate amount of calculus, then you'll find similarities +# between functional programming in computer science and Functionals in the +# calculus of variations. +# +# diff --git a/ch07dry/025Iterators.html b/ch07dry/025Iterators.html new file mode 100644 index 000000000..7ae4f4707 --- /dev/null +++ b/ch07dry/025Iterators.html @@ -0,0 +1,1780 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Iterators, Generators, Decorators, and Contexts + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Iterators and Generators

+
+
+
+
+
+
+

In Python, anything which can be iterated over is called an iterable:

+
+
+
+
+
+
In [1]:
+
+
+
bowl = {
+    "apple": 5,
+    "banana": 3,
+    "orange": 7
+}
+
+for fruit in bowl:
+    print(fruit.upper())
+
+
+
+
+
+
+
+
+
+
APPLE
+BANANA
+ORANGE
+
+
+
+
+
+
+
+
+
+

Surprisingly often, we want to iterate over something that takes a moderately +large amount of memory to store - for example, our map images in the +green-graph example.

+

Our green-graph example involved making an array of all the maps between London +and Birmingham. This kept them all in memory at the same time: first we +downloaded all the maps, then we counted the green pixels in each of them.

+

This would NOT work if we used more points: eventually, we would run out of memory. +We need to use a generator instead. This chapter will look at iterators and generators in more detail: +how they work, when to use them, how to create our own.

+
+
+
+
+
+
+

Iterators

+
+
+
+
+
+
+

Consider the basic python range function:

+
+
+
+
+
+
In [2]:
+
+
+
range(10)
+
+
+
+
+
+
+
+
Out[2]:
+
+
range(0, 10)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
total = 0
+for x in range(int(1e6)):
+    total += x
+
+total
+
+
+
+
+
+
+
+
Out[3]:
+
+
499999500000
+
+
+
+
+
+
+
+
+

In order to avoid allocating a million integers, range actually uses an iterator.

+

We don't actually need a million integers at once, just each +integer in turn up to a million.

+

Because we can get an iterator from it, we say that a range is an iterable.

+
+
+
+
+
+
+

So we can for-loop over it:

+
+
+
+
+
+
In [4]:
+
+
+
for i in range(3): 
+    print(i)
+
+
+
+
+
+
+
+
+
+
0
+1
+2
+
+
+
+
+
+
+
+
+
+

There are two important Python built-in functions for working with iterables. +First is iter, which lets us create an iterator from any iterable object.

+
+
+
+
+
+
In [5]:
+
+
+
a = iter(range(3))
+
+
+
+
+
+
+
+
+

Once we have an iterator object, we can pass it to the next function. This +moves the iterator forward, and gives us its next element:

+
+
+
+
+
+
In [6]:
+
+
+
next(a)
+
+
+
+
+
+
+
+
Out[6]:
+
+
0
+
+
+
+
+
+
+
+
In [7]:
+
+
+
next(a)
+
+
+
+
+
+
+
+
Out[7]:
+
+
1
+
+
+
+
+
+
+
+
In [8]:
+
+
+
next(a)
+
+
+
+
+
+
+
+
Out[8]:
+
+
2
+
+
+
+
+
+
+
+
+

When we are out of elements, a StopIteration exception is raised:

+
+
+
+
+
+
In [9]:
+
+
+
next(a)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+StopIteration                             Traceback (most recent call last)
+Cell In[9], line 1
+----> 1 next(a)
+
+StopIteration: 
+
+
+
+
+
+
+
+
+

This tells Python that the iteration is over. For example, if we are in a for i in range(3) loop, this lets us know when we should exit the loop.

+
+
+
+
+
+
+

We can turn an iterable or iterator into a list with the list constructor function:

+
+
+
+
+
+
In [10]:
+
+
+
list(range(5))
+
+
+
+
+
+
+
+
Out[10]:
+
+
[0, 1, 2, 3, 4]
+
+
+
+
+
+
+
+
+

Defining Our Own Iterable

+
+
+
+
+
+
+

When we write next(a), under the hood Python tries to call the __next__() method of a. Similarly, iter(a) calls a.__iter__().

+

We can make our own iterators by defining classes that can be used with the next() and iter() functions: this is the iterator protocol.

+

For each of the concepts in Python, like sequence, container, iterable, the language defines a protocol, a set of methods a class must implement, in order to be treated as a member of that concept.

+

To define an iterator, the methods that must be supported are __next__() and __iter__().

+

__next__() must update the iterator.

+

We'll see why we need to define __iter__ in a moment.

+
+
+
+
+
+
+

Here is an example of defining a custom iterator class:

+
+
+
+
+
+
In [11]:
+
+
+
class fib_iterator:
+    """An iterator over part of the Fibonacci sequence."""
+
+    def __init__(self, limit, seed1=1, seed2=1):
+        self.limit = limit
+        self.previous = seed1
+        self.current = seed2
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        (self.previous, self.current) = (self.current, self.previous + self.current)
+        self.limit -= 1
+        if self.limit < 0:
+            raise StopIteration()
+        return self.current
+
+
+
+
+
+
+
+
In [12]:
+
+
+
x = fib_iterator(5)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[13]:
+
+
2
+
+
+
+
+
+
+
+
In [14]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[14]:
+
+
3
+
+
+
+
+
+
+
+
In [15]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[15]:
+
+
5
+
+
+
+
+
+
+
+
In [16]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[16]:
+
+
8
+
+
+
+
+
+
+
+
In [17]:
+
+
+
for x in fib_iterator(5):
+    print(x)
+
+
+
+
+
+
+
+
+
+
2
+3
+5
+8
+13
+
+
+
+
+
+
+
+
+
In [18]:
+
+
+
sum(fib_iterator(1000))
+
+
+
+
+
+
+
+
Out[18]:
+
+
297924218508143360336882819981631900915673130543819759032778173440536722190488904520034508163846345539055096533885943242814978469042830417586260359446115245634668393210192357419233828310479227982326069668668250
+
+
+
+
+
+
+
+
+

A shortcut to iterables: the __iter__ method

+
+
+
+
+
+
+

In fact, we don't always have to define both __iter__ and __next__!

+

If, to be iterated over, a class just wants to behave as if it were some other iterable, you can just implement __iter__ and return iter(some_other_iterable), without implementing next. For example, an image class might want to implement some metadata, but behave just as if it were just a 1-d pixel array when being iterated:

+
+
+
+
+
+
In [19]:
+
+
+
from numpy import array
+from matplotlib import pyplot as plt
+
+
+class MyImage(object):
+    def __init__(self, pixels):
+        self.pixels = array(pixels, dtype='uint8')
+        self.channels = self.pixels.shape[2]
+
+    def __iter__(self):
+        # return an iterator over just the pixel values
+        return iter(self.pixels.reshape(-1, self.channels))
+
+    def show(self):
+        plt.imshow(self.pixels, interpolation="None")
+
+
+x = [[[255, 255, 0], [0, 255, 0]], [[0, 0, 255], [255, 255, 255]]]
+image = MyImage(x)
+
+
+
+
+
+
+
+
In [20]:
+
+
+
%matplotlib inline
+image.show()
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [21]:
+
+
+
image.channels
+
+
+
+
+
+
+
+
Out[21]:
+
+
3
+
+
+
+
+
+
+
+
In [22]:
+
+
+
from webcolors import rgb_to_name
+for pixel in image:
+    print(rgb_to_name(pixel))
+
+
+
+
+
+
+
+
+
+
yellow
+lime
+blue
+white
+
+
+
+
+
+
+
+
+
+

See how we used image in a for loop, even though it doesn't satisfy the iterator protocol (we didn't define both __iter__ and __next__ for it)?

+

The key here is that we can use any iterable object (like image) in a for expression, +not just iterators! Internally, Python will create an iterator from the iterable (by calling its __iter__ method), but this means we don't need to define a __next__ method explicitly.

+
+
+
+
+
+
+

The iterator protocol is to implement both __iter__ and +__next__, while the iterable protocol is to implement __iter__ and return +an iterator.

+
+
+
+
+
+
+

Generators

+
+
+
+
+
+
+

There's a fair amount of "boiler-plate" in the above class-based definition of +an iterable.

+

Python provides another way to specify something +which meets the iterator protocol: generators.

+
+
+
+
+
+
In [23]:
+
+
+
def my_generator():
+    yield 5
+    yield 10
+
+
+x = my_generator()
+
+
+
+
+
+
+
+
In [24]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[24]:
+
+
5
+
+
+
+
+
+
+
+
In [25]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
Out[25]:
+
+
10
+
+
+
+
+
+
+
+
In [26]:
+
+
+
next(x)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+StopIteration                             Traceback (most recent call last)
+Cell In[26], line 1
+----> 1 next(x)
+
+StopIteration: 
+
+
+
+
+
+
+
+
In [27]:
+
+
+
for a in my_generator():
+    print(a)
+
+
+
+
+
+
+
+
+
+
5
+10
+
+
+
+
+
+
+
+
+
In [28]:
+
+
+
sum(my_generator())
+
+
+
+
+
+
+
+
Out[28]:
+
+
15
+
+
+
+
+
+
+
+
+

A function which has yield statements instead of a return statement returns +temporarily: it automagically becomes something which implements __next__.

+
+
+
+
+
+
+

Each call of next() returns control to the function where it +left off.

+
+
+
+
+
+
+

Control passes back-and-forth between the generator and the caller. +Our Fibonacci example therefore becomes a function rather than a class.

+
+
+
+
+
+
In [29]:
+
+
+
def yield_fibs(limit, seed1=1, seed2=1):
+    current = seed1
+    previous = seed2
+
+    while limit > 0:
+        limit -= 1
+        current, previous = current + previous, current
+        yield current
+
+
+
+
+
+
+
+
+

We can now use the output of the function like a normal iterable:

+
+
+
+
+
+
In [30]:
+
+
+
sum(yield_fibs(5))
+
+
+
+
+
+
+
+
Out[30]:
+
+
31
+
+
+
+
+
+
+
+
In [31]:
+
+
+
for a in yield_fibs(10):
+    if a % 2 == 0:
+        print(a)
+
+
+
+
+
+
+
+
+
+
2
+8
+34
+144
+
+
+
+
+
+
+
+
+
+

Sometimes we may need to gather all values from a generator into a list, such as before passing them to a function that expects a list:

+
+
+
+
+
+
In [32]:
+
+
+
list(yield_fibs(10))
+
+
+
+
+
+
+
+
Out[32]:
+
+
[2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
+
+
+
+
+
+
+
+
In [33]:
+
+
+
plt.plot(list(yield_fibs(20)))
+
+
+
+
+
+
+
+
Out[33]:
+
+
[<matplotlib.lines.Line2D at 0x7efec86a6070>]
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Iterables and generators can be used to achieve complex behaviour, especially when combined with functional programming. In fact, Python itself contains some very useful language features that make use of these practices: context managers and decorators. We have already seen these in this class, but here we discuss them in more detail.

+
+
+
+
+
+
+

Context managers

+
+
+
+
+
+
+

We have seen before [notebook] that, instead of separately opening and closeing a file, we can have +the file be automatically closed using a context manager:

+
+
+
+
+
+
In [34]:
+
+
+
%%writefile example.yaml
+modelname: brilliant
+
+
+
+
+
+
+
+
+
+
Writing example.yaml
+
+
+
+
+
+
+
+
+
In [35]:
+
+
+
import yaml
+
+with open('example.yaml') as foo:
+    print(yaml.safe_load(foo))
+
+
+
+
+
+
+
+
+
+
{'modelname': 'brilliant'}
+
+
+
+
+
+
+
+
+
+

In addition to more convenient syntax, this takes care of any clean-up that has to be done after the file is closed, even if any errors occur while we are working on the file.

+
+
+
+
+
+
+

How could we define our own one of these, if we too have clean-up code we +always want to run after a calling function has done its work, or set-up code +we want to do first?

+

We can define a class that meets an appropriate protocol:

+
+
+
+
+
+
In [36]:
+
+
+
class verbose_context():
+    def __init__(self, name):
+        self.name=name
+    def __enter__(self):
+        print("Get ready, ", self.name)
+    def __exit__(self, exc_type, exc_value, traceback):
+        print("OK, done")
+
+with verbose_context("Monty"):
+    print("Doing it!")
+
+
+
+
+
+
+
+
+
+
Get ready,  Monty
+Doing it!
+OK, done
+
+
+
+
+
+
+
+
+
+

However, this is pretty verbose! Again, a generator with yield makes for an easier syntax:

+
+
+
+
+
+
In [37]:
+
+
+
from contextlib import contextmanager
+
+@contextmanager
+def verbose_context(name):
+    print("Get ready for action, ", name)
+    yield name.upper()
+    print("You did it")
+
+with verbose_context("Monty") as shouty:
+    print(f"Doing it, {shouty}")
+
+
+
+
+
+
+
+
+
+
Get ready for action,  Monty
+Doing it, MONTY
+You did it
+
+
+
+
+
+
+
+
+
+

Again, we use yield to temporarily return from a function.

+
+
+
+
+
+
+

Decorators

+
+
+
+
+
+
+

When doing functional programming, we may often want to define mutator +functions which take in one function and return a new function, such as our +derivative example earlier.

+
+
+
+
+
+
In [38]:
+
+
+
from math import sqrt
+
+
+def repeater(count):
+    def wrap_function_in_repeat(func):
+
+        def _repeated(x):
+            counter = count
+            while counter > 0:
+                counter -= 1
+                x = func(x)
+            return x
+
+        return _repeated
+    return wrap_function_in_repeat
+
+
+fiftytimes = repeater(50)
+
+fiftyroots = fiftytimes(sqrt)
+
+print(fiftyroots(100))
+
+
+
+
+
+
+
+
+
+
1.000000000000004
+
+
+
+
+
+
+
+
+
+

It turns out that, quite often, we want to apply one of these to a function as we're defining a class. +For example, we may want to specify that after certain methods are called, data should always be stored:

+
+
+
+
+
+
+

Any function which accepts a function as its first argument and returns a function can be used as a decorator like this.

+

Much of Python's standard functionality is implemented as decorators: we've +seen @contextmanager, @classmethod and @attribute. The @contextmanager +metafunction, for example, takes in an iterator, and yields a class conforming +to the context manager protocol.

+
+
+
+
+
+
In [39]:
+
+
+
@repeater(3)
+def hello(name):
+    return f"Hello, {name}"
+
+
+
+
+
+
+
+
In [40]:
+
+
+
hello("Cleese")
+
+
+
+
+
+
+
+
Out[40]:
+
+
'Hello, Hello, Hello, Cleese'
+
+
+
+
+
+
+
+
+

Supplementary material

The remainder of this page contains an example of the flexibility of the features discussed above. Specifically, it shows how generators and context managers can be combined to create a testing framework like the one previously seen in the course.

+
+
+
+
+
+
+

Test generators

A few weeks ago we saw a test which loaded its test cases from a YAML file and +asserted each input with each output. This was nice and concise, but had one +flaw: we had just one test, covering all the fixtures, so we got just one . in +the test output when we ran the tests, and if any test failed, the rest were +not run. We can do a nicer job with a test generator:

+
+
+
+
+
+
In [41]:
+
+
+
def assert_exemplar(**fixture):
+    answer = fixture.pop('answer')
+    assert_equal(greet(**fixture), answer)
+
+
+def test_greeter():
+    with open(os.path.join(os.path.dirname(
+        __file__), 'fixtures', 'samples.yaml')
+    ) as fixtures_file:
+        fixtures = yaml.safe_load(fixtures_file)
+
+        for fixture in fixtures:
+
+            yield assert_exemplar(**fixture)
+
+
+
+
+
+
+
+
+

Each time a function beginning with test_ does a yield it results in another test.

+
+
+
+
+
+
+

Negative test contexts managers

+
+
+
+
+
+
+

We have seen this:

+
+
+
+
+
+
In [42]:
+
+
+
from pytest import raises
+
+with raises(AttributeError):
+    x = 2
+    x.foo()
+
+
+
+
+
+
+
+
+

We can now see how pytest might have implemented this:

+
+
+
+
+
+
In [43]:
+
+
+
from contextlib import contextmanager
+
+
+@contextmanager
+def reimplement_raises(exception):
+    try:
+        yield
+    except exception:
+        pass
+    else:
+        raise Exception("Expected,", exception,
+                        " to be raised, nothing was.")
+
+
+
+
+
+
+
+
In [44]:
+
+
+
with reimplement_raises(AttributeError):
+    x = 2
+    x.foo()
+
+
+
+
+
+
+
+
+

Negative test decorators

+
+
+
+
+
+
+

Some frameworks, like nose, also implement a very nice negative test decorator, which lets us marks tests that we know should produce an exception:

+
+
+
+
+
+
In [45]:
+
+
+
import nose
+
+
+@nose.tools.raises(TypeError, ValueError)
+def test_raises_type_error():
+    raise TypeError("This test passes")
+
+
+
+
+
+
+
+
In [46]:
+
+
+
test_raises_type_error()
+
+
+
+
+
+
+
+
In [47]:
+
+
+
@nose.tools.raises(Exception)
+def test_that_fails_by_passing():
+    pass
+
+
+
+
+
+
+
+
In [48]:
+
+
+
test_that_fails_by_passing()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+AssertionError                            Traceback (most recent call last)
+Cell In[48], line 1
+----> 1 test_that_fails_by_passing()
+
+File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/nose/tools/nontrivial.py:67, in raises.<locals>.decorate.<locals>.newfunc(*arg, **kw)
+     65 else:
+     66     message = "%s() did not raise %s" % (name, valid)
+---> 67     raise AssertionError(message)
+
+AssertionError: test_that_fails_by_passing() did not raise Exception
+
+
+
+
+
+
+
+
+

We could reimplement this ourselves now too, using the context manager we wrote above:

+
+
+
+
+
+
In [49]:
+
+
+
def homemade_raises_decorator(exception):
+    def wrap_function(func):  # Closure over exception
+        # Define a function which runs another function under our "raises" context:
+        def _output(*args):  # Closure over func and exception
+            with reimplement_raises(exception):
+                func(*args)
+        # Return it
+        return _output
+    return wrap_function
+
+
+
+
+
+
+
+
In [50]:
+
+
+
@homemade_raises_decorator(TypeError)
+def test_raises_type_error():
+    raise TypeError("This test passes")
+
+
+
+
+
+
+
+
In [51]:
+
+
+
test_raises_type_error()
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/025Iterators.ipynb b/ch07dry/025Iterators.ipynb new file mode 100644 index 000000000..487dd7b45 --- /dev/null +++ b/ch07dry/025Iterators.ipynb @@ -0,0 +1,1132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5f70b3f2", + "metadata": {}, + "source": [ + "## Iterators and Generators" + ] + }, + { + "cell_type": "markdown", + "id": "3e7e9b38", + "metadata": {}, + "source": [ + "In Python, anything which can be iterated over is called an iterable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3031f39c", + "metadata": {}, + "outputs": [], + "source": [ + "bowl = {\n", + " \"apple\": 5,\n", + " \"banana\": 3,\n", + " \"orange\": 7\n", + "}\n", + "\n", + "for fruit in bowl:\n", + " print(fruit.upper())" + ] + }, + { + "cell_type": "markdown", + "id": "5a735826", + "metadata": {}, + "source": [ + "Surprisingly often, we want to iterate over something that takes a moderately\n", + "large amount of memory to store - for example, our map images in the\n", + "green-graph example.\n", + "\n", + "Our green-graph example involved making an array of all the maps between London\n", + "and Birmingham. This kept them all in memory *at the same time*: first we\n", + "downloaded all the maps, then we counted the green pixels in each of them. \n", + "\n", + "This would NOT work if we used more points: eventually, we would run out of memory.\n", + "We need to use a **generator** instead. This chapter will look at iterators and generators in more detail:\n", + "how they work, when to use them, how to create our own." + ] + }, + { + "cell_type": "markdown", + "id": "f8b067ba", + "metadata": {}, + "source": [ + "### Iterators" + ] + }, + { + "cell_type": "markdown", + "id": "756bd0b1", + "metadata": {}, + "source": [ + "Consider the basic python `range` function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e451374", + "metadata": {}, + "outputs": [], + "source": [ + "range(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "141e525c", + "metadata": {}, + "outputs": [], + "source": [ + "total = 0\n", + "for x in range(int(1e6)):\n", + " total += x\n", + "\n", + "total" + ] + }, + { + "cell_type": "markdown", + "id": "d1d7dfe3", + "metadata": {}, + "source": [ + "In order to avoid allocating a million integers, `range` actually uses an **iterator**.\n", + "\n", + "We don't actually need a million integers *at once*, just each\n", + "integer *in turn* up to a million.\n", + "\n", + "Because we can get an iterator from it, we say that a range is an **iterable**." + ] + }, + { + "cell_type": "markdown", + "id": "7f294d33", + "metadata": {}, + "source": [ + "So we can `for`-loop over it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e242da5e", + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(3): \n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "id": "c8422ba8", + "metadata": {}, + "source": [ + "There are two important Python built-in functions for working with iterables.\n", + "First is `iter`, which lets us create an iterator from any iterable object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70728c1c", + "metadata": {}, + "outputs": [], + "source": [ + "a = iter(range(3))" + ] + }, + { + "cell_type": "markdown", + "id": "bcc754d3", + "metadata": {}, + "source": [ + "Once we have an iterator object, we can pass it to the `next` function. This\n", + "moves the iterator forward, and gives us its next element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf76b14c", + "metadata": {}, + "outputs": [], + "source": [ + "next(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f6f6282", + "metadata": {}, + "outputs": [], + "source": [ + "next(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00ce230b", + "metadata": {}, + "outputs": [], + "source": [ + "next(a)" + ] + }, + { + "cell_type": "markdown", + "id": "5d6792c9", + "metadata": {}, + "source": [ + "When we are out of elements, a `StopIteration` exception is raised:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb6ec0c5", + "metadata": {}, + "outputs": [], + "source": [ + "next(a)" + ] + }, + { + "cell_type": "markdown", + "id": "db6f38d1", + "metadata": {}, + "source": [ + "This tells Python that the iteration is over. For example, if we are in a `for i in range(3)` loop, this lets us know when we should exit the loop." + ] + }, + { + "cell_type": "markdown", + "id": "9eaaf206", + "metadata": {}, + "source": [ + "We can turn an iterable or iterator into a list with the `list` constructor function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faa9164d", + "metadata": {}, + "outputs": [], + "source": [ + "list(range(5))" + ] + }, + { + "cell_type": "markdown", + "id": "3e916298", + "metadata": {}, + "source": [ + "### Defining Our Own Iterable" + ] + }, + { + "cell_type": "markdown", + "id": "5db197e5", + "metadata": {}, + "source": [ + "When we write `next(a)`, under the hood Python tries to call the `__next__()` method of `a`. Similarly, `iter(a)` calls `a.__iter__()`.\n", + "\n", + "We can make our own iterators by defining *classes* that can be used with the `next()` and `iter()` functions: this is the **iterator protocol**.\n", + "\n", + "For each of the *concepts* in Python, like sequence, container, iterable, the language defines a *protocol*, a set of methods a class must implement, in order to be treated as a member of that concept.\n", + "\n", + "To define an iterator, the methods that must be supported are `__next__()` and `__iter__()`.\n", + "\n", + "`__next__()` must update the iterator.\n", + "\n", + "We'll see why we need to define `__iter__` in a moment." + ] + }, + { + "cell_type": "markdown", + "id": "23b6d26b", + "metadata": {}, + "source": [ + "Here is an example of defining a custom iterator class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06632ef6", + "metadata": {}, + "outputs": [], + "source": [ + "class fib_iterator:\n", + " \"\"\"An iterator over part of the Fibonacci sequence.\"\"\"\n", + "\n", + " def __init__(self, limit, seed1=1, seed2=1):\n", + " self.limit = limit\n", + " self.previous = seed1\n", + " self.current = seed2\n", + "\n", + " def __iter__(self):\n", + " return self\n", + "\n", + " def __next__(self):\n", + " (self.previous, self.current) = (self.current, self.previous + self.current)\n", + " self.limit -= 1\n", + " if self.limit < 0:\n", + " raise StopIteration()\n", + " return self.current" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec736b8f", + "metadata": {}, + "outputs": [], + "source": [ + "x = fib_iterator(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d16ae909", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74dd4465", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b89c7cbb", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5274ab12", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a792a4e", + "metadata": {}, + "outputs": [], + "source": [ + "for x in fib_iterator(5):\n", + " print(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fbf5dc1", + "metadata": {}, + "outputs": [], + "source": [ + "sum(fib_iterator(1000))" + ] + }, + { + "cell_type": "markdown", + "id": "620d8e58", + "metadata": {}, + "source": [ + "### A shortcut to iterables: the `__iter__` method" + ] + }, + { + "cell_type": "markdown", + "id": "708a25db", + "metadata": {}, + "source": [ + "In fact, we don't always have to define both `__iter__` and `__next__`!\n", + "\n", + "If, to be iterated over, a class just wants to behave as if it were some other iterable, you can just implement `__iter__` and return `iter(some_other_iterable)`, without implementing `next`. For example, an image class might want to implement some metadata, but behave just as if it were just a 1-d pixel array when being iterated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f692a60", + "metadata": {}, + "outputs": [], + "source": [ + "from numpy import array\n", + "from matplotlib import pyplot as plt\n", + "\n", + "\n", + "class MyImage(object):\n", + " def __init__(self, pixels):\n", + " self.pixels = array(pixels, dtype='uint8')\n", + " self.channels = self.pixels.shape[2]\n", + "\n", + " def __iter__(self):\n", + " # return an iterator over just the pixel values\n", + " return iter(self.pixels.reshape(-1, self.channels))\n", + "\n", + " def show(self):\n", + " plt.imshow(self.pixels, interpolation=\"None\")\n", + "\n", + "\n", + "x = [[[255, 255, 0], [0, 255, 0]], [[0, 0, 255], [255, 255, 255]]]\n", + "image = MyImage(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1f06932", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "image.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d5aaea9", + "metadata": {}, + "outputs": [], + "source": [ + "image.channels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39590b69", + "metadata": {}, + "outputs": [], + "source": [ + "from webcolors import rgb_to_name\n", + "for pixel in image:\n", + " print(rgb_to_name(pixel))" + ] + }, + { + "cell_type": "markdown", + "id": "854d7784", + "metadata": {}, + "source": [ + "See how we used `image` in a `for` loop, even though it doesn't satisfy the iterator protocol (we didn't define both `__iter__` and `__next__` for it)?\n", + "\n", + "The key here is that we can use any *iterable* object (like `image`) in a `for` expression,\n", + "not just iterators! Internally, Python will create an iterator from the iterable (by calling its `__iter__` method), but this means we don't need to define a `__next__` method explicitly." + ] + }, + { + "cell_type": "markdown", + "id": "d08c3154", + "metadata": {}, + "source": [ + "The *iterator* protocol is to implement both `__iter__` and\n", + "`__next__`, while the *iterable* protocol is to implement `__iter__` and return\n", + "an iterator." + ] + }, + { + "cell_type": "markdown", + "id": "0df7a550", + "metadata": {}, + "source": [ + "### Generators" + ] + }, + { + "cell_type": "markdown", + "id": "b9743719", + "metadata": {}, + "source": [ + "There's a fair amount of \"boiler-plate\" in the above class-based definition of\n", + "an iterable.\n", + "\n", + "Python provides another way to specify something\n", + "which meets the iterator protocol: **generators**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54b016d1", + "metadata": {}, + "outputs": [], + "source": [ + "def my_generator():\n", + " yield 5\n", + " yield 10\n", + "\n", + "\n", + "x = my_generator()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd6bcb93", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a87e4bd", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a64e506", + "metadata": {}, + "outputs": [], + "source": [ + "next(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "751e5b50", + "metadata": {}, + "outputs": [], + "source": [ + "for a in my_generator():\n", + " print(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b556a21", + "metadata": {}, + "outputs": [], + "source": [ + "sum(my_generator())" + ] + }, + { + "cell_type": "markdown", + "id": "81979264", + "metadata": {}, + "source": [ + "A function which has `yield` statements instead of a `return` statement returns\n", + "**temporarily**: it automagically becomes something which implements `__next__`." + ] + }, + { + "cell_type": "markdown", + "id": "7ff194f4", + "metadata": {}, + "source": [ + "Each call of `next()` returns control to the function where it\n", + "left off." + ] + }, + { + "cell_type": "markdown", + "id": "dd680c6d", + "metadata": {}, + "source": [ + " Control passes back-and-forth between the generator and the caller.\n", + "Our Fibonacci example therefore becomes a function rather than a class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43ed1bf6", + "metadata": {}, + "outputs": [], + "source": [ + "def yield_fibs(limit, seed1=1, seed2=1):\n", + " current = seed1\n", + " previous = seed2\n", + "\n", + " while limit > 0:\n", + " limit -= 1\n", + " current, previous = current + previous, current\n", + " yield current" + ] + }, + { + "cell_type": "markdown", + "id": "fb6f6000", + "metadata": {}, + "source": [ + "We can now use the output of the function like a normal iterable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dc7bcfa", + "metadata": {}, + "outputs": [], + "source": [ + "sum(yield_fibs(5))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "518c0ae1", + "metadata": {}, + "outputs": [], + "source": [ + "for a in yield_fibs(10):\n", + " if a % 2 == 0:\n", + " print(a)" + ] + }, + { + "cell_type": "markdown", + "id": "a63d9219", + "metadata": {}, + "source": [ + "Sometimes we may need to gather all values from a generator into a list, such as before passing them to a function that expects a list:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edb19060", + "metadata": {}, + "outputs": [], + "source": [ + "list(yield_fibs(10))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2422c36c", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(list(yield_fibs(20)))" + ] + }, + { + "cell_type": "markdown", + "id": "32d91653", + "metadata": {}, + "source": [ + "## Related Concepts\n", + "\n", + "Iterables and generators can be used to achieve complex behaviour, especially when combined with functional programming. In fact, Python itself contains some very useful language features that make use of these practices: context managers and decorators. We have already seen these in this class, but here we discuss them in more detail." + ] + }, + { + "cell_type": "markdown", + "id": "4a00e6f4", + "metadata": {}, + "source": [ + "### Context managers" + ] + }, + { + "cell_type": "markdown", + "id": "d61efaac", + "metadata": {}, + "source": [ + "[We have seen before](../ch02data/060files.html#Closing-files) [[notebook](../ch02data/060files.ipynb#Closing-files)] that, instead of separately `open`ing and `close`ing a file, we can have\n", + "the file be automatically closed using a context manager:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6de041", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile example.yaml\n", + "modelname: brilliant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "409fe275", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "with open('example.yaml') as foo:\n", + " print(yaml.safe_load(foo))" + ] + }, + { + "cell_type": "markdown", + "id": "d29b4e99", + "metadata": {}, + "source": [ + "In addition to more convenient syntax, this takes care of any clean-up that has to be done after the file is closed, even if any errors occur while we are working on the file." + ] + }, + { + "cell_type": "markdown", + "id": "bd875442", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "How could we define our own one of these, if we too have clean-up code we\n", + "always want to run after a calling function has done its work, or set-up code\n", + "we want to do first?\n", + "\n", + "We can define a class that meets an appropriate protocol:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d993a28e", + "metadata": {}, + "outputs": [], + "source": [ + "class verbose_context():\n", + " def __init__(self, name):\n", + " self.name=name\n", + " def __enter__(self):\n", + " print(\"Get ready, \", self.name)\n", + " def __exit__(self, exc_type, exc_value, traceback):\n", + " print(\"OK, done\")\n", + "\n", + "with verbose_context(\"Monty\"):\n", + " print(\"Doing it!\")" + ] + }, + { + "cell_type": "markdown", + "id": "4c509e16", + "metadata": {}, + "source": [ + "\n", + "\n", + "However, this is pretty verbose! Again, a generator with `yield` makes for an easier syntax:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46a04212", + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager\n", + "\n", + "@contextmanager\n", + "def verbose_context(name):\n", + " print(\"Get ready for action, \", name)\n", + " yield name.upper()\n", + " print(\"You did it\")\n", + "\n", + "with verbose_context(\"Monty\") as shouty:\n", + " print(f\"Doing it, {shouty}\")" + ] + }, + { + "cell_type": "markdown", + "id": "83086999", + "metadata": {}, + "source": [ + "\n", + "\n", + "Again, we use `yield` to temporarily return from a function.\n" + ] + }, + { + "cell_type": "markdown", + "id": "469f67aa", + "metadata": {}, + "source": [ + "### Decorators" + ] + }, + { + "cell_type": "markdown", + "id": "59406293", + "metadata": {}, + "source": [ + "\n", + "When doing functional programming, we may often want to define mutator\n", + "functions which take in one function and return a new function, such as our\n", + "derivative example earlier.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f8b42ba", + "metadata": {}, + "outputs": [], + "source": [ + "from math import sqrt\n", + "\n", + "\n", + "def repeater(count):\n", + " def wrap_function_in_repeat(func):\n", + "\n", + " def _repeated(x):\n", + " counter = count\n", + " while counter > 0:\n", + " counter -= 1\n", + " x = func(x)\n", + " return x\n", + "\n", + " return _repeated\n", + " return wrap_function_in_repeat\n", + "\n", + "\n", + "fiftytimes = repeater(50)\n", + "\n", + "fiftyroots = fiftytimes(sqrt)\n", + "\n", + "print(fiftyroots(100))" + ] + }, + { + "cell_type": "markdown", + "id": "1c73b3fa", + "metadata": {}, + "source": [ + "It turns out that, quite often, we want to apply one of these to a function as we're defining a class.\n", + "For example, we may want to specify that after certain methods are called, data should always be stored:" + ] + }, + { + "cell_type": "markdown", + "id": "a5340519", + "metadata": {}, + "source": [ + "Any function which accepts a function as its first argument and returns a function can be used as a **decorator** like this.\n", + "\n", + "Much of Python's standard functionality is implemented as decorators: we've\n", + "seen @contextmanager, @classmethod and @attribute. The @contextmanager\n", + "metafunction, for example, takes in an iterator, and yields a class conforming\n", + "to the context manager protocol.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fb9494a", + "metadata": {}, + "outputs": [], + "source": [ + "@repeater(3)\n", + "def hello(name):\n", + " return f\"Hello, {name}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f1da837", + "metadata": {}, + "outputs": [], + "source": [ + "hello(\"Cleese\")" + ] + }, + { + "cell_type": "markdown", + "id": "a0947b9b", + "metadata": {}, + "source": [ + "## Supplementary material\n", + "\n", + "The remainder of this page contains an example of the flexibility of the features discussed above. Specifically, it shows how generators and context managers can be combined to create a testing framework like the one previously seen in the course." + ] + }, + { + "cell_type": "markdown", + "id": "f925d5dd", + "metadata": {}, + "source": [ + "### Test generators\n", + "\n", + "\n", + "A few weeks ago we saw a test which loaded its test cases from a YAML file and\n", + "asserted each input with each output. This was nice and concise, but had one\n", + "flaw: we had just one test, covering all the fixtures, so we got just one . in\n", + "the test output when we ran the tests, and if any test failed, the rest were\n", + "not run. We can do a nicer job with a test **generator**:\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6e90d60", + "metadata": {}, + "outputs": [], + "source": [ + "def assert_exemplar(**fixture):\n", + " answer = fixture.pop('answer')\n", + " assert_equal(greet(**fixture), answer)\n", + "\n", + "\n", + "def test_greeter():\n", + " with open(os.path.join(os.path.dirname(\n", + " __file__), 'fixtures', 'samples.yaml')\n", + " ) as fixtures_file:\n", + " fixtures = yaml.safe_load(fixtures_file)\n", + "\n", + " for fixture in fixtures:\n", + "\n", + " yield assert_exemplar(**fixture)" + ] + }, + { + "cell_type": "markdown", + "id": "5bdc6291", + "metadata": {}, + "source": [ + "Each time a function beginning with `test_` does a `yield` it results in another test." + ] + }, + { + "cell_type": "markdown", + "id": "839f5731", + "metadata": {}, + "source": [ + "### Negative test contexts managers" + ] + }, + { + "cell_type": "markdown", + "id": "32bb5357", + "metadata": {}, + "source": [ + "We have seen this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6821ab6d", + "metadata": {}, + "outputs": [], + "source": [ + "from pytest import raises\n", + "\n", + "with raises(AttributeError):\n", + " x = 2\n", + " x.foo()" + ] + }, + { + "cell_type": "markdown", + "id": "1d668f1b", + "metadata": {}, + "source": [ + "We can now see how `pytest` might have implemented this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47b20855", + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager\n", + "\n", + "\n", + "@contextmanager\n", + "def reimplement_raises(exception):\n", + " try:\n", + " yield\n", + " except exception:\n", + " pass\n", + " else:\n", + " raise Exception(\"Expected,\", exception,\n", + " \" to be raised, nothing was.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "843bbe62", + "metadata": {}, + "outputs": [], + "source": [ + "with reimplement_raises(AttributeError):\n", + " x = 2\n", + " x.foo()" + ] + }, + { + "cell_type": "markdown", + "id": "849a27ca", + "metadata": {}, + "source": [ + "### Negative test decorators" + ] + }, + { + "cell_type": "markdown", + "id": "f8d79506", + "metadata": {}, + "source": [ + "Some frameworks, like `nose`, also implement a very nice negative test decorator, which lets us marks tests that we know should produce an exception:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "903422ed", + "metadata": {}, + "outputs": [], + "source": [ + "import nose\n", + "\n", + "\n", + "@nose.tools.raises(TypeError, ValueError)\n", + "def test_raises_type_error():\n", + " raise TypeError(\"This test passes\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7be2bca8", + "metadata": {}, + "outputs": [], + "source": [ + "test_raises_type_error()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de5b558f", + "metadata": {}, + "outputs": [], + "source": [ + "@nose.tools.raises(Exception)\n", + "def test_that_fails_by_passing():\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e4b455", + "metadata": {}, + "outputs": [], + "source": [ + "test_that_fails_by_passing()" + ] + }, + { + "cell_type": "markdown", + "id": "a83969f8", + "metadata": {}, + "source": [ + "We could reimplement this ourselves now too, using the context manager we wrote above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e821a7d9", + "metadata": {}, + "outputs": [], + "source": [ + "def homemade_raises_decorator(exception):\n", + " def wrap_function(func): # Closure over exception\n", + " # Define a function which runs another function under our \"raises\" context:\n", + " def _output(*args): # Closure over func and exception\n", + " with reimplement_raises(exception):\n", + " func(*args)\n", + " # Return it\n", + " return _output\n", + " return wrap_function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13532171", + "metadata": {}, + "outputs": [], + "source": [ + "@homemade_raises_decorator(TypeError)\n", + "def test_raises_type_error():\n", + " raise TypeError(\"This test passes\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bece5881", + "metadata": {}, + "outputs": [], + "source": [ + "test_raises_type_error()" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Iterators, Generators, Decorators, and Contexts" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/025Iterators.ipynb.py b/ch07dry/025Iterators.ipynb.py new file mode 100644 index 000000000..23b57a8ab --- /dev/null +++ b/ch07dry/025Iterators.ipynb.py @@ -0,0 +1,565 @@ +# --- +# jupyter: +# jekyll: +# display_name: Iterators, Generators, Decorators, and Contexts +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Iterators and Generators + +# %% [markdown] +# In Python, anything which can be iterated over is called an iterable: + +# %% +bowl = { + "apple": 5, + "banana": 3, + "orange": 7 +} + +for fruit in bowl: + print(fruit.upper()) + +# %% [markdown] +# Surprisingly often, we want to iterate over something that takes a moderately +# large amount of memory to store - for example, our map images in the +# green-graph example. +# +# Our green-graph example involved making an array of all the maps between London +# and Birmingham. This kept them all in memory *at the same time*: first we +# downloaded all the maps, then we counted the green pixels in each of them. +# +# This would NOT work if we used more points: eventually, we would run out of memory. +# We need to use a **generator** instead. This chapter will look at iterators and generators in more detail: +# how they work, when to use them, how to create our own. + +# %% [markdown] +# ### Iterators + +# %% [markdown] +# Consider the basic python `range` function: + +# %% +range(10) + +# %% +total = 0 +for x in range(int(1e6)): + total += x + +total + +# %% [markdown] +# In order to avoid allocating a million integers, `range` actually uses an **iterator**. +# +# We don't actually need a million integers *at once*, just each +# integer *in turn* up to a million. +# +# Because we can get an iterator from it, we say that a range is an **iterable**. + +# %% [markdown] +# So we can `for`-loop over it: + +# %% +for i in range(3): + print(i) + +# %% [markdown] +# There are two important Python built-in functions for working with iterables. +# First is `iter`, which lets us create an iterator from any iterable object. + +# %% +a = iter(range(3)) + +# %% [markdown] +# Once we have an iterator object, we can pass it to the `next` function. This +# moves the iterator forward, and gives us its next element: + +# %% +next(a) + +# %% +next(a) + +# %% +next(a) + +# %% [markdown] +# When we are out of elements, a `StopIteration` exception is raised: + +# %% +next(a) + +# %% [markdown] +# This tells Python that the iteration is over. For example, if we are in a `for i in range(3)` loop, this lets us know when we should exit the loop. + +# %% [markdown] +# We can turn an iterable or iterator into a list with the `list` constructor function: + +# %% +list(range(5)) + + +# %% [markdown] +# ### Defining Our Own Iterable + +# %% [markdown] +# When we write `next(a)`, under the hood Python tries to call the `__next__()` method of `a`. Similarly, `iter(a)` calls `a.__iter__()`. +# +# We can make our own iterators by defining *classes* that can be used with the `next()` and `iter()` functions: this is the **iterator protocol**. +# +# For each of the *concepts* in Python, like sequence, container, iterable, the language defines a *protocol*, a set of methods a class must implement, in order to be treated as a member of that concept. +# +# To define an iterator, the methods that must be supported are `__next__()` and `__iter__()`. +# +# `__next__()` must update the iterator. +# +# We'll see why we need to define `__iter__` in a moment. + +# %% [markdown] +# Here is an example of defining a custom iterator class: + +# %% +class fib_iterator: + """An iterator over part of the Fibonacci sequence.""" + + def __init__(self, limit, seed1=1, seed2=1): + self.limit = limit + self.previous = seed1 + self.current = seed2 + + def __iter__(self): + return self + + def __next__(self): + (self.previous, self.current) = (self.current, self.previous + self.current) + self.limit -= 1 + if self.limit < 0: + raise StopIteration() + return self.current + + +# %% +x = fib_iterator(5) + +# %% +next(x) + +# %% +next(x) + +# %% +next(x) + +# %% +next(x) + +# %% +for x in fib_iterator(5): + print(x) + +# %% +sum(fib_iterator(1000)) + +# %% [markdown] +# ### A shortcut to iterables: the `__iter__` method + +# %% [markdown] +# In fact, we don't always have to define both `__iter__` and `__next__`! +# +# If, to be iterated over, a class just wants to behave as if it were some other iterable, you can just implement `__iter__` and return `iter(some_other_iterable)`, without implementing `next`. For example, an image class might want to implement some metadata, but behave just as if it were just a 1-d pixel array when being iterated: + +# %% +from numpy import array +from matplotlib import pyplot as plt + + +class MyImage(object): + def __init__(self, pixels): + self.pixels = array(pixels, dtype='uint8') + self.channels = self.pixels.shape[2] + + def __iter__(self): + # return an iterator over just the pixel values + return iter(self.pixels.reshape(-1, self.channels)) + + def show(self): + plt.imshow(self.pixels, interpolation="None") + + +x = [[[255, 255, 0], [0, 255, 0]], [[0, 0, 255], [255, 255, 255]]] +image = MyImage(x) + +# %% +# %matplotlib inline +image.show() + +# %% +image.channels + +# %% +from webcolors import rgb_to_name +for pixel in image: + print(rgb_to_name(pixel)) + + +# %% [markdown] +# See how we used `image` in a `for` loop, even though it doesn't satisfy the iterator protocol (we didn't define both `__iter__` and `__next__` for it)? +# +# The key here is that we can use any *iterable* object (like `image`) in a `for` expression, +# not just iterators! Internally, Python will create an iterator from the iterable (by calling its `__iter__` method), but this means we don't need to define a `__next__` method explicitly. + +# %% [markdown] +# The *iterator* protocol is to implement both `__iter__` and +# `__next__`, while the *iterable* protocol is to implement `__iter__` and return +# an iterator. + +# %% [markdown] +# ### Generators + +# %% [markdown] +# There's a fair amount of "boiler-plate" in the above class-based definition of +# an iterable. +# +# Python provides another way to specify something +# which meets the iterator protocol: **generators**. + +# %% +def my_generator(): + yield 5 + yield 10 + + +x = my_generator() + +# %% +next(x) + +# %% +next(x) + +# %% +next(x) + +# %% +for a in my_generator(): + print(a) + +# %% +sum(my_generator()) + + +# %% [markdown] +# A function which has `yield` statements instead of a `return` statement returns +# **temporarily**: it automagically becomes something which implements `__next__`. + +# %% [markdown] +# Each call of `next()` returns control to the function where it +# left off. + +# %% [markdown] +# Control passes back-and-forth between the generator and the caller. +# Our Fibonacci example therefore becomes a function rather than a class. + +# %% +def yield_fibs(limit, seed1=1, seed2=1): + current = seed1 + previous = seed2 + + while limit > 0: + limit -= 1 + current, previous = current + previous, current + yield current + + +# %% [markdown] +# We can now use the output of the function like a normal iterable: + +# %% +sum(yield_fibs(5)) + +# %% +for a in yield_fibs(10): + if a % 2 == 0: + print(a) + +# %% [markdown] +# Sometimes we may need to gather all values from a generator into a list, such as before passing them to a function that expects a list: + +# %% +list(yield_fibs(10)) + +# %% +plt.plot(list(yield_fibs(20))) + +# %% [markdown] +# ## Related Concepts +# +# Iterables and generators can be used to achieve complex behaviour, especially when combined with functional programming. In fact, Python itself contains some very useful language features that make use of these practices: context managers and decorators. We have already seen these in this class, but here we discuss them in more detail. + +# %% [markdown] +# ### Context managers + +# %% [markdown] +# [We have seen before](../ch02data/060files.html#Closing-files) [[notebook](../ch02data/060files.ipynb#Closing-files)] that, instead of separately `open`ing and `close`ing a file, we can have +# the file be automatically closed using a context manager: + +# %% +# %%writefile example.yaml +modelname: brilliant + +# %% +import yaml + +with open('example.yaml') as foo: + print(yaml.safe_load(foo)) + + +# %% [markdown] +# In addition to more convenient syntax, this takes care of any clean-up that has to be done after the file is closed, even if any errors occur while we are working on the file. + +# %% [markdown] +# +# +# +# How could we define our own one of these, if we too have clean-up code we +# always want to run after a calling function has done its work, or set-up code +# we want to do first? +# +# We can define a class that meets an appropriate protocol: +# +# +# + +# %% +class verbose_context(): + def __init__(self, name): + self.name=name + def __enter__(self): + print("Get ready, ", self.name) + def __exit__(self, exc_type, exc_value, traceback): + print("OK, done") + +with verbose_context("Monty"): + print("Doing it!") + +# %% [markdown] +# +# +# However, this is pretty verbose! Again, a generator with `yield` makes for an easier syntax: +# +# +# + +# %% +from contextlib import contextmanager + +@contextmanager +def verbose_context(name): + print("Get ready for action, ", name) + yield name.upper() + print("You did it") + +with verbose_context("Monty") as shouty: + print(f"Doing it, {shouty}") + +# %% [markdown] +# +# +# Again, we use `yield` to temporarily return from a function. +# + +# %% [markdown] +# ### Decorators + +# %% [markdown] +# +# When doing functional programming, we may often want to define mutator +# functions which take in one function and return a new function, such as our +# derivative example earlier. +# +# +# + +# %% +from math import sqrt + + +def repeater(count): + def wrap_function_in_repeat(func): + + def _repeated(x): + counter = count + while counter > 0: + counter -= 1 + x = func(x) + return x + + return _repeated + return wrap_function_in_repeat + + +fiftytimes = repeater(50) + +fiftyroots = fiftytimes(sqrt) + +print(fiftyroots(100)) + + +# %% [markdown] +# It turns out that, quite often, we want to apply one of these to a function as we're defining a class. +# For example, we may want to specify that after certain methods are called, data should always be stored: + +# %% [markdown] +# Any function which accepts a function as its first argument and returns a function can be used as a **decorator** like this. +# +# Much of Python's standard functionality is implemented as decorators: we've +# seen @contextmanager, @classmethod and @attribute. The @contextmanager +# metafunction, for example, takes in an iterator, and yields a class conforming +# to the context manager protocol. +# + +# %% +@repeater(3) +def hello(name): + return f"Hello, {name}" + + +# %% +hello("Cleese") + + +# %% [markdown] +# ## Supplementary material +# +# The remainder of this page contains an example of the flexibility of the features discussed above. Specifically, it shows how generators and context managers can be combined to create a testing framework like the one previously seen in the course. + +# %% [markdown] +# ### Test generators +# +# +# A few weeks ago we saw a test which loaded its test cases from a YAML file and +# asserted each input with each output. This was nice and concise, but had one +# flaw: we had just one test, covering all the fixtures, so we got just one . in +# the test output when we ran the tests, and if any test failed, the rest were +# not run. We can do a nicer job with a test **generator**: +# +# +# +# + +# %% +def assert_exemplar(**fixture): + answer = fixture.pop('answer') + assert_equal(greet(**fixture), answer) + + +def test_greeter(): + with open(os.path.join(os.path.dirname( + __file__), 'fixtures', 'samples.yaml') + ) as fixtures_file: + fixtures = yaml.safe_load(fixtures_file) + + for fixture in fixtures: + + yield assert_exemplar(**fixture) + + +# %% [markdown] +# Each time a function beginning with `test_` does a `yield` it results in another test. + +# %% [markdown] +# ### Negative test contexts managers + +# %% [markdown] +# We have seen this: + +# %% +from pytest import raises + +with raises(AttributeError): + x = 2 + x.foo() + +# %% [markdown] +# We can now see how `pytest` might have implemented this: + +# %% +from contextlib import contextmanager + + +@contextmanager +def reimplement_raises(exception): + try: + yield + except exception: + pass + else: + raise Exception("Expected,", exception, + " to be raised, nothing was.") + + +# %% +with reimplement_raises(AttributeError): + x = 2 + x.foo() + +# %% [markdown] +# ### Negative test decorators + +# %% [markdown] +# Some frameworks, like `nose`, also implement a very nice negative test decorator, which lets us marks tests that we know should produce an exception: + +# %% +import nose + + +@nose.tools.raises(TypeError, ValueError) +def test_raises_type_error(): + raise TypeError("This test passes") + + +# %% +test_raises_type_error() + + +# %% +@nose.tools.raises(Exception) +def test_that_fails_by_passing(): + pass + + +# %% +test_that_fails_by_passing() + + +# %% [markdown] +# We could reimplement this ourselves now too, using the context manager we wrote above: + +# %% +def homemade_raises_decorator(exception): + def wrap_function(func): # Closure over exception + # Define a function which runs another function under our "raises" context: + def _output(*args): # Closure over func and exception + with reimplement_raises(exception): + func(*args) + # Return it + return _output + return wrap_function + + +# %% +@homemade_raises_decorator(TypeError) +def test_raises_type_error(): + raise TypeError("This test passes") + + +# %% +test_raises_type_error() diff --git a/ch07dry/040Exceptions.html b/ch07dry/040Exceptions.html new file mode 100644 index 000000000..f72f0c506 --- /dev/null +++ b/ch07dry/040Exceptions.html @@ -0,0 +1,1281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exceptions + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Exceptions

+
+
+
+
+
+
+

When we learned about testing, we saw that Python complains when things go wrong by raising an "Exception" naming a type of error:

+
+
+
+
+
+
In [1]:
+
+
+
1/0
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ZeroDivisionError                         Traceback (most recent call last)
+Cell In[1], line 1
+----> 1 1/0
+
+ZeroDivisionError: division by zero
+
+
+
+
+
+
+
+
+

Exceptions are objects, forming a class hierarchy. We just raised an instance +of the ZeroDivisionError class, making the program crash. If we want more +information about where this class fits in the hierarchy, we can use Python's +inspect module to get a chain of classes, from ZeroDivisionError up to object:

+
+
+
+
+
+
In [2]:
+
+
+
import inspect
+inspect.getmro(ZeroDivisionError)
+
+
+
+
+
+
+
+
Out[2]:
+
+
(ZeroDivisionError, ArithmeticError, Exception, BaseException, object)
+
+
+
+
+
+
+
+
+

So we can see that a zero division error is a particular kind of Arithmetic Error.

+
+
+
+
+
+
In [3]:
+
+
+
x = 1
+
+for y in x:
+    print(y)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[3], line 3
+      1 x = 1
+----> 3 for y in x:
+      4     print(y)
+
+TypeError: 'int' object is not iterable
+
+
+
+
+
+
+
+
In [4]:
+
+
+
inspect.getmro(TypeError)
+
+
+
+
+
+
+
+
Out[4]:
+
+
(TypeError, Exception, BaseException, object)
+
+
+
+
+
+
+
+
+

Create your own Exception

+
+
+
+
+
+
+

When we were looking at testing, we saw that it is important for code to crash with a meaningful exception type when something is wrong. +We raise an Exception with raise. Often, we can look for an appropriate exception from the standard set to raise.

+

However, we may want to define our own exceptions. Doing this is as simple as inheriting from Exception (or one of its subclasses):

+
+
+
+
+
+
In [5]:
+
+
+
class MyCustomErrorType(ArithmeticError):
+    pass
+
+
+raise(MyCustomErrorType("Problem"))
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+MyCustomErrorType                         Traceback (most recent call last)
+Cell In[5], line 5
+      1 class MyCustomErrorType(ArithmeticError):
+      2     pass
+----> 5 raise(MyCustomErrorType("Problem"))
+
+MyCustomErrorType: Problem
+
+
+
+
+
+
+
+
+

You can add custom data to your exception:

+
+
+
+
+
+
In [6]:
+
+
+
class MyCustomErrorType(Exception):
+    def __init__(self, category=None):
+        self.category = category
+
+    def __str__(self):
+        return f"Error, category {self.category}"
+
+
+raise(MyCustomErrorType(404))
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+MyCustomErrorType                         Traceback (most recent call last)
+Cell In[6], line 9
+      5     def __str__(self):
+      6         return f"Error, category {self.category}"
+----> 9 raise(MyCustomErrorType(404))
+
+MyCustomErrorType: Error, category 404
+
+
+
+
+
+
+
+
+

The real power of exceptions comes, however, not in letting them crash the program, but in letting your program handle them. We say that an exception has been "thrown" and then "caught".

+
+
+
+
+
+
In [7]:
+
+
+
import yaml
+
+try:
+    config = yaml.safe_load(open("datasource.yaml"))
+    user = config["userid"]
+    password = config["password"]
+
+except FileNotFoundError:
+    print("No password file found, using anonymous user.")
+    user = "anonymous"
+    password = None
+
+
+print(user)
+
+
+
+
+
+
+
+
+
+
No password file found, using anonymous user.
+anonymous
+
+
+
+
+
+
+
+
+
+

Note that we specify only the error we expect to happen and want to handle. Sometimes you see code that catches everything:

+
+
+
+
+
+
In [8]:
+
+
+
try:
+    config = yaml.lod(open("datasource.yaml"))
+    user = config["userid"]
+    password = config["password"]
+except:
+    user = "anonymous"
+    password = None
+
+print(user)
+
+
+
+
+
+
+
+
+
+
anonymous
+
+
+
+
+
+
+
+
+
+

This can be dangerous and can make it hard to find errors! There was a mistyped function name there ('lod'), but we did not notice the error, as the generic except caught it. +Therefore, we should be specific and catch only the type of error we want.

+
+
+
+
+
+
+

Managing multiple exceptions

+
+
+
+
+
+
+

Let's create two credential files to read

+
+
+
+
+
+
In [9]:
+
+
+
with open('datasource2.yaml', 'w') as outfile:
+    outfile.write('userid: eidle\n')
+    outfile.write('password: secret\n')
+
+with open('datasource3.yaml', 'w') as outfile:
+    outfile.write('user: eidle\n')
+    outfile.write('password: secret\n')
+
+
+
+
+
+
+
+
+

And create a function that reads credentials files and returns the username and password to use.

+
+
+
+
+
+
In [10]:
+
+
+
def read_credentials(source):
+    try:
+        datasource = open(source)
+        config = yaml.safe_load(datasource)
+        user = config["userid"]
+        password = config["password"]
+        datasource.close()
+    except FileNotFoundError:
+        print("Password file missing")
+        user = "anonymous"
+        password = None
+    except KeyError:
+        print("Expected keys not found in file")
+        user = "anonymous"
+        password = None
+    return user, password
+
+
+
+
+
+
+
+
In [11]:
+
+
+
print(read_credentials('datasource2.yaml'))
+
+
+
+
+
+
+
+
+
+
('eidle', 'secret')
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
print(read_credentials('datasource.yaml'))
+
+
+
+
+
+
+
+
+
+
Password file missing
+('anonymous', None)
+
+
+
+
+
+
+
+
+
In [13]:
+
+
+
print(read_credentials('datasource3.yaml'))
+
+
+
+
+
+
+
+
+
+
Expected keys not found in file
+('anonymous', None)
+
+
+
+
+
+
+
+
+
+

This last code has a flaw: the file was successfully opened, the missing key was noticed, but not explicitly closed. It's normally OK, as Python will close the file as soon as it notices there are no longer any references to datasource in memory, after the function exits. But this is not good practice, you should keep a file handle for as short a time as possible.

+
+
+
+
+
+
In [14]:
+
+
+
def read_credentials(source):
+    try:
+        datasource = open(source)
+        config = yaml.safe_load(datasource)
+        user = config["userid"]
+        password = config["password"]
+    except FileNotFoundError:
+        user = "anonymous"
+        password = None
+    finally:
+        datasource.close()
+
+    return user, password
+
+
+
+
+
+
+
+
+

The finally clause is executed whether or not an exception occurs.

+

The last optional clause of a try statement, an else clause is called only if an exception is NOT raised. It can be a better place than the try clause to put code other than that which you expect to raise the error, and which you do not want to be executed if the error is raised. It is executed in the same circumstances as code put in the end of the try block, the only difference is that errors raised during the else clause are not caught. Don't worry if this seems useless to you; most languages' implementations of try/except don't support such a clause.

+
+
+
+
+
+
In [15]:
+
+
+
def read_credentials(source):
+    try:
+        datasource = open(source)
+    except FileNotFoundError:
+        user = "anonymous"
+        password = None
+    else:
+        config = yaml.safe_load(datasource)
+        user = config["userid"]
+        password = config["password"]
+    finally:
+        datasource.close()
+    return user, password
+
+
+
+
+
+
+
+
+

Exceptions do not have to be caught close to the part of the program calling +them. They can be caught anywhere "above" the calling point in +the call stack: control can jump arbitrarily far in the program: up to the except clause of the "highest" containing try statement.

+
+
+
+
+
+
In [16]:
+
+
+
def f4(x):
+    if x == 0:
+        return
+    if x == 1:
+        raise ArithmeticError()
+    if x == 2:
+        raise SyntaxError()
+    if x == 3:
+        raise TypeError()
+
+
+
+
+
+
+
+
In [17]:
+
+
+
def f3(x):
+    try:
+        print("F3Before")
+        f4(x)
+        print("F3After")
+    except ArithmeticError:
+        print("F3Except (💣)")
+
+
+
+
+
+
+
+
In [18]:
+
+
+
def f2(x):
+    try:
+        print("F2Before")
+        f3(x)
+        print("F2After")
+    except SyntaxError:
+        print("F2Except (💣)")
+
+
+
+
+
+
+
+
In [19]:
+
+
+
def f1(x):
+    try:
+        print("F1Before")
+        f2(x)
+        print("F1After")
+    except TypeError:
+        print("F1Except (💣)")
+
+
+
+
+
+
+
+
In [20]:
+
+
+
f1(0)
+
+
+
+
+
+
+
+
+
+
F1Before
+F2Before
+F3Before
+F3After
+F2After
+F1After
+
+
+
+
+
+
+
+
+
In [21]:
+
+
+
f1(1)
+
+
+
+
+
+
+
+
+
+
F1Before
+F2Before
+F3Before
+F3Except (💣)
+F2After
+F1After
+
+
+
+
+
+
+
+
+
In [22]:
+
+
+
f1(2)
+
+
+
+
+
+
+
+
+
+
F1Before
+F2Before
+F3Before
+F2Except (💣)
+F1After
+
+
+
+
+
+
+
+
+
In [23]:
+
+
+
f1(3)
+
+
+
+
+
+
+
+
+
+
F1Before
+F2Before
+F3Before
+F1Except (💣)
+
+
+
+
+
+
+
+
+
+

Design with Exceptions

+
+
+
+
+
+
+

Now we know how exceptions work, we need to think about the design implications... How best to use them.

+

Traditional software design theory will tell you that they should only be used +to describe and recover from exceptional conditions: things going wrong. +Normal program flow shouldn't use them.

+

Python's designers take a different view: use of exceptions in normal flow is +considered OK. For example, all iterators raise a StopIteration exception to +indicate the iteration is complete.

+

A commonly recommended Python design pattern is to use exceptions to determine +whether an object implements a protocol (concept/interface), rather than testing +on type.

+

For example, we might want a function which can be supplied either a data +series or a path to a location on disk where data can be found. We can +examine the type of the supplied content:

+
+
+
+
+
+
In [24]:
+
+
+
import yaml
+
+
+def analysis(source):
+    if type(source) == dict:
+        name = source['modelname']
+    else:
+        content = open(source)
+        source = yaml.safe_load(content)
+        name = source['modelname']
+    print(name)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
analysis({'modelname': 'Super'})
+
+
+
+
+
+
+
+
+
+
Super
+
+
+
+
+
+
+
+
+
In [26]:
+
+
+
with open('example.yaml', 'w') as outfile:
+    outfile.write('modelname: brilliant\n')
+
+
+
+
+
+
+
+
In [27]:
+
+
+
analysis('example.yaml')
+
+
+
+
+
+
+
+
+
+
brilliant
+
+
+
+
+
+
+
+
+
+

However, we can also use the try-it-and-handle-exceptions approach to this.

+
+
+
+
+
+
In [28]:
+
+
+
def analysis(source):
+    try:
+        name = source['modelname']
+    except TypeError:
+        content = open(source)
+        source = yaml.safe_load(content)
+        name = source['modelname']
+    print(name)
+
+
+analysis('example.yaml')
+
+
+
+
+
+
+
+
+
+
brilliant
+
+
+
+
+
+
+
+
+
+

This approach is more extensible, and behaves properly if we give it some +other data-source which responds like a dictionary or string.

+
+
+
+
+
+
In [29]:
+
+
+
def analysis(source):
+    try:
+        name = source['modelname']
+    except TypeError:
+        # Source was not a dictionary-like object
+        # Maybe it is a file path
+        try:
+            content = open(source)
+            source = yaml.safe_load(content)
+            name = source['modelname']
+        except IOError:
+            # Maybe it was already raw YAML content
+            source = yaml.safe_load(source)
+            name = source['modelname']
+    print(name)
+
+
+analysis("modelname: Amazing")
+
+
+
+
+
+
+
+
+
+
Amazing
+
+
+
+
+
+
+
+
+
+

Sometimes we want to catch an error, partially handle it, perhaps add some +extra data to the exception, and then re-raise to be caught again further up +the call stack.

+

The keyword "raise" with no argument in an except: clause will cause the +caught error to be re-thrown. Doing this is the only circumstance where it is +safe to do except: without catching a specific type of error.

+
+
+
+
+
+
In [30]:
+
+
+
try:
+    # Something
+    pass
+except:
+    # Do this code here if anything goes wrong
+    raise
+
+
+
+
+
+
+
+
+

If you want to be more explicit about where the error came from, you can use the raise from syntax, which will create a chain of exceptions:

+
+
+
+
+
+
In [31]:
+
+
+
def lower_function():
+    raise ValueError("Error in lower function!")
+
+
+def higher_function():
+    try:
+        lower_function()
+    except ValueError as e:
+        raise RuntimeError("Error in higher function!") from e
+
+
+higher_function()
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[31], line 7, in higher_function()
+      6 try:
+----> 7     lower_function()
+      8 except ValueError as e:
+
+Cell In[31], line 2, in lower_function()
+      1 def lower_function():
+----> 2     raise ValueError("Error in lower function!")
+
+ValueError: Error in lower function!
+
+The above exception was the direct cause of the following exception:
+
+RuntimeError                              Traceback (most recent call last)
+Cell In[31], line 12
+      8     except ValueError as e:
+      9         raise RuntimeError("Error in higher function!") from e
+---> 12 higher_function()
+
+Cell In[31], line 9, in higher_function()
+      7     lower_function()
+      8 except ValueError as e:
+----> 9     raise RuntimeError("Error in higher function!") from e
+
+RuntimeError: Error in higher function!
+
+
+
+
+
+
+
+
+

It can be useful to catch and re-throw an error as you go up the chain, doing any clean-up needed for each layer of a program.

+

The error will finally be caught and not re-thrown only at a higher program +layer that knows how to recover. This is known as the "throw low catch high" +principle.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/040Exceptions.ipynb b/ch07dry/040Exceptions.ipynb new file mode 100644 index 000000000..9ede2308c --- /dev/null +++ b/ch07dry/040Exceptions.ipynb @@ -0,0 +1,742 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "17cfbce4", + "metadata": {}, + "source": [ + "## Exceptions" + ] + }, + { + "cell_type": "markdown", + "id": "1a1c0f02", + "metadata": {}, + "source": [ + "\n", + "When we learned about testing, we saw that Python complains when things go wrong by raising an \"Exception\" naming a type of error:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e7093ba", + "metadata": {}, + "outputs": [], + "source": [ + "1/0" + ] + }, + { + "cell_type": "markdown", + "id": "06c65f28", + "metadata": {}, + "source": [ + "Exceptions are objects, forming a [class hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy). We just raised an instance\n", + "of the `ZeroDivisionError` class, making the program crash. If we want more\n", + "information about where this class fits in the hierarchy, we can use [Python's\n", + "`inspect` module](https://docs.python.org/3/library/inspect.html) to get a chain of classes, from `ZeroDivisionError` up to `object`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32a59bcf", + "metadata": {}, + "outputs": [], + "source": [ + "import inspect\n", + "inspect.getmro(ZeroDivisionError)" + ] + }, + { + "cell_type": "markdown", + "id": "9f6a0c11", + "metadata": {}, + "source": [ + "\n", + "\n", + "So we can see that a zero division error is a particular kind of Arithmetic Error.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79de95f9", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n", + "\n", + "for y in x:\n", + " print(y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19ace4d1", + "metadata": {}, + "outputs": [], + "source": [ + "inspect.getmro(TypeError)" + ] + }, + { + "cell_type": "markdown", + "id": "643d4922", + "metadata": {}, + "source": [ + "### Create your own Exception" + ] + }, + { + "cell_type": "markdown", + "id": "9e0fc645", + "metadata": {}, + "source": [ + "When we were looking at testing, we saw that it is important for code to crash with a meaningful exception type when something is wrong.\n", + "We raise an Exception with `raise`. Often, we can look for an appropriate exception from the standard set to raise. \n", + "\n", + "However, we may want to define our own exceptions. Doing this is as simple as inheriting from Exception (or one of its subclasses):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ca01064", + "metadata": {}, + "outputs": [], + "source": [ + "class MyCustomErrorType(ArithmeticError):\n", + " pass\n", + "\n", + "\n", + "raise(MyCustomErrorType(\"Problem\"))" + ] + }, + { + "cell_type": "markdown", + "id": "ace69c52", + "metadata": {}, + "source": [ + "\n", + "\n", + "You can add custom data to your exception:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42beeb0a", + "metadata": {}, + "outputs": [], + "source": [ + "class MyCustomErrorType(Exception):\n", + " def __init__(self, category=None):\n", + " self.category = category\n", + "\n", + " def __str__(self):\n", + " return f\"Error, category {self.category}\"\n", + "\n", + "\n", + "raise(MyCustomErrorType(404))" + ] + }, + { + "cell_type": "markdown", + "id": "1f18bfdf", + "metadata": {}, + "source": [ + "\n", + "\n", + "The real power of exceptions comes, however, not in letting them crash the program, but in letting your program handle them. We say that an exception has been \"thrown\" and then \"caught\".\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db1b6466", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "try:\n", + " config = yaml.safe_load(open(\"datasource.yaml\"))\n", + " user = config[\"userid\"]\n", + " password = config[\"password\"]\n", + "\n", + "except FileNotFoundError:\n", + " print(\"No password file found, using anonymous user.\")\n", + " user = \"anonymous\"\n", + " password = None\n", + "\n", + "\n", + "print(user)" + ] + }, + { + "cell_type": "markdown", + "id": "4cf64403", + "metadata": {}, + "source": [ + "\n", + "\n", + "Note that we specify only the error we expect to happen and want to handle. Sometimes you see code that catches everything:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01c7e456", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " config = yaml.lod(open(\"datasource.yaml\"))\n", + " user = config[\"userid\"]\n", + " password = config[\"password\"]\n", + "except:\n", + " user = \"anonymous\"\n", + " password = None\n", + "\n", + "print(user)" + ] + }, + { + "cell_type": "markdown", + "id": "423d775b", + "metadata": {}, + "source": [ + "This can be dangerous and can make it hard to find errors! There was a mistyped function name there ('`lod`'), but we did not notice the error, as the generic except caught it. \n", + "Therefore, we should be specific and catch only the type of error we want." + ] + }, + { + "cell_type": "markdown", + "id": "1b969c77", + "metadata": {}, + "source": [ + "### Managing multiple exceptions" + ] + }, + { + "cell_type": "markdown", + "id": "487123c6", + "metadata": {}, + "source": [ + "Let's create two credential files to read" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f39bc206", + "metadata": {}, + "outputs": [], + "source": [ + "with open('datasource2.yaml', 'w') as outfile:\n", + " outfile.write('userid: eidle\\n')\n", + " outfile.write('password: secret\\n')\n", + "\n", + "with open('datasource3.yaml', 'w') as outfile:\n", + " outfile.write('user: eidle\\n')\n", + " outfile.write('password: secret\\n')" + ] + }, + { + "cell_type": "markdown", + "id": "7f1aa2a7", + "metadata": {}, + "source": [ + "And create a function that reads credentials files and returns the username and password to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17bc7d34", + "metadata": {}, + "outputs": [], + "source": [ + "def read_credentials(source):\n", + " try:\n", + " datasource = open(source)\n", + " config = yaml.safe_load(datasource)\n", + " user = config[\"userid\"]\n", + " password = config[\"password\"]\n", + " datasource.close()\n", + " except FileNotFoundError:\n", + " print(\"Password file missing\")\n", + " user = \"anonymous\"\n", + " password = None\n", + " except KeyError:\n", + " print(\"Expected keys not found in file\")\n", + " user = \"anonymous\"\n", + " password = None\n", + " return user, password" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33c81573", + "metadata": {}, + "outputs": [], + "source": [ + "print(read_credentials('datasource2.yaml'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62ad11e4", + "metadata": {}, + "outputs": [], + "source": [ + "print(read_credentials('datasource.yaml'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d3bc091", + "metadata": {}, + "outputs": [], + "source": [ + "print(read_credentials('datasource3.yaml'))" + ] + }, + { + "cell_type": "markdown", + "id": "ed1ebe87", + "metadata": {}, + "source": [ + "This last code has a flaw: the file was successfully opened, the missing key was noticed, but not explicitly closed. It's normally OK, as Python will close the file as soon as it notices there are no longer any references to datasource in memory, after the function exits. But this is not good practice, you should keep a file handle for as short a time as possible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bff7c7e", + "metadata": {}, + "outputs": [], + "source": [ + "def read_credentials(source):\n", + " try:\n", + " datasource = open(source)\n", + " config = yaml.safe_load(datasource)\n", + " user = config[\"userid\"]\n", + " password = config[\"password\"]\n", + " except FileNotFoundError:\n", + " user = \"anonymous\"\n", + " password = None\n", + " finally:\n", + " datasource.close()\n", + "\n", + " return user, password" + ] + }, + { + "cell_type": "markdown", + "id": "1e8f497c", + "metadata": {}, + "source": [ + "The `finally` clause is executed whether or not an exception occurs.\n", + "\n", + "The last optional clause of a `try` statement, an `else` clause is called only if an exception is NOT raised. It can be a better place than the `try` clause to put code other than that which you expect to raise the error, and which you do not want to be executed if the error is raised. It is executed in the same circumstances as code put in the end of the `try` block, the only difference is that errors raised during the `else` clause are not caught. Don't worry if this seems useless to you; most languages' implementations of try/except don't support such a clause." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ccd1d8a", + "metadata": {}, + "outputs": [], + "source": [ + "def read_credentials(source):\n", + " try:\n", + " datasource = open(source)\n", + " except FileNotFoundError:\n", + " user = \"anonymous\"\n", + " password = None\n", + " else:\n", + " config = yaml.safe_load(datasource)\n", + " user = config[\"userid\"]\n", + " password = config[\"password\"]\n", + " finally:\n", + " datasource.close()\n", + " return user, password" + ] + }, + { + "cell_type": "markdown", + "id": "80d67800", + "metadata": {}, + "source": [ + "\n", + "\n", + "Exceptions do not have to be caught close to the part of the program calling\n", + "them. They can be caught anywhere \"above\" the calling point in\n", + "the call stack: control can jump arbitrarily far in the program: up to the `except` clause of the \"highest\" containing try statement.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7397cf58", + "metadata": {}, + "outputs": [], + "source": [ + "def f4(x):\n", + " if x == 0:\n", + " return\n", + " if x == 1:\n", + " raise ArithmeticError()\n", + " if x == 2:\n", + " raise SyntaxError()\n", + " if x == 3:\n", + " raise TypeError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13c50e41", + "metadata": {}, + "outputs": [], + "source": [ + "def f3(x):\n", + " try:\n", + " print(\"F3Before\")\n", + " f4(x)\n", + " print(\"F3After\")\n", + " except ArithmeticError:\n", + " print(\"F3Except (💣)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f2c09ce", + "metadata": {}, + "outputs": [], + "source": [ + "def f2(x):\n", + " try:\n", + " print(\"F2Before\")\n", + " f3(x)\n", + " print(\"F2After\")\n", + " except SyntaxError:\n", + " print(\"F2Except (💣)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "298cb539", + "metadata": {}, + "outputs": [], + "source": [ + "def f1(x):\n", + " try:\n", + " print(\"F1Before\")\n", + " f2(x)\n", + " print(\"F1After\")\n", + " except TypeError:\n", + " print(\"F1Except (💣)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "930fc106", + "metadata": {}, + "outputs": [], + "source": [ + "f1(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00bb5618", + "metadata": {}, + "outputs": [], + "source": [ + "f1(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08288884", + "metadata": {}, + "outputs": [], + "source": [ + "f1(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d06d42d", + "metadata": {}, + "outputs": [], + "source": [ + "f1(3)" + ] + }, + { + "cell_type": "markdown", + "id": "7ba9cab4", + "metadata": {}, + "source": [ + "### Design with Exceptions" + ] + }, + { + "cell_type": "markdown", + "id": "cc5f8bc0", + "metadata": {}, + "source": [ + "\n", + "Now we know how exceptions work, we need to think about the design implications... How best to use them.\n", + "\n", + "Traditional software design theory will tell you that they should only be used\n", + "to describe and recover from **exceptional** conditions: things going wrong.\n", + "Normal program flow shouldn't use them.\n", + "\n", + "Python's designers take a different view: use of exceptions in normal flow is\n", + "considered OK. For example, all iterators raise a `StopIteration` exception to\n", + "indicate the iteration is complete.\n", + "\n", + "A commonly recommended Python design pattern is to use exceptions to determine\n", + "whether an object implements a protocol (concept/interface), rather than testing\n", + "on type.\n", + "\n", + "For example, we might want a function which can be supplied *either* a data\n", + "series *or* a path to a location on disk where data can be found. We can\n", + "examine the type of the supplied content:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d658b1bd", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "\n", + "def analysis(source):\n", + " if type(source) == dict:\n", + " name = source['modelname']\n", + " else:\n", + " content = open(source)\n", + " source = yaml.safe_load(content)\n", + " name = source['modelname']\n", + " print(name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c779c1e", + "metadata": {}, + "outputs": [], + "source": [ + "analysis({'modelname': 'Super'})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c2c1fca", + "metadata": {}, + "outputs": [], + "source": [ + "with open('example.yaml', 'w') as outfile:\n", + " outfile.write('modelname: brilliant\\n')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76333c9e", + "metadata": {}, + "outputs": [], + "source": [ + "analysis('example.yaml')" + ] + }, + { + "cell_type": "markdown", + "id": "0e532268", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "However, we can also use the try-it-and-handle-exceptions approach to this. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f82abf1e", + "metadata": {}, + "outputs": [], + "source": [ + "def analysis(source):\n", + " try:\n", + " name = source['modelname']\n", + " except TypeError:\n", + " content = open(source)\n", + " source = yaml.safe_load(content)\n", + " name = source['modelname']\n", + " print(name)\n", + "\n", + "\n", + "analysis('example.yaml')" + ] + }, + { + "cell_type": "markdown", + "id": "a7a51f71", + "metadata": {}, + "source": [ + "This approach is more extensible, and **behaves properly if we give it some\n", + "other data-source which responds like a dictionary or string.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6879f4c1", + "metadata": {}, + "outputs": [], + "source": [ + "def analysis(source):\n", + " try:\n", + " name = source['modelname']\n", + " except TypeError:\n", + " # Source was not a dictionary-like object\n", + " # Maybe it is a file path\n", + " try:\n", + " content = open(source)\n", + " source = yaml.safe_load(content)\n", + " name = source['modelname']\n", + " except IOError:\n", + " # Maybe it was already raw YAML content\n", + " source = yaml.safe_load(source)\n", + " name = source['modelname']\n", + " print(name)\n", + "\n", + "\n", + "analysis(\"modelname: Amazing\")" + ] + }, + { + "cell_type": "markdown", + "id": "141887ed", + "metadata": {}, + "source": [ + "Sometimes we want to catch an error, partially handle it, perhaps add some\n", + "extra data to the exception, and then re-raise to be caught again further up\n", + "the call stack. \n", + "\n", + "The keyword \"`raise`\" with no argument in an `except:` clause will cause the\n", + "caught error to be re-thrown. Doing this is the only circumstance where it is\n", + "safe to do `except:` without catching a specific type of error." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fb29691", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " # Something\n", + " pass\n", + "except:\n", + " # Do this code here if anything goes wrong\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "id": "1c304a9d", + "metadata": {}, + "source": [ + "If you want to be more explicit about where the error came from, you can use the `raise from` syntax, which will create a chain of exceptions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28fbe84f", + "metadata": {}, + "outputs": [], + "source": [ + "def lower_function():\n", + " raise ValueError(\"Error in lower function!\")\n", + "\n", + "\n", + "def higher_function():\n", + " try:\n", + " lower_function()\n", + " except ValueError as e:\n", + " raise RuntimeError(\"Error in higher function!\") from e\n", + "\n", + "\n", + "higher_function()" + ] + }, + { + "cell_type": "markdown", + "id": "55631c2b", + "metadata": {}, + "source": [ + "\n", + "\n", + "It can be useful to catch and re-throw an error as you go up the chain, doing any clean-up needed for each layer of a program.\n", + "\n", + "The error will finally be caught and not re-thrown only at a higher program\n", + "layer that knows how to recover. This is known as the \"throw low catch high\"\n", + "principle.\n", + "\n", + "\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Exceptions" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/040Exceptions.ipynb.py b/ch07dry/040Exceptions.ipynb.py new file mode 100644 index 000000000..48ce7e3c1 --- /dev/null +++ b/ch07dry/040Exceptions.ipynb.py @@ -0,0 +1,430 @@ +# --- +# jupyter: +# jekyll: +# display_name: Exceptions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Exceptions + +# %% [markdown] +# +# When we learned about testing, we saw that Python complains when things go wrong by raising an "Exception" naming a type of error: +# +# +# + +# %% +1/0 + +# %% [markdown] +# Exceptions are objects, forming a [class hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy). We just raised an instance +# of the `ZeroDivisionError` class, making the program crash. If we want more +# information about where this class fits in the hierarchy, we can use [Python's +# `inspect` module](https://docs.python.org/3/library/inspect.html) to get a chain of classes, from `ZeroDivisionError` up to `object`: + +# %% +import inspect +inspect.getmro(ZeroDivisionError) + +# %% [markdown] +# +# +# So we can see that a zero division error is a particular kind of Arithmetic Error. +# +# +# + +# %% +x = 1 + +for y in x: + print(y) + +# %% +inspect.getmro(TypeError) + + +# %% [markdown] +# ### Create your own Exception + +# %% [markdown] +# When we were looking at testing, we saw that it is important for code to crash with a meaningful exception type when something is wrong. +# We raise an Exception with `raise`. Often, we can look for an appropriate exception from the standard set to raise. +# +# However, we may want to define our own exceptions. Doing this is as simple as inheriting from Exception (or one of its subclasses): + +# %% +class MyCustomErrorType(ArithmeticError): + pass + + +raise(MyCustomErrorType("Problem")) + + +# %% [markdown] +# +# +# You can add custom data to your exception: +# +# +# + +# %% +class MyCustomErrorType(Exception): + def __init__(self, category=None): + self.category = category + + def __str__(self): + return f"Error, category {self.category}" + + +raise(MyCustomErrorType(404)) + +# %% [markdown] +# +# +# The real power of exceptions comes, however, not in letting them crash the program, but in letting your program handle them. We say that an exception has been "thrown" and then "caught". +# +# +# + +# %% +import yaml + +try: + config = yaml.safe_load(open("datasource.yaml")) + user = config["userid"] + password = config["password"] + +except FileNotFoundError: + print("No password file found, using anonymous user.") + user = "anonymous" + password = None + + +print(user) + +# %% [markdown] +# +# +# Note that we specify only the error we expect to happen and want to handle. Sometimes you see code that catches everything: +# +# +# + +# %% +try: + config = yaml.lod(open("datasource.yaml")) + user = config["userid"] + password = config["password"] +except: + user = "anonymous" + password = None + +print(user) + +# %% [markdown] +# This can be dangerous and can make it hard to find errors! There was a mistyped function name there ('`lod`'), but we did not notice the error, as the generic except caught it. +# Therefore, we should be specific and catch only the type of error we want. + +# %% [markdown] +# ### Managing multiple exceptions + +# %% [markdown] +# Let's create two credential files to read + +# %% +with open('datasource2.yaml', 'w') as outfile: + outfile.write('userid: eidle\n') + outfile.write('password: secret\n') + +with open('datasource3.yaml', 'w') as outfile: + outfile.write('user: eidle\n') + outfile.write('password: secret\n') + + +# %% [markdown] +# And create a function that reads credentials files and returns the username and password to use. + +# %% +def read_credentials(source): + try: + datasource = open(source) + config = yaml.safe_load(datasource) + user = config["userid"] + password = config["password"] + datasource.close() + except FileNotFoundError: + print("Password file missing") + user = "anonymous" + password = None + except KeyError: + print("Expected keys not found in file") + user = "anonymous" + password = None + return user, password + + +# %% +print(read_credentials('datasource2.yaml')) + +# %% +print(read_credentials('datasource.yaml')) + +# %% +print(read_credentials('datasource3.yaml')) + + +# %% [markdown] +# This last code has a flaw: the file was successfully opened, the missing key was noticed, but not explicitly closed. It's normally OK, as Python will close the file as soon as it notices there are no longer any references to datasource in memory, after the function exits. But this is not good practice, you should keep a file handle for as short a time as possible. + +# %% +def read_credentials(source): + try: + datasource = open(source) + config = yaml.safe_load(datasource) + user = config["userid"] + password = config["password"] + except FileNotFoundError: + user = "anonymous" + password = None + finally: + datasource.close() + + return user, password + + +# %% [markdown] +# The `finally` clause is executed whether or not an exception occurs. +# +# The last optional clause of a `try` statement, an `else` clause is called only if an exception is NOT raised. It can be a better place than the `try` clause to put code other than that which you expect to raise the error, and which you do not want to be executed if the error is raised. It is executed in the same circumstances as code put in the end of the `try` block, the only difference is that errors raised during the `else` clause are not caught. Don't worry if this seems useless to you; most languages' implementations of try/except don't support such a clause. + +# %% +def read_credentials(source): + try: + datasource = open(source) + except FileNotFoundError: + user = "anonymous" + password = None + else: + config = yaml.safe_load(datasource) + user = config["userid"] + password = config["password"] + finally: + datasource.close() + return user, password + + +# %% [markdown] +# +# +# Exceptions do not have to be caught close to the part of the program calling +# them. They can be caught anywhere "above" the calling point in +# the call stack: control can jump arbitrarily far in the program: up to the `except` clause of the "highest" containing try statement. +# +# +# + +# %% +def f4(x): + if x == 0: + return + if x == 1: + raise ArithmeticError() + if x == 2: + raise SyntaxError() + if x == 3: + raise TypeError() + + +# %% +def f3(x): + try: + print("F3Before") + f4(x) + print("F3After") + except ArithmeticError: + print("F3Except (💣)") + + +# %% +def f2(x): + try: + print("F2Before") + f3(x) + print("F2After") + except SyntaxError: + print("F2Except (💣)") + + +# %% +def f1(x): + try: + print("F1Before") + f2(x) + print("F1After") + except TypeError: + print("F1Except (💣)") + + +# %% +f1(0) + +# %% +f1(1) + +# %% +f1(2) + +# %% +f1(3) + +# %% [markdown] +# ### Design with Exceptions + +# %% [markdown] +# +# Now we know how exceptions work, we need to think about the design implications... How best to use them. +# +# Traditional software design theory will tell you that they should only be used +# to describe and recover from **exceptional** conditions: things going wrong. +# Normal program flow shouldn't use them. +# +# Python's designers take a different view: use of exceptions in normal flow is +# considered OK. For example, all iterators raise a `StopIteration` exception to +# indicate the iteration is complete. +# +# A commonly recommended Python design pattern is to use exceptions to determine +# whether an object implements a protocol (concept/interface), rather than testing +# on type. +# +# For example, we might want a function which can be supplied *either* a data +# series *or* a path to a location on disk where data can be found. We can +# examine the type of the supplied content: + +# %% +import yaml + + +def analysis(source): + if type(source) == dict: + name = source['modelname'] + else: + content = open(source) + source = yaml.safe_load(content) + name = source['modelname'] + print(name) + + +# %% +analysis({'modelname': 'Super'}) + +# %% +with open('example.yaml', 'w') as outfile: + outfile.write('modelname: brilliant\n') + +# %% +analysis('example.yaml') + + +# %% [markdown] +# +# +# +# However, we can also use the try-it-and-handle-exceptions approach to this. +# +# +# + +# %% +def analysis(source): + try: + name = source['modelname'] + except TypeError: + content = open(source) + source = yaml.safe_load(content) + name = source['modelname'] + print(name) + + +analysis('example.yaml') + + +# %% [markdown] +# This approach is more extensible, and **behaves properly if we give it some +# other data-source which responds like a dictionary or string.** + +# %% +def analysis(source): + try: + name = source['modelname'] + except TypeError: + # Source was not a dictionary-like object + # Maybe it is a file path + try: + content = open(source) + source = yaml.safe_load(content) + name = source['modelname'] + except IOError: + # Maybe it was already raw YAML content + source = yaml.safe_load(source) + name = source['modelname'] + print(name) + + +analysis("modelname: Amazing") + +# %% [markdown] +# Sometimes we want to catch an error, partially handle it, perhaps add some +# extra data to the exception, and then re-raise to be caught again further up +# the call stack. +# +# The keyword "`raise`" with no argument in an `except:` clause will cause the +# caught error to be re-thrown. Doing this is the only circumstance where it is +# safe to do `except:` without catching a specific type of error. + +# %% +try: + # Something + pass +except: + # Do this code here if anything goes wrong + raise + + +# %% [markdown] +# If you want to be more explicit about where the error came from, you can use the `raise from` syntax, which will create a chain of exceptions: + +# %% +def lower_function(): + raise ValueError("Error in lower function!") + + +def higher_function(): + try: + lower_function() + except ValueError as e: + raise RuntimeError("Error in higher function!") from e + + +higher_function() + +# %% [markdown] +# +# +# It can be useful to catch and re-throw an error as you go up the chain, doing any clean-up needed for each layer of a program. +# +# The error will finally be caught and not re-thrown only at a higher program +# layer that knows how to recover. This is known as the "throw low catch high" +# principle. +# +# +# diff --git a/ch07dry/049Operators.html b/ch07dry/049Operators.html new file mode 100644 index 000000000..ce95a381d --- /dev/null +++ b/ch07dry/049Operators.html @@ -0,0 +1,868 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Operator Overloading + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Operator overloading

+
+
+
+
+
+
+

We've seen already during the course that some operators behave differently depending on the data type.

+

For example, + adds numbers but concatenates strings or lists:

+
+
+
+
+
+
In [1]:
+
+
+
4 + 2
+
+
+
+
+
+
+
+
Out[1]:
+
+
6
+
+
+
+
+
+
+
+
In [2]:
+
+
+
'4' + '2'
+
+
+
+
+
+
+
+
Out[2]:
+
+
'42'
+
+
+
+
+
+
+
+
+

* is used for multiplication, or repeated addition:

+
+
+
+
+
+
In [3]:
+
+
+
6 * 7
+
+
+
+
+
+
+
+
Out[3]:
+
+
42
+
+
+
+
+
+
+
+
In [4]:
+
+
+
'me' * 3
+
+
+
+
+
+
+
+
Out[4]:
+
+
'mememe'
+
+
+
+
+
+
+
+
+

/ is division for numbers, and wouldn't have a real meaning on strings. However, it's used to separate files and directories on your file system. Therefore, this has been overloaded in the pathlib module:

+
+
+
+
+
+
In [5]:
+
+
+
import os
+from pathlib import Path
+
+
+performance = Path('..') / 'ch08performance'
+os.listdir(performance)
+
+
+
+
+
+
+
+
Out[5]:
+
+
['index.md',
+ 'list_memory.svg',
+ '020numpy.ipynb.py',
+ '040cython.ipynb.py',
+ 'deque_memory.svg',
+ '050scaling.ipynb.py',
+ 'array_memory.svg',
+ '010intro.ipynb.py',
+ '015mandels.ipynb.py']
+
+
+
+
+
+
+
+
+

The above works because one of the elements is a Path object. Note, that the / works similarly to os.path.join(), so whether you are using Unix file systems or Windows, pathlib will know what path separator to use.

+
+
+
+
+
+
In [6]:
+
+
+
performance = os.path.join('..', 'ch08performance')
+
+
+
+
+
+
+
+
+

Overloading operators for your own classes

+
+
+
+
+
+
+

Now that we have seen that in Python operators do different things, how can we use + or other operators on our own classes to achieve similar behaviour?

+

Let's go back to our Maze example, and simplify our room object so it's defined as:

+
+
+
+
+
+
In [7]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+
+
+
+
+
+
+
+
+

We can now create a room as:

+
+
+
+
+
+
In [8]:
+
+
+
small = Room('small', 9)
+print(small)
+
+
+
+
+
+
+
+
+
+
<__main__.Room object at 0x7f3d28aac4c0>
+
+
+
+
+
+
+
+
+
+

However, when we print it we don't get much infomation on the object. So, the first operator we are overloading is its string represenation defining __str__:

+
+
+
+
+
+
In [9]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+    def __str__(self):
+        return f"<Room: {self.name} {self.area}m²>"
+
+
+
+
+
+
+
+
In [10]:
+
+
+
small = Room('small', 9)
+print(small)
+
+
+
+
+
+
+
+
+
+
<Room: small 9m²>
+
+
+
+
+
+
+
+
+
+

How can we add two rooms together? What does it mean? Let's define that the addition (+) of two rooms makes up one with the combined size. We produce this behaviour by defining the __add__ method.

+
+
+
+
+
+
In [11]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+    def __add__(self, other):
+        return Room(f"{self.name}_{other.name}", self.area + other.area)
+    def __str__(self):
+        return f"<Room: {self.name} {self.area}m²>"
+
+
+
+
+
+
+
+
In [12]:
+
+
+
small = Room('small', 9)
+big = Room('big', 21)
+print(small, big, small + big)
+
+
+
+
+
+
+
+
+
+
<Room: small 9m²> <Room: big 21m²> <Room: small_big 30m²>
+
+
+
+
+
+
+
+
+
+

Would the order of how the rooms are added affect the final room? As they are added now, the name is determined by the order, but do we want that? Or would we prefer to have:

+
small + big == big + small
+
+

That bring us to another operator, equal to: ==. The method needed to produce such comparison is __eq__.

+
+
+
+
+
+
In [13]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+    def __add__(self, other):
+        return Room(f"{self.name}_{other.name}", self.area + other.area)
+    def __eq__(self, other):
+        return self.area == other.area and set(self.name.split('_')) == set(other.name.split('_'))
+
+
+
+
+
+
+
+
+

So, in this way two rooms of the same area are "equal" if their names are composed by the same.

+
+
+
+
+
+
In [14]:
+
+
+
small = Room('small', 9)
+big = Room('big', 21)
+large = Room('superbig', 30)
+print(small + big == big + small)
+print(small + big == large)
+
+
+
+
+
+
+
+
+
+
True
+False
+
+
+
+
+
+
+
+
+
+

You can add the other comparisons to know which room is bigger or smaller with the following functions:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
OperatorFunction
<__lt__(self, other)
<=__le__(self, other)
>__gt__(self, other)
>=__ge__(self, other)
+
+
+
+
+
+
+

Let's add people to the rooms and check whether they are in one room or not.

+
+
+
+
+
+
In [15]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+        self.occupants = []
+    def add_occupant(self, name):
+        self.occupants.append(name)
+
+circus = Room('Circus', 3)
+circus.add_occupant('Graham')
+circus.add_occupant('Eric')
+circus.add_occupant('Terry')
+
+
+
+
+
+
+
+
+

How do we know if John is in the room? We can check the occupants list:

+
+
+
+
+
+
In [16]:
+
+
+
'John' in circus.occupants
+
+
+
+
+
+
+
+
Out[16]:
+
+
False
+
+
+
+
+
+
+
+
+

Or making it more readable adding a membership definition:

+
+
+
+
+
+
In [17]:
+
+
+
class Room:
+    def __init__(self, name, area):
+        self.name = name
+        self.area = area
+        self.occupants = []
+    def add_occupant(self, name):
+        self.occupants.append(name)
+    def __contains__(self, value):
+        return value in self.occupants
+
+circus = Room('Circus', 3)
+circus.add_occupant('Graham')
+circus.add_occupant('Eric')
+circus.add_occupant('Terry')
+
+'Terry' in circus
+
+
+
+
+
+
+
+
Out[17]:
+
+
True
+
+
+
+
+
+
+
+
+

We can add lots more operators to classes. For example, __getitem__ to let you index or access part of your object like a sequence or dictionary, e.g., newObject[1] or newObject["data"], or __len__ to return a number of elements in your object. Probably the most exciting +one is __call__, which overrides the () operator; this allows us to define classes that behave like functions! We call these callables.

+
+
+
+
+
+
In [18]:
+
+
+
class Greeter(object):
+    def __init__(self, greeting):
+        self.greeting = greeting
+        
+    def __call__(self, name):
+        print(self.greeting, name)
+
+greeter_instance = Greeter("Hello")
+
+greeter_instance("Eric")
+
+
+
+
+
+
+
+
+
+
Hello Eric
+
+
+
+
+
+
+
+
+
+

We've now come full circle in the blurring of the distinction between functions and objects! The full power of functional programming is really remarkable.

+

If you want to know more about the topics in this lecture, using a different +language syntax, I recommend you watch the Abelson and Sussman +"Structure and Interpretation of Computer Programs" lectures. These are the Computer Science +equivalent of the Feynman Lectures!

+
+
+
+
+
+
+

Next notebook shows a detailed example of how to apply operator overloading to create your own symbolic algebra system.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/049Operators.ipynb b/ch07dry/049Operators.ipynb new file mode 100644 index 000000000..fe88959da --- /dev/null +++ b/ch07dry/049Operators.ipynb @@ -0,0 +1,442 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "34c8fee1", + "metadata": {}, + "source": [ + "# Operator overloading" + ] + }, + { + "cell_type": "markdown", + "id": "41c3e067", + "metadata": {}, + "source": [ + "We've seen already during the course that some operators behave differently depending on the data type.\n", + "\n", + "For example, `+` adds numbers but concatenates strings or lists:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa2f000d", + "metadata": {}, + "outputs": [], + "source": [ + "4 + 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4545b3d", + "metadata": {}, + "outputs": [], + "source": [ + "'4' + '2'" + ] + }, + { + "cell_type": "markdown", + "id": "0575260f", + "metadata": {}, + "source": [ + "`*` is used for multiplication, or repeated addition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1afea4ce", + "metadata": {}, + "outputs": [], + "source": [ + "6 * 7" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3309ec8", + "metadata": {}, + "outputs": [], + "source": [ + "'me' * 3" + ] + }, + { + "cell_type": "markdown", + "id": "9960600e", + "metadata": {}, + "source": [ + "`/` is division for numbers, and wouldn't have a real meaning on strings. However, it's used to separate files and directories on your file system. Therefore, this has been *overloaded* in the `pathlib` module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6496c4d8", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "\n", + "\n", + "performance = Path('..') / 'ch08performance'\n", + "os.listdir(performance)" + ] + }, + { + "cell_type": "markdown", + "id": "34ac07d3", + "metadata": {}, + "source": [ + "The above works because one of the elements is a `Path` object. Note, that the `/` works similarly to `os.path.join()`, so whether you are using Unix file systems or Windows, `pathlib` will know what path separator to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccb0b7bf", + "metadata": {}, + "outputs": [], + "source": [ + "performance = os.path.join('..', 'ch08performance')" + ] + }, + { + "cell_type": "markdown", + "id": "67ed3326", + "metadata": {}, + "source": [ + "## Overloading operators for your own classes" + ] + }, + { + "cell_type": "markdown", + "id": "c9e78333", + "metadata": {}, + "source": [ + "Now that we have seen that in Python operators do different things, how can we use `+` or other operators on our own classes to achieve similar behaviour?\n", + "\n", + "Let's go back to our Maze example, and simplify our room object so it's defined as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3efc4e94", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area" + ] + }, + { + "cell_type": "markdown", + "id": "a69897e1", + "metadata": {}, + "source": [ + "We can now create a room as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ecca8b5", + "metadata": {}, + "outputs": [], + "source": [ + "small = Room('small', 9)\n", + "print(small)" + ] + }, + { + "cell_type": "markdown", + "id": "ad0c6b6f", + "metadata": {}, + "source": [ + "However, when we print it we don't get much infomation on the object. So, the first operator we are overloading is its string represenation defining `__str__`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a4914b6", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area\n", + " def __str__(self):\n", + " return f\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "721260c7", + "metadata": {}, + "outputs": [], + "source": [ + "small = Room('small', 9)\n", + "print(small)" + ] + }, + { + "cell_type": "markdown", + "id": "7f386751", + "metadata": {}, + "source": [ + "How can we add two rooms together? What does it mean? Let's define that the addition (`+`) of two rooms makes up one with the combined size. We produce this behaviour by defining the `__add__` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c11f3ff0", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area\n", + " def __add__(self, other):\n", + " return Room(f\"{self.name}_{other.name}\", self.area + other.area)\n", + " def __str__(self):\n", + " return f\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0e6c78b", + "metadata": {}, + "outputs": [], + "source": [ + "small = Room('small', 9)\n", + "big = Room('big', 21)\n", + "print(small, big, small + big)" + ] + }, + { + "cell_type": "markdown", + "id": "ac554673", + "metadata": {}, + "source": [ + "Would the order of how the rooms are added affect the final room? As they are added now, the name is determined by the order, but do we want that? Or would we prefer to have:\n", + "```python\n", + " small + big == big + small\n", + "```\n", + "That bring us to another operator, equal to: `==`. The method needed to produce such comparison is `__eq__`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "772c5404", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area\n", + " def __add__(self, other):\n", + " return Room(f\"{self.name}_{other.name}\", self.area + other.area)\n", + " def __eq__(self, other):\n", + " return self.area == other.area and set(self.name.split('_')) == set(other.name.split('_'))\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "6262bb1f", + "metadata": {}, + "source": [ + "So, in this way two rooms of the same area are \"equal\" if their names are composed by the same." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63b4d43b", + "metadata": {}, + "outputs": [], + "source": [ + "small = Room('small', 9)\n", + "big = Room('big', 21)\n", + "large = Room('superbig', 30)\n", + "print(small + big == big + small)\n", + "print(small + big == large)" + ] + }, + { + "cell_type": "markdown", + "id": "6fefe59d", + "metadata": {}, + "source": [ + "You can add the other comparisons to know which room is bigger or smaller with the following functions:\n", + "\n", + "| Operator | Function |\n", + "|----|----|\n", + "| `<` | `__lt__(self, other)` |\n", + "| `<=` | `__le__(self, other)` |\n", + "| `>` | `__gt__(self, other)`|\n", + "| `>=` | `__ge__(self, other)` |" + ] + }, + { + "cell_type": "markdown", + "id": "e38d54af", + "metadata": {}, + "source": [ + "Let's add people to the rooms and check whether they are in one room or not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "695f62ff", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area\n", + " self.occupants = []\n", + " def add_occupant(self, name):\n", + " self.occupants.append(name)\n", + "\n", + "circus = Room('Circus', 3)\n", + "circus.add_occupant('Graham')\n", + "circus.add_occupant('Eric')\n", + "circus.add_occupant('Terry')" + ] + }, + { + "cell_type": "markdown", + "id": "d0a4a455", + "metadata": {}, + "source": [ + "How do we know if John is in the room? We can check the `occupants` list:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a491777", + "metadata": {}, + "outputs": [], + "source": [ + "'John' in circus.occupants" + ] + }, + { + "cell_type": "markdown", + "id": "838e4260", + "metadata": {}, + "source": [ + "Or making it more readable adding a membership definition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68cb49e8", + "metadata": {}, + "outputs": [], + "source": [ + "class Room:\n", + " def __init__(self, name, area):\n", + " self.name = name\n", + " self.area = area\n", + " self.occupants = []\n", + " def add_occupant(self, name):\n", + " self.occupants.append(name)\n", + " def __contains__(self, value):\n", + " return value in self.occupants\n", + "\n", + "circus = Room('Circus', 3)\n", + "circus.add_occupant('Graham')\n", + "circus.add_occupant('Eric')\n", + "circus.add_occupant('Terry')\n", + "\n", + "'Terry' in circus" + ] + }, + { + "cell_type": "markdown", + "id": "26b2a937", + "metadata": {}, + "source": [ + "We can add lots more operators to classes. For example, `__getitem__` to let you index or access part of your object like a sequence or dictionary, _e.g._, `newObject[1]` or `newObject[\"data\"]`, or `__len__` to return a number of elements in your object. Probably the most exciting\n", + "one is `__call__`, which overrides the `()` operator; this allows us to define classes that *behave like functions*! We call these **callables**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1ae190a", + "metadata": {}, + "outputs": [], + "source": [ + "class Greeter(object):\n", + " def __init__(self, greeting):\n", + " self.greeting = greeting\n", + " \n", + " def __call__(self, name):\n", + " print(self.greeting, name)\n", + "\n", + "greeter_instance = Greeter(\"Hello\")\n", + "\n", + "greeter_instance(\"Eric\")" + ] + }, + { + "cell_type": "markdown", + "id": "b17fbdd5", + "metadata": {}, + "source": [ + "\n", + "We've now come full circle in the blurring of the distinction between functions and objects! The full power of functional programming is really remarkable.\n", + "\n", + "If you want to know more about the topics in this lecture, using a different\n", + "language syntax, I recommend you watch the [Abelson and Sussman](https://www.youtube.com/watch?v=2Op3QLzMgSY)\n", + "\"Structure and Interpretation of Computer Programs\" lectures. These are the Computer Science\n", + "equivalent of the Feynman Lectures!\n" + ] + }, + { + "cell_type": "markdown", + "id": "129883d5", + "metadata": {}, + "source": [ + "Next [notebook](./050Operators.ipynb) shows a detailed example of how to apply operator overloading to create your own symbolic algebra system." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Operator Overloading" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/049Operators.ipynb.py b/ch07dry/049Operators.ipynb.py new file mode 100644 index 000000000..7988ba41d --- /dev/null +++ b/ch07dry/049Operators.ipynb.py @@ -0,0 +1,229 @@ +# --- +# jupyter: +# jekyll: +# display_name: Operator Overloading +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Operator overloading + +# %% [markdown] +# We've seen already during the course that some operators behave differently depending on the data type. +# +# For example, `+` adds numbers but concatenates strings or lists: + +# %% +4 + 2 + +# %% +'4' + '2' + +# %% [markdown] +# `*` is used for multiplication, or repeated addition: + +# %% +6 * 7 + +# %% +'me' * 3 + +# %% [markdown] +# `/` is division for numbers, and wouldn't have a real meaning on strings. However, it's used to separate files and directories on your file system. Therefore, this has been *overloaded* in the `pathlib` module: + +# %% +import os +from pathlib import Path + + +performance = Path('..') / 'ch08performance' +os.listdir(performance) + +# %% [markdown] +# The above works because one of the elements is a `Path` object. Note, that the `/` works similarly to `os.path.join()`, so whether you are using Unix file systems or Windows, `pathlib` will know what path separator to use. + +# %% +performance = os.path.join('..', 'ch08performance') + + +# %% [markdown] +# ## Overloading operators for your own classes + +# %% [markdown] +# Now that we have seen that in Python operators do different things, how can we use `+` or other operators on our own classes to achieve similar behaviour? +# +# Let's go back to our Maze example, and simplify our room object so it's defined as: + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + + +# %% [markdown] +# We can now create a room as: + +# %% +small = Room('small', 9) +print(small) + + +# %% [markdown] +# However, when we print it we don't get much infomation on the object. So, the first operator we are overloading is its string represenation defining `__str__`: + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + def __str__(self): + return f"" + + +# %% +small = Room('small', 9) +print(small) + + +# %% [markdown] +# How can we add two rooms together? What does it mean? Let's define that the addition (`+`) of two rooms makes up one with the combined size. We produce this behaviour by defining the `__add__` method. + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + def __add__(self, other): + return Room(f"{self.name}_{other.name}", self.area + other.area) + def __str__(self): + return f"" + + +# %% +small = Room('small', 9) +big = Room('big', 21) +print(small, big, small + big) + + +# %% [markdown] +# Would the order of how the rooms are added affect the final room? As they are added now, the name is determined by the order, but do we want that? Or would we prefer to have: +# ```python +# small + big == big + small +# ``` +# That bring us to another operator, equal to: `==`. The method needed to produce such comparison is `__eq__`. + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + def __add__(self, other): + return Room(f"{self.name}_{other.name}", self.area + other.area) + def __eq__(self, other): + return self.area == other.area and set(self.name.split('_')) == set(other.name.split('_')) + + + +# %% [markdown] +# So, in this way two rooms of the same area are "equal" if their names are composed by the same. + +# %% +small = Room('small', 9) +big = Room('big', 21) +large = Room('superbig', 30) +print(small + big == big + small) +print(small + big == large) + + +# %% [markdown] +# You can add the other comparisons to know which room is bigger or smaller with the following functions: +# +# | Operator | Function | +# |----|----| +# | `<` | `__lt__(self, other)` | +# | `<=` | `__le__(self, other)` | +# | `>` | `__gt__(self, other)`| +# | `>=` | `__ge__(self, other)` | + +# %% [markdown] +# Let's add people to the rooms and check whether they are in one room or not. + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + self.occupants = [] + def add_occupant(self, name): + self.occupants.append(name) + +circus = Room('Circus', 3) +circus.add_occupant('Graham') +circus.add_occupant('Eric') +circus.add_occupant('Terry') + + +# %% [markdown] +# How do we know if John is in the room? We can check the `occupants` list: + +# %% +'John' in circus.occupants + + +# %% [markdown] +# Or making it more readable adding a membership definition: + +# %% +class Room: + def __init__(self, name, area): + self.name = name + self.area = area + self.occupants = [] + def add_occupant(self, name): + self.occupants.append(name) + def __contains__(self, value): + return value in self.occupants + +circus = Room('Circus', 3) +circus.add_occupant('Graham') +circus.add_occupant('Eric') +circus.add_occupant('Terry') + +'Terry' in circus + + +# %% [markdown] +# We can add lots more operators to classes. For example, `__getitem__` to let you index or access part of your object like a sequence or dictionary, _e.g._, `newObject[1]` or `newObject["data"]`, or `__len__` to return a number of elements in your object. Probably the most exciting +# one is `__call__`, which overrides the `()` operator; this allows us to define classes that *behave like functions*! We call these **callables**. + +# %% +class Greeter(object): + def __init__(self, greeting): + self.greeting = greeting + + def __call__(self, name): + print(self.greeting, name) + +greeter_instance = Greeter("Hello") + +greeter_instance("Eric") + +# %% [markdown] +# +# We've now come full circle in the blurring of the distinction between functions and objects! The full power of functional programming is really remarkable. +# +# If you want to know more about the topics in this lecture, using a different +# language syntax, I recommend you watch the [Abelson and Sussman](https://www.youtube.com/watch?v=2Op3QLzMgSY) +# "Structure and Interpretation of Computer Programs" lectures. These are the Computer Science +# equivalent of the Feynman Lectures! +# + +# %% [markdown] +# Next [notebook](./050Operators.ipynb) shows a detailed example of how to apply operator overloading to create your own symbolic algebra system. diff --git a/ch07dry/050OperatorsExample.html b/ch07dry/050OperatorsExample.html new file mode 100644 index 000000000..ff8a21b13 --- /dev/null +++ b/ch07dry/050OperatorsExample.html @@ -0,0 +1,859 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Operator Overloading (example) + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Operator overloading

+
+
+
+
+
+
+

Warning: Advanced Topic!

+
+
+
+
+
+
+

Setup for this notebook

+
+
+
+
+
+
+

We need to use a metaprogramming trick to make this teaching notebook work. +I want to be able to put explanatory text in between parts of a class definition, +so I'll define a decorator to help me build up a class definition gradually.

+
+
+
+
+
+
In [1]:
+
+
+
def extend(class_to_extend):
+    """ Metaprogramming to allow gradual implementation
+    of class during notebook. Thanks to
+    http://www.ianbicking.org/blog/2007/08/opening-python-classes.html """
+    def decorator(extending_class):
+        for name, value in extending_class.__dict__.items():
+            if name in ['__dict__', '__module__', '__weakref__', '__doc__']:
+                continue
+            setattr(class_to_extend, name, value)
+        return class_to_extend
+    return decorator
+
+
+
+
+
+
+
+
+

Operator overloading

+
+
+
+
+
+
+

Imagine we wanted to make a library to describe some kind of symbolic algebra system:

+
+
+
+
+
+
In [2]:
+
+
+
class Term:
+    def __init__(self, symbols=[], powers=[], coefficient=1):
+        self.coefficient = coefficient
+        self.data={symbol: exponent for symbol,exponent
+                in zip(symbols, powers)}
+
+
+
+
+
+
+
+
In [3]:
+
+
+
class Expression:
+    def __init__(self, terms):
+        self.terms = terms
+
+
+
+
+
+
+
+
+

So that $5x^2y+7x+2$ might be constructed as:

+
+
+
+
+
+
In [4]:
+
+
+
first = Term(['x', 'y'], [2, 1], 5)
+
+second = Term(['x'], [1], 7)
+
+third = Term([], [], 2)
+
+result = Expression([first, second, third])
+
+
+
+
+
+
+
+
+

This is pretty cumbersome.

+

What we'd really like is to have 2x+y give an appropriate expression.

+

First, we'll define things so that we can construct our terms and expressions in different ways.

+
+
+
+
+
+
In [5]:
+
+
+
class Term:
+    def __init__(self, *args):
+        lead = args[0]
+        if type(lead) == type(self):
+            # Copy constructor
+            self.data = dict(lead.data)
+            self.coefficient = lead.coefficient
+        elif type(lead) == int:
+            self.from_constant(lead)
+        elif type(lead) == str:
+            self.from_symbol(*args)
+        elif type(lead) == dict:
+            self.from_dictionary(*args)
+        else:
+            self.from_lists(*args)
+            
+    def from_constant(self, constant):
+        self.coefficient = constant
+        self.data = {}
+        
+    def from_symbol(self, symbol, coefficient=1, power=1):
+        self.coefficient = coefficient
+        self.data = {symbol: power}
+        
+    def from_dictionary(self, data, coefficient=1):
+        self.data = data
+        self.coefficient = coefficient
+        
+    def from_lists(self, symbols=[], powers=[], coefficient=1):
+        self.coefficient = coefficient
+        self.data={symbol: exponent for symbol,exponent
+                   in zip(symbols, powers)}
+
+
+
+
+
+
+
+
In [6]:
+
+
+
class Expression:
+    def __init__(self, terms=[]):
+        self.terms = list(terms)
+
+
+
+
+
+
+
+
+

We could define add() and multiply() operations on expressions and terms:

+
+
+
+
+
+
In [7]:
+
+
+
@extend(Term)
+class Term:
+    def add(self, *others):
+        return Expression((self,) + others)
+    
+
+
+
+
+
+
+
+
In [8]:
+
+
+
@extend(Term)
+class Term:
+    def multiply(self, *others):
+        result_data = dict(self.data)
+        result_coeff = self.coefficient
+        # Convert arguments to Terms first if they are
+        # constants or integers
+        others = map(Term, others)
+        
+        for another in others:
+            for symbol, exponent in another.data.items():
+                if symbol in result_data:
+                    result_data[symbol] += another.data[symbol]
+                else:
+                    result_data[symbol] = another.data[symbol]
+            result_coeff *= another.coefficient
+        
+        return Term(result_data, result_coeff)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
@extend(Expression)
+class Expression:
+    def add(self, *others):
+        result = Expression(self.terms)
+        
+        for another in others:
+            if type(another) == Term:
+                result.terms.append(another)
+            else:
+                result.terms += another.terms
+                
+        return result
+
+
+
+
+
+
+
+
+

We can now construct the above expression as:

+
+
+
+
+
+
In [10]:
+
+
+
x = Term('x')
+y = Term('y')
+
+first = Term(5).multiply(Term('x'), Term('x'), Term('y'))
+second = Term(7).multiply(Term('x'))
+third = Term(2)
+expr = first.add(second, third)
+
+
+
+
+
+
+
+
+

This is better, but we still can't write the expression in a 'natural' way.

+

However, we can define what * and + do when applied to Terms!:

+
+
+
+
+
+
In [11]:
+
+
+
@extend(Term)
+class Term:
+    
+    def __add__(self, other):
+        return self.add(other)
+    
+    def __mul__(self, other):
+        return self.multiply(other)
+
+
+
+
+
+
+
+
In [12]:
+
+
+
@extend(Expression)
+class Expression:
+    def multiply(self, another):
+        # Distributive law left as exercise
+        pass
+    
+    def __add__(self, other):
+        return self.add(other)
+
+
+
+
+
+
+
+
In [13]:
+
+
+
x_plus_y = Term('x') + 'y'
+x_plus_y.terms[1]
+
+
+
+
+
+
+
+
Out[13]:
+
+
'y'
+
+
+
+
+
+
+
+
In [14]:
+
+
+
five_x_ysq = Term('x') * 5 * 'y' * 'y'
+
+print(five_x_ysq.data, five_x_ysq.coefficient)
+
+
+
+
+
+
+
+
+
+
{'x': 1, 'y': 2} 5
+
+
+
+
+
+
+
+
+
+

This is called operator overloading. We can define what add and multiply mean when applied to our class.

+

Note that this only works so far if we multiply on the right-hand-side! +However, we can define a multiplication that works backwards, which is used as a fallback if the left multiply raises an error:

+
+
+
+
+
+
In [15]:
+
+
+
@extend(Expression)
+class Expression:
+    def __radd__(self, other):
+        return self.__add__(other)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
@extend(Term)
+class Term:
+    def __rmul__(self, other):
+        return self.__mul__(other)
+    
+    def __radd__(self, other):
+        return self.__add__(other)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [17]:
+
+
+
5 * Term('x')
+
+
+
+
+
+
+
+
Out[17]:
+
+
<__main__.Term at 0x7f256c709d90>
+
+
+
+
+
+
+
+
+

It's not easy at the moment to see if these things are working!

+
+
+
+
+
+
In [18]:
+
+
+
fivex = 5 * Term('x')
+fivex.data, fivex.coefficient
+
+
+
+
+
+
+
+
Out[18]:
+
+
({'x': 1}, 5)
+
+
+
+
+
+
+
+
+

We can add another operator method __str__, which defines what happens if we try to print our class:

+
+
+
+
+
+
In [19]:
+
+
+
@extend(Term)
+class Term:
+    def __str__(self):
+        def symbol_string(symbol, power):
+            if power == 1:
+                return symbol
+            else:
+                return f"{symbol}^{power}"
+            
+        symbol_strings=[symbol_string(symbol, power)
+                for symbol, power in self.data.items()]
+        
+        prod = '*'.join(symbol_strings)
+        
+        if not prod:
+            return str(self.coefficient)
+        if self.coefficient == 1:
+            return prod
+        else:
+            return f"{self.coefficient}*{prod}"
+
+
+
+
+
+
+
+
In [20]:
+
+
+
@extend(Expression)
+class Expression:
+    def __str__(self):
+        return '+'.join(map(str, self.terms))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
In [21]:
+
+
+
first = Term(5) * 'x' * 'x' * 'y'
+second = Term(7) * 'x'
+third = Term(2)
+expr = first + second + third
+
+
+
+
+
+
+
+
In [22]:
+
+
+
print(expr)
+
+
+
+
+
+
+
+
+
+
5*x^2*y+7*x+2
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/050OperatorsExample.ipynb b/ch07dry/050OperatorsExample.ipynb new file mode 100644 index 000000000..37a4b2553 --- /dev/null +++ b/ch07dry/050OperatorsExample.ipynb @@ -0,0 +1,581 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f002713e", + "metadata": {}, + "source": [ + "# Operator overloading" + ] + }, + { + "cell_type": "markdown", + "id": "6cce6cca", + "metadata": {}, + "source": [ + "Warning: Advanced Topic!" + ] + }, + { + "cell_type": "markdown", + "id": "3bbbf13b", + "metadata": {}, + "source": [ + "### Setup for this notebook" + ] + }, + { + "cell_type": "markdown", + "id": "b73d4167", + "metadata": {}, + "source": [ + "We need to use a metaprogramming trick to make this teaching notebook work.\n", + "I want to be able to put explanatory text in between parts of a class definition,\n", + "so I'll define a decorator to help me build up a class definition gradually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbee353e", + "metadata": {}, + "outputs": [], + "source": [ + "def extend(class_to_extend):\n", + " \"\"\" Metaprogramming to allow gradual implementation\n", + " of class during notebook. Thanks to\n", + " http://www.ianbicking.org/blog/2007/08/opening-python-classes.html \"\"\"\n", + " def decorator(extending_class):\n", + " for name, value in extending_class.__dict__.items():\n", + " if name in ['__dict__', '__module__', '__weakref__', '__doc__']:\n", + " continue\n", + " setattr(class_to_extend, name, value)\n", + " return class_to_extend\n", + " return decorator" + ] + }, + { + "cell_type": "markdown", + "id": "bbae7c01", + "metadata": {}, + "source": [ + "### Operator overloading" + ] + }, + { + "cell_type": "markdown", + "id": "15c00c8d", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n", + "Imagine we wanted to make a library to describe some kind of symbolic algebra system:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e166e766", + "metadata": {}, + "outputs": [], + "source": [ + "class Term:\n", + " def __init__(self, symbols=[], powers=[], coefficient=1):\n", + " self.coefficient = coefficient\n", + " self.data={symbol: exponent for symbol,exponent\n", + " in zip(symbols, powers)}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93f8e6c4", + "metadata": {}, + "outputs": [], + "source": [ + "class Expression:\n", + " def __init__(self, terms):\n", + " self.terms = terms" + ] + }, + { + "cell_type": "markdown", + "id": "821c406b", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "So that $5x^2y+7x+2$ might be constructed as:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45d2fbee", + "metadata": {}, + "outputs": [], + "source": [ + "first = Term(['x', 'y'], [2, 1], 5)\n", + "\n", + "second = Term(['x'], [1], 7)\n", + "\n", + "third = Term([], [], 2)\n", + "\n", + "result = Expression([first, second, third])" + ] + }, + { + "cell_type": "markdown", + "id": "76f9a14a", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "This is pretty cumbersome.\n", + "\n", + "What we'd really like is to have `2x+y` give an appropriate expression.\n", + "\n", + "First, we'll define things so that we can construct our terms and expressions in different ways.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc5408b6", + "metadata": {}, + "outputs": [], + "source": [ + "class Term:\n", + " def __init__(self, *args):\n", + " lead = args[0]\n", + " if type(lead) == type(self):\n", + " # Copy constructor\n", + " self.data = dict(lead.data)\n", + " self.coefficient = lead.coefficient\n", + " elif type(lead) == int:\n", + " self.from_constant(lead)\n", + " elif type(lead) == str:\n", + " self.from_symbol(*args)\n", + " elif type(lead) == dict:\n", + " self.from_dictionary(*args)\n", + " else:\n", + " self.from_lists(*args)\n", + " \n", + " def from_constant(self, constant):\n", + " self.coefficient = constant\n", + " self.data = {}\n", + " \n", + " def from_symbol(self, symbol, coefficient=1, power=1):\n", + " self.coefficient = coefficient\n", + " self.data = {symbol: power}\n", + " \n", + " def from_dictionary(self, data, coefficient=1):\n", + " self.data = data\n", + " self.coefficient = coefficient\n", + " \n", + " def from_lists(self, symbols=[], powers=[], coefficient=1):\n", + " self.coefficient = coefficient\n", + " self.data={symbol: exponent for symbol,exponent\n", + " in zip(symbols, powers)}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf3d8cf7", + "metadata": {}, + "outputs": [], + "source": [ + "class Expression:\n", + " def __init__(self, terms=[]):\n", + " self.terms = list(terms)" + ] + }, + { + "cell_type": "markdown", + "id": "350f4e1e", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "We could define add() and multiply() operations on expressions and terms:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b867a2d", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Term)\n", + "class Term:\n", + " def add(self, *others):\n", + " return Expression((self,) + others)\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01ad171b", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Term)\n", + "class Term:\n", + " def multiply(self, *others):\n", + " result_data = dict(self.data)\n", + " result_coeff = self.coefficient\n", + " # Convert arguments to Terms first if they are\n", + " # constants or integers\n", + " others = map(Term, others)\n", + " \n", + " for another in others:\n", + " for symbol, exponent in another.data.items():\n", + " if symbol in result_data:\n", + " result_data[symbol] += another.data[symbol]\n", + " else:\n", + " result_data[symbol] = another.data[symbol]\n", + " result_coeff *= another.coefficient\n", + " \n", + " return Term(result_data, result_coeff)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9bcb721", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Expression)\n", + "class Expression:\n", + " def add(self, *others):\n", + " result = Expression(self.terms)\n", + " \n", + " for another in others:\n", + " if type(another) == Term:\n", + " result.terms.append(another)\n", + " else:\n", + " result.terms += another.terms\n", + " \n", + " return result" + ] + }, + { + "cell_type": "markdown", + "id": "aa5ff485", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "We can now construct the above expression as:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "663b82f8", + "metadata": {}, + "outputs": [], + "source": [ + "x = Term('x')\n", + "y = Term('y')\n", + "\n", + "first = Term(5).multiply(Term('x'), Term('x'), Term('y'))\n", + "second = Term(7).multiply(Term('x'))\n", + "third = Term(2)\n", + "expr = first.add(second, third)" + ] + }, + { + "cell_type": "markdown", + "id": "1df4a9e6", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "This is better, but we still can't write the expression in a 'natural' way.\n", + "\n", + "However, we can define what `*` and `+` do when applied to Terms!:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64732ba0", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Term)\n", + "class Term:\n", + " \n", + " def __add__(self, other):\n", + " return self.add(other)\n", + " \n", + " def __mul__(self, other):\n", + " return self.multiply(other)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8acab7a", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Expression)\n", + "class Expression:\n", + " def multiply(self, another):\n", + " # Distributive law left as exercise\n", + " pass\n", + " \n", + " def __add__(self, other):\n", + " return self.add(other)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2e4fbc2", + "metadata": {}, + "outputs": [], + "source": [ + "x_plus_y = Term('x') + 'y'\n", + "x_plus_y.terms[1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22ad70c3", + "metadata": {}, + "outputs": [], + "source": [ + "five_x_ysq = Term('x') * 5 * 'y' * 'y'\n", + "\n", + "print(five_x_ysq.data, five_x_ysq.coefficient)" + ] + }, + { + "cell_type": "markdown", + "id": "fa42cd16", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "This is called operator overloading. We can define what add and multiply mean when applied to our class.\n", + "\n", + "Note that this only works so far if we multiply on the right-hand-side!\n", + "However, we can define a multiplication that works backwards, which is used as a fallback if the left multiply raises an error:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36926e52", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Expression)\n", + "class Expression:\n", + " def __radd__(self, other):\n", + " return self.__add__(other)" + ] + }, + { + "cell_type": "markdown", + "id": "1a42f4d1", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "642c134d", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Term)\n", + "class Term:\n", + " def __rmul__(self, other):\n", + " return self.__mul__(other)\n", + " \n", + " def __radd__(self, other):\n", + " return self.__add__(other)" + ] + }, + { + "cell_type": "markdown", + "id": "5e7accba", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "705e3e19", + "metadata": {}, + "outputs": [], + "source": [ + "5 * Term('x')" + ] + }, + { + "cell_type": "markdown", + "id": "c32ed4ff", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "It's not easy at the moment to see if these things are working!\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26a58b93", + "metadata": {}, + "outputs": [], + "source": [ + "fivex = 5 * Term('x')\n", + "fivex.data, fivex.coefficient" + ] + }, + { + "cell_type": "markdown", + "id": "5362519e", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "We can add another operator method `__str__`, which defines what happens if we try to print our class:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b764f17", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Term)\n", + "class Term:\n", + " def __str__(self):\n", + " def symbol_string(symbol, power):\n", + " if power == 1:\n", + " return symbol\n", + " else:\n", + " return f\"{symbol}^{power}\"\n", + " \n", + " symbol_strings=[symbol_string(symbol, power)\n", + " for symbol, power in self.data.items()]\n", + " \n", + " prod = '*'.join(symbol_strings)\n", + " \n", + " if not prod:\n", + " return str(self.coefficient)\n", + " if self.coefficient == 1:\n", + " return prod\n", + " else:\n", + " return f\"{self.coefficient}*{prod}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5d9fd35", + "metadata": {}, + "outputs": [], + "source": [ + "@extend(Expression)\n", + "class Expression:\n", + " def __str__(self):\n", + " return '+'.join(map(str, self.terms))" + ] + }, + { + "cell_type": "markdown", + "id": "c6dd7309", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e247ec3", + "metadata": {}, + "outputs": [], + "source": [ + "first = Term(5) * 'x' * 'x' * 'y'\n", + "second = Term(7) * 'x'\n", + "third = Term(2)\n", + "expr = first + second + third" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25e01093", + "metadata": {}, + "outputs": [], + "source": [ + "print(expr)" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Operator Overloading (example)" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/050OperatorsExample.ipynb.py b/ch07dry/050OperatorsExample.ipynb.py new file mode 100644 index 000000000..fee0fc04f --- /dev/null +++ b/ch07dry/050OperatorsExample.ipynb.py @@ -0,0 +1,369 @@ +# --- +# jupyter: +# jekyll: +# display_name: Operator Overloading (example) +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Operator overloading + +# %% [markdown] +# Warning: Advanced Topic! + +# %% [markdown] +# ### Setup for this notebook + +# %% [markdown] +# We need to use a metaprogramming trick to make this teaching notebook work. +# I want to be able to put explanatory text in between parts of a class definition, +# so I'll define a decorator to help me build up a class definition gradually. + +# %% +def extend(class_to_extend): + """ Metaprogramming to allow gradual implementation + of class during notebook. Thanks to + http://www.ianbicking.org/blog/2007/08/opening-python-classes.html """ + def decorator(extending_class): + for name, value in extending_class.__dict__.items(): + if name in ['__dict__', '__module__', '__weakref__', '__doc__']: + continue + setattr(class_to_extend, name, value) + return class_to_extend + return decorator + + +# %% [markdown] +# ### Operator overloading + +# %% [markdown] +# +# +# +# +# Imagine we wanted to make a library to describe some kind of symbolic algebra system: +# +# +# + +# %% +class Term: + def __init__(self, symbols=[], powers=[], coefficient=1): + self.coefficient = coefficient + self.data={symbol: exponent for symbol,exponent + in zip(symbols, powers)} + + +# %% +class Expression: + def __init__(self, terms): + self.terms = terms + + +# %% [markdown] +# +# +# +# So that $5x^2y+7x+2$ might be constructed as: +# +# +# + +# %% +first = Term(['x', 'y'], [2, 1], 5) + +second = Term(['x'], [1], 7) + +third = Term([], [], 2) + +result = Expression([first, second, third]) + + +# %% [markdown] +# +# +# +# This is pretty cumbersome. +# +# What we'd really like is to have `2x+y` give an appropriate expression. +# +# First, we'll define things so that we can construct our terms and expressions in different ways. +# +# +# + +# %% +class Term: + def __init__(self, *args): + lead = args[0] + if type(lead) == type(self): + # Copy constructor + self.data = dict(lead.data) + self.coefficient = lead.coefficient + elif type(lead) == int: + self.from_constant(lead) + elif type(lead) == str: + self.from_symbol(*args) + elif type(lead) == dict: + self.from_dictionary(*args) + else: + self.from_lists(*args) + + def from_constant(self, constant): + self.coefficient = constant + self.data = {} + + def from_symbol(self, symbol, coefficient=1, power=1): + self.coefficient = coefficient + self.data = {symbol: power} + + def from_dictionary(self, data, coefficient=1): + self.data = data + self.coefficient = coefficient + + def from_lists(self, symbols=[], powers=[], coefficient=1): + self.coefficient = coefficient + self.data={symbol: exponent for symbol,exponent + in zip(symbols, powers)} + + +# %% +class Expression: + def __init__(self, terms=[]): + self.terms = list(terms) + + +# %% [markdown] +# +# +# +# We could define add() and multiply() operations on expressions and terms: +# +# +# + +# %% +@extend(Term) +class Term: + def add(self, *others): + return Expression((self,) + others) + + + +# %% +@extend(Term) +class Term: + def multiply(self, *others): + result_data = dict(self.data) + result_coeff = self.coefficient + # Convert arguments to Terms first if they are + # constants or integers + others = map(Term, others) + + for another in others: + for symbol, exponent in another.data.items(): + if symbol in result_data: + result_data[symbol] += another.data[symbol] + else: + result_data[symbol] = another.data[symbol] + result_coeff *= another.coefficient + + return Term(result_data, result_coeff) + + +# %% +@extend(Expression) +class Expression: + def add(self, *others): + result = Expression(self.terms) + + for another in others: + if type(another) == Term: + result.terms.append(another) + else: + result.terms += another.terms + + return result + + +# %% [markdown] +# +# +# +# We can now construct the above expression as: +# +# +# + +# %% +x = Term('x') +y = Term('y') + +first = Term(5).multiply(Term('x'), Term('x'), Term('y')) +second = Term(7).multiply(Term('x')) +third = Term(2) +expr = first.add(second, third) + + +# %% [markdown] +# +# +# +# This is better, but we still can't write the expression in a 'natural' way. +# +# However, we can define what `*` and `+` do when applied to Terms!: +# +# +# + +# %% +@extend(Term) +class Term: + + def __add__(self, other): + return self.add(other) + + def __mul__(self, other): + return self.multiply(other) + + +# %% +@extend(Expression) +class Expression: + def multiply(self, another): + # Distributive law left as exercise + pass + + def __add__(self, other): + return self.add(other) + + +# %% +x_plus_y = Term('x') + 'y' +x_plus_y.terms[1] + +# %% +five_x_ysq = Term('x') * 5 * 'y' * 'y' + +print(five_x_ysq.data, five_x_ysq.coefficient) + + +# %% [markdown] +# +# +# +# This is called operator overloading. We can define what add and multiply mean when applied to our class. +# +# Note that this only works so far if we multiply on the right-hand-side! +# However, we can define a multiplication that works backwards, which is used as a fallback if the left multiply raises an error: +# +# +# + +# %% +@extend(Expression) +class Expression: + def __radd__(self, other): + return self.__add__(other) + + +# %% [markdown] +# +# +# +# + +# %% +@extend(Term) +class Term: + def __rmul__(self, other): + return self.__mul__(other) + + def __radd__(self, other): + return self.__add__(other) + + +# %% [markdown] +# +# +# +# +# +# + +# %% +5 * Term('x') + +# %% [markdown] +# +# +# +# It's not easy at the moment to see if these things are working! +# +# +# + +# %% +fivex = 5 * Term('x') +fivex.data, fivex.coefficient + + +# %% [markdown] +# +# +# +# We can add another operator method `__str__`, which defines what happens if we try to print our class: +# +# +# + +# %% +@extend(Term) +class Term: + def __str__(self): + def symbol_string(symbol, power): + if power == 1: + return symbol + else: + return f"{symbol}^{power}" + + symbol_strings=[symbol_string(symbol, power) + for symbol, power in self.data.items()] + + prod = '*'.join(symbol_strings) + + if not prod: + return str(self.coefficient) + if self.coefficient == 1: + return prod + else: + return f"{self.coefficient}*{prod}" + + +# %% +@extend(Expression) +class Expression: + def __str__(self): + return '+'.join(map(str, self.terms)) + + +# %% [markdown] +# +# +# +# + +# %% +first = Term(5) * 'x' * 'x' * 'y' +second = Term(7) * 'x' +third = Term(2) +expr = first + second + third + +# %% +print(expr) diff --git a/ch07dry/060Metaprogramming.html b/ch07dry/060Metaprogramming.html new file mode 100644 index 000000000..1c1977de7 --- /dev/null +++ b/ch07dry/060Metaprogramming.html @@ -0,0 +1,1095 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metaprogramming + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Metaprogramming

+
+
+
+
+
+
+

Warning: Advanced topic!

+
+
+
+
+
+
+

Metaprogramming globals

+
+
+
+
+
+
+

Consider a bunch of variables, each of which need initialising and incrementing:

+
+
+
+
+
+
In [1]:
+
+
+
bananas = 0
+apples = 0
+oranges = 0
+bananas += 1
+apples += 1
+oranges += 1
+
+
+
+
+
+
+
+
+

The right hand side of these assignments doesn't respect the DRY principle. We +could of course define a variable for our initial value:

+
+
+
+
+
+
In [2]:
+
+
+
initial_fruit_count = 0
+bananas = initial_fruit_count
+apples = initial_fruit_count
+oranges = initial_fruit_count
+
+
+
+
+
+
+
+
+

However, this is still not as DRY as it could be: what if we wanted to replace +the assignment with, say, a class constructor and a buy operation:

+
+
+
+
+
+
In [3]:
+
+
+
class Basket:
+    def __init__(self):
+        self.count = 0
+    def buy(self):
+        self.count += 1
+
+bananas = Basket()
+apples = Basket()
+oranges = Basket()
+bananas.buy()
+apples.buy()
+oranges.buy()
+
+
+
+
+
+
+
+
+

We had to make the change in three places. Whenever you see a situation where a +refactoring or change of design might require you to change the code in +multiple places, you have an opportunity to make the code DRYer.

+

In this case, metaprogramming for incrementing these variables would involve +just a loop over all the variables we want to initialise:

+
+
+
+
+
+
In [4]:
+
+
+
baskets = [bananas, apples, oranges]
+for basket in baskets: 
+    basket.buy()
+
+
+
+
+
+
+
+
+

However, this trick doesn't work for initialising a new variable:

+
+
+
+
+
+
In [5]:
+
+
+
from pytest import raises
+with raises(NameError):
+    baskets = [bananas, apples, oranges, kiwis]
+
+
+
+
+
+
+
+
+

So can we declare a new variable programmatically? Given a list of the +names of fruit baskets we want, initialise a variable with that name?

+
+
+
+
+
+
In [6]:
+
+
+
basket_names = ['bananas', 'apples', 'oranges', 'kiwis']
+
+globals()['apples']
+
+
+
+
+
+
+
+
Out[6]:
+
+
<__main__.Basket at 0x7f31546abd60>
+
+
+
+
+
+
+
+
+

Wow, we can! Every module or class in Python, is, under the hood, a special +dictionary, storing the values in its namespace. So we can create new +variables by assigning to this dictionary. globals() gives a reference to the +attribute dictionary for the current module

+
+
+
+
+
+
In [7]:
+
+
+
for name in basket_names:
+    globals()[name] = Basket()
+
+
+kiwis.count
+
+
+
+
+
+
+
+
Out[7]:
+
+
0
+
+
+
+
+
+
+
+
+

This is metaprogramming.

+

I would NOT recommend using it for an example as trivial as the one above. +A better, more Pythonic choice here would be to use a data structure to manage your set of fruit baskets:

+
+
+
+
+
+
In [8]:
+
+
+
baskets = {}
+for name in basket_names:
+    baskets[name] = Basket()
+
+baskets['kiwis'].count
+
+
+
+
+
+
+
+
Out[8]:
+
+
0
+
+
+
+
+
+
+
+
+

Or even, using a dictionary comprehension:

+
+
+
+
+
+
In [9]:
+
+
+
baskets = {name: Basket() for name in baskets}
+baskets['kiwis'].count
+
+
+
+
+
+
+
+
Out[9]:
+
+
0
+
+
+
+
+
+
+
+
+

Which is the nicest way to do this, I think. Code which feels like +metaprogramming is needed to make it less repetitive can often instead be DRYed +up using a refactored data structure, in a way which is cleaner and more easy +to understand. Nevertheless, metaprogramming is worth knowing.

+
+
+
+
+
+
+

Metaprogramming class attributes

+
+
+
+
+
+
+

We can metaprogram the attributes of a module using the globals() function.

+

We will also want to be able to metaprogram a class, by accessing its attribute dictionary.

+

This will allow us, for example, to programmatically add members to a class.

+
+
+
+
+
+
In [10]:
+
+
+
class Boring: 
+    pass
+
+
+
+
+
+
+
+
+

If we are adding our own attributes, we can just do so directly:

+
+
+
+
+
+
In [11]:
+
+
+
x = Boring()
+
+x.name = "Michael"
+
+
+
+
+
+
+
+
In [12]:
+
+
+
x.name
+
+
+
+
+
+
+
+
Out[12]:
+
+
'Michael'
+
+
+
+
+
+
+
+
+

And these turn up, as expected, in an attribute dictionary for the class:

+
+
+
+
+
+
In [13]:
+
+
+
x.__dict__
+
+
+
+
+
+
+
+
Out[13]:
+
+
{'name': 'Michael'}
+
+
+
+
+
+
+
+
+

We can use getattr to access this special dictionary:

+
+
+
+
+
+
In [14]:
+
+
+
getattr(x, 'name')
+
+
+
+
+
+
+
+
Out[14]:
+
+
'Michael'
+
+
+
+
+
+
+
+
+

If we want to add an attribute given it's name as a string, we can use setattr:

+
+
+
+
+
+
In [15]:
+
+
+
setattr(x, 'age', 75)
+
+x.age
+
+
+
+
+
+
+
+
Out[15]:
+
+
75
+
+
+
+
+
+
+
+
+

And we could do this in a loop to programmatically add many attributes.

+
+
+
+
+
+
+

The real power of accessing the attribute dictionary comes when we realise that +there is very little difference between member data and member functions.

+
+
+
+
+
+
+

Now that we know, from our functional programming, that a function is just a +variable that can be called with (), we can set an attribute to a function, +and +it becomes a member function!

+
+
+
+
+
+
In [16]:
+
+
+
setattr(Boring, 'describe', lambda self: f"{self.name} is {self.age}")
+
+
+
+
+
+
+
+
In [17]:
+
+
+
x.describe()
+
+
+
+
+
+
+
+
Out[17]:
+
+
'Michael is 75'
+
+
+
+
+
+
+
+
In [18]:
+
+
+
x.describe
+
+
+
+
+
+
+
+
Out[18]:
+
+
<bound method <lambda> of <__main__.Boring object at 0x7f31546ab6a0>>
+
+
+
+
+
+
+
+
In [19]:
+
+
+
Boring.describe
+
+
+
+
+
+
+
+
Out[19]:
+
+
<function __main__.<lambda>(self)>
+
+
+
+
+
+
+
+
+

Note that we set this method as an attribute of the class, not the instance, so it is available to other instances of Boring:

+
+
+
+
+
+
In [20]:
+
+
+
y = Boring()
+y.name = 'Terry'
+y.age  = 78
+
+
+
+
+
+
+
+
In [21]:
+
+
+
y.describe()
+
+
+
+
+
+
+
+
Out[21]:
+
+
'Terry is 78'
+
+
+
+
+
+
+
+
+

We can define a standalone function, and then bind it to the class. Its first argument automagically becomes +self.

+
+
+
+
+
+
In [22]:
+
+
+
def broken_birth_year(b_instance):
+    import datetime
+    current = datetime.datetime.now().year
+    return current - b_instance.age
+
+
+
+
+
+
+
+
In [23]:
+
+
+
Boring.birth_year = broken_birth_year
+
+
+
+
+
+
+
+
In [24]:
+
+
+
x.birth_year()
+
+
+
+
+
+
+
+
Out[24]:
+
+
1948
+
+
+
+
+
+
+
+
In [25]:
+
+
+
x.birth_year
+
+
+
+
+
+
+
+
Out[25]:
+
+
<bound method broken_birth_year of <__main__.Boring object at 0x7f31546ab6a0>>
+
+
+
+
+
+
+
+
In [26]:
+
+
+
x.birth_year.__name__
+
+
+
+
+
+
+
+
Out[26]:
+
+
'broken_birth_year'
+
+
+
+
+
+
+
+
+

Metaprogramming function locals

+
+
+
+
+
+
+

We can access the attribute dictionary for the local namespace inside a +function with locals() but this cannot be written to.

+

Lack of safe +programmatic creation of function-local variables is a flaw in Python.

+
+
+
+
+
+
In [27]:
+
+
+
class Person:
+    def __init__(self, name, age, job, children_count):
+        for name, value in locals().items():
+            if name == 'self': 
+                continue
+            print(f"Setting self.{name} to {value}")
+            setattr(self, name, value)
+
+
+
+
+
+
+
+
In [28]:
+
+
+
terry = Person("Terry", 78, "Screenwriter", 0)
+
+
+
+
+
+
+
+
+
+
Setting self.name to Terry
+Setting self.age to 78
+Setting self.job to Screenwriter
+Setting self.children_count to 0
+
+
+
+
+
+
+
+
+
In [29]:
+
+
+
terry.name
+
+
+
+
+
+
+
+
Out[29]:
+
+
'Terry'
+
+
+
+
+
+
+
+
+

Metaprogramming warning!

+
+
+
+
+
+
+

Use this stuff sparingly!

+

The above example worked, but it produced Python code which is not particularly understandable. +Remember, your objective when programming is to produce code which is descriptive of what it does.

+

The above code is definitely less readable, less maintainable and more error prone than:

+
+
+
+
+
+
In [30]:
+
+
+
class Person:
+    def __init__(self, name, age, job, children_count):
+        self.name = name
+        self.age = age
+        self.job = job
+        self.children_count = children_count
+
+
+
+
+
+
+
+
+

Sometimes, metaprogramming will be really helpful in making non-repetitive +code, and you should have it in your toolbox, which is why I'm teaching you it. +But doing it all the time overcomplicated matters. We've talked a lot about the +DRY principle, but there is another equally important principle:

+
+

KISS: Keep it simple, Stupid!

+
+

Whenever you write code and you think, "Gosh, I'm really clever",you're +probably doing it wrong. Code should be about clarity, not showing off.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch07dry/060Metaprogramming.ipynb b/ch07dry/060Metaprogramming.ipynb new file mode 100644 index 000000000..7488f2635 --- /dev/null +++ b/ch07dry/060Metaprogramming.ipynb @@ -0,0 +1,689 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "39607f4f", + "metadata": {}, + "source": [ + "## Metaprogramming" + ] + }, + { + "cell_type": "markdown", + "id": "21784ca9", + "metadata": {}, + "source": [ + "Warning: Advanced topic!" + ] + }, + { + "cell_type": "markdown", + "id": "d9b6f318", + "metadata": {}, + "source": [ + "### Metaprogramming globals" + ] + }, + { + "cell_type": "markdown", + "id": "f1ac4cd6", + "metadata": {}, + "source": [ + "\n", + "Consider a bunch of variables, each of which need initialising and incrementing:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc93d9b7", + "metadata": {}, + "outputs": [], + "source": [ + "bananas = 0\n", + "apples = 0\n", + "oranges = 0\n", + "bananas += 1\n", + "apples += 1\n", + "oranges += 1" + ] + }, + { + "cell_type": "markdown", + "id": "efb5886f", + "metadata": {}, + "source": [ + "\n", + "\n", + "The right hand side of these assignments doesn't respect the DRY principle. We\n", + "could of course define a variable for our initial value:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b3071a3", + "metadata": {}, + "outputs": [], + "source": [ + "initial_fruit_count = 0\n", + "bananas = initial_fruit_count\n", + "apples = initial_fruit_count\n", + "oranges = initial_fruit_count" + ] + }, + { + "cell_type": "markdown", + "id": "af5aeb42", + "metadata": {}, + "source": [ + "\n", + "\n", + "However, this is still not as DRY as it could be: what if we wanted to replace\n", + "the assignment with, say, a class constructor and a buy operation:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "487a90cf", + "metadata": {}, + "outputs": [], + "source": [ + "class Basket:\n", + " def __init__(self):\n", + " self.count = 0\n", + " def buy(self):\n", + " self.count += 1\n", + "\n", + "bananas = Basket()\n", + "apples = Basket()\n", + "oranges = Basket()\n", + "bananas.buy()\n", + "apples.buy()\n", + "oranges.buy()" + ] + }, + { + "cell_type": "markdown", + "id": "af93b30b", + "metadata": {}, + "source": [ + "\n", + "\n", + "We had to make the change in three places. Whenever you see a situation where a\n", + "refactoring or change of design might require you to change the code in\n", + "multiple places, you have an opportunity to make the code DRYer.\n", + "\n", + "In this case, metaprogramming for incrementing these variables would involve\n", + "just a loop over all the variables we want to initialise:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e44127c", + "metadata": {}, + "outputs": [], + "source": [ + "baskets = [bananas, apples, oranges]\n", + "for basket in baskets: \n", + " basket.buy()" + ] + }, + { + "cell_type": "markdown", + "id": "a16b5090", + "metadata": {}, + "source": [ + "\n", + "\n", + "However, this trick **doesn't** work for initialising a new variable:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20df9670", + "metadata": {}, + "outputs": [], + "source": [ + "from pytest import raises\n", + "with raises(NameError):\n", + " baskets = [bananas, apples, oranges, kiwis]" + ] + }, + { + "cell_type": "markdown", + "id": "a7e8fc6a", + "metadata": {}, + "source": [ + "\n", + "\n", + "So can we declare a new variable programmatically? Given a list of the\n", + "**names** of fruit baskets we want, initialise a variable with that name?\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "057b1cd0", + "metadata": {}, + "outputs": [], + "source": [ + "basket_names = ['bananas', 'apples', 'oranges', 'kiwis']\n", + "\n", + "globals()['apples']" + ] + }, + { + "cell_type": "markdown", + "id": "10b88bb4", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Wow, we can! Every module or class in Python, is, under the hood, a special\n", + "dictionary, storing the values in its **namespace**. So we can create new\n", + "variables by assigning to this dictionary. globals() gives a reference to the\n", + "attribute dictionary for the current module\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "822d9c33", + "metadata": {}, + "outputs": [], + "source": [ + "for name in basket_names:\n", + " globals()[name] = Basket()\n", + "\n", + "\n", + "kiwis.count" + ] + }, + { + "cell_type": "markdown", + "id": "64678c08", + "metadata": {}, + "source": [ + "\n", + "\n", + "This is **metaprogramming**.\n", + "\n", + "I would NOT recommend using it for an example as trivial as the one above. \n", + "A better, more Pythonic choice here would be to use a data structure to manage your set of fruit baskets:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b8866c2", + "metadata": {}, + "outputs": [], + "source": [ + "baskets = {}\n", + "for name in basket_names:\n", + " baskets[name] = Basket()\n", + "\n", + "baskets['kiwis'].count" + ] + }, + { + "cell_type": "markdown", + "id": "6915c200", + "metadata": {}, + "source": [ + "\n", + "\n", + "Or even, using a dictionary comprehension:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfa91da3", + "metadata": {}, + "outputs": [], + "source": [ + "baskets = {name: Basket() for name in baskets}\n", + "baskets['kiwis'].count" + ] + }, + { + "cell_type": "markdown", + "id": "86b15ab3", + "metadata": {}, + "source": [ + "\n", + "\n", + "Which is the nicest way to do this, I think. Code which feels like\n", + "metaprogramming is needed to make it less repetitive can often instead be DRYed\n", + "up using a refactored data structure, in a way which is cleaner and more easy\n", + "to understand. Nevertheless, metaprogramming is worth knowing. \n" + ] + }, + { + "cell_type": "markdown", + "id": "52c77c46", + "metadata": {}, + "source": [ + "### Metaprogramming class attributes" + ] + }, + { + "cell_type": "markdown", + "id": "c583913d", + "metadata": {}, + "source": [ + "We can metaprogram the attributes of a **module** using the globals() function.\n", + "\n", + "We will also want to be able to metaprogram a class, by accessing its attribute dictionary.\n", + "\n", + "This will allow us, for example, to programmatically add members to a class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e02288e2", + "metadata": {}, + "outputs": [], + "source": [ + "class Boring: \n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "a4034321", + "metadata": {}, + "source": [ + "If we are adding our own attributes, we can just do so directly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dd6e16d", + "metadata": {}, + "outputs": [], + "source": [ + "x = Boring()\n", + "\n", + "x.name = \"Michael\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7afe4dad", + "metadata": {}, + "outputs": [], + "source": [ + "x.name" + ] + }, + { + "cell_type": "markdown", + "id": "6b782228", + "metadata": {}, + "source": [ + "And these turn up, as expected, in an attribute dictionary for the class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42879340", + "metadata": {}, + "outputs": [], + "source": [ + "x.__dict__" + ] + }, + { + "cell_type": "markdown", + "id": "56756ab6", + "metadata": {}, + "source": [ + "We can use `getattr` to access this special dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9a8b026", + "metadata": {}, + "outputs": [], + "source": [ + "getattr(x, 'name')" + ] + }, + { + "cell_type": "markdown", + "id": "d2246de9", + "metadata": {}, + "source": [ + "If we want to add an attribute given it's name as a string, we can use setattr:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d18aa440", + "metadata": {}, + "outputs": [], + "source": [ + "setattr(x, 'age', 75)\n", + "\n", + "x.age" + ] + }, + { + "cell_type": "markdown", + "id": "889b5706", + "metadata": {}, + "source": [ + "And we could do this in a loop to programmatically add many attributes." + ] + }, + { + "cell_type": "markdown", + "id": "4093461a", + "metadata": {}, + "source": [ + "The real power of accessing the attribute dictionary comes when we realise that\n", + "there is *very little difference* between member data and member functions." + ] + }, + { + "cell_type": "markdown", + "id": "0f5a4edd", + "metadata": {}, + "source": [ + "Now that we know, from our functional programming, that **a function is just a\n", + "variable that can be *called* with `()`**, we can set an attribute to a function,\n", + "and\n", + "it becomes a member function!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dbb2ceb", + "metadata": {}, + "outputs": [], + "source": [ + "setattr(Boring, 'describe', lambda self: f\"{self.name} is {self.age}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5d71ec7", + "metadata": {}, + "outputs": [], + "source": [ + "x.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f38126e5", + "metadata": {}, + "outputs": [], + "source": [ + "x.describe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da2ef1de", + "metadata": {}, + "outputs": [], + "source": [ + "Boring.describe" + ] + }, + { + "cell_type": "markdown", + "id": "68d04096", + "metadata": {}, + "source": [ + "Note that we set this method as an attribute of the class, not the instance, so it is available to other instances of `Boring`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2219d284", + "metadata": {}, + "outputs": [], + "source": [ + "y = Boring()\n", + "y.name = 'Terry'\n", + "y.age = 78" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c473a123", + "metadata": {}, + "outputs": [], + "source": [ + "y.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "9ade027b", + "metadata": {}, + "source": [ + "We can define a standalone function, and then **bind** it to the class. Its first argument automagically becomes\n", + "`self`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51fd5953", + "metadata": {}, + "outputs": [], + "source": [ + "def broken_birth_year(b_instance):\n", + " import datetime\n", + " current = datetime.datetime.now().year\n", + " return current - b_instance.age" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bd38af1", + "metadata": {}, + "outputs": [], + "source": [ + "Boring.birth_year = broken_birth_year" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa2762c6", + "metadata": {}, + "outputs": [], + "source": [ + "x.birth_year()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b48f34f4", + "metadata": {}, + "outputs": [], + "source": [ + "x.birth_year" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba2f8aab", + "metadata": {}, + "outputs": [], + "source": [ + "x.birth_year.__name__" + ] + }, + { + "cell_type": "markdown", + "id": "eedf12f7", + "metadata": {}, + "source": [ + "### Metaprogramming function locals" + ] + }, + { + "cell_type": "markdown", + "id": "dc4df0ac", + "metadata": {}, + "source": [ + "We can access the attribute dictionary for the local namespace inside a\n", + "function with `locals()` but this *cannot be written to*.\n", + "\n", + "Lack of safe\n", + "programmatic creation of function-local variables is a flaw in Python." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a69f25f8", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age, job, children_count):\n", + " for name, value in locals().items():\n", + " if name == 'self': \n", + " continue\n", + " print(f\"Setting self.{name} to {value}\")\n", + " setattr(self, name, value)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a143197", + "metadata": {}, + "outputs": [], + "source": [ + "terry = Person(\"Terry\", 78, \"Screenwriter\", 0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "837a1de4", + "metadata": {}, + "outputs": [], + "source": [ + "terry.name" + ] + }, + { + "cell_type": "markdown", + "id": "f0ac45b5", + "metadata": {}, + "source": [ + "### Metaprogramming warning!" + ] + }, + { + "cell_type": "markdown", + "id": "15ea3404", + "metadata": {}, + "source": [ + "\n", + "Use this stuff **sparingly**!\n", + "\n", + "The above example worked, but it produced Python code which is not particularly understandable.\n", + "Remember, your objective when programming is to produce code which is **descriptive of what it does**.\n", + "\n", + "The above code is **definitely** less readable, less maintainable and more error prone than:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c23d5d6", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age, job, children_count):\n", + " self.name = name\n", + " self.age = age\n", + " self.job = job\n", + " self.children_count = children_count" + ] + }, + { + "cell_type": "markdown", + "id": "a0f20d18", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Sometimes, metaprogramming will be **really** helpful in making non-repetitive\n", + "code, and you should have it in your toolbox, which is why I'm teaching you it.\n", + "But doing it all the time overcomplicated matters. We've talked a lot about the\n", + "DRY principle, but there is another equally important principle:\n", + "\n", + "> **KISS**: *Keep it simple, Stupid!*\n", + "\n", + "Whenever you write code and you think, \"Gosh, I'm really clever\",you're\n", + "probably *doing it wrong*. Code should be about clarity, not showing off.\n" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Metaprogramming" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch07dry/060Metaprogramming.ipynb.py b/ch07dry/060Metaprogramming.ipynb.py new file mode 100644 index 000000000..6d3bfecef --- /dev/null +++ b/ch07dry/060Metaprogramming.ipynb.py @@ -0,0 +1,351 @@ +# --- +# jupyter: +# jekyll: +# display_name: Metaprogramming +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Metaprogramming + +# %% [markdown] +# Warning: Advanced topic! + +# %% [markdown] +# ### Metaprogramming globals + +# %% [markdown] +# +# Consider a bunch of variables, each of which need initialising and incrementing: +# +# +# + +# %% +bananas = 0 +apples = 0 +oranges = 0 +bananas += 1 +apples += 1 +oranges += 1 + +# %% [markdown] +# +# +# The right hand side of these assignments doesn't respect the DRY principle. We +# could of course define a variable for our initial value: +# +# +# + +# %% +initial_fruit_count = 0 +bananas = initial_fruit_count +apples = initial_fruit_count +oranges = initial_fruit_count + + +# %% [markdown] +# +# +# However, this is still not as DRY as it could be: what if we wanted to replace +# the assignment with, say, a class constructor and a buy operation: +# +# +# + +# %% +class Basket: + def __init__(self): + self.count = 0 + def buy(self): + self.count += 1 + +bananas = Basket() +apples = Basket() +oranges = Basket() +bananas.buy() +apples.buy() +oranges.buy() + +# %% [markdown] +# +# +# We had to make the change in three places. Whenever you see a situation where a +# refactoring or change of design might require you to change the code in +# multiple places, you have an opportunity to make the code DRYer. +# +# In this case, metaprogramming for incrementing these variables would involve +# just a loop over all the variables we want to initialise: +# +# +# + +# %% +baskets = [bananas, apples, oranges] +for basket in baskets: + basket.buy() + +# %% [markdown] +# +# +# However, this trick **doesn't** work for initialising a new variable: +# +# +# + +# %% +from pytest import raises +with raises(NameError): + baskets = [bananas, apples, oranges, kiwis] + +# %% [markdown] +# +# +# So can we declare a new variable programmatically? Given a list of the +# **names** of fruit baskets we want, initialise a variable with that name? +# +# +# + +# %% +basket_names = ['bananas', 'apples', 'oranges', 'kiwis'] + +globals()['apples'] + +# %% [markdown] +# +# +# +# Wow, we can! Every module or class in Python, is, under the hood, a special +# dictionary, storing the values in its **namespace**. So we can create new +# variables by assigning to this dictionary. globals() gives a reference to the +# attribute dictionary for the current module +# +# +# + +# %% +for name in basket_names: + globals()[name] = Basket() + + +kiwis.count + +# %% [markdown] +# +# +# This is **metaprogramming**. +# +# I would NOT recommend using it for an example as trivial as the one above. +# A better, more Pythonic choice here would be to use a data structure to manage your set of fruit baskets: +# +# +# + +# %% +baskets = {} +for name in basket_names: + baskets[name] = Basket() + +baskets['kiwis'].count + +# %% [markdown] +# +# +# Or even, using a dictionary comprehension: +# +# +# + +# %% +baskets = {name: Basket() for name in baskets} +baskets['kiwis'].count + + +# %% [markdown] +# +# +# Which is the nicest way to do this, I think. Code which feels like +# metaprogramming is needed to make it less repetitive can often instead be DRYed +# up using a refactored data structure, in a way which is cleaner and more easy +# to understand. Nevertheless, metaprogramming is worth knowing. +# + +# %% [markdown] +# ### Metaprogramming class attributes + +# %% [markdown] +# We can metaprogram the attributes of a **module** using the globals() function. +# +# We will also want to be able to metaprogram a class, by accessing its attribute dictionary. +# +# This will allow us, for example, to programmatically add members to a class. + +# %% +class Boring: + pass + + +# %% [markdown] +# If we are adding our own attributes, we can just do so directly: + +# %% +x = Boring() + +x.name = "Michael" + +# %% +x.name + +# %% [markdown] +# And these turn up, as expected, in an attribute dictionary for the class: + +# %% +x.__dict__ + +# %% [markdown] +# We can use `getattr` to access this special dictionary: + +# %% +getattr(x, 'name') + +# %% [markdown] +# If we want to add an attribute given it's name as a string, we can use setattr: + +# %% +setattr(x, 'age', 75) + +x.age + +# %% [markdown] +# And we could do this in a loop to programmatically add many attributes. + +# %% [markdown] +# The real power of accessing the attribute dictionary comes when we realise that +# there is *very little difference* between member data and member functions. + +# %% [markdown] +# Now that we know, from our functional programming, that **a function is just a +# variable that can be *called* with `()`**, we can set an attribute to a function, +# and +# it becomes a member function! + +# %% +setattr(Boring, 'describe', lambda self: f"{self.name} is {self.age}") + +# %% +x.describe() + +# %% +x.describe + +# %% +Boring.describe + +# %% [markdown] +# Note that we set this method as an attribute of the class, not the instance, so it is available to other instances of `Boring`: + +# %% +y = Boring() +y.name = 'Terry' +y.age = 78 + +# %% +y.describe() + + +# %% [markdown] +# We can define a standalone function, and then **bind** it to the class. Its first argument automagically becomes +# `self`. + +# %% +def broken_birth_year(b_instance): + import datetime + current = datetime.datetime.now().year + return current - b_instance.age + + +# %% +Boring.birth_year = broken_birth_year + +# %% +x.birth_year() + +# %% +x.birth_year + +# %% +x.birth_year.__name__ + + +# %% [markdown] +# ### Metaprogramming function locals + +# %% [markdown] +# We can access the attribute dictionary for the local namespace inside a +# function with `locals()` but this *cannot be written to*. +# +# Lack of safe +# programmatic creation of function-local variables is a flaw in Python. + +# %% +class Person: + def __init__(self, name, age, job, children_count): + for name, value in locals().items(): + if name == 'self': + continue + print(f"Setting self.{name} to {value}") + setattr(self, name, value) + + +# %% +terry = Person("Terry", 78, "Screenwriter", 0) + +# %% +terry.name + + +# %% [markdown] +# ### Metaprogramming warning! + +# %% [markdown] +# +# Use this stuff **sparingly**! +# +# The above example worked, but it produced Python code which is not particularly understandable. +# Remember, your objective when programming is to produce code which is **descriptive of what it does**. +# +# The above code is **definitely** less readable, less maintainable and more error prone than: +# +# +# + +# %% +class Person: + def __init__(self, name, age, job, children_count): + self.name = name + self.age = age + self.job = job + self.children_count = children_count + +# %% [markdown] +# +# +# +# Sometimes, metaprogramming will be **really** helpful in making non-repetitive +# code, and you should have it in your toolbox, which is why I'm teaching you it. +# But doing it all the time overcomplicated matters. We've talked a lot about the +# DRY principle, but there is another equally important principle: +# +# > **KISS**: *Keep it simple, Stupid!* +# +# Whenever you write code and you think, "Gosh, I'm really clever",you're +# probably *doing it wrong*. Code should be about clarity, not showing off. +# diff --git a/ch07dry/datasource2.yaml b/ch07dry/datasource2.yaml new file mode 100644 index 000000000..d4a8f78d5 --- /dev/null +++ b/ch07dry/datasource2.yaml @@ -0,0 +1,2 @@ +userid: eidle +password: secret diff --git a/ch07dry/datasource3.yaml b/ch07dry/datasource3.yaml new file mode 100644 index 000000000..393ec3fae --- /dev/null +++ b/ch07dry/datasource3.yaml @@ -0,0 +1,2 @@ +user: eidle +password: secret diff --git a/ch07dry/example.yaml b/ch07dry/example.yaml new file mode 100644 index 000000000..4b2765749 --- /dev/null +++ b/ch07dry/example.yaml @@ -0,0 +1 @@ +modelname: brilliant diff --git a/ch07dry/index.html b/ch07dry/index.html new file mode 100644 index 000000000..d18e1f2b5 --- /dev/null +++ b/ch07dry/index.html @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced Programming Techniques + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Functional programming
  • +
  • Metaprogramming
  • +
  • Duck typing and exceptions
  • +
  • Operator overloading
  • +
  • Iterators and Generators
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/010intro.html b/ch08performance/010intro.html new file mode 100644 index 000000000..44899f1e8 --- /dev/null +++ b/ch08performance/010intro.html @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Two Mandelbrots + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Performance programming

+
+
+
+
+
+
+

We've spent most of this course looking at how to make code readable and reliable. For research work, it is often also important that code is efficient: that it does what it needs to do quickly.

+
+
+
+
+
+
+

It is very hard to work out beforehand whether code will be efficient or not: it is essential to Profile code, to measure its performance, to determine what aspects of it are slow.

+
+
+
+
+
+
+

When we looked at Functional programming, we claimed that code which is conceptualised in terms of actions on whole data-sets rather than individual elements is more efficient. Let's measure the performance of some different ways of implementing some code and see how they perform.

+
+
+
+
+
+
+

Two Mandelbrots

+
+
+
+
+
+
+

You're probably familiar with a famous fractal called the Mandelbrot Set.

+
+
+
+
+
+
+

For a complex number $c$, $c$ is in the Mandelbrot set if the series $z_{i+1}=z_{i}^2+c$ (With $z_0=c$) stays close to $0$. +Traditionally, we plot a color showing how many steps are needed for $\left|z_i\right|>2$, whereupon we are sure the series will diverge.

+
+
+
+
+
+
+

Here's a trivial python implementation:

+
+
+
+
+
+
In [1]:
+
+
+
def mandel1(position, limit=50):
+    
+    value = position
+    
+    while abs(value) < 2:
+        limit -= 1        
+        value = value**2 + position
+        if limit < 0:
+            return 0
+        
+    return limit
+
+
+
+
+
+
+
+
In [2]:
+
+
+
xmin = -1.5
+ymin = -1.0
+xmax = 0.5
+ymax = 1.0
+resolution = 300
+xstep = (xmax - xmin) / resolution
+ystep = (ymax - ymin) / resolution
+xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]
+ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]
+
+
+
+
+
+
+
+
In [3]:
+
+
+
%%timeit
+data = [[mandel1(complex(x, y)) for x in xs] for y in ys]
+
+
+
+
+
+
+
+
+
+
528 ms ± 3.49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [4]:
+
+
+
data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]
+
+
+
+
+
+
+
+
In [5]:
+
+
+
%matplotlib inline
+import matplotlib.pyplot as plt
+plt.imshow(data1, interpolation='none')
+
+
+
+
+
+
+
+
Out[5]:
+
+
<matplotlib.image.AxesImage at 0x7f31182030d0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We will learn this lesson how to make a version of this code which works Ten Times faster:

+
+
+
+
+
+
In [6]:
+
+
+
import numpy as np
+def mandel_numpy(position,limit=50):
+    value = position
+    diverged_at_count = np.zeros(position.shape)
+    while limit > 0:
+        limit -= 1
+        value = value**2+position
+        diverging = value * np.conj(value) > 4
+        first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0)
+        diverged_at_count[first_diverged_this_time] = limit
+        value[diverging] = 2
+        
+    return diverged_at_count
+
+
+
+
+
+
+
+
In [7]:
+
+
+
ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]
+
+
+
+
+
+
+
+
In [8]:
+
+
+
values = xmatrix + 1j * ymatrix
+
+
+
+
+
+
+
+
In [9]:
+
+
+
data_numpy = mandel_numpy(values)
+
+
+
+
+
+
+
+
In [10]:
+
+
+
%matplotlib inline
+import matplotlib.pyplot as plt
+plt.imshow(data_numpy, interpolation='none')
+
+
+
+
+
+
+
+
Out[10]:
+
+
<matplotlib.image.AxesImage at 0x7f311816fbe0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [11]:
+
+
+
%%timeit
+data_numpy = mandel_numpy(values)
+
+
+
+
+
+
+
+
+
+
25.6 ms ± 86.5 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
+

Note we get the same answer:

+
+
+
+
+
+
In [12]:
+
+
+
sum(sum(abs(data_numpy - data1)))
+
+
+
+
+
+
+
+
Out[12]:
+
+
0.0
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/010intro.ipynb b/ch08performance/010intro.ipynb new file mode 100644 index 000000000..5d950e88e --- /dev/null +++ b/ch08performance/010intro.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f29200ca", + "metadata": {}, + "source": [ + "# Performance programming" + ] + }, + { + "cell_type": "markdown", + "id": "02438492", + "metadata": {}, + "source": [ + "We've spent most of this course looking at how to make code readable and reliable. For research work, it is often also important that code is efficient: that it does what it needs to do *quickly*." + ] + }, + { + "cell_type": "markdown", + "id": "834ecd39", + "metadata": {}, + "source": [ + "It is very hard to work out beforehand whether code will be efficient or not: it is essential to *Profile* code, to measure its performance, to determine what aspects of it are slow." + ] + }, + { + "cell_type": "markdown", + "id": "a4b7b464", + "metadata": {}, + "source": [ + "When we looked at Functional programming, we claimed that code which is conceptualised in terms of actions on whole data-sets rather than individual elements is more efficient. Let's measure the performance of some different ways of implementing some code and see how they perform." + ] + }, + { + "cell_type": "markdown", + "id": "1e7bce0e", + "metadata": {}, + "source": [ + "## Two Mandelbrots" + ] + }, + { + "cell_type": "markdown", + "id": "c8c27854", + "metadata": {}, + "source": [ + "You're probably familiar with a famous fractal called the [Mandelbrot Set](https://www.youtube.com/watch?v=ZDU40eUcTj0)." + ] + }, + { + "cell_type": "markdown", + "id": "54fbe4e3", + "metadata": {}, + "source": [ + "For a complex number $c$, $c$ is in the Mandelbrot set if the series $z_{i+1}=z_{i}^2+c$ (With $z_0=c$) stays close to $0$.\n", + "Traditionally, we plot a color showing how many steps are needed for $\\left|z_i\\right|>2$, whereupon we are sure the series will diverge." + ] + }, + { + "cell_type": "markdown", + "id": "81d644da", + "metadata": {}, + "source": [ + "Here's a trivial python implementation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f88ce8d", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel1(position, limit=50):\n", + " \n", + " value = position\n", + " \n", + " while abs(value) < 2:\n", + " limit -= 1 \n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " \n", + " return limit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7f55a5d", + "metadata": {}, + "outputs": [], + "source": [ + "xmin = -1.5\n", + "ymin = -1.0\n", + "xmax = 0.5\n", + "ymax = 1.0\n", + "resolution = 300\n", + "xstep = (xmax - xmin) / resolution\n", + "ystep = (ymax - ymin) / resolution\n", + "xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]\n", + "ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5fa777af", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "data = [[mandel1(complex(x, y)) for x in xs] for y in ys]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b99b7a3", + "metadata": {}, + "outputs": [], + "source": [ + "data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "474a86bd", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "plt.imshow(data1, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "b7f31d57", + "metadata": {}, + "source": [ + "We will learn this lesson how to make a version of this code which works Ten Times faster:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56b02aee", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "def mandel_numpy(position,limit=50):\n", + " value = position\n", + " diverged_at_count = np.zeros(position.shape)\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2+position\n", + " diverging = value * np.conj(value) > 4\n", + " first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0)\n", + " diverged_at_count[first_diverged_this_time] = limit\n", + " value[diverging] = 2\n", + " \n", + " return diverged_at_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24cf7a28", + "metadata": {}, + "outputs": [], + "source": [ + "ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd43c7be", + "metadata": {}, + "outputs": [], + "source": [ + "values = xmatrix + 1j * ymatrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "524a7768", + "metadata": {}, + "outputs": [], + "source": [ + "data_numpy = mandel_numpy(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d932021", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "plt.imshow(data_numpy, interpolation='none')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9cb705a6", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "data_numpy = mandel_numpy(values)" + ] + }, + { + "cell_type": "markdown", + "id": "1566d54e", + "metadata": {}, + "source": [ + "Note we get the same answer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10984d7e", + "metadata": {}, + "outputs": [], + "source": [ + "sum(sum(abs(data_numpy - data1)))" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Two Mandelbrots" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch08performance/010intro.ipynb.py b/ch08performance/010intro.ipynb.py new file mode 100644 index 000000000..07b6f67c7 --- /dev/null +++ b/ch08performance/010intro.ipynb.py @@ -0,0 +1,117 @@ +# --- +# jupyter: +# jekyll: +# display_name: Two Mandelbrots +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Performance programming + +# %% [markdown] +# We've spent most of this course looking at how to make code readable and reliable. For research work, it is often also important that code is efficient: that it does what it needs to do *quickly*. + +# %% [markdown] +# It is very hard to work out beforehand whether code will be efficient or not: it is essential to *Profile* code, to measure its performance, to determine what aspects of it are slow. + +# %% [markdown] +# When we looked at Functional programming, we claimed that code which is conceptualised in terms of actions on whole data-sets rather than individual elements is more efficient. Let's measure the performance of some different ways of implementing some code and see how they perform. + +# %% [markdown] +# ## Two Mandelbrots + +# %% [markdown] +# You're probably familiar with a famous fractal called the [Mandelbrot Set](https://www.youtube.com/watch?v=ZDU40eUcTj0). + +# %% [markdown] +# For a complex number $c$, $c$ is in the Mandelbrot set if the series $z_{i+1}=z_{i}^2+c$ (With $z_0=c$) stays close to $0$. +# Traditionally, we plot a color showing how many steps are needed for $\left|z_i\right|>2$, whereupon we are sure the series will diverge. + +# %% [markdown] +# Here's a trivial python implementation: + +# %% +def mandel1(position, limit=50): + + value = position + + while abs(value) < 2: + limit -= 1 + value = value**2 + position + if limit < 0: + return 0 + + return limit + + +# %% +xmin = -1.5 +ymin = -1.0 +xmax = 0.5 +ymax = 1.0 +resolution = 300 +xstep = (xmax - xmin) / resolution +ystep = (ymax - ymin) / resolution +xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)] +ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)] + +# %% +# %%timeit +data = [[mandel1(complex(x, y)) for x in xs] for y in ys] + +# %% +data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys] + +# %% +# %matplotlib inline +import matplotlib.pyplot as plt +plt.imshow(data1, interpolation='none') + +# %% [markdown] +# We will learn this lesson how to make a version of this code which works Ten Times faster: + +# %% +import numpy as np +def mandel_numpy(position,limit=50): + value = position + diverged_at_count = np.zeros(position.shape) + while limit > 0: + limit -= 1 + value = value**2+position + diverging = value * np.conj(value) > 4 + first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0) + diverged_at_count[first_diverged_this_time] = limit + value[diverging] = 2 + + return diverged_at_count + + +# %% +ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep] + +# %% +values = xmatrix + 1j * ymatrix + +# %% +data_numpy = mandel_numpy(values) + +# %% +# %matplotlib inline +import matplotlib.pyplot as plt +plt.imshow(data_numpy, interpolation='none') + +# %% +# %%timeit +data_numpy = mandel_numpy(values) + +# %% [markdown] +# Note we get the same answer: + +# %% +sum(sum(abs(data_numpy - data1))) diff --git a/ch08performance/015mandels.html b/ch08performance/015mandels.html new file mode 100644 index 000000000..ea2238084 --- /dev/null +++ b/ch08performance/015mandels.html @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Faster Mandelbrots? + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
In [1]:
+
+
+
xmin = -1.5
+ymin = -1.0
+xmax = 0.5
+ymax = 1.0
+resolution = 300
+xstep = (xmax - xmin) / resolution
+ystep = (ymax - ymin) / resolution
+xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]
+ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]
+
+
+
+
+
+
+
+
In [2]:
+
+
+
def mandel1(position, limit=50):
+    value = position    
+    while abs(value) < 2:
+        limit -= 1        
+        value = value**2 + position        
+        if limit < 0:
+            return 0        
+    return limit
+
+
+
+
+
+
+
+
In [3]:
+
+
+
data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]
+
+
+
+
+
+
+
+
+

Many Mandelbrots

+
+
+
+
+
+
+

Let's compare our naive python implementation which used a list comprehension, taking 662ms, with the following:

+
+
+
+
+
+
In [4]:
+
+
+
%%timeit
+data2 = []
+for y in ys:
+    row = []
+    for x in xs:
+        row.append(mandel1(complex(x, y)))
+    data2.append(row)
+
+
+
+
+
+
+
+
+
+
532 ms ± 1.72 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
data2 = []
+for y in ys:
+    row = []
+    for x in xs:
+        row.append(mandel1(complex(x, y)))
+    data2.append(row)
+
+
+
+
+
+
+
+
+

Interestingly, not much difference. I would have expected this to be slower, due to the normally high cost of appending to data.

+
+
+
+
+
+
In [6]:
+
+
+
from matplotlib import pyplot as plt
+%matplotlib inline
+plt.imshow(data2, interpolation='none')
+
+
+
+
+
+
+
+
Out[6]:
+
+
<matplotlib.image.AxesImage at 0x7f593c733ac0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We ought to be checking if these results are the same by comparing the values in a test, rather than re-plotting. This is cumbersome in pure Python, but easy with NumPy, so we'll do this later.

+
+
+
+
+
+
+

Let's try a pre-allocated data structure:

+
+
+
+
+
+
In [7]:
+
+
+
data3 = [[0 for i in range(resolution)] for j in range(resolution)]
+
+
+
+
+
+
+
+
In [8]:
+
+
+
%%timeit
+for j, y in enumerate(ys):
+    for i, x in enumerate(xs):
+        data3[j][i] = mandel1(complex(x, y))
+
+
+
+
+
+
+
+
+
+
538 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [9]:
+
+
+
for j, y in enumerate(ys):
+    for i, x in enumerate(xs):
+        data3[j][i] = mandel1(complex(x, y))
+
+
+
+
+
+
+
+
In [10]:
+
+
+
plt.imshow(data3, interpolation='none')
+
+
+
+
+
+
+
+
Out[10]:
+
+
<matplotlib.image.AxesImage at 0x7f593c38a3d0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Nope, no gain there.

+
+
+
+
+
+
+

Let's try using functional programming approaches:

+
+
+
+
+
+
In [11]:
+
+
+
%%timeit
+data4 = []
+for y in ys:
+    bind_mandel = lambda x: mandel1(complex(x, y))
+    data4.append(list(map(bind_mandel, xs)))
+
+
+
+
+
+
+
+
+
+
538 ms ± 976 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
data4 = []
+for y in ys:
+    bind_mandel = lambda x: mandel1(complex(x, y))
+    data4.append(list(map(bind_mandel, xs)))
+
+
+
+
+
+
+
+
In [13]:
+
+
+
plt.imshow(data4, interpolation='none')
+
+
+
+
+
+
+
+
Out[13]:
+
+
<matplotlib.image.AxesImage at 0x7f593c306430>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

That was a tiny bit slower.

+
+
+
+
+
+
+

So, what do we learn from this? Our mental image of what code should be faster or slower is often wrong, or doesn't make much difference. The only way to really improve code performance is empirically, through measurements.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/015mandels.ipynb b/ch08performance/015mandels.ipynb new file mode 100644 index 000000000..c222618bf --- /dev/null +++ b/ch08performance/015mandels.ipynb @@ -0,0 +1,257 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7089ec8c", + "metadata": {}, + "outputs": [], + "source": [ + "xmin = -1.5\n", + "ymin = -1.0\n", + "xmax = 0.5\n", + "ymax = 1.0\n", + "resolution = 300\n", + "xstep = (xmax - xmin) / resolution\n", + "ystep = (ymax - ymin) / resolution\n", + "xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]\n", + "ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e748f91b", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel1(position, limit=50):\n", + " value = position \n", + " while abs(value) < 2:\n", + " limit -= 1 \n", + " value = value**2 + position \n", + " if limit < 0:\n", + " return 0 \n", + " return limit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd66e3ca", + "metadata": {}, + "outputs": [], + "source": [ + "data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]" + ] + }, + { + "cell_type": "markdown", + "id": "526ab541", + "metadata": {}, + "source": [ + "## Many Mandelbrots" + ] + }, + { + "cell_type": "markdown", + "id": "14903863", + "metadata": {}, + "source": [ + "Let's compare our naive python implementation which used a list comprehension, taking 662ms, with the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "999278f9", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "data2 = []\n", + "for y in ys:\n", + " row = []\n", + " for x in xs:\n", + " row.append(mandel1(complex(x, y)))\n", + " data2.append(row)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d30d273", + "metadata": {}, + "outputs": [], + "source": [ + "data2 = []\n", + "for y in ys:\n", + " row = []\n", + " for x in xs:\n", + " row.append(mandel1(complex(x, y)))\n", + " data2.append(row)" + ] + }, + { + "cell_type": "markdown", + "id": "1657ce68", + "metadata": {}, + "source": [ + "Interestingly, not much difference. I would have expected this to be slower, due to the normally high cost of **appending** to data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26fcbc67", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "plt.imshow(data2, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "e6d7bc41", + "metadata": {}, + "source": [ + "We ought to be checking if these results are the same by comparing the values in a test, rather than re-plotting. This is cumbersome in pure Python, but easy with NumPy, so we'll do this later." + ] + }, + { + "cell_type": "markdown", + "id": "f30b5015", + "metadata": {}, + "source": [ + "Let's try a pre-allocated data structure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb8839e1", + "metadata": {}, + "outputs": [], + "source": [ + "data3 = [[0 for i in range(resolution)] for j in range(resolution)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e554b7c", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "for j, y in enumerate(ys):\n", + " for i, x in enumerate(xs):\n", + " data3[j][i] = mandel1(complex(x, y))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bac34dc5", + "metadata": {}, + "outputs": [], + "source": [ + "for j, y in enumerate(ys):\n", + " for i, x in enumerate(xs):\n", + " data3[j][i] = mandel1(complex(x, y))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49ca7f25", + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow(data3, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "3d9076a9", + "metadata": {}, + "source": [ + "Nope, no gain there. " + ] + }, + { + "cell_type": "markdown", + "id": "0e21d208", + "metadata": {}, + "source": [ + "Let's try using functional programming approaches:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6b64c6d", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "data4 = []\n", + "for y in ys:\n", + " bind_mandel = lambda x: mandel1(complex(x, y))\n", + " data4.append(list(map(bind_mandel, xs)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00c54ff7", + "metadata": {}, + "outputs": [], + "source": [ + "data4 = []\n", + "for y in ys:\n", + " bind_mandel = lambda x: mandel1(complex(x, y))\n", + " data4.append(list(map(bind_mandel, xs)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f7f0563", + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow(data4, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "8501e51e", + "metadata": {}, + "source": [ + "That was a tiny bit slower." + ] + }, + { + "cell_type": "markdown", + "id": "421828d1", + "metadata": {}, + "source": [ + "So, what do we learn from this? Our mental image of what code should be faster or slower is often wrong, or doesn't make much difference. The only way to really improve code performance is empirically, through measurements." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Faster Mandelbrots?" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch08performance/015mandels.ipynb.py b/ch08performance/015mandels.ipynb.py new file mode 100644 index 000000000..cbdce6d8e --- /dev/null +++ b/ch08performance/015mandels.ipynb.py @@ -0,0 +1,120 @@ +# --- +# jupyter: +# jekyll: +# display_name: Faster Mandelbrots? +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% +xmin = -1.5 +ymin = -1.0 +xmax = 0.5 +ymax = 1.0 +resolution = 300 +xstep = (xmax - xmin) / resolution +ystep = (ymax - ymin) / resolution +xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)] +ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)] + + +# %% +def mandel1(position, limit=50): + value = position + while abs(value) < 2: + limit -= 1 + value = value**2 + position + if limit < 0: + return 0 + return limit + + +# %% +data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys] + +# %% [markdown] +# ## Many Mandelbrots + +# %% [markdown] +# Let's compare our naive python implementation which used a list comprehension, taking 662ms, with the following: + +# %% +# %%timeit +data2 = [] +for y in ys: + row = [] + for x in xs: + row.append(mandel1(complex(x, y))) + data2.append(row) + +# %% +data2 = [] +for y in ys: + row = [] + for x in xs: + row.append(mandel1(complex(x, y))) + data2.append(row) + +# %% [markdown] +# Interestingly, not much difference. I would have expected this to be slower, due to the normally high cost of **appending** to data. + +# %% +from matplotlib import pyplot as plt +# %matplotlib inline +plt.imshow(data2, interpolation='none') + +# %% [markdown] +# We ought to be checking if these results are the same by comparing the values in a test, rather than re-plotting. This is cumbersome in pure Python, but easy with NumPy, so we'll do this later. + +# %% [markdown] +# Let's try a pre-allocated data structure: + +# %% +data3 = [[0 for i in range(resolution)] for j in range(resolution)] + +# %% +# %%timeit +for j, y in enumerate(ys): + for i, x in enumerate(xs): + data3[j][i] = mandel1(complex(x, y)) + +# %% +for j, y in enumerate(ys): + for i, x in enumerate(xs): + data3[j][i] = mandel1(complex(x, y)) + +# %% +plt.imshow(data3, interpolation='none') + +# %% [markdown] +# Nope, no gain there. + +# %% [markdown] +# Let's try using functional programming approaches: + +# %% +# %%timeit +data4 = [] +for y in ys: + bind_mandel = lambda x: mandel1(complex(x, y)) + data4.append(list(map(bind_mandel, xs))) + +# %% +data4 = [] +for y in ys: + bind_mandel = lambda x: mandel1(complex(x, y)) + data4.append(list(map(bind_mandel, xs))) + +# %% +plt.imshow(data4, interpolation='none') + +# %% [markdown] +# That was a tiny bit slower. + +# %% [markdown] +# So, what do we learn from this? Our mental image of what code should be faster or slower is often wrong, or doesn't make much difference. The only way to really improve code performance is empirically, through measurements. diff --git a/ch08performance/020numpy.html b/ch08performance/020numpy.html new file mode 100644 index 000000000..9e7ea9766 --- /dev/null +++ b/ch08performance/020numpy.html @@ -0,0 +1,2230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NumPy + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

NumPy for Performance

+
+
+
+
+
+
+

NumPy constructors

+
+
+
+
+
+
+

We saw previously that NumPy's core type is the ndarray, or N-Dimensional Array:

+
+
+
+
+
+
In [1]:
+
+
+
import numpy as np
+np.zeros([3, 4, 2, 5])[2, :, :, 1]
+
+
+
+
+
+
+
+
Out[1]:
+
+
array([[0., 0.],
+       [0., 0.],
+       [0., 0.],
+       [0., 0.]])
+
+
+
+
+
+
+
+
+

The real magic of numpy arrays is that most python operations are applied, quickly, on an elementwise basis:

+
+
+
+
+
+
In [2]:
+
+
+
x = np.arange(0, 256, 4).reshape(8, 8)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
y = np.zeros((8, 8))
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%timeit
+for i in range(8):
+    for j in range(8):
+        y[i][j] = x[i][j] + 10
+
+
+
+
+
+
+
+
+
+
33.5 µs ± 34.6 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
+
+
+
+
+
+
+
+
+
In [5]:
+
+
+
x + 10
+
+
+
+
+
+
+
+
Out[5]:
+
+
array([[ 10,  14,  18,  22,  26,  30,  34,  38],
+       [ 42,  46,  50,  54,  58,  62,  66,  70],
+       [ 74,  78,  82,  86,  90,  94,  98, 102],
+       [106, 110, 114, 118, 122, 126, 130, 134],
+       [138, 142, 146, 150, 154, 158, 162, 166],
+       [170, 174, 178, 182, 186, 190, 194, 198],
+       [202, 206, 210, 214, 218, 222, 226, 230],
+       [234, 238, 242, 246, 250, 254, 258, 262]])
+
+
+
+
+
+
+
+
+

Numpy's mathematical functions also happen this way, and are said to be "vectorized" functions.

+
+
+
+
+
+
In [6]:
+
+
+
np.sqrt(x)
+
+
+
+
+
+
+
+
Out[6]:
+
+
array([[ 0.        ,  2.        ,  2.82842712,  3.46410162,  4.        ,
+         4.47213595,  4.89897949,  5.29150262],
+       [ 5.65685425,  6.        ,  6.32455532,  6.63324958,  6.92820323,
+         7.21110255,  7.48331477,  7.74596669],
+       [ 8.        ,  8.24621125,  8.48528137,  8.71779789,  8.94427191,
+         9.16515139,  9.38083152,  9.59166305],
+       [ 9.79795897, 10.        , 10.19803903, 10.39230485, 10.58300524,
+        10.77032961, 10.95445115, 11.13552873],
+       [11.3137085 , 11.48912529, 11.66190379, 11.83215957, 12.        ,
+        12.16552506, 12.32882801, 12.489996  ],
+       [12.64911064, 12.80624847, 12.9614814 , 13.11487705, 13.26649916,
+        13.41640786, 13.56465997, 13.7113092 ],
+       [13.85640646, 14.        , 14.14213562, 14.28285686, 14.4222051 ,
+        14.56021978, 14.69693846, 14.83239697],
+       [14.96662955, 15.09966887, 15.23154621, 15.3622915 , 15.49193338,
+        15.62049935, 15.74801575, 15.87450787]])
+
+
+
+
+
+
+
+
+

Numpy contains many useful functions for creating matrices. In our earlier lectures we've seen linspace and arange for evenly spaced numbers.

+
+
+
+
+
+
In [7]:
+
+
+
np.linspace(0, 10, 21)
+
+
+
+
+
+
+
+
Out[7]:
+
+
array([ 0. ,  0.5,  1. ,  1.5,  2. ,  2.5,  3. ,  3.5,  4. ,  4.5,  5. ,
+        5.5,  6. ,  6.5,  7. ,  7.5,  8. ,  8.5,  9. ,  9.5, 10. ])
+
+
+
+
+
+
+
+
In [8]:
+
+
+
np.arange(0, 10, 0.5)
+
+
+
+
+
+
+
+
Out[8]:
+
+
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. ,
+       6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5])
+
+
+
+
+
+
+
+
+

Here's one for creating matrices like coordinates in a grid:

+
+
+
+
+
+
In [9]:
+
+
+
xmin = -1.5
+ymin = -1.0
+xmax = 0.5
+ymax = 1.0
+resolution = 300
+xstep = (xmax - xmin) / resolution
+ystep = (ymax - ymin) / resolution
+
+ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]
+
+
+
+
+
+
+
+
In [10]:
+
+
+
print(ymatrix)
+
+
+
+
+
+
+
+
+
+
[[-1.         -1.         -1.         ... -1.         -1.
+  -1.        ]
+ [-0.99333333 -0.99333333 -0.99333333 ... -0.99333333 -0.99333333
+  -0.99333333]
+ [-0.98666667 -0.98666667 -0.98666667 ... -0.98666667 -0.98666667
+  -0.98666667]
+ ...
+ [ 0.98        0.98        0.98       ...  0.98        0.98
+   0.98      ]
+ [ 0.98666667  0.98666667  0.98666667 ...  0.98666667  0.98666667
+   0.98666667]
+ [ 0.99333333  0.99333333  0.99333333 ...  0.99333333  0.99333333
+   0.99333333]]
+
+
+
+
+
+
+
+
+
+

We can add these together to make a grid containing the complex numbers we want to test for membership in the Mandelbrot set.

+
+
+
+
+
+
In [11]:
+
+
+
values = xmatrix + 1j * ymatrix
+
+
+
+
+
+
+
+
In [12]:
+
+
+
print(values)
+
+
+
+
+
+
+
+
+
+
[[-1.5       -1.j         -1.49333333-1.j         -1.48666667-1.j
+  ...  0.48      -1.j          0.48666667-1.j
+   0.49333333-1.j        ]
+ [-1.5       -0.99333333j -1.49333333-0.99333333j -1.48666667-0.99333333j
+  ...  0.48      -0.99333333j  0.48666667-0.99333333j
+   0.49333333-0.99333333j]
+ [-1.5       -0.98666667j -1.49333333-0.98666667j -1.48666667-0.98666667j
+  ...  0.48      -0.98666667j  0.48666667-0.98666667j
+   0.49333333-0.98666667j]
+ ...
+ [-1.5       +0.98j       -1.49333333+0.98j       -1.48666667+0.98j
+  ...  0.48      +0.98j        0.48666667+0.98j
+   0.49333333+0.98j      ]
+ [-1.5       +0.98666667j -1.49333333+0.98666667j -1.48666667+0.98666667j
+  ...  0.48      +0.98666667j  0.48666667+0.98666667j
+   0.49333333+0.98666667j]
+ [-1.5       +0.99333333j -1.49333333+0.99333333j -1.48666667+0.99333333j
+  ...  0.48      +0.99333333j  0.48666667+0.99333333j
+   0.49333333+0.99333333j]]
+
+
+
+
+
+
+
+
+
+

Arraywise Algorithms

+
+
+
+
+
+
+

We can use this to apply the mandelbrot algorithm to whole ARRAYS

+
+
+
+
+
+
In [13]:
+
+
+
z0 = values
+z1 = z0 * z0 + values
+z2 = z1 * z1 + values
+z3 = z2 * z2 + values
+
+
+
+
+
+
+
+
In [14]:
+
+
+
print(z3)
+
+
+
+
+
+
+
+
+
+
[[24.06640625+20.75j       23.16610231+20.97899073j
+  22.27540349+21.18465854j ... 11.20523832 -1.88650846j
+  11.5734533  -1.6076251j  11.94394738 -1.31225596j]
+ [23.82102149+19.85687829j 22.94415031+20.09504528j
+  22.07634812+20.31020645j ... 10.93323949 -1.5275283j
+  11.28531994 -1.24641067j 11.63928527 -0.94911594j]
+ [23.56689029+18.98729242j 22.71312709+19.23410533j
+  21.86791017+19.4582314j  ... 10.65905064 -1.18433756j
+  10.99529965 -0.90137318j 11.33305161 -0.60254144j]
+ ...
+ [23.30453709-18.14090998j 22.47355537-18.39585192j
+  21.65061048-18.62842771j ... 10.38305264 +0.85663867j
+  10.70377437 +0.57220289j 11.02562928 +0.27221042j]
+ [23.56689029-18.98729242j 22.71312709-19.23410533j
+  21.86791017-19.4582314j  ... 10.65905064 +1.18433756j
+  10.99529965 +0.90137318j 11.33305161 +0.60254144j]
+ [23.82102149-19.85687829j 22.94415031-20.09504528j
+  22.07634812-20.31020645j ... 10.93323949 +1.5275283j
+  11.28531994 +1.24641067j 11.63928527 +0.94911594j]]
+
+
+
+
+
+
+
+
+
+

So can we just apply our mandel1 function to the whole matrix?

+
+
+
+
+
+
In [15]:
+
+
+
def mandel1(position,limit=50):
+    value = position
+    while abs(value) < 2:
+        limit -= 1
+        value = value**2 + position
+        if limit < 0:
+            return 0
+    return limit
+
+
+
+
+
+
+
+
In [16]:
+
+
+
mandel1(values)
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ValueError                                Traceback (most recent call last)
+Cell In[16], line 1
+----> 1 mandel1(values)
+
+Cell In[15], line 3, in mandel1(position, limit)
+      1 def mandel1(position,limit=50):
+      2     value = position
+----> 3     while abs(value) < 2:
+      4         limit -= 1
+      5         value = value**2 + position
+
+ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
+
+
+
+
+
+
+
+
+

No. The logic of our current routine would require stopping for some elements and not for others.

+
+
+
+
+
+
+

We can ask numpy to vectorise our method for us:

+
+
+
+
+
+
In [17]:
+
+
+
mandel2 = np.vectorize(mandel1)
+
+
+
+
+
+
+
+
In [18]:
+
+
+
data5 = mandel2(values)
+
+
+
+
+
+
+
+
In [19]:
+
+
+
from matplotlib import pyplot as plt
+%matplotlib inline
+plt.imshow(data5, interpolation='none')
+
+
+
+
+
+
+
+
Out[19]:
+
+
<matplotlib.image.AxesImage at 0x7fc3840cd760>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Is that any faster?

+
+
+
+
+
+
In [20]:
+
+
+
%%timeit
+data5 = mandel2(values)
+
+
+
+
+
+
+
+
+
+
525 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
+

This is not significantly faster. When we use vectorize it's just hiding an plain old python for loop under the hood. We want to make the loop over matrix elements take place in the "C Layer".

+
+
+
+
+
+
+

What if we just apply the Mandelbrot algorithm without checking for divergence until the end:

+
+
+
+
+
+
In [21]:
+
+
+
def mandel_numpy_explode(position, limit=50):
+    value = position
+    while limit > 0:
+        limit -= 1
+        value = value**2 + position
+        diverging = abs(value) > 2
+
+        
+    return abs(value) < 2
+
+
+
+
+
+
+
+
In [22]:
+
+
+
data6 = mandel_numpy_explode(values)
+
+
+
+
+
+
+
+
+
+
/tmp/ipykernel_13597/3053579176.py:5: RuntimeWarning: overflow encountered in square
+  value = value**2 + position
+/tmp/ipykernel_13597/3053579176.py:5: RuntimeWarning: invalid value encountered in square
+  value = value**2 + position
+/tmp/ipykernel_13597/3053579176.py:6: RuntimeWarning: overflow encountered in absolute
+  diverging = abs(value) > 2
+
+
+
+
+
+
+
+
+
+

OK, we need to prevent it from running off to $\infty$

+
+
+
+
+
+
In [23]:
+
+
+
def mandel_numpy(position, limit=50):
+    value = position
+    while limit > 0:
+        limit -= 1
+        value = value**2 + position
+        diverging = abs(value) > 2
+        # Avoid overflow
+        value[diverging] = 2
+        
+    return abs(value) < 2
+
+
+
+
+
+
+
+
In [24]:
+
+
+
data6 = mandel_numpy(values)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
%%timeit
+
+data6 = mandel_numpy(values)
+
+
+
+
+
+
+
+
+
+
52.3 ms ± 475 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
In [26]:
+
+
+
from matplotlib import pyplot as plt
+%matplotlib inline
+plt.imshow(data6, interpolation='none')
+
+
+
+
+
+
+
+
Out[26]:
+
+
<matplotlib.image.AxesImage at 0x7fc376f81160>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Wow, that was TEN TIMES faster.

+
+
+
+
+
+
+

There's quite a few NumPy tricks there, let's remind ourselves of how they work:

+
+
+
+
+
+
In [27]:
+
+
+
diverging = abs(z3) > 2
+z3[diverging] = 2
+
+
+
+
+
+
+
+
+

When we apply a logical condition to a NumPy array, we get a logical array.

+
+
+
+
+
+
In [28]:
+
+
+
x = np.arange(10)
+y = np.ones([10]) * 5
+z = x > y
+
+
+
+
+
+
+
+
In [29]:
+
+
+
x
+
+
+
+
+
+
+
+
Out[29]:
+
+
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+
+
+
+
+
+
+
In [30]:
+
+
+
y
+
+
+
+
+
+
+
+
Out[30]:
+
+
array([5., 5., 5., 5., 5., 5., 5., 5., 5., 5.])
+
+
+
+
+
+
+
+
In [31]:
+
+
+
print(z)
+
+
+
+
+
+
+
+
+
+
[False False False False False False  True  True  True  True]
+
+
+
+
+
+
+
+
+
+

Logical arrays can be used to index into arrays:

+
+
+
+
+
+
In [32]:
+
+
+
x[x>3]
+
+
+
+
+
+
+
+
Out[32]:
+
+
array([4, 5, 6, 7, 8, 9])
+
+
+
+
+
+
+
+
In [33]:
+
+
+
x[np.logical_not(z)]
+
+
+
+
+
+
+
+
Out[33]:
+
+
array([0, 1, 2, 3, 4, 5])
+
+
+
+
+
+
+
+
+

And you can use such an index as the target of an assignment:

+
+
+
+
+
+
In [34]:
+
+
+
x[z] = 5
+x
+
+
+
+
+
+
+
+
Out[34]:
+
+
array([0, 1, 2, 3, 4, 5, 5, 5, 5, 5])
+
+
+
+
+
+
+
+
+

Note that we didn't compare two arrays to get our logical array, but an array to a scalar integer -- this was broadcasting again.

+
+
+
+
+
+
+

More Mandelbrot

+
+
+
+
+
+
+

Of course, we didn't calculate the number-of-iterations-to-diverge, just whether the point was in the set.

+
+
+
+
+
+
+

Let's correct our code to do that:

+
+
+
+
+
+
In [35]:
+
+
+
def mandel4(position,limit=50):
+    value = position
+    diverged_at_count = np.zeros(position.shape)
+    while limit > 0:
+        limit -= 1
+        value = value**2 + position
+        diverging = abs(value) > 2
+        first_diverged_this_time = np.logical_and(diverging, 
+                                                  diverged_at_count == 0)
+        diverged_at_count[first_diverged_this_time] = limit
+        value[diverging] = 2
+        
+    return diverged_at_count
+
+
+
+
+
+
+
+
In [36]:
+
+
+
data7 = mandel4(values)
+
+
+
+
+
+
+
+
In [37]:
+
+
+
plt.imshow(data7, interpolation='none')
+
+
+
+
+
+
+
+
Out[37]:
+
+
<matplotlib.image.AxesImage at 0x7fc376f51340>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [38]:
+
+
+
%%timeit
+
+data7 = mandel4(values)
+
+
+
+
+
+
+
+
+
+
53.5 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
+

Note that here, all the looping over mandelbrot steps was in Python, but everything below the loop-over-positions happened in C. The code was amazingly quick compared to pure Python.

+
+
+
+
+
+
+

Can we do better by avoiding a square root?

+
+
+
+
+
+
In [39]:
+
+
+
def mandel5(position, limit=50):
+    value = position
+    diverged_at_count = np.zeros(position.shape)
+    while limit > 0:
+        limit -= 1
+        value = value**2 + position
+        diverging = value * np.conj(value) > 4
+        first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0)
+        diverged_at_count[first_diverged_this_time] = limit
+        value[diverging] = 2
+        
+    return diverged_at_count
+
+
+
+
+
+
+
+
In [40]:
+
+
+
%%timeit
+
+data8 = mandel5(values)
+
+
+
+
+
+
+
+
+
+
25.6 ms ± 95.7 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
+

Probably not worth the time I spent thinking about it!

+
+
+
+
+
+
+

NumPy Testing

+
+
+
+
+
+
+

Now, let's look at calculating those residuals, the differences between the different datasets.

+
+
+
+
+
+
In [41]:
+
+
+
data8 = mandel5(values)
+data5 = mandel2(values)
+
+
+
+
+
+
+
+
In [42]:
+
+
+
np.sum((data8 - data5)**2)
+
+
+
+
+
+
+
+
Out[42]:
+
+
0.0
+
+
+
+
+
+
+
+
+

For our non-numpy datasets, numpy knows to turn them into arrays:

+
+
+
+
+
+
In [43]:
+
+
+
xmin = -1.5
+ymin = -1.0
+xmax = 0.5
+ymax = 1.0
+resolution = 300
+xstep = (xmax-xmin)/resolution
+ystep = (ymax-ymin)/resolution
+xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]
+ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]
+data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]
+sum(sum((data1 - data7)**2))
+
+
+
+
+
+
+
+
Out[43]:
+
+
0.0
+
+
+
+
+
+
+
+
+

But this doesn't work for pure non-numpy arrays

+
+
+
+
+
+
In [44]:
+
+
+
data2 = []
+for y in ys:
+    row = []
+    for x in xs:
+        row.append(mandel1(complex(x, y)))
+    data2.append(row)
+
+
+
+
+
+
+
+
In [45]:
+
+
+
data2 - data1
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+TypeError                                 Traceback (most recent call last)
+Cell In[45], line 1
+----> 1 data2 - data1
+
+TypeError: unsupported operand type(s) for -: 'list' and 'list'
+
+
+
+
+
+
+
+
+

So we have to convert to NumPy arrays explicitly:

+
+
+
+
+
+
In [46]:
+
+
+
sum(sum((np.array(data2) - np.array(data1))**2))
+
+
+
+
+
+
+
+
Out[46]:
+
+
0
+
+
+
+
+
+
+
+
+

NumPy provides some convenient assertions to help us write unit tests with NumPy arrays:

+
+
+
+
+
+
In [47]:
+
+
+
x = [1e-5, 1e-3, 1e-1]
+y = np.arccos(np.cos(x))
+y
+
+
+
+
+
+
+
+
Out[47]:
+
+
array([1.00000004e-05, 1.00000000e-03, 1.00000000e-01])
+
+
+
+
+
+
+
+
In [48]:
+
+
+
np.testing.assert_allclose(x, y, rtol=1e-6, atol=1e-20)
+
+
+
+
+
+
+
+
In [49]:
+
+
+
np.testing.assert_allclose(data7, data1)
+
+
+
+
+
+
+
+
+

Arraywise operations are fast

+
+
+
+
+
+
+

Note that we might worry that we carry on calculating the mandelbrot values for points that have already diverged.

+
+
+
+
+
+
In [50]:
+
+
+
def mandel6(position, limit=50):
+    value = np.zeros(position.shape) + position
+    calculating = np.ones(position.shape, dtype='bool')
+    diverged_at_count = np.zeros(position.shape)
+    while limit > 0:
+        limit -= 1
+        value[calculating] = value[calculating]**2 + position[calculating]
+        diverging_now = np.zeros(position.shape, dtype='bool')
+        diverging_now[calculating] = value[calculating] * \
+                                     np.conj(value[calculating])>4
+        calculating = np.logical_and(calculating,
+                                     np.logical_not(diverging_now))
+        diverged_at_count[diverging_now] = limit
+        
+    return diverged_at_count
+
+
+
+
+
+
+
+
In [51]:
+
+
+
data8 = mandel6(values)
+
+
+
+
+
+
+
+
In [52]:
+
+
+
%%timeit
+
+data8 = mandel6(values)
+
+
+
+
+
+
+
+
+
+
43.6 ms ± 144 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
In [53]:
+
+
+
plt.imshow(data8, interpolation='none')
+
+
+
+
+
+
+
+
Out[53]:
+
+
<matplotlib.image.AxesImage at 0x7fc376e7a130>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

This was not faster even though it was doing less work

+
+
+
+
+
+
+

This often happens: on modern computers, branches (if statements, function calls) and memory access is usually the rate-determining step, not maths.

+
+
+
+
+
+
+

Complicating your logic to avoid calculations sometimes therefore slows you down. The only way to know is to measure

+
+
+
+
+
+
+

Indexing with arrays

+
+
+
+
+
+
+

We've been using Boolean arrays a lot to get access to some elements of an array. We can also do this with integers:

+
+
+
+
+
+
In [54]:
+
+
+
x = np.arange(64)
+y = x.reshape([8,8])
+y
+
+
+
+
+
+
+
+
Out[54]:
+
+
array([[ 0,  1,  2,  3,  4,  5,  6,  7],
+       [ 8,  9, 10, 11, 12, 13, 14, 15],
+       [16, 17, 18, 19, 20, 21, 22, 23],
+       [24, 25, 26, 27, 28, 29, 30, 31],
+       [32, 33, 34, 35, 36, 37, 38, 39],
+       [40, 41, 42, 43, 44, 45, 46, 47],
+       [48, 49, 50, 51, 52, 53, 54, 55],
+       [56, 57, 58, 59, 60, 61, 62, 63]])
+
+
+
+
+
+
+
+
In [55]:
+
+
+
y[[2, 5]]
+
+
+
+
+
+
+
+
Out[55]:
+
+
array([[16, 17, 18, 19, 20, 21, 22, 23],
+       [40, 41, 42, 43, 44, 45, 46, 47]])
+
+
+
+
+
+
+
+
In [56]:
+
+
+
y[[0, 2, 5], [1, 2, 7]]
+
+
+
+
+
+
+
+
Out[56]:
+
+
array([ 1, 18, 47])
+
+
+
+
+
+
+
+
+

We can use a : to indicate we want all the values from a particular axis:

+
+
+
+
+
+
In [57]:
+
+
+
y[0:4:2, [0, 2]]
+
+
+
+
+
+
+
+
Out[57]:
+
+
array([[ 0,  2],
+       [16, 18]])
+
+
+
+
+
+
+
+
+

We can mix array selectors, boolean selectors, :s and ordinary array seqeuencers:

+
+
+
+
+
+
In [58]:
+
+
+
z = x.reshape([4, 4, 4])
+z
+
+
+
+
+
+
+
+
Out[58]:
+
+
array([[[ 0,  1,  2,  3],
+        [ 4,  5,  6,  7],
+        [ 8,  9, 10, 11],
+        [12, 13, 14, 15]],
+
+       [[16, 17, 18, 19],
+        [20, 21, 22, 23],
+        [24, 25, 26, 27],
+        [28, 29, 30, 31]],
+
+       [[32, 33, 34, 35],
+        [36, 37, 38, 39],
+        [40, 41, 42, 43],
+        [44, 45, 46, 47]],
+
+       [[48, 49, 50, 51],
+        [52, 53, 54, 55],
+        [56, 57, 58, 59],
+        [60, 61, 62, 63]]])
+
+
+
+
+
+
+
+
In [59]:
+
+
+
z[:, [1, 3], 0:3]
+
+
+
+
+
+
+
+
Out[59]:
+
+
array([[[ 4,  5,  6],
+        [12, 13, 14]],
+
+       [[20, 21, 22],
+        [28, 29, 30]],
+
+       [[36, 37, 38],
+        [44, 45, 46]],
+
+       [[52, 53, 54],
+        [60, 61, 62]]])
+
+
+
+
+
+
+
+
+

We can manipulate shapes by adding new indices in selectors with np.newaxis:

+
+
+
+
+
+
In [60]:
+
+
+
z[:, np.newaxis, [1, 3], 0].shape
+
+
+
+
+
+
+
+
Out[60]:
+
+
(4, 1, 2)
+
+
+
+
+
+
+
+
+

When we use basic indexing with integers and : expressions, we get a view on the matrix so a copy is avoided:

+
+
+
+
+
+
In [61]:
+
+
+
a = z[:, :, 2]
+a[0, 0] = -500
+z
+
+
+
+
+
+
+
+
Out[61]:
+
+
array([[[   0,    1, -500,    3],
+        [   4,    5,    6,    7],
+        [   8,    9,   10,   11],
+        [  12,   13,   14,   15]],
+
+       [[  16,   17,   18,   19],
+        [  20,   21,   22,   23],
+        [  24,   25,   26,   27],
+        [  28,   29,   30,   31]],
+
+       [[  32,   33,   34,   35],
+        [  36,   37,   38,   39],
+        [  40,   41,   42,   43],
+        [  44,   45,   46,   47]],
+
+       [[  48,   49,   50,   51],
+        [  52,   53,   54,   55],
+        [  56,   57,   58,   59],
+        [  60,   61,   62,   63]]])
+
+
+
+
+
+
+
+
+

We can also use ... to specify ": for as many as possible intervening axes":

+
+
+
+
+
+
In [62]:
+
+
+
z[1]
+
+
+
+
+
+
+
+
Out[62]:
+
+
array([[16, 17, 18, 19],
+       [20, 21, 22, 23],
+       [24, 25, 26, 27],
+       [28, 29, 30, 31]])
+
+
+
+
+
+
+
+
In [63]:
+
+
+
z[...,2]
+
+
+
+
+
+
+
+
Out[63]:
+
+
array([[-500,    6,   10,   14],
+       [  18,   22,   26,   30],
+       [  34,   38,   42,   46],
+       [  50,   54,   58,   62]])
+
+
+
+
+
+
+
+
+

However, boolean mask indexing and array filter indexing always causes a copy.

+
+
+
+
+
+
+

Let's try again at avoiding doing unnecessary work by using new arrays containing the reduced data instead of a mask:

+
+
+
+
+
+
In [64]:
+
+
+
def mandel7(position, limit=50):
+    positions = np.zeros(position.shape) + position
+    value = np.zeros(position.shape) + position
+    indices = np.mgrid[0:values.shape[0], 0:values.shape[1]]
+    diverged_at_count = np.zeros(position.shape)
+    while limit > 0:
+        limit -= 1
+        value = value**2 + positions
+        diverging_now = value * np.conj(value) > 4
+        diverging_now_indices = indices[:, diverging_now]
+        carry_on = np.logical_not(diverging_now)
+
+        value = value[carry_on]
+        indices = indices[:, carry_on]
+        positions = positions[carry_on]
+        diverged_at_count[diverging_now_indices[0,:],
+                          diverging_now_indices[1,:]] = limit
+
+    return diverged_at_count
+
+
+
+
+
+
+
+
In [65]:
+
+
+
data9 = mandel7(values)
+
+
+
+
+
+
+
+
In [66]:
+
+
+
plt.imshow(data9, interpolation='none')
+
+
+
+
+
+
+
+
Out[66]:
+
+
<matplotlib.image.AxesImage at 0x7fc376bb55e0>
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [67]:
+
+
+
%%timeit
+
+data9 = mandel7(values)
+
+
+
+
+
+
+
+
+
+
55.3 ms ± 202 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
+
+
+
+
+
+
+
+
+
+

Still slower. Probably due to lots of copies -- the point here is that you need to experiment to see which optimisations will work. Performance programming needs to be empirical.

+
+
+
+
+
+
+

Profiling

+
+
+
+
+
+
+

We've seen how to compare different functions by the time they take to run. However, we haven't obtained much information about where the code is spending more time. For that we need to use a profiler. IPython offers a profiler through the %prun magic. Let's use it to see how it works:

+
+
+
+
+
+
In [68]:
+
+
+
%prun mandel7(values)
+
+
+
+
+
+
+
+
+
+
 
+
+
+
+
+
+
+
+
+

%prun shows a line per each function call ordered by the total time spent on each of these. However, sometimes a line-by-line output may be more helpful. For that we can use the line_profiler package (you need to install it using pip). Once installed you can activate it in any notebook by running:

+
+
+
+
+
+
In [69]:
+
+
+
%load_ext line_profiler
+
+
+
+
+
+
+
+
+

And the %lprun magic should be now available:

+
+
+
+
+
+
In [70]:
+
+
+
%lprun -f mandel7 mandel7(values)
+
+
+
+
+
+
+
+
+

Here, it is clearer to see which operations are keeping the code busy.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/020numpy.ipynb b/ch08performance/020numpy.ipynb new file mode 100644 index 000000000..f32a77a00 --- /dev/null +++ b/ch08performance/020numpy.ipynb @@ -0,0 +1,1291 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b0494c34", + "metadata": {}, + "source": [ + "## NumPy for Performance" + ] + }, + { + "cell_type": "markdown", + "id": "923f02ec", + "metadata": {}, + "source": [ + "### NumPy constructors" + ] + }, + { + "cell_type": "markdown", + "id": "2e83a68d", + "metadata": {}, + "source": [ + "We saw previously that NumPy's core type is the `ndarray`, or N-Dimensional Array:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eecc9f8e", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.zeros([3, 4, 2, 5])[2, :, :, 1]" + ] + }, + { + "cell_type": "markdown", + "id": "68361de6", + "metadata": {}, + "source": [ + "The real magic of numpy arrays is that most python operations are applied, quickly, on an elementwise basis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edda26eb", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(0, 256, 4).reshape(8, 8)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15e45e1b", + "metadata": {}, + "outputs": [], + "source": [ + "y = np.zeros((8, 8))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c431c2c2", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "for i in range(8):\n", + " for j in range(8):\n", + " y[i][j] = x[i][j] + 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53aa09f0", + "metadata": {}, + "outputs": [], + "source": [ + "x + 10" + ] + }, + { + "cell_type": "markdown", + "id": "f0d8d724", + "metadata": {}, + "source": [ + "Numpy's mathematical functions also happen this way, and are said to be \"vectorized\" functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e902653", + "metadata": {}, + "outputs": [], + "source": [ + "np.sqrt(x)" + ] + }, + { + "cell_type": "markdown", + "id": "e6c25190", + "metadata": {}, + "source": [ + "Numpy contains many useful functions for creating matrices. In our earlier lectures we've seen `linspace` and `arange` for evenly spaced numbers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c265a82", + "metadata": {}, + "outputs": [], + "source": [ + "np.linspace(0, 10, 21)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b479ef38", + "metadata": {}, + "outputs": [], + "source": [ + "np.arange(0, 10, 0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "51c8e897", + "metadata": {}, + "source": [ + " Here's one for creating matrices like coordinates in a grid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eeeae5e", + "metadata": {}, + "outputs": [], + "source": [ + "xmin = -1.5\n", + "ymin = -1.0\n", + "xmax = 0.5\n", + "ymax = 1.0\n", + "resolution = 300\n", + "xstep = (xmax - xmin) / resolution\n", + "ystep = (ymax - ymin) / resolution\n", + "\n", + "ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7504ca20", + "metadata": {}, + "outputs": [], + "source": [ + "print(ymatrix)" + ] + }, + { + "cell_type": "markdown", + "id": "7f98c61c", + "metadata": {}, + "source": [ + "We can add these together to make a grid containing the complex numbers we want to test for membership in the Mandelbrot set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23f1d6b2", + "metadata": {}, + "outputs": [], + "source": [ + "values = xmatrix + 1j * ymatrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f63fa17", + "metadata": {}, + "outputs": [], + "source": [ + "print(values)" + ] + }, + { + "cell_type": "markdown", + "id": "72032d47", + "metadata": {}, + "source": [ + "### Arraywise Algorithms" + ] + }, + { + "cell_type": "markdown", + "id": "9c7c7768", + "metadata": {}, + "source": [ + "We can use this to apply the mandelbrot algorithm to whole *ARRAYS*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143aa4d9", + "metadata": {}, + "outputs": [], + "source": [ + "z0 = values\n", + "z1 = z0 * z0 + values\n", + "z2 = z1 * z1 + values\n", + "z3 = z2 * z2 + values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b161aea4", + "metadata": {}, + "outputs": [], + "source": [ + "print(z3)" + ] + }, + { + "cell_type": "markdown", + "id": "1080684e", + "metadata": {}, + "source": [ + "So can we just apply our `mandel1` function to the whole matrix?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40bf344d", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel1(position,limit=50):\n", + " value = position\n", + " while abs(value) < 2:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " return limit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "032fa99c", + "metadata": {}, + "outputs": [], + "source": [ + "mandel1(values)" + ] + }, + { + "cell_type": "markdown", + "id": "90263fe2", + "metadata": {}, + "source": [ + "No. The *logic* of our current routine would require stopping for some elements and not for others. " + ] + }, + { + "cell_type": "markdown", + "id": "aa2294fb", + "metadata": {}, + "source": [ + "We can ask numpy to **vectorise** our method for us:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b26c9f0d", + "metadata": {}, + "outputs": [], + "source": [ + "mandel2 = np.vectorize(mandel1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5eae9e8d", + "metadata": {}, + "outputs": [], + "source": [ + "data5 = mandel2(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e02f2413", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "plt.imshow(data5, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "f0820e9c", + "metadata": {}, + "source": [ + "Is that any faster?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c3bac16", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "data5 = mandel2(values)" + ] + }, + { + "cell_type": "markdown", + "id": "702bcbf6", + "metadata": {}, + "source": [ + "This is not significantly faster. When we use *vectorize* it's just hiding an plain old python for loop under the hood. We want to make the loop over matrix elements take place in the \"**C Layer**\"." + ] + }, + { + "cell_type": "markdown", + "id": "73c67b8d", + "metadata": {}, + "source": [ + "What if we just apply the Mandelbrot algorithm without checking for divergence until the end:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb2fa102", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel_numpy_explode(position, limit=50):\n", + " value = position\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " diverging = abs(value) > 2\n", + "\n", + " \n", + " return abs(value) < 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "405fcfb9", + "metadata": {}, + "outputs": [], + "source": [ + "data6 = mandel_numpy_explode(values)" + ] + }, + { + "cell_type": "markdown", + "id": "8364482c", + "metadata": {}, + "source": [ + "OK, we need to prevent it from running off to $\\infty$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "748b6402", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel_numpy(position, limit=50):\n", + " value = position\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " diverging = abs(value) > 2\n", + " # Avoid overflow\n", + " value[diverging] = 2\n", + " \n", + " return abs(value) < 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1f1fedb", + "metadata": {}, + "outputs": [], + "source": [ + "data6 = mandel_numpy(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "395dd0e6", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "\n", + "data6 = mandel_numpy(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6852184", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "plt.imshow(data6, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "0c19fe50", + "metadata": {}, + "source": [ + "Wow, that was TEN TIMES faster." + ] + }, + { + "cell_type": "markdown", + "id": "34a6d14d", + "metadata": {}, + "source": [ + "There's quite a few NumPy tricks there, let's remind ourselves of how they work:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b8ee698", + "metadata": {}, + "outputs": [], + "source": [ + "diverging = abs(z3) > 2\n", + "z3[diverging] = 2" + ] + }, + { + "cell_type": "markdown", + "id": "0b010e97", + "metadata": {}, + "source": [ + "When we apply a logical condition to a NumPy array, we get a logical array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "632d555a", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(10)\n", + "y = np.ones([10]) * 5\n", + "z = x > y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f1067f6", + "metadata": {}, + "outputs": [], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04678030", + "metadata": {}, + "outputs": [], + "source": [ + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8165f72c", + "metadata": {}, + "outputs": [], + "source": [ + "print(z)" + ] + }, + { + "cell_type": "markdown", + "id": "9f89019e", + "metadata": {}, + "source": [ + "Logical arrays can be used to index into arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e88c0ca", + "metadata": {}, + "outputs": [], + "source": [ + "x[x>3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8da1965", + "metadata": {}, + "outputs": [], + "source": [ + "x[np.logical_not(z)]" + ] + }, + { + "cell_type": "markdown", + "id": "e3b6b2b4", + "metadata": {}, + "source": [ + "And you can use such an index as the target of an assignment:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d55525cd", + "metadata": {}, + "outputs": [], + "source": [ + "x[z] = 5\n", + "x" + ] + }, + { + "cell_type": "markdown", + "id": "abf4b789", + "metadata": {}, + "source": [ + "Note that we didn't compare two arrays to get our logical array, but an array to a scalar integer -- this was broadcasting again." + ] + }, + { + "cell_type": "markdown", + "id": "af044308", + "metadata": {}, + "source": [ + "### More Mandelbrot" + ] + }, + { + "cell_type": "markdown", + "id": "297313f7", + "metadata": {}, + "source": [ + "Of course, we didn't calculate the number-of-iterations-to-diverge, just whether the point was in the set." + ] + }, + { + "cell_type": "markdown", + "id": "c524331d", + "metadata": {}, + "source": [ + "Let's correct our code to do that:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d46e6920", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel4(position,limit=50):\n", + " value = position\n", + " diverged_at_count = np.zeros(position.shape)\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " diverging = abs(value) > 2\n", + " first_diverged_this_time = np.logical_and(diverging, \n", + " diverged_at_count == 0)\n", + " diverged_at_count[first_diverged_this_time] = limit\n", + " value[diverging] = 2\n", + " \n", + " return diverged_at_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e31c46b", + "metadata": {}, + "outputs": [], + "source": [ + "data7 = mandel4(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "001e0ce5", + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow(data7, interpolation='none')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33f7eca6", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "\n", + "data7 = mandel4(values)" + ] + }, + { + "cell_type": "markdown", + "id": "f7b5a25f", + "metadata": {}, + "source": [ + "Note that here, all the looping over mandelbrot steps was in Python, but everything below the loop-over-positions happened in C. The code was amazingly quick compared to pure Python." + ] + }, + { + "cell_type": "markdown", + "id": "f7b5d39a", + "metadata": {}, + "source": [ + "Can we do better by avoiding a square root?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06a63bed", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel5(position, limit=50):\n", + " value = position\n", + " diverged_at_count = np.zeros(position.shape)\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " diverging = value * np.conj(value) > 4\n", + " first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0)\n", + " diverged_at_count[first_diverged_this_time] = limit\n", + " value[diverging] = 2\n", + " \n", + " return diverged_at_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c078d2e1", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "\n", + "data8 = mandel5(values)" + ] + }, + { + "cell_type": "markdown", + "id": "f42d663f", + "metadata": {}, + "source": [ + "Probably not worth the time I spent thinking about it!" + ] + }, + { + "cell_type": "markdown", + "id": "14681e10", + "metadata": {}, + "source": [ + "### NumPy Testing" + ] + }, + { + "cell_type": "markdown", + "id": "41e97903", + "metadata": {}, + "source": [ + "Now, let's look at calculating those residuals, the differences between the different datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92740f80", + "metadata": {}, + "outputs": [], + "source": [ + "data8 = mandel5(values)\n", + "data5 = mandel2(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb2f4e7d", + "metadata": {}, + "outputs": [], + "source": [ + "np.sum((data8 - data5)**2)" + ] + }, + { + "cell_type": "markdown", + "id": "f7cc140a", + "metadata": {}, + "source": [ + "For our non-numpy datasets, numpy knows to turn them into arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06cf365e", + "metadata": {}, + "outputs": [], + "source": [ + "xmin = -1.5\n", + "ymin = -1.0\n", + "xmax = 0.5\n", + "ymax = 1.0\n", + "resolution = 300\n", + "xstep = (xmax-xmin)/resolution\n", + "ystep = (ymax-ymin)/resolution\n", + "xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]\n", + "ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]\n", + "data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys]\n", + "sum(sum((data1 - data7)**2))" + ] + }, + { + "cell_type": "markdown", + "id": "4b0cf9a1", + "metadata": {}, + "source": [ + "But this doesn't work for pure non-numpy arrays" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d005a896", + "metadata": {}, + "outputs": [], + "source": [ + "data2 = []\n", + "for y in ys:\n", + " row = []\n", + " for x in xs:\n", + " row.append(mandel1(complex(x, y)))\n", + " data2.append(row)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fea4c7e5", + "metadata": {}, + "outputs": [], + "source": [ + "data2 - data1" + ] + }, + { + "cell_type": "markdown", + "id": "97a074dd", + "metadata": {}, + "source": [ + "So we have to convert to NumPy arrays explicitly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3749362", + "metadata": {}, + "outputs": [], + "source": [ + "sum(sum((np.array(data2) - np.array(data1))**2))" + ] + }, + { + "cell_type": "markdown", + "id": "8ef85ea5", + "metadata": {}, + "source": [ + "NumPy provides some convenient assertions to help us write unit tests with NumPy arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c51e3910", + "metadata": {}, + "outputs": [], + "source": [ + "x = [1e-5, 1e-3, 1e-1]\n", + "y = np.arccos(np.cos(x))\n", + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eeac600f", + "metadata": {}, + "outputs": [], + "source": [ + "np.testing.assert_allclose(x, y, rtol=1e-6, atol=1e-20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee46e700", + "metadata": {}, + "outputs": [], + "source": [ + "np.testing.assert_allclose(data7, data1)" + ] + }, + { + "cell_type": "markdown", + "id": "f34c377d", + "metadata": {}, + "source": [ + "### Arraywise operations are fast" + ] + }, + { + "cell_type": "markdown", + "id": "17b8124a", + "metadata": {}, + "source": [ + "Note that we might worry that we carry on calculating the mandelbrot values for points that have already diverged." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "589275ef", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel6(position, limit=50):\n", + " value = np.zeros(position.shape) + position\n", + " calculating = np.ones(position.shape, dtype='bool')\n", + " diverged_at_count = np.zeros(position.shape)\n", + " while limit > 0:\n", + " limit -= 1\n", + " value[calculating] = value[calculating]**2 + position[calculating]\n", + " diverging_now = np.zeros(position.shape, dtype='bool')\n", + " diverging_now[calculating] = value[calculating] * \\\n", + " np.conj(value[calculating])>4\n", + " calculating = np.logical_and(calculating,\n", + " np.logical_not(diverging_now))\n", + " diverged_at_count[diverging_now] = limit\n", + " \n", + " return diverged_at_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "168c3802", + "metadata": {}, + "outputs": [], + "source": [ + "data8 = mandel6(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "172e6d35", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "\n", + "data8 = mandel6(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f512e370", + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow(data8, interpolation='none')" + ] + }, + { + "cell_type": "markdown", + "id": "4cc3739b", + "metadata": {}, + "source": [ + "This was **not faster** even though it was **doing less work**" + ] + }, + { + "cell_type": "markdown", + "id": "dafc295e", + "metadata": {}, + "source": [ + "This often happens: on modern computers, **branches** (if statements, function calls) and **memory access** is usually the rate-determining step, not maths." + ] + }, + { + "cell_type": "markdown", + "id": "769653a7", + "metadata": {}, + "source": [ + "Complicating your logic to avoid calculations sometimes therefore slows you down. The only way to know is to **measure**" + ] + }, + { + "cell_type": "markdown", + "id": "8a543e3d", + "metadata": {}, + "source": [ + "### Indexing with arrays" + ] + }, + { + "cell_type": "markdown", + "id": "046c1d52", + "metadata": {}, + "source": [ + "We've been using Boolean arrays a lot to get access to some elements of an array. We can also do this with integers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e9bc396", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(64)\n", + "y = x.reshape([8,8])\n", + "y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9906a9f7", + "metadata": {}, + "outputs": [], + "source": [ + "y[[2, 5]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f11bb99d", + "metadata": {}, + "outputs": [], + "source": [ + "y[[0, 2, 5], [1, 2, 7]]" + ] + }, + { + "cell_type": "markdown", + "id": "747e2f31", + "metadata": {}, + "source": [ + "We can use a : to indicate we want all the values from a particular axis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67ae8392", + "metadata": {}, + "outputs": [], + "source": [ + "y[0:4:2, [0, 2]]" + ] + }, + { + "cell_type": "markdown", + "id": "52d59399", + "metadata": {}, + "source": [ + "We can mix array selectors, boolean selectors, :s and ordinary array seqeuencers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "521084a6", + "metadata": {}, + "outputs": [], + "source": [ + "z = x.reshape([4, 4, 4])\n", + "z" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff88f2c5", + "metadata": {}, + "outputs": [], + "source": [ + "z[:, [1, 3], 0:3]" + ] + }, + { + "cell_type": "markdown", + "id": "5d87d7be", + "metadata": {}, + "source": [ + "We can manipulate shapes by adding new indices in selectors with np.newaxis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b94b8f2", + "metadata": {}, + "outputs": [], + "source": [ + "z[:, np.newaxis, [1, 3], 0].shape" + ] + }, + { + "cell_type": "markdown", + "id": "9e3edba3", + "metadata": {}, + "source": [ + "When we use basic indexing with integers and : expressions, we get a **view** on the matrix so a copy is avoided:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29b02b24", + "metadata": {}, + "outputs": [], + "source": [ + "a = z[:, :, 2]\n", + "a[0, 0] = -500\n", + "z" + ] + }, + { + "cell_type": "markdown", + "id": "78ed2e7c", + "metadata": {}, + "source": [ + "We can also use ... to specify \": for as many as possible intervening axes\":" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a81776ae", + "metadata": {}, + "outputs": [], + "source": [ + "z[1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68de7474", + "metadata": {}, + "outputs": [], + "source": [ + "z[...,2]" + ] + }, + { + "cell_type": "markdown", + "id": "ae3206d4", + "metadata": {}, + "source": [ + "However, boolean mask indexing and array filter indexing always causes a copy." + ] + }, + { + "cell_type": "markdown", + "id": "0ab2139e", + "metadata": {}, + "source": [ + "Let's try again at avoiding doing unnecessary work by using new arrays containing the reduced data instead of a mask:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "beeefa9d", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel7(position, limit=50):\n", + " positions = np.zeros(position.shape) + position\n", + " value = np.zeros(position.shape) + position\n", + " indices = np.mgrid[0:values.shape[0], 0:values.shape[1]]\n", + " diverged_at_count = np.zeros(position.shape)\n", + " while limit > 0:\n", + " limit -= 1\n", + " value = value**2 + positions\n", + " diverging_now = value * np.conj(value) > 4\n", + " diverging_now_indices = indices[:, diverging_now]\n", + " carry_on = np.logical_not(diverging_now)\n", + "\n", + " value = value[carry_on]\n", + " indices = indices[:, carry_on]\n", + " positions = positions[carry_on]\n", + " diverged_at_count[diverging_now_indices[0,:],\n", + " diverging_now_indices[1,:]] = limit\n", + "\n", + " return diverged_at_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15e35809", + "metadata": {}, + "outputs": [], + "source": [ + "data9 = mandel7(values)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a11bf7af", + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow(data9, interpolation='none')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80d6e1fd", + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit\n", + "\n", + "data9 = mandel7(values)" + ] + }, + { + "cell_type": "markdown", + "id": "b5fbad4c", + "metadata": {}, + "source": [ + "Still slower. Probably due to lots of copies -- the point here is that you need to *experiment* to see which optimisations will work. Performance programming needs to be empirical." + ] + }, + { + "cell_type": "markdown", + "id": "689155f3", + "metadata": {}, + "source": [ + "## Profiling" + ] + }, + { + "cell_type": "markdown", + "id": "c05e672c", + "metadata": {}, + "source": [ + "We've seen how to compare different functions by the time they take to run. However, we haven't obtained much information about where the code is spending more time. For that we need to use a profiler. IPython offers a profiler through the `%prun` magic. Let's use it to see how it works:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07bdd97f", + "metadata": {}, + "outputs": [], + "source": [ + "%prun mandel7(values)" + ] + }, + { + "cell_type": "markdown", + "id": "86f739b0", + "metadata": {}, + "source": [ + "`%prun` shows a line per each function call ordered by the total time spent on each of these. However, sometimes a line-by-line output may be more helpful. For that we can use the `line_profiler` package (you need to install it using `pip`). Once installed you can activate it in any notebook by running:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "307a9317", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext line_profiler" + ] + }, + { + "cell_type": "markdown", + "id": "8052f019", + "metadata": {}, + "source": [ + "And the `%lprun` magic should be now available:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e486aa0", + "metadata": {}, + "outputs": [], + "source": [ + "%lprun -f mandel7 mandel7(values)" + ] + }, + { + "cell_type": "markdown", + "id": "558cb390", + "metadata": {}, + "source": [ + "Here, it is clearer to see which operations are keeping the code busy." + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "NumPy" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch08performance/020numpy.ipynb.py b/ch08performance/020numpy.ipynb.py new file mode 100644 index 000000000..91da6a29f --- /dev/null +++ b/ch08performance/020numpy.ipynb.py @@ -0,0 +1,537 @@ +# --- +# jupyter: +# jekyll: +# display_name: NumPy +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## NumPy for Performance + +# %% [markdown] +# ### NumPy constructors + +# %% [markdown] +# We saw previously that NumPy's core type is the `ndarray`, or N-Dimensional Array: + +# %% +import numpy as np +np.zeros([3, 4, 2, 5])[2, :, :, 1] + +# %% [markdown] +# The real magic of numpy arrays is that most python operations are applied, quickly, on an elementwise basis: + +# %% +x = np.arange(0, 256, 4).reshape(8, 8) + +# %% +y = np.zeros((8, 8)) + +# %% +# %%timeit +for i in range(8): + for j in range(8): + y[i][j] = x[i][j] + 10 + +# %% +x + 10 + +# %% [markdown] +# Numpy's mathematical functions also happen this way, and are said to be "vectorized" functions. + +# %% +np.sqrt(x) + +# %% [markdown] +# Numpy contains many useful functions for creating matrices. In our earlier lectures we've seen `linspace` and `arange` for evenly spaced numbers. + +# %% +np.linspace(0, 10, 21) + +# %% +np.arange(0, 10, 0.5) + +# %% [markdown] +# Here's one for creating matrices like coordinates in a grid: + +# %% +xmin = -1.5 +ymin = -1.0 +xmax = 0.5 +ymax = 1.0 +resolution = 300 +xstep = (xmax - xmin) / resolution +ystep = (ymax - ymin) / resolution + +ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep] + +# %% +print(ymatrix) + +# %% [markdown] +# We can add these together to make a grid containing the complex numbers we want to test for membership in the Mandelbrot set. + +# %% +values = xmatrix + 1j * ymatrix + +# %% +print(values) + +# %% [markdown] +# ### Arraywise Algorithms + +# %% [markdown] +# We can use this to apply the mandelbrot algorithm to whole *ARRAYS* + +# %% +z0 = values +z1 = z0 * z0 + values +z2 = z1 * z1 + values +z3 = z2 * z2 + values + +# %% +print(z3) + + +# %% [markdown] +# So can we just apply our `mandel1` function to the whole matrix? + +# %% +def mandel1(position,limit=50): + value = position + while abs(value) < 2: + limit -= 1 + value = value**2 + position + if limit < 0: + return 0 + return limit + + +# %% +mandel1(values) + +# %% [markdown] +# No. The *logic* of our current routine would require stopping for some elements and not for others. + +# %% [markdown] +# We can ask numpy to **vectorise** our method for us: + +# %% +mandel2 = np.vectorize(mandel1) + +# %% +data5 = mandel2(values) + +# %% +from matplotlib import pyplot as plt +# %matplotlib inline +plt.imshow(data5, interpolation='none') + +# %% [markdown] +# Is that any faster? + +# %% +# %%timeit +data5 = mandel2(values) + + +# %% [markdown] +# This is not significantly faster. When we use *vectorize* it's just hiding an plain old python for loop under the hood. We want to make the loop over matrix elements take place in the "**C Layer**". + +# %% [markdown] +# What if we just apply the Mandelbrot algorithm without checking for divergence until the end: + +# %% +def mandel_numpy_explode(position, limit=50): + value = position + while limit > 0: + limit -= 1 + value = value**2 + position + diverging = abs(value) > 2 + + + return abs(value) < 2 + +# %% +data6 = mandel_numpy_explode(values) + + +# %% [markdown] +# OK, we need to prevent it from running off to $\infty$ + +# %% +def mandel_numpy(position, limit=50): + value = position + while limit > 0: + limit -= 1 + value = value**2 + position + diverging = abs(value) > 2 + # Avoid overflow + value[diverging] = 2 + + return abs(value) < 2 + + +# %% +data6 = mandel_numpy(values) + +# %% +# %%timeit + +data6 = mandel_numpy(values) + +# %% +from matplotlib import pyplot as plt +# %matplotlib inline +plt.imshow(data6, interpolation='none') + +# %% [markdown] +# Wow, that was TEN TIMES faster. + +# %% [markdown] +# There's quite a few NumPy tricks there, let's remind ourselves of how they work: + +# %% +diverging = abs(z3) > 2 +z3[diverging] = 2 + +# %% [markdown] +# When we apply a logical condition to a NumPy array, we get a logical array. + +# %% +x = np.arange(10) +y = np.ones([10]) * 5 +z = x > y + +# %% +x + +# %% +y + +# %% +print(z) + +# %% [markdown] +# Logical arrays can be used to index into arrays: + +# %% +x[x>3] + +# %% +x[np.logical_not(z)] + +# %% [markdown] +# And you can use such an index as the target of an assignment: + +# %% +x[z] = 5 +x + + +# %% [markdown] +# Note that we didn't compare two arrays to get our logical array, but an array to a scalar integer -- this was broadcasting again. + +# %% [markdown] +# ### More Mandelbrot + +# %% [markdown] +# Of course, we didn't calculate the number-of-iterations-to-diverge, just whether the point was in the set. + +# %% [markdown] +# Let's correct our code to do that: +# + +# %% +def mandel4(position,limit=50): + value = position + diverged_at_count = np.zeros(position.shape) + while limit > 0: + limit -= 1 + value = value**2 + position + diverging = abs(value) > 2 + first_diverged_this_time = np.logical_and(diverging, + diverged_at_count == 0) + diverged_at_count[first_diverged_this_time] = limit + value[diverging] = 2 + + return diverged_at_count + + +# %% +data7 = mandel4(values) + +# %% +plt.imshow(data7, interpolation='none') + +# %% +# %%timeit + +data7 = mandel4(values) + + +# %% [markdown] +# Note that here, all the looping over mandelbrot steps was in Python, but everything below the loop-over-positions happened in C. The code was amazingly quick compared to pure Python. + +# %% [markdown] +# Can we do better by avoiding a square root? + +# %% +def mandel5(position, limit=50): + value = position + diverged_at_count = np.zeros(position.shape) + while limit > 0: + limit -= 1 + value = value**2 + position + diverging = value * np.conj(value) > 4 + first_diverged_this_time = np.logical_and(diverging, diverged_at_count == 0) + diverged_at_count[first_diverged_this_time] = limit + value[diverging] = 2 + + return diverged_at_count + + +# %% +# %%timeit + +data8 = mandel5(values) + +# %% [markdown] +# Probably not worth the time I spent thinking about it! + +# %% [markdown] +# ### NumPy Testing + +# %% [markdown] +# Now, let's look at calculating those residuals, the differences between the different datasets. + +# %% +data8 = mandel5(values) +data5 = mandel2(values) + +# %% +np.sum((data8 - data5)**2) + +# %% [markdown] +# For our non-numpy datasets, numpy knows to turn them into arrays: + +# %% +xmin = -1.5 +ymin = -1.0 +xmax = 0.5 +ymax = 1.0 +resolution = 300 +xstep = (xmax-xmin)/resolution +ystep = (ymax-ymin)/resolution +xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)] +ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)] +data1 = [[mandel1(complex(x, y)) for x in xs] for y in ys] +sum(sum((data1 - data7)**2)) + +# %% [markdown] +# But this doesn't work for pure non-numpy arrays + +# %% +data2 = [] +for y in ys: + row = [] + for x in xs: + row.append(mandel1(complex(x, y))) + data2.append(row) + +# %% +data2 - data1 + +# %% [markdown] +# So we have to convert to NumPy arrays explicitly: + +# %% +sum(sum((np.array(data2) - np.array(data1))**2)) + +# %% [markdown] +# NumPy provides some convenient assertions to help us write unit tests with NumPy arrays: + +# %% +x = [1e-5, 1e-3, 1e-1] +y = np.arccos(np.cos(x)) +y + +# %% +np.testing.assert_allclose(x, y, rtol=1e-6, atol=1e-20) + +# %% +np.testing.assert_allclose(data7, data1) + + +# %% [markdown] +# ### Arraywise operations are fast + +# %% [markdown] +# Note that we might worry that we carry on calculating the mandelbrot values for points that have already diverged. + +# %% +def mandel6(position, limit=50): + value = np.zeros(position.shape) + position + calculating = np.ones(position.shape, dtype='bool') + diverged_at_count = np.zeros(position.shape) + while limit > 0: + limit -= 1 + value[calculating] = value[calculating]**2 + position[calculating] + diverging_now = np.zeros(position.shape, dtype='bool') + diverging_now[calculating] = value[calculating] * \ + np.conj(value[calculating])>4 + calculating = np.logical_and(calculating, + np.logical_not(diverging_now)) + diverged_at_count[diverging_now] = limit + + return diverged_at_count + + +# %% +data8 = mandel6(values) + +# %% +# %%timeit + +data8 = mandel6(values) + +# %% +plt.imshow(data8, interpolation='none') + +# %% [markdown] +# This was **not faster** even though it was **doing less work** + +# %% [markdown] +# This often happens: on modern computers, **branches** (if statements, function calls) and **memory access** is usually the rate-determining step, not maths. + +# %% [markdown] +# Complicating your logic to avoid calculations sometimes therefore slows you down. The only way to know is to **measure** + +# %% [markdown] +# ### Indexing with arrays + +# %% [markdown] +# We've been using Boolean arrays a lot to get access to some elements of an array. We can also do this with integers: + +# %% +x = np.arange(64) +y = x.reshape([8,8]) +y + +# %% +y[[2, 5]] + +# %% +y[[0, 2, 5], [1, 2, 7]] + +# %% [markdown] +# We can use a : to indicate we want all the values from a particular axis: + +# %% +y[0:4:2, [0, 2]] + +# %% [markdown] +# We can mix array selectors, boolean selectors, :s and ordinary array seqeuencers: + +# %% +z = x.reshape([4, 4, 4]) +z + +# %% +z[:, [1, 3], 0:3] + +# %% [markdown] +# We can manipulate shapes by adding new indices in selectors with np.newaxis: + +# %% +z[:, np.newaxis, [1, 3], 0].shape + +# %% [markdown] +# When we use basic indexing with integers and : expressions, we get a **view** on the matrix so a copy is avoided: + +# %% +a = z[:, :, 2] +a[0, 0] = -500 +z + +# %% [markdown] +# We can also use ... to specify ": for as many as possible intervening axes": + +# %% +z[1] + +# %% +z[...,2] + + +# %% [markdown] +# However, boolean mask indexing and array filter indexing always causes a copy. + +# %% [markdown] +# Let's try again at avoiding doing unnecessary work by using new arrays containing the reduced data instead of a mask: + +# %% +def mandel7(position, limit=50): + positions = np.zeros(position.shape) + position + value = np.zeros(position.shape) + position + indices = np.mgrid[0:values.shape[0], 0:values.shape[1]] + diverged_at_count = np.zeros(position.shape) + while limit > 0: + limit -= 1 + value = value**2 + positions + diverging_now = value * np.conj(value) > 4 + diverging_now_indices = indices[:, diverging_now] + carry_on = np.logical_not(diverging_now) + + value = value[carry_on] + indices = indices[:, carry_on] + positions = positions[carry_on] + diverged_at_count[diverging_now_indices[0,:], + diverging_now_indices[1,:]] = limit + + return diverged_at_count + + +# %% +data9 = mandel7(values) + +# %% +plt.imshow(data9, interpolation='none') + +# %% +# %%timeit + +data9 = mandel7(values) + +# %% [markdown] +# Still slower. Probably due to lots of copies -- the point here is that you need to *experiment* to see which optimisations will work. Performance programming needs to be empirical. + +# %% [markdown] +# ## Profiling + +# %% [markdown] +# We've seen how to compare different functions by the time they take to run. However, we haven't obtained much information about where the code is spending more time. For that we need to use a profiler. IPython offers a profiler through the `%prun` magic. Let's use it to see how it works: + +# %% +# %prun mandel7(values) + +# %% [markdown] +# `%prun` shows a line per each function call ordered by the total time spent on each of these. However, sometimes a line-by-line output may be more helpful. For that we can use the `line_profiler` package (you need to install it using `pip`). Once installed you can activate it in any notebook by running: + +# %% +# %load_ext line_profiler + +# %% [markdown] +# And the `%lprun` magic should be now available: + +# %% +# %lprun -f mandel7 mandel7(values) + +# %% [markdown] +# Here, it is clearer to see which operations are keeping the code busy. diff --git a/ch08performance/040cython.html b/ch08performance/040cython.html new file mode 100644 index 000000000..81f399596 --- /dev/null +++ b/ch08performance/040cython.html @@ -0,0 +1,943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cython + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Cython

Cython can be viewed as an extension of Python where variables and functions are annotated with extra information, in particular types. The resulting Cython source code will be compiled into optimized C or C++ code, and thereby yielding substantial speed-up of slow Python code. In other words, Cython provides a way of writing Python with comparable performance to that of C/C++.

+
+
+
+
+
+
+

Start Coding in Cython

+
+
+
+
+
+
+

Cython code must, unlike Python, be compiled. This happens in the following stages:

+
    +
  • The cython code in .pyx file will be translated to a C file.
  • +
  • The C file will be compiled by a C compiler into a shared library, which will be directly loaded into Python.
  • +
+

In a Jupyter notebook, everything is a lot easier. One needs only to load the Cython extension (%load_ext Cython) at the beginning and put %%cython mark in front of cells of Cython code. Cells with Cython mark will be treated as a .pyx code and consequently, compiled into C.

+

For details, please see Building Cython Code.

+
+
+
+
+
+
+

Pure python Mandelbrot set:

+
+
+
+
+
+
In [1]:
+
+
+
xmin = -1.5
+ymin = -1.0
+xmax = 0.5
+ymax = 1.0
+resolution = 300
+xstep = (xmax - xmin) / resolution
+ystep = (ymax - ymin) / resolution
+xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]
+ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]
+
+
+
+
+
+
+
+
In [2]:
+
+
+
def mandel(position, limit=50):
+    value = position
+    while abs(value) < 2:
+        limit -= 1
+        value = value**2 + position
+        if limit < 0:
+            return 0
+    return limit
+
+
+
+
+
+
+
+
+

Compiled by Cython:

+
+
+
+
+
+
In [3]:
+
+
+
%load_ext Cython
+
+
+
+
+
+
+
+
In [4]:
+
+
+
%%cython
+
+def mandel_cython(position, limit=50):
+    value = position
+    while abs(value) < 2:
+        limit -= 1
+        value = value**2 + position
+        if limit < 0:
+            return 0
+    return limit
+
+
+
+
+
+
+
+
+

Let's verify the result

+
+
+
+
+
+
In [5]:
+
+
+
from matplotlib import pyplot as plt
+%matplotlib inline
+f, axarr = plt.subplots(1, 2)
+axarr[0].imshow([[mandel(complex(x, y)) for x in xs] for y in ys], interpolation='none')
+axarr[0].set_title('Pure Python')
+axarr[1].imshow([[mandel_cython(complex(x, y)) for x in xs] for y in ys], interpolation='none')
+axarr[1].set_title('Cython')
+
+
+
+
+
+
+
+
Out[5]:
+
+
Text(0.5, 1.0, 'Cython')
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [6]:
+
+
+
%timeit [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python
+%timeit [[mandel_cython(complex(x,y)) for x in xs] for y in ys] # cython
+
+
+
+
+
+
+
+
+
+
531 ms ± 5.87 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+402 ms ± 3.01 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
+

We have improved the performance of a factor of 1.5 by just using the Cython compiler, without changing the code!

+
+
+
+
+
+
+

Cython with C Types

But we can do better by telling Cython what C data type we would use in the code. Note we're not actually writing C, we're writing Python with C types.

+
+
+
+
+
+
+

typed variable

+
+
+
+
+
+
In [7]:
+
+
+
%%cython
+def var_typed_mandel_cython(position, limit=50):
+    cdef double complex value # typed variable
+    value = position
+    while abs(value) < 2:
+        limit -= 1
+        value = value**2 + position
+        if limit < 0:
+            return 0
+    return limit
+
+
+
+
+
+
+
+
+

typed function + typed variable

+
+
+
+
+
+
In [8]:
+
+
+
%%cython
+cpdef call_typed_mandel_cython(double complex position,
+                               int limit=50): # typed function
+    cdef double complex value # typed variable
+    value = position
+    while abs(value)<2:
+        limit -= 1
+        value = value**2 + position
+        if limit < 0:
+            return 0
+    return limit
+
+
+
+
+
+
+
+
+

performance of one number:

+
+
+
+
+
+
In [9]:
+
+
+
# pure python
+%timeit a = mandel(complex(0, 0)) 
+
+
+
+
+
+
+
+
+
+
11.8 µs ± 26.9 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
+
+
+
+
+
+
+
+
+
In [10]:
+
+
+
# primitive cython
+%timeit a = mandel_cython(complex(0, 0)) 
+
+
+
+
+
+
+
+
+
+
8.63 µs ± 26.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
+
+
+
+
+
+
+
+
+
In [11]:
+
+
+
# cython with C type variable
+%timeit a = var_typed_mandel_cython(complex(0, 0)) 
+
+
+
+
+
+
+
+
+
+
3.47 µs ± 2.15 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
+
+
+
+
+
+
+
+
+
In [12]:
+
+
+
# cython with typed variable + function
+%timeit a = call_typed_mandel_cython(complex(0, 0))
+
+
+
+
+
+
+
+
+
+
939 ns ± 2.59 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
+
+
+
+
+
+
+
+
+
+

Cython with numpy ndarray

You can use NumPy from Cython exactly the same as in regular Python, but by doing so you are losing potentially high speedups because Cython has support for fast access to NumPy arrays.

+
+
+
+
+
+
In [13]:
+
+
+
import numpy as np
+ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]
+values = xmatrix + 1j * ymatrix
+
+
+
+
+
+
+
+
In [14]:
+
+
+
%%cython
+import numpy as np
+cimport numpy as np 
+
+cpdef numpy_cython_1(np.ndarray[double complex, ndim=2] position, 
+                     int limit=50): 
+    cdef np.ndarray[long,ndim=2] diverged_at
+    cdef double complex value
+    cdef int xlim
+    cdef int ylim
+    cdef double complex pos
+    cdef int steps
+    cdef int x, y
+
+    xlim = position.shape[1]
+    ylim = position.shape[0]
+    diverged_at = np.zeros([ylim, xlim], dtype=int)
+    for x in xrange(xlim):
+        for y in xrange(ylim):
+            steps = limit
+            value = position[y,x]
+            pos = position[y,x]
+            while abs(value) < 2 and steps >= 0:
+                steps -= 1
+                value = value**2 + pos
+            diverged_at[y,x] = steps
+  
+    return diverged_at
+
+
+
+
+
+
+
+
+
+
Content of stderr:
+In file included from /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,
+                 from /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,
+                 from /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,
+                 from /home/runner/.cache/ipython/cython/_cython_magic_7b2295f9408431350bb6fb01f51796079edb0915.c:1215:
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning "Using deprecated NumPy API, disable it with " "#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
+   17 | #warning "Using deprecated NumPy API, disable it with " \
+      |  ^~~~~~~
+
+
+
+
+
+
+
+
+

Note the double import of numpy: the standard numpy module and a Cython-enabled version of numpy that ensures fast indexing of and other operations on arrays. Both import statements are necessary in code that uses numpy arrays. The new thing in the code above is declaration of arrays by np.ndarray.

+
+
+
+
+
+
In [15]:
+
+
+
%timeit data_cy = [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python
+
+
+
+
+
+
+
+
+
+
534 ms ± 2.79 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [16]:
+
+
+
%timeit data_cy = [[call_typed_mandel_cython(complex(x,y)) for x in xs] for y in ys] # typed cython
+
+
+
+
+
+
+
+
+
+
237 ms ± 958 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [17]:
+
+
+
%timeit numpy_cython_1(values) # ndarray
+
+
+
+
+
+
+
+
+
+
222 ms ± 314 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
+

A trick of using np.vectorize

+
+
+
+
+
+
In [18]:
+
+
+
numpy_cython_2 = np.vectorize(call_typed_mandel_cython)
+
+
+
+
+
+
+
+
In [19]:
+
+
+
%timeit numpy_cython_2(values) #  vectorize
+
+
+
+
+
+
+
+
+
+
/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/lib/function_base.py:2412: RuntimeWarning: divide by zero encountered in call_typed_mandel_cython (vectorized)
+  outputs = ufunc(*inputs)
+/opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/lib/function_base.py:2412: RuntimeWarning: invalid value encountered in call_typed_mandel_cython (vectorized)
+  outputs = ufunc(*inputs)
+
+
+
+
+
+
+
231 ms ± 768 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
+

Calling C functions from Cython

Example: compare sin() from Python and C library

+
+
+
+
+
+
In [20]:
+
+
+
%%cython
+import math
+cpdef py_sin():
+    cdef int x
+    cdef double y
+    for x in range(1e7):
+        y = math.sin(x)
+
+
+
+
+
+
+
+
In [21]:
+
+
+
%%cython
+from libc.math cimport sin as csin # import from C library
+cpdef c_sin():
+    cdef int x
+    cdef double y
+    for x in range(1e7):
+        y = csin(x)
+
+
+
+
+
+
+
+
In [22]:
+
+
+
%timeit [math.sin(i) for i in range(int(1e7))] # python
+
+
+
+
+
+
+
+
+
+
1.41 s ± 4.76 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [23]:
+
+
+
%timeit py_sin()                                # cython call python library
+
+
+
+
+
+
+
+
+
+
714 ms ± 12.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
+
+
+
+
+
+
+
+
+
In [24]:
+
+
+
%timeit c_sin()                                 # cython call C library
+
+
+
+
+
+
+
+
+
+
3.14 ms ± 13.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/040cython.ipynb b/ch08performance/040cython.ipynb new file mode 100644 index 000000000..2ee1da683 --- /dev/null +++ b/ch08performance/040cython.ipynb @@ -0,0 +1,482 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "05d841bb", + "metadata": {}, + "source": [ + "## Cython\n", + "Cython can be viewed as an extension of Python where variables and functions are annotated with extra information, in particular types. The resulting Cython source code will be compiled into optimized C or C++ code, and thereby yielding substantial speed-up of slow Python code. In other words, Cython provides a way of writing Python with comparable performance to that of C/C++." + ] + }, + { + "cell_type": "markdown", + "id": "87393465", + "metadata": {}, + "source": [ + "### Start Coding in Cython" + ] + }, + { + "cell_type": "markdown", + "id": "968e7815", + "metadata": {}, + "source": [ + "Cython code must, unlike Python, be compiled. This happens in the following stages:\n", + "\n", + "* The cython code in `.pyx` file will be translated to a `C` file.\n", + "* The `C` file will be compiled by a C compiler into a shared library, which will be directly loaded into Python. \n", + "\n", + "In a Jupyter notebook, everything is a lot easier. One needs only to load the Cython extension (`%load_ext Cython`) at the beginning and put `%%cython` mark in front of cells of Cython code. Cells with Cython mark will be treated as a `.pyx` code and consequently, compiled into C. \n", + "\n", + "For details, please see [Building Cython Code](http://docs.cython.org/src/quickstart/build.html).\n" + ] + }, + { + "cell_type": "markdown", + "id": "eef62c7e", + "metadata": {}, + "source": [ + "**Pure python Mandelbrot set:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74772d07", + "metadata": {}, + "outputs": [], + "source": [ + "xmin = -1.5\n", + "ymin = -1.0\n", + "xmax = 0.5\n", + "ymax = 1.0\n", + "resolution = 300\n", + "xstep = (xmax - xmin) / resolution\n", + "ystep = (ymax - ymin) / resolution\n", + "xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)]\n", + "ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f94f34a8", + "metadata": {}, + "outputs": [], + "source": [ + "def mandel(position, limit=50):\n", + " value = position\n", + " while abs(value) < 2:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " return limit" + ] + }, + { + "cell_type": "markdown", + "id": "e7e6aebf", + "metadata": {}, + "source": [ + "**Compiled by Cython:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e9f1523", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext Cython" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "379e2c8f", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "\n", + "def mandel_cython(position, limit=50):\n", + " value = position\n", + " while abs(value) < 2:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " return limit" + ] + }, + { + "cell_type": "markdown", + "id": "49f06580", + "metadata": {}, + "source": [ + "Let's verify the result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73f12402", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "%matplotlib inline\n", + "f, axarr = plt.subplots(1, 2)\n", + "axarr[0].imshow([[mandel(complex(x, y)) for x in xs] for y in ys], interpolation='none')\n", + "axarr[0].set_title('Pure Python')\n", + "axarr[1].imshow([[mandel_cython(complex(x, y)) for x in xs] for y in ys], interpolation='none')\n", + "axarr[1].set_title('Cython')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60804515", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python\n", + "%timeit [[mandel_cython(complex(x,y)) for x in xs] for y in ys] # cython" + ] + }, + { + "cell_type": "markdown", + "id": "92af295f", + "metadata": {}, + "source": [ + "We have improved the performance of a factor of 1.5 by just using the Cython compiler, **without changing the code**!" + ] + }, + { + "cell_type": "markdown", + "id": "ae145053", + "metadata": {}, + "source": [ + "### Cython with C Types\n", + "But we can do better by telling Cython what C data type we would use in the code. Note we're not actually writing C, we're writing Python with C types." + ] + }, + { + "cell_type": "markdown", + "id": "9b127485", + "metadata": {}, + "source": [ + "_typed variable_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ea02008", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "def var_typed_mandel_cython(position, limit=50):\n", + " cdef double complex value # typed variable\n", + " value = position\n", + " while abs(value) < 2:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " return limit" + ] + }, + { + "cell_type": "markdown", + "id": "ffb95637", + "metadata": {}, + "source": [ + "_typed function + typed variable_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d62e5253", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "cpdef call_typed_mandel_cython(double complex position,\n", + " int limit=50): # typed function\n", + " cdef double complex value # typed variable\n", + " value = position\n", + " while abs(value)<2:\n", + " limit -= 1\n", + " value = value**2 + position\n", + " if limit < 0:\n", + " return 0\n", + " return limit" + ] + }, + { + "cell_type": "markdown", + "id": "b0626534", + "metadata": {}, + "source": [ + "performance of one number:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9218680b", + "metadata": {}, + "outputs": [], + "source": [ + "# pure python\n", + "%timeit a = mandel(complex(0, 0)) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb7c2924", + "metadata": {}, + "outputs": [], + "source": [ + "# primitive cython\n", + "%timeit a = mandel_cython(complex(0, 0)) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "261bdb7e", + "metadata": {}, + "outputs": [], + "source": [ + "# cython with C type variable\n", + "%timeit a = var_typed_mandel_cython(complex(0, 0)) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aeb28442", + "metadata": {}, + "outputs": [], + "source": [ + "# cython with typed variable + function\n", + "%timeit a = call_typed_mandel_cython(complex(0, 0))" + ] + }, + { + "cell_type": "markdown", + "id": "fa3d8896", + "metadata": {}, + "source": [ + "### Cython with numpy ndarray\n", + "You can use NumPy from Cython exactly the same as in regular Python, but by doing so you are losing potentially high speedups because Cython has support for fast access to NumPy arrays. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7266f8d9", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep]\n", + "values = xmatrix + 1j * ymatrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8bf771", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "import numpy as np\n", + "cimport numpy as np \n", + "\n", + "cpdef numpy_cython_1(np.ndarray[double complex, ndim=2] position, \n", + " int limit=50): \n", + " cdef np.ndarray[long,ndim=2] diverged_at\n", + " cdef double complex value\n", + " cdef int xlim\n", + " cdef int ylim\n", + " cdef double complex pos\n", + " cdef int steps\n", + " cdef int x, y\n", + "\n", + " xlim = position.shape[1]\n", + " ylim = position.shape[0]\n", + " diverged_at = np.zeros([ylim, xlim], dtype=int)\n", + " for x in xrange(xlim):\n", + " for y in xrange(ylim):\n", + " steps = limit\n", + " value = position[y,x]\n", + " pos = position[y,x]\n", + " while abs(value) < 2 and steps >= 0:\n", + " steps -= 1\n", + " value = value**2 + pos\n", + " diverged_at[y,x] = steps\n", + " \n", + " return diverged_at" + ] + }, + { + "cell_type": "markdown", + "id": "4d39298e", + "metadata": {}, + "source": [ + "Note the double import of numpy: the standard numpy module and a Cython-enabled version of numpy that ensures fast indexing of and other operations on arrays. Both import statements are necessary in code that uses numpy arrays. The new thing in the code above is declaration of arrays by np.ndarray." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e96b9293", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit data_cy = [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "914cb7d8", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit data_cy = [[call_typed_mandel_cython(complex(x,y)) for x in xs] for y in ys] # typed cython" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b694e37", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit numpy_cython_1(values) # ndarray" + ] + }, + { + "cell_type": "markdown", + "id": "988722fc", + "metadata": {}, + "source": [ + "**A trick of using `np.vectorize`**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32831193", + "metadata": {}, + "outputs": [], + "source": [ + "numpy_cython_2 = np.vectorize(call_typed_mandel_cython)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5d530ea", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit numpy_cython_2(values) # vectorize" + ] + }, + { + "cell_type": "markdown", + "id": "358a6a0d", + "metadata": {}, + "source": [ + "### Calling C functions from Cython\n", + "\n", + "**Example: compare `sin()` from Python and C library**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4638280e", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "import math\n", + "cpdef py_sin():\n", + " cdef int x\n", + " cdef double y\n", + " for x in range(1e7):\n", + " y = math.sin(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35b4c7b1", + "metadata": {}, + "outputs": [], + "source": [ + "%%cython\n", + "from libc.math cimport sin as csin # import from C library\n", + "cpdef c_sin():\n", + " cdef int x\n", + " cdef double y\n", + " for x in range(1e7):\n", + " y = csin(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e39996d5", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit [math.sin(i) for i in range(int(1e7))] # python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2761d868", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit py_sin() # cython call python library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aea5791b", + "metadata": {}, + "outputs": [], + "source": [ + "%timeit c_sin() # cython call C library" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Cython" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch08performance/040cython.ipynb.py b/ch08performance/040cython.ipynb.py new file mode 100644 index 000000000..2cd5411aa --- /dev/null +++ b/ch08performance/040cython.ipynb.py @@ -0,0 +1,233 @@ +# --- +# jupyter: +# jekyll: +# display_name: Cython +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Cython +# Cython can be viewed as an extension of Python where variables and functions are annotated with extra information, in particular types. The resulting Cython source code will be compiled into optimized C or C++ code, and thereby yielding substantial speed-up of slow Python code. In other words, Cython provides a way of writing Python with comparable performance to that of C/C++. + +# %% [markdown] +# ### Start Coding in Cython + +# %% [markdown] +# Cython code must, unlike Python, be compiled. This happens in the following stages: +# +# * The cython code in `.pyx` file will be translated to a `C` file. +# * The `C` file will be compiled by a C compiler into a shared library, which will be directly loaded into Python. +# +# In a Jupyter notebook, everything is a lot easier. One needs only to load the Cython extension (`%load_ext Cython`) at the beginning and put `%%cython` mark in front of cells of Cython code. Cells with Cython mark will be treated as a `.pyx` code and consequently, compiled into C. +# +# For details, please see [Building Cython Code](http://docs.cython.org/src/quickstart/build.html). +# + +# %% [markdown] +# **Pure python Mandelbrot set:** + +# %% +xmin = -1.5 +ymin = -1.0 +xmax = 0.5 +ymax = 1.0 +resolution = 300 +xstep = (xmax - xmin) / resolution +ystep = (ymax - ymin) / resolution +xs = [(xmin + (xmax - xmin) * i / resolution) for i in range(resolution)] +ys = [(ymin + (ymax - ymin) * i / resolution) for i in range(resolution)] + + +# %% +def mandel(position, limit=50): + value = position + while abs(value) < 2: + limit -= 1 + value = value**2 + position + if limit < 0: + return 0 + return limit + + +# %% [markdown] +# **Compiled by Cython:** + +# %% +# %load_ext Cython + +# %% language="cython" +# +# def mandel_cython(position, limit=50): +# value = position +# while abs(value) < 2: +# limit -= 1 +# value = value**2 + position +# if limit < 0: +# return 0 +# return limit + +# %% [markdown] +# Let's verify the result + +# %% +from matplotlib import pyplot as plt +# %matplotlib inline +f, axarr = plt.subplots(1, 2) +axarr[0].imshow([[mandel(complex(x, y)) for x in xs] for y in ys], interpolation='none') +axarr[0].set_title('Pure Python') +axarr[1].imshow([[mandel_cython(complex(x, y)) for x in xs] for y in ys], interpolation='none') +axarr[1].set_title('Cython') + +# %% +# %timeit [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python +# %timeit [[mandel_cython(complex(x,y)) for x in xs] for y in ys] # cython + +# %% [markdown] +# We have improved the performance of a factor of 1.5 by just using the Cython compiler, **without changing the code**! + +# %% [markdown] +# ### Cython with C Types +# But we can do better by telling Cython what C data type we would use in the code. Note we're not actually writing C, we're writing Python with C types. + +# %% [markdown] +# _typed variable_ + +# %% language="cython" +# def var_typed_mandel_cython(position, limit=50): +# cdef double complex value # typed variable +# value = position +# while abs(value) < 2: +# limit -= 1 +# value = value**2 + position +# if limit < 0: +# return 0 +# return limit + +# %% [markdown] +# _typed function + typed variable_ + +# %% language="cython" +# cpdef call_typed_mandel_cython(double complex position, +# int limit=50): # typed function +# cdef double complex value # typed variable +# value = position +# while abs(value)<2: +# limit -= 1 +# value = value**2 + position +# if limit < 0: +# return 0 +# return limit + +# %% [markdown] +# performance of one number: + +# %% +# pure python +# %timeit a = mandel(complex(0, 0)) + +# %% +# primitive cython +# %timeit a = mandel_cython(complex(0, 0)) + +# %% +# cython with C type variable +# %timeit a = var_typed_mandel_cython(complex(0, 0)) + +# %% +# cython with typed variable + function +# %timeit a = call_typed_mandel_cython(complex(0, 0)) + +# %% [markdown] +# ### Cython with numpy ndarray +# You can use NumPy from Cython exactly the same as in regular Python, but by doing so you are losing potentially high speedups because Cython has support for fast access to NumPy arrays. + +# %% +import numpy as np +ymatrix, xmatrix = np.mgrid[ymin:ymax:ystep, xmin:xmax:xstep] +values = xmatrix + 1j * ymatrix + +# %% language="cython" +# import numpy as np +# cimport numpy as np +# +# cpdef numpy_cython_1(np.ndarray[double complex, ndim=2] position, +# int limit=50): +# cdef np.ndarray[long,ndim=2] diverged_at +# cdef double complex value +# cdef int xlim +# cdef int ylim +# cdef double complex pos +# cdef int steps +# cdef int x, y +# +# xlim = position.shape[1] +# ylim = position.shape[0] +# diverged_at = np.zeros([ylim, xlim], dtype=int) +# for x in xrange(xlim): +# for y in xrange(ylim): +# steps = limit +# value = position[y,x] +# pos = position[y,x] +# while abs(value) < 2 and steps >= 0: +# steps -= 1 +# value = value**2 + pos +# diverged_at[y,x] = steps +# +# return diverged_at + +# %% [markdown] +# Note the double import of numpy: the standard numpy module and a Cython-enabled version of numpy that ensures fast indexing of and other operations on arrays. Both import statements are necessary in code that uses numpy arrays. The new thing in the code above is declaration of arrays by np.ndarray. + +# %% +# %timeit data_cy = [[mandel(complex(x,y)) for x in xs] for y in ys] # pure python + +# %% +# %timeit data_cy = [[call_typed_mandel_cython(complex(x,y)) for x in xs] for y in ys] # typed cython + +# %% +# %timeit numpy_cython_1(values) # ndarray + +# %% [markdown] +# **A trick of using `np.vectorize`** + +# %% +numpy_cython_2 = np.vectorize(call_typed_mandel_cython) + +# %% +# %timeit numpy_cython_2(values) # vectorize + +# %% [markdown] +# ### Calling C functions from Cython +# +# **Example: compare `sin()` from Python and C library** + +# %% language="cython" +# import math +# cpdef py_sin(): +# cdef int x +# cdef double y +# for x in range(1e7): +# y = math.sin(x) + +# %% language="cython" +# from libc.math cimport sin as csin # import from C library +# cpdef c_sin(): +# cdef int x +# cdef double y +# for x in range(1e7): +# y = csin(x) + +# %% +# %timeit [math.sin(i) for i in range(int(1e7))] # python + +# %% +# %timeit py_sin() # cython call python library + +# %% +# %timeit c_sin() # cython call C library diff --git a/ch08performance/050scaling.html b/ch08performance/050scaling.html new file mode 100644 index 000000000..6c2ad2478 --- /dev/null +++ b/ch08performance/050scaling.html @@ -0,0 +1,1103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Scaling + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Scaling for containers and algorithms

+
+
+
+
+
+
+

We've seen that NumPy arrays are really useful. Why wouldn't we always want to use them for data which is all the same type?

+
+
+
+
+
+
In [1]:
+
+
+
import numpy as np
+from timeit import repeat
+from matplotlib import pyplot as plt
+%matplotlib inline
+
+
+
+
+
+
+
+
+

Let's look at appending data into a NumPy array, compared to a plain Python list:

+
+
+
+
+
+
In [2]:
+
+
+
def time_append_to_ndarray(count):
+    # the function repeat does the same that the `%timeit` magic
+    # but as a function; so we can plot it.
+    return repeat('np.append(before, [0])',
+                  f'import numpy as np; before=np.ndarray({count})',
+                  number=10000)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
def time_append_to_list(count):
+    return repeat('before.append(0)',
+                  f'before = [0] * {count}',
+                  number=10000)
+
+
+
+
+
+
+
+
In [4]:
+
+
+
counts = np.arange(1, 100000, 10000)
+
+def plot_time(function, counts, title=None):
+    plt.plot(counts, list(map(function, counts)))
+    plt.ylim(bottom=0) 
+    plt.ylabel('seconds')
+    plt.xlabel('array size')
+    plt.title(title or function.__name__)
+
+
+
+
+
+
+
+
In [5]:
+
+
+
plot_time(time_append_to_list, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [6]:
+
+
+
plot_time(time_append_to_ndarray, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Adding an element to a Python list is way faster! Also, it seems that adding an element to a Python list is independent of the length of the list, but it's not so for a NumPy array.

+
+
+
+
+
+
+

How do they perform when accessing an element in the middle?

+
+
+
+
+
+
In [7]:
+
+
+
def time_lookup_middle_element_in_list(count):
+    before = [0] * count
+    def totime():
+        x = before[count // 2]
+    return repeat(totime, number=10000)
+
+
+
+
+
+
+
+
In [8]:
+
+
+
def time_lookup_middle_element_in_ndarray(count):
+    before = np.ndarray(count)
+    def totime():
+        x = before[count // 2]
+    return repeat(totime, number=10000)
+
+
+
+
+
+
+
+
In [9]:
+
+
+
plot_time(time_lookup_middle_element_in_list, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
In [10]:
+
+
+
plot_time(time_lookup_middle_element_in_ndarray, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Both scale well for accessing the middle element.

+
+
+
+
+
+
+

What about inserting at the beginning?

+

If we want to insert an element at the beginning of a Python list we can do:

+
+
+
+
+
+
In [11]:
+
+
+
x = list(range(5))
+x
+
+
+
+
+
+
+
+
Out[11]:
+
+
[0, 1, 2, 3, 4]
+
+
+
+
+
+
+
+
In [12]:
+
+
+
x[0:0] = [-1]
+x
+
+
+
+
+
+
+
+
Out[12]:
+
+
[-1, 0, 1, 2, 3, 4]
+
+
+
+
+
+
+
+
In [13]:
+
+
+
def time_insert_to_list(count):
+    return repeat('before[0:0] = [0]',
+                  f'before = [0] * {count}',number=10000)
+
+
+
+
+
+
+
+
In [14]:
+
+
+
plot_time(time_insert_to_list, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

list performs badly for insertions at the beginning!

+
+
+
+
+
+
+

There are containers in Python that work well for insertion at the start:

+
+
+
+
+
+
In [15]:
+
+
+
from collections import deque
+
+
+
+
+
+
+
+
In [16]:
+
+
+
def time_insert_to_deque(count):
+    return repeat('before.appendleft(0)', 
+                  f'from collections import deque; before = deque([0] * {count})',
+                  number=10000)
+
+
+
+
+
+
+
+
In [17]:
+
+
+
plot_time(time_insert_to_deque, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

But looking up in the middle scales badly:

+
+
+
+
+
+
In [18]:
+
+
+
def time_lookup_middle_element_in_deque(count):
+    before = deque([0] * count)
+    def totime():
+        x = before[count // 2]
+    return repeat(totime, number=10000)
+
+
+
+
+
+
+
+
In [19]:
+
+
+
plot_time(time_lookup_middle_element_in_deque, counts)
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

What is going on here?

+
+
+
+
+
+
+

Arrays are stored as contiguous memory. Anything which changes the length of the array requires the whole array to be copied elsewhere in memory.

+
+
+
+
+
+
+

This copy takes time proportional to the array size.

+
+
+
+
+
+
+

Adding an element to an array - memory representation

+
+
+
+
+
+
+

The Python list type is also an array, but it is allocated with extra memory. Only when that memory is exhausted is a copy needed.

+
+
+
+
+
+
+

Adding an element to a list - memory representation

+
+
+
+
+
+
+

If the extra memory is typically the size of the current array, a copy is needed every 1/N appends, and costs N to make, so on average copies are cheap. We call this amortized constant time.

+

This makes it fast to look up values in the middle. However, it may also use more space than is needed.

+
+
+
+
+
+
+

The deque type works differently: each element contains a pointer to the next. Inserting elements is therefore very cheap, but looking up the Nth element requires traversing N such pointers.

+
+
+
+
+
+
+

Adding an element to a deque - memory representation

+
+
+
+
+
+
+

Dictionary performance

+
+
+
+
+
+
+

For another example, let's consider the performance of a dictionary versus a couple of other ways in which we could implement an associative array.

+
+
+
+
+
+
In [20]:
+
+
+
class evildict:
+    def __init__(self, data):
+        self.data = data
+        
+    def __getitem__(self, akey):
+        for key, value in self.data:
+            if key == akey:
+                return value
+        raise KeyError()
+
+
+
+
+
+
+
+
+

If we have an evil dictionary of N elements, how long would it take - on average - to find an element?

+
+
+
+
+
+
In [21]:
+
+
+
eric = [["Name", "Eric Idle"], ["Job", "Comedian"], ["Home", "London"]]
+
+
+
+
+
+
+
+
In [22]:
+
+
+
eric_evil = evildict(eric)
+
+
+
+
+
+
+
+
In [23]:
+
+
+
eric_evil["Job"]
+
+
+
+
+
+
+
+
Out[23]:
+
+
'Comedian'
+
+
+
+
+
+
+
+
In [24]:
+
+
+
eric_dict = dict(eric)
+
+
+
+
+
+
+
+
In [25]:
+
+
+
eric_evil["Job"]
+
+
+
+
+
+
+
+
Out[25]:
+
+
'Comedian'
+
+
+
+
+
+
+
+
In [26]:
+
+
+
x = ["Hello", "License", "Fish", "Eric", "Pet", "Halibut"]
+
+
+
+
+
+
+
+
In [27]:
+
+
+
sorted(x, key=lambda el: el.lower())
+
+
+
+
+
+
+
+
Out[27]:
+
+
['Eric', 'Fish', 'Halibut', 'Hello', 'License', 'Pet']
+
+
+
+
+
+
+
+
+

What if we created a dictionary where we bisect the search?

+
+
+
+
+
+
In [28]:
+
+
+
class sorteddict:
+    def __init__(self, data):
+        self.data = sorted(data, key = lambda x:x[0])
+        self.keys = list(map(lambda x:x[0], self.data))
+        
+    def __getitem__(self,akey):
+        from bisect import bisect_left
+        loc = bisect_left(self.keys, akey)
+        
+        if loc != len(self.data):
+            return self.data[loc][1]
+        
+        raise KeyError()
+
+
+
+
+
+
+
+
In [29]:
+
+
+
eric_sorted = sorteddict(eric)
+
+
+
+
+
+
+
+
In [30]:
+
+
+
eric_sorted["Job"]
+
+
+
+
+
+
+
+
Out[30]:
+
+
'Comedian'
+
+
+
+
+
+
+
+
In [31]:
+
+
+
def time_dict_generic(ttype, count, number=10000):
+    from random import randrange
+    keys = list(range(count))
+    values = [0] * count
+    data = ttype(list(zip(keys, values)))
+    def totime():
+        x = data[keys[count // 2]]
+    return repeat(totime, number=10000)
+
+
+
+
+
+
+
+
In [32]:
+
+
+
time_dict = lambda count: time_dict_generic(dict, count)
+time_sorted = lambda count: time_dict_generic(sorteddict, count)
+time_evil = lambda count: time_dict_generic(evildict, count)
+
+
+
+
+
+
+
+
In [33]:
+
+
+
plot_time(time_sorted, counts, title='sorted')
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

We can't really see what's going on here for the sorted example as there's too much noise, but theoretically we should get logarithmic asymptotic performance. We write this down as $O(\ln N)$. This doesn't mean there isn't also a constant term, or a term proportional to something that grows slower (such as $\ln(\ln N)$): we always write down just the term that is dominant for large $N$. We saw before that list is $O(1)$ for appends, $O(N)$ for inserts. Numpy's array is $O(N)$ for appends.

+
+
+
+
+
+
In [34]:
+
+
+
counts = np.arange(1, 1000, 100)
+plot_time(time_evil, counts, title='evil')
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

The simple check-each-in-turn solution is $O(N)$ - linear time.

+
+
+
+
+
+
In [35]:
+
+
+
counts = np.arange(1, 100000, 10000)
+plot_time(time_dict, counts, title='dict')
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Python's built-in dictionary is, amazingly, O(1): the time is independent of the size of the dictionary.

+

This uses a miracle of programming called the Hash Table: +you can learn more about these issues at this video from Harvard University. This material is pretty advanced, but, I think, really interesting!

+
+
+
+
+
+
+

Optional exercise: determine what the asymptotic peformance for the Boids model in terms of the number of Boids. Make graphs to support this. Bonus: how would the performance scale with the number of dimensions?

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/050scaling.ipynb b/ch08performance/050scaling.ipynb new file mode 100644 index 000000000..82f13b8d9 --- /dev/null +++ b/ch08performance/050scaling.ipynb @@ -0,0 +1,657 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5d15e913", + "metadata": {}, + "source": [ + "## Scaling for containers and algorithms" + ] + }, + { + "cell_type": "markdown", + "id": "f0077187", + "metadata": {}, + "source": [ + "We've seen that NumPy arrays are really useful. Why wouldn't we always want to use them for data which is all the same type?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70bef109", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from timeit import repeat\n", + "from matplotlib import pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "1b0b6499", + "metadata": {}, + "source": [ + "Let's look at appending data into a NumPy array, compared to a plain Python list: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba722c3a", + "metadata": {}, + "outputs": [], + "source": [ + "def time_append_to_ndarray(count):\n", + " # the function repeat does the same that the `%timeit` magic\n", + " # but as a function; so we can plot it.\n", + " return repeat('np.append(before, [0])',\n", + " f'import numpy as np; before=np.ndarray({count})',\n", + " number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "067acee0", + "metadata": {}, + "outputs": [], + "source": [ + "def time_append_to_list(count):\n", + " return repeat('before.append(0)',\n", + " f'before = [0] * {count}',\n", + " number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f335c3a", + "metadata": {}, + "outputs": [], + "source": [ + "counts = np.arange(1, 100000, 10000)\n", + "\n", + "def plot_time(function, counts, title=None):\n", + " plt.plot(counts, list(map(function, counts)))\n", + " plt.ylim(bottom=0) \n", + " plt.ylabel('seconds')\n", + " plt.xlabel('array size')\n", + " plt.title(title or function.__name__)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb592f16", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_append_to_list, counts)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65392a9f", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_append_to_ndarray, counts)" + ] + }, + { + "cell_type": "markdown", + "id": "c8f8ea7e", + "metadata": {}, + "source": [ + "Adding an element to a Python list is way faster! Also, it seems that adding an element to a Python list is independent of the length of the list, but it's not so for a NumPy array." + ] + }, + { + "cell_type": "markdown", + "id": "d78d4509", + "metadata": {}, + "source": [ + "How do they perform when accessing an element in the middle?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a36fd83", + "metadata": {}, + "outputs": [], + "source": [ + "def time_lookup_middle_element_in_list(count):\n", + " before = [0] * count\n", + " def totime():\n", + " x = before[count // 2]\n", + " return repeat(totime, number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6153befb", + "metadata": {}, + "outputs": [], + "source": [ + "def time_lookup_middle_element_in_ndarray(count):\n", + " before = np.ndarray(count)\n", + " def totime():\n", + " x = before[count // 2]\n", + " return repeat(totime, number=10000)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74a7cf94", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_lookup_middle_element_in_list, counts)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80a60867", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_lookup_middle_element_in_ndarray, counts)" + ] + }, + { + "cell_type": "markdown", + "id": "44c4be09", + "metadata": {}, + "source": [ + "Both scale well for accessing the middle element." + ] + }, + { + "cell_type": "markdown", + "id": "3b81ece5", + "metadata": {}, + "source": [ + "What about inserting at the beginning?\n", + "\n", + "If we want to insert an element at the beginning of a Python list we can do:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52888589", + "metadata": {}, + "outputs": [], + "source": [ + "x = list(range(5))\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "478714a2", + "metadata": {}, + "outputs": [], + "source": [ + "x[0:0] = [-1]\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3f6130f", + "metadata": {}, + "outputs": [], + "source": [ + "def time_insert_to_list(count):\n", + " return repeat('before[0:0] = [0]',\n", + " f'before = [0] * {count}',number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe402a90", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_insert_to_list, counts)" + ] + }, + { + "cell_type": "markdown", + "id": "34226a93", + "metadata": {}, + "source": [ + "`list` performs **badly** for insertions at the beginning!" + ] + }, + { + "cell_type": "markdown", + "id": "35b6d1b7", + "metadata": {}, + "source": [ + "There are containers in Python that work well for insertion at the start:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7de86f96", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import deque" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d89dac8", + "metadata": {}, + "outputs": [], + "source": [ + "def time_insert_to_deque(count):\n", + " return repeat('before.appendleft(0)', \n", + " f'from collections import deque; before = deque([0] * {count})',\n", + " number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "961a4dbe", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_insert_to_deque, counts)" + ] + }, + { + "cell_type": "markdown", + "id": "7a4921c0", + "metadata": {}, + "source": [ + "But looking up in the middle scales badly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "496b1221", + "metadata": {}, + "outputs": [], + "source": [ + "def time_lookup_middle_element_in_deque(count):\n", + " before = deque([0] * count)\n", + " def totime():\n", + " x = before[count // 2]\n", + " return repeat(totime, number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c2a6ebe", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_lookup_middle_element_in_deque, counts)" + ] + }, + { + "cell_type": "markdown", + "id": "fd32330b", + "metadata": {}, + "source": [ + "What is going on here?" + ] + }, + { + "cell_type": "markdown", + "id": "4a7c36b3", + "metadata": {}, + "source": [ + "Arrays are stored as contiguous memory. Anything which changes the length of the array requires the whole array to be copied elsewhere in memory." + ] + }, + { + "cell_type": "markdown", + "id": "31672807", + "metadata": {}, + "source": [ + "This copy takes time proportional to the array size." + ] + }, + { + "cell_type": "markdown", + "id": "fdaa6f41", + "metadata": {}, + "source": [ + "![Adding an element to an array - memory representation](./array_memory.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "a2db1952", + "metadata": {}, + "source": [ + "The Python `list` type is **also** an array, but it is allocated with **extra memory**. Only when that memory is exhausted is a copy needed." + ] + }, + { + "cell_type": "markdown", + "id": "292fa60a", + "metadata": {}, + "source": [ + "![Adding an element to a list - memory representation](list_memory.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "b9e91549", + "metadata": {}, + "source": [ + "If the extra memory is typically the size of the current array, a copy is needed every 1/N appends, and costs N to make, so **on average** copies are cheap. We call this **amortized constant time**. \n", + "\n", + "This makes it fast to look up values in the middle. However, it may also use more space than is needed." + ] + }, + { + "cell_type": "markdown", + "id": "cab47352", + "metadata": {}, + "source": [ + "The deque type works differently: each element contains a pointer to the next. Inserting elements is therefore very cheap, but looking up the Nth element requires traversing N such pointers." + ] + }, + { + "cell_type": "markdown", + "id": "73a08dcb", + "metadata": {}, + "source": [ + "![Adding an element to a deque - memory representation](deque_memory.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "c0e9b92f", + "metadata": {}, + "source": [ + "### Dictionary performance" + ] + }, + { + "cell_type": "markdown", + "id": "3fe94a4a", + "metadata": {}, + "source": [ + "For another example, let's consider the performance of a dictionary versus a couple of other ways in which we could implement an associative array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "640d681d", + "metadata": {}, + "outputs": [], + "source": [ + "class evildict:\n", + " def __init__(self, data):\n", + " self.data = data\n", + " \n", + " def __getitem__(self, akey):\n", + " for key, value in self.data:\n", + " if key == akey:\n", + " return value\n", + " raise KeyError()" + ] + }, + { + "cell_type": "markdown", + "id": "eeb84f5a", + "metadata": {}, + "source": [ + "If we have an evil dictionary of N elements, how long would it take - on average - to find an element?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56f2aff6", + "metadata": {}, + "outputs": [], + "source": [ + "eric = [[\"Name\", \"Eric Idle\"], [\"Job\", \"Comedian\"], [\"Home\", \"London\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49fae789", + "metadata": {}, + "outputs": [], + "source": [ + "eric_evil = evildict(eric)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddb90f7b", + "metadata": {}, + "outputs": [], + "source": [ + "eric_evil[\"Job\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "244612b7", + "metadata": {}, + "outputs": [], + "source": [ + "eric_dict = dict(eric)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25a39b79", + "metadata": {}, + "outputs": [], + "source": [ + "eric_evil[\"Job\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08467529", + "metadata": {}, + "outputs": [], + "source": [ + "x = [\"Hello\", \"License\", \"Fish\", \"Eric\", \"Pet\", \"Halibut\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28d31499", + "metadata": {}, + "outputs": [], + "source": [ + "sorted(x, key=lambda el: el.lower())" + ] + }, + { + "cell_type": "markdown", + "id": "8c980600", + "metadata": {}, + "source": [ + "What if we created a dictionary where we bisect the search?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be8ab5be", + "metadata": {}, + "outputs": [], + "source": [ + "class sorteddict:\n", + " def __init__(self, data):\n", + " self.data = sorted(data, key = lambda x:x[0])\n", + " self.keys = list(map(lambda x:x[0], self.data))\n", + " \n", + " def __getitem__(self,akey):\n", + " from bisect import bisect_left\n", + " loc = bisect_left(self.keys, akey)\n", + " \n", + " if loc != len(self.data):\n", + " return self.data[loc][1]\n", + " \n", + " raise KeyError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c23b269", + "metadata": {}, + "outputs": [], + "source": [ + "eric_sorted = sorteddict(eric)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9cf999e", + "metadata": {}, + "outputs": [], + "source": [ + "eric_sorted[\"Job\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d2a2c83", + "metadata": {}, + "outputs": [], + "source": [ + "def time_dict_generic(ttype, count, number=10000):\n", + " from random import randrange\n", + " keys = list(range(count))\n", + " values = [0] * count\n", + " data = ttype(list(zip(keys, values)))\n", + " def totime():\n", + " x = data[keys[count // 2]]\n", + " return repeat(totime, number=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a037c4b", + "metadata": {}, + "outputs": [], + "source": [ + "time_dict = lambda count: time_dict_generic(dict, count)\n", + "time_sorted = lambda count: time_dict_generic(sorteddict, count)\n", + "time_evil = lambda count: time_dict_generic(evildict, count)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25f8760c", + "metadata": {}, + "outputs": [], + "source": [ + "plot_time(time_sorted, counts, title='sorted')" + ] + }, + { + "cell_type": "markdown", + "id": "efd6d178", + "metadata": {}, + "source": [ + "We can't really see what's going on here for the sorted example as there's too much noise, but theoretically we should get **logarithmic** asymptotic performance. We write this down as $O(\\ln N)$. This doesn't mean there isn't also a constant term, or a term proportional to something that grows slower (such as $\\ln(\\ln N)$): we always write down just the term that is dominant for large $N$. We saw before that `list` is $O(1)$ for appends, $O(N)$ for inserts. Numpy's `array` is $O(N)$ for appends." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b548a717", + "metadata": {}, + "outputs": [], + "source": [ + "counts = np.arange(1, 1000, 100)\n", + "plot_time(time_evil, counts, title='evil')" + ] + }, + { + "cell_type": "markdown", + "id": "2a8b3f44", + "metadata": {}, + "source": [ + "The simple check-each-in-turn solution is $O(N)$ - linear time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea0f6f11", + "metadata": {}, + "outputs": [], + "source": [ + "counts = np.arange(1, 100000, 10000)\n", + "plot_time(time_dict, counts, title='dict')" + ] + }, + { + "cell_type": "markdown", + "id": "2b7a13cf", + "metadata": {}, + "source": [ + "Python's built-in dictionary is, amazingly, O(1): the time is **independent** of the size of the dictionary.\n", + "\n", + "This uses a miracle of programming called the _Hash Table_:\n", + "you can learn more about [these issues at this video from Harvard University](https://www.youtube.com/watch?v=h2d9b_nEzoA). This material is pretty advanced, but, I think, really interesting!" + ] + }, + { + "cell_type": "markdown", + "id": "d97528e4", + "metadata": {}, + "source": [ + "Optional exercise: determine what the asymptotic peformance for the Boids model in terms of the number of Boids. Make graphs to support this. Bonus: how would the performance scale with the number of dimensions?" + ] + } + ], + "metadata": { + "jekyll": { + "display_name": "Scaling" + }, + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch08performance/050scaling.ipynb.py b/ch08performance/050scaling.ipynb.py new file mode 100644 index 000000000..4b84c9a0b --- /dev/null +++ b/ch08performance/050scaling.ipynb.py @@ -0,0 +1,292 @@ +# --- +# jupyter: +# jekyll: +# display_name: Scaling +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# ## Scaling for containers and algorithms + +# %% [markdown] +# We've seen that NumPy arrays are really useful. Why wouldn't we always want to use them for data which is all the same type? + +# %% +import numpy as np +from timeit import repeat +from matplotlib import pyplot as plt +# %matplotlib inline + +# %% [markdown] +# Let's look at appending data into a NumPy array, compared to a plain Python list: + +# %% +def time_append_to_ndarray(count): + # the function repeat does the same that the `%timeit` magic + # but as a function; so we can plot it. + return repeat('np.append(before, [0])', + f'import numpy as np; before=np.ndarray({count})', + number=10000) + + +# %% +def time_append_to_list(count): + return repeat('before.append(0)', + f'before = [0] * {count}', + number=10000) + + +# %% +counts = np.arange(1, 100000, 10000) + +def plot_time(function, counts, title=None): + plt.plot(counts, list(map(function, counts))) + plt.ylim(bottom=0) + plt.ylabel('seconds') + plt.xlabel('array size') + plt.title(title or function.__name__) + + +# %% +plot_time(time_append_to_list, counts) + +# %% +plot_time(time_append_to_ndarray, counts) + + +# %% [markdown] +# Adding an element to a Python list is way faster! Also, it seems that adding an element to a Python list is independent of the length of the list, but it's not so for a NumPy array. + +# %% [markdown] +# How do they perform when accessing an element in the middle? + +# %% +def time_lookup_middle_element_in_list(count): + before = [0] * count + def totime(): + x = before[count // 2] + return repeat(totime, number=10000) + + +# %% +def time_lookup_middle_element_in_ndarray(count): + before = np.ndarray(count) + def totime(): + x = before[count // 2] + return repeat(totime, number=10000) + + + +# %% +plot_time(time_lookup_middle_element_in_list, counts) + +# %% +plot_time(time_lookup_middle_element_in_ndarray, counts) + +# %% [markdown] +# Both scale well for accessing the middle element. + +# %% [markdown] +# What about inserting at the beginning? +# +# If we want to insert an element at the beginning of a Python list we can do: + +# %% +x = list(range(5)) +x + +# %% +x[0:0] = [-1] +x + + +# %% +def time_insert_to_list(count): + return repeat('before[0:0] = [0]', + f'before = [0] * {count}',number=10000) + + +# %% +plot_time(time_insert_to_list, counts) + +# %% [markdown] +# `list` performs **badly** for insertions at the beginning! + +# %% [markdown] +# There are containers in Python that work well for insertion at the start: + +# %% +from collections import deque + + +# %% +def time_insert_to_deque(count): + return repeat('before.appendleft(0)', + f'from collections import deque; before = deque([0] * {count})', + number=10000) + + +# %% +plot_time(time_insert_to_deque, counts) + + +# %% [markdown] +# But looking up in the middle scales badly: + +# %% +def time_lookup_middle_element_in_deque(count): + before = deque([0] * count) + def totime(): + x = before[count // 2] + return repeat(totime, number=10000) + + +# %% +plot_time(time_lookup_middle_element_in_deque, counts) + + +# %% [markdown] +# What is going on here? + +# %% [markdown] +# Arrays are stored as contiguous memory. Anything which changes the length of the array requires the whole array to be copied elsewhere in memory. + +# %% [markdown] +# This copy takes time proportional to the array size. + +# %% [markdown] +# ![Adding an element to an array - memory representation](./array_memory.svg) + +# %% [markdown] +# The Python `list` type is **also** an array, but it is allocated with **extra memory**. Only when that memory is exhausted is a copy needed. + +# %% [markdown] +# ![Adding an element to a list - memory representation](list_memory.svg) + +# %% [markdown] +# If the extra memory is typically the size of the current array, a copy is needed every 1/N appends, and costs N to make, so **on average** copies are cheap. We call this **amortized constant time**. +# +# This makes it fast to look up values in the middle. However, it may also use more space than is needed. + +# %% [markdown] +# The deque type works differently: each element contains a pointer to the next. Inserting elements is therefore very cheap, but looking up the Nth element requires traversing N such pointers. + +# %% [markdown] +# ![Adding an element to a deque - memory representation](deque_memory.svg) + +# %% [markdown] +# ### Dictionary performance + +# %% [markdown] +# For another example, let's consider the performance of a dictionary versus a couple of other ways in which we could implement an associative array. + +# %% +class evildict: + def __init__(self, data): + self.data = data + + def __getitem__(self, akey): + for key, value in self.data: + if key == akey: + return value + raise KeyError() + + +# %% [markdown] +# If we have an evil dictionary of N elements, how long would it take - on average - to find an element? + +# %% +eric = [["Name", "Eric Idle"], ["Job", "Comedian"], ["Home", "London"]] + +# %% +eric_evil = evildict(eric) + +# %% +eric_evil["Job"] + +# %% +eric_dict = dict(eric) + +# %% +eric_evil["Job"] + +# %% +x = ["Hello", "License", "Fish", "Eric", "Pet", "Halibut"] + +# %% +sorted(x, key=lambda el: el.lower()) + + +# %% [markdown] +# What if we created a dictionary where we bisect the search? + +# %% +class sorteddict: + def __init__(self, data): + self.data = sorted(data, key = lambda x:x[0]) + self.keys = list(map(lambda x:x[0], self.data)) + + def __getitem__(self,akey): + from bisect import bisect_left + loc = bisect_left(self.keys, akey) + + if loc != len(self.data): + return self.data[loc][1] + + raise KeyError() + + +# %% +eric_sorted = sorteddict(eric) + +# %% +eric_sorted["Job"] + + +# %% +def time_dict_generic(ttype, count, number=10000): + from random import randrange + keys = list(range(count)) + values = [0] * count + data = ttype(list(zip(keys, values))) + def totime(): + x = data[keys[count // 2]] + return repeat(totime, number=10000) + + +# %% +time_dict = lambda count: time_dict_generic(dict, count) +time_sorted = lambda count: time_dict_generic(sorteddict, count) +time_evil = lambda count: time_dict_generic(evildict, count) + +# %% +plot_time(time_sorted, counts, title='sorted') + +# %% [markdown] +# We can't really see what's going on here for the sorted example as there's too much noise, but theoretically we should get **logarithmic** asymptotic performance. We write this down as $O(\ln N)$. This doesn't mean there isn't also a constant term, or a term proportional to something that grows slower (such as $\ln(\ln N)$): we always write down just the term that is dominant for large $N$. We saw before that `list` is $O(1)$ for appends, $O(N)$ for inserts. Numpy's `array` is $O(N)$ for appends. + +# %% +counts = np.arange(1, 1000, 100) +plot_time(time_evil, counts, title='evil') + +# %% [markdown] +# The simple check-each-in-turn solution is $O(N)$ - linear time. + +# %% +counts = np.arange(1, 100000, 10000) +plot_time(time_dict, counts, title='dict') + +# %% [markdown] +# Python's built-in dictionary is, amazingly, O(1): the time is **independent** of the size of the dictionary. +# +# This uses a miracle of programming called the _Hash Table_: +# you can learn more about [these issues at this video from Harvard University](https://www.youtube.com/watch?v=h2d9b_nEzoA). This material is pretty advanced, but, I think, really interesting! + +# %% [markdown] +# Optional exercise: determine what the asymptotic peformance for the Boids model in terms of the number of Boids. Make graphs to support this. Bonus: how would the performance scale with the number of dimensions? diff --git a/ch08performance/array_memory.svg b/ch08performance/array_memory.svg new file mode 100644 index 000000000..5acb8b9bc --- /dev/null +++ b/ch08performance/array_memory.svg @@ -0,0 +1,938 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch08performance/deque_memory.svg b/ch08performance/deque_memory.svg new file mode 100644 index 000000000..8f0d42ec4 --- /dev/null +++ b/ch08performance/deque_memory.svg @@ -0,0 +1,942 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch08performance/index.html b/ch08performance/index.html new file mode 100644 index 000000000..8e845a1e9 --- /dev/null +++ b/ch08performance/index.html @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Programming for Speed + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +
    +
  • Optimisation
  • +
  • Profiling
  • +
  • Scaling laws
  • +
  • NumPy
  • +
  • Cython
  • +
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch08performance/list_memory.svg b/ch08performance/list_memory.svg new file mode 100644 index 000000000..a3c0d31eb --- /dev/null +++ b/ch08performance/list_memory.svg @@ -0,0 +1,1364 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch98rubrics/Assessment1.md b/ch98rubrics/Assessment1.md new file mode 100644 index 000000000..a7c25d86f --- /dev/null +++ b/ch98rubrics/Assessment1.md @@ -0,0 +1,136 @@ +MPHYG001 First Continuous Assessment: Packaging Greengraph +========================================================== + +Summary +------- + +In an appendix, taken from +http://development.rc.ucl.ac.uk/training/ +engineering/ch02data/110Capstone.html, +are classes which generate a graph of the proportion of green pixels in a series of satellite images between two points. + +Your task is to take this code, and do the work needed to make it into a proper package which could be released, +meeting minimum software engineering standards + +* **package** this code, into a git repository, suitable to be installed with `pip` +* create an appropriate command line entry point, so that the code can be invoked with + `greengraph --from London --to Oxford --steps 10 --out graph.png` or a similar interface +* Implement automated tests for each element of the code +* Add appropriate standard supplementary files to the code, describing license, citation and typical usage + +Assignment Presentation +----------------------- + +For this coursework assignment, you are expected to submit a short report and your code. +The purpose of the report is to answer the non-coding questions below, to present your results and provide a brief +description of your design choices and implementation. The report need not be very long or overly detailed, +but should provide a succinct record of your coursework. The report must have a cover sheet +stating your name, your student number, and the code of the module (MPHYG001). + +Submission +---------- + +You should submit your report and all of your source code so that an independent person can run the code. +The code and report must be submitted as a single zip or tgz archive of a folder which contains **git** version control information for your project. +Your report should be included as a PDF file, report.pdf, in the root folder of your archive. + +All coursework should be submitted electronically through the Moodle for the course. +There is no need to include your source code +in your report, but you can refer to it and if necessary reproduce lines if it helps to explain your solution. +The deadline for submissions is 5pm on 6th January 2017. Marks will be available by 30th January. + +Marks Scheme +------------ + +* Code broken up into appropriate files, and arranged into an appropriate folder structure [2 marks] +* Git version control used, with a series of sensible commit messages [2 marks] +* Command line entry point, using appropriate library to parse arguments [3 marks] +* Packaging for `pip` installation, with `setup.py` file with appropriate content [5 marks] +* Automated tests for each method and class (2 marks), with appropriate fixtures defined (1 mark) + and mocks used to avoid tests interacting with internet (2 marks). [5 marks total] +* Supplementary files to define license, usage, and citation. [3 marks] +* A text report which: + * Documents the usage of your entry point [1 mark] + * Discusses problems encountered in completing your work [1 mark] + * Discusses in your own words the advantages and costs involved in preparing work for release, + the use of package managers like pip and package indexes like PyPI [2 marks] + * Discusses further steps you would need to take to build a community of users for a project [1 mark] + +[25 marks total] + +Appendix +-------- + +``` python + +import numpy as np +import geopy +from StringIO import StringIO +from matplotlib import image as img + + +class Greengraph(object): + def __init__(self, start, end): + self.start=start + self.end=end + self.geocoder=geopy.geocoders.GoogleV3( + domain="maps.google.co.uk") + + def geolocate(self, place): + return self.geocoder.geocode(place, + exactly_one=False)[0][1] + + def location_sequence(self, start,end,steps): + lats = np.linspace(start[0], end[0], steps) + longs = np.linspace(start[1],end[1], steps) + return np.vstack([lats, longs]).transpose() + + def green_between(self, steps): + return [Map(*location).count_green() + for location in self.location_sequence( + self.geolocate(self.start), + self.geolocate(self.end), + steps)] + +class Map(object): + def __init__(self, lat, long, satellite=True, + zoom=10, size=(400,400), sensor=False): + base="http://maps.googleapis.com/maps/api/staticmap?" + + params=dict( + sensor= str(sensor).lower(), + zoom= zoom, + size= "x".join(map(str, size)), + center= ",".join(map(str, (lat, long) )), + style="feature:all|element:labels|visibility:off" + ) + + if satellite: + params["maptype"]="satellite" + + self.image = requests.get(base, params=params).content + # Fetch our PNG image data + self.pixels= img.imread(StringIO(self.image)) + # Parse our PNG image as a numpy array + + def green(self, threshold): + # Use NumPy to build an element-by-element logical array + greener_than_red = self.pixels[:,:,1] > threshold* self.pixels[:,:,0] + greener_than_blue = self.pixels[:,:,1] > threshold*self.pixels[:,:,2] + green = np.logical_and(greener_than_red, greener_than_blue) + return green + + def count_green(self, threshold = 1.1): + return np.sum(self.green(threshold)) + + def show_green(data, threshold = 1.1): + green = self.green(threshold) + out = green[:,:,np.newaxis]*array([0,1,0])[np.newaxis,np.newaxis,:] + buffer = StringIO() + result = img.imsave(buffer, out, format='png') + return buffer.getvalue() + +mygraph=Greengraph('New York','Chicago') +data = mygraph.green_between(20) +plt.plot(data) +``` diff --git a/ch98rubrics/Assessment1.pdf b/ch98rubrics/Assessment1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..009564ffc4bd7ca969f1e50c67a32bd2ce31fb0f GIT binary patch literal 155283 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

@GgTE|mgL=W2FhrG;I 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 literal 0 HcmV?d00001 diff --git a/ch98rubrics/Assessment2.md b/ch98rubrics/Assessment2.md new file mode 100644 index 000000000..2ea9e4029 --- /dev/null +++ b/ch98rubrics/Assessment2.md @@ -0,0 +1,117 @@ +MPHYG001 Second Continuous Assessment: Refactoring the Bad Boids +========================================================== + +Summary +------- + +In an appendix, taken from https://github.com/jamespjh/bad-boids, +is a very poor implementation of the Boids flocking example from the course. + +Your task is to take this code, and build from it a clean implementation of the +flocking code, finishing with an appropriate object oriented design, using the +**refactoring** approach to gradually and safely build your better solution from +the supplied poor solution. Simply submitting a good, clean solution will not +complete this assignment; you are being assessed on the step-by-step refactoring +process, on the basis of individual git commits. You should develop unit tests for +your code as units (functions, classes, methods and modules) emerge, and wrap +the code in an appropriate command line entry point providing an appropriate +configuration system for simulation setup. + +Assignment Presentation +----------------------- + +For this coursework assignment, you are expected to submit a short report and your code. +The purpose of the report is to answer the non-coding questions below, to present your results and provide a brief +description of your design choices and implementation. The report need not be very long or overly detailed, +but should provide a succinct record of your coursework. The report must have a cover sheet +stating your name, your student number, and the code of the module (MPHYG001). + +Submission +---------- + +You should submit your report and all of your source code so that an independent person can run the code. +The code and report must be submitted as a single zip or tgz archive of a folder which contains **git** version control information for your project. +Your report should be included as a PDF file, report.pdf, in the root folder of your archive. + +All coursework should be submitted electronically through the Moodle for the course. +There is no need to include your source code +in your report, but you can refer to it and if necessary reproduce lines if it helps to explain your solution. +The deadline for submission is February 24th, 2017. Marks will be available by 20st March. + +Marks Scheme +------------ + +* Final state of code is well broken down into functions, classes, methods and modules [3 marks] +* Final state of code is readable with good variable, function, class and method names and necessary comments [2 marks] +* Git version control used, with a series of sensible commit messages [2 marks] +* Command line entry point and configuration file, using appropriate libraries [3 marks] +* Packaging for `pip` installation, with `setup.py` file with appropriate content [2 marks] +* Automated tests for each method and class. [5 marks total] +* Supplementary files to define license, usage, and citation. [3 marks] +* A text report which: + * Lists by name the code smells identified refactorings used, making reference to the git commit log [2 marks] + * Includes a UML diagram of the final class structure [1 mark] + * Discusses in your own words the advantages of a refactoring approach to improving code [1 marks] + * Discusses problems encountered during the project [1 mark] + +[25 marks total] + +Appendix +-------- + +``` python +from matplotlib import pyplot as plt +from matplotlib import animation +import random + +# Deliberately terrible code for teaching purposes + +boids_x=[random.uniform(-450,50.0) for x in range(50)] +boids_y=[random.uniform(300.0,600.0) for x in range(50)] +boid_x_velocities=[random.uniform(0,10.0) for x in range(50)] +boid_y_velocities=[random.uniform(-20.0,20.0) for x in range(50)] +boids=(boids_x,boids_y,boid_x_velocities,boid_y_velocities) + +def update_boids(boids): + xs,ys,xvs,yvs=boids + # Fly towards the middle + for i in range(50): + for j in range(50): + xvs[i]=xvs[i]+(xs[j]-xs[i])*0.01/len(xs) + for i in range(50): + for j in range(50): + yvs[i]=yvs[i]+(ys[j]-ys[i])*0.01/len(xs) + # Fly away from nearby boids + for i in range(50): + for j in range(50): + if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 100: + xvs[i]=xvs[i]+(xs[i]-xs[j]) + yvs[i]=yvs[i]+(ys[i]-ys[j]) + # Try to match speed with nearby boids + for i in range(50): + for j in range(50): + if (xs[j]-xs[i])**2 + (ys[j]-ys[i])**2 < 10000: + xvs[i]=xvs[i]+(xvs[j]-xvs[i])*0.125/len(xs) + yvs[i]=yvs[i]+(yvs[j]-yvs[i])*0.125/len(xs) + # Move according to velocities + for i in range(50): + xs[i]=xs[i]+xvs[i] + ys[i]=ys[i]+yvs[i] + + +figure=plt.figure() +axes=plt.axes(xlim=(-500,1500), ylim=(-500,1500)) +scatter=axes.scatter(boids[0],boids[1]) + +def animate(frame): + update_boids(boids) + scatter.set_offsets(zip(boids[0],boids[1])) + + +anim = animation.FuncAnimation(figure, animate, + frames=50, interval=50) + +if __name__ == "__main__": + plt.show() + +``` diff --git a/ch98rubrics/Assessment2.pdf b/ch98rubrics/Assessment2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c8f8d204076850e6d1975d261f9e7b2e74547f8 GIT binary patch literal 151939 zcma&NQ-d%}u%y|xZQHhO+qP}nwr$(CZTH)@J>Tr^T%C)Zx{CS%SrPeU7O8@W7%d|m zD-`L%%IF3Z6DI)!fxVFx6b}y+y^N`yxr+q>6C)=l!T)oh=*28;Tuhw^=*4UdT}(wx zjqOcLq4@ZqoL!tu4Q-)3wqiAQlZ)Aq{P+8Z;ybLu1OpJJcx`X(lDn&Fx2EQ#sV0MA znq^WK5eS|svUJi{%*T%$%-l;@;mc!7x#zN zMdrBNn;KHYmfg4a^ZwxL`Jhm*XiB5~o4l1}e>PmgZ;Z=KSa4}!``B6GKg--a(l}5X z)0Ui})JwV4v=&tJ&z2`+%(p0`V&hyB(ez3<5F4=J=$9sy#)!Z#_9)w_{jKb5Xy$H4|$2oti1;Q1JgwS@9qR7+xkpfG&_p*>1!?fIaCCS28?khMuBbJUVdZ9@?x{~9;-Ck2Hef;B{hn4dln0MVTO|E3xY<8T{aQda`WYG zUNzX%{?6+R0TV|V**ogA!wYMB_zcDb<<^V=qSV)mXpiS4!ts2=XiIHER(ewQm~X#0B*sV+}BN6)~l~n2>b?=m;V{6URY*857+*;$CN(lUC#M z011TV3DrWx>`g)WMuKr0^K?Xt{6t>N8-qQ#O9%T2Ys2*s83u3xBy+IH;Sn`RB%z_{ zH0JlF4|25aFHo}Ch8M}`b{ICfW<1_l&q(LAf{5 zL{U-h(GQ{~1_<*yS+>AMKA*A=M0&~Jmr4LoNW+x8H`M+Y!#bLD1T|MtzqdGsUKChe zfPiyUAQzUV8i9^;#L%E$Nz;H@3f|LKPJe)=<5ESz8XJleh z8u!$qLquHQnyK%EA7Y+(eva9U*95M}HADOr*4(mPuJfKGK7@88#E9N-lp4(KwPnEd zCdG<0`$0lv`G$c*?-%6}ePLy-A-_<{61bGVARm>CjDtVVQTo3B6XXC5p$V<~8OjENxd;Rd$R71xfr&Z!GJr zu=w6vz&%$7jL4d7xdcJW3(4J-E5vN%l;_n27{X9UaLbD{G5tx! zyer@a#blZdlhrIlm)ayFgdAykEqjmJNO8gn;6G*rE%1&vleP{k4kbwk54)sOwa@)S6C+f&n6xE!GE@Qwu# z;PAM2Otv+?N+R1q@d^;=&cS8qfr`_9n(NOttb5^fnjD=@*V3|lC|`$!;0L*!MK2wm zK_{CH+1X#~`#!LA)u@?;xG%`G!w+1Y@(;eLg)`L2QlW z?C2fskA~~ZmIXh~U_QyOmn8zxkydGP=W?;J9|c=@NG&pfGSSN3G{o_`s!vs+5@f&r zl+MH!Lsr&cYS}bJ!~l&Dc6=G6?{ZlST+Q-6kiZ~6Ek-Bzz_h&UY@isWA=&x;AyBra z(`@w%Egp^r4&nXjhV~^X3>)rl>sIAe*Wo*QJEEHpwT^4c?I4SS7d(b8`{8N<%PROe zHUE5xAGlyxE1NQ0zQE{x|CjHT37u%42aVL_JEduKHSLeD{|kDAex?n|)XwDpODO-T|DA4_IsRwNVPt1y z{lASlyBgB=+hPcPXX*&&p;o45_ ggtl1)qChCI-cB&VWa~)Ad?0)_SNrZ}=9I}T zI1*|nSe+T&#krfC_I;VTtj%a>&3rvZ|Gu^_s@Ly8n>pNx=8@MfQb$W&fcRT{=p90y zYnE5In)ydxT$#BO1I_*A%#yaHRVy|>F7&ab>-VMCqjDzD``UI||3gd=BCI+#iy$g@Q^G>A?L;Or~(F*?fsE=`S9DKVikJT0%x{B77NJw`+8lECj=urAWFnJ?n7SG3&t3b<$xQS&?9A zZvrK3gO{U$!Wn_h;r%SIHKsexA9e(Et3+63b?Pm%R749Y#&{o(G85l~<_+rve}&hE zB0-5lNI(TH!4L>Fl@iNcQ%*$B=W||8bwVaKHv9{1nzbHZORmF+nv;|?7ayd!jPP5s*0=5S5k;Mu)tA3 zxVnkDS!uTL)DAg`&WF8rqzY9rX#Jw;Ty0eY+RGAJs|u4&FBPmZhZw?Nm)==x!^fq3{=J}q$z!}%5A+{XnpF6c{om4S8*ikc!S{;LNMdi?$zr!k@iM0QR?>N=vVpM3i!)23n8=0?2YLa=T z+j+r$et%sQ6WXKEUv>~w-pby=3;L9kp&=)0;ia(K?vmQ7XPcUT^NGhVrZV_L^-kl` zvn=#aXO#@|q30TGpbn=4+ujG=SS9VT&oL~@x~Z0n4dbM5J3<|Xz0L>Itj@x`U~BzF z^)c37N0w|BooY0Kqs9>vw>620#L>g9HUqUNoQ+{53CryZf6=fSh7h|@xPtm>7@Aed zRK-;hnuJmsTB_TB?;+sJhoqiVOKpXlFNiPP)kF+>M}y0efZq6%m6h({rhHy>$7?nK z;A#IWu~%2<2XIv_+uTk2%8uQ4Q+EZ;3CBievsU+96&e?DhKQ^S8;2%k$-pbKPvCvUQXIMH~FFB4+mtO+T{As^;XoOOrt zQCd){@{vV?FqNHUn@gf%CH6I64&u6k#mJt0TXM&E z76|B&gN+@tXarUd*2I{kATVs07j|Fsz@BNfbv$kqFz-S5Ir>!~A3{rB_ZFj50r<{+ z)BqdW2gX1kbsn1>fYHo{WbEdo)kM3vP5sUHwf7KTat!$f9?kD=So+8Ci|&rjPE}tZ zIQMDv^v7_>fOdZ;1LX7i8Q2dG`94h^IE_OOHD()4Hu(8Y!m$TR1V>AFK z)@S$6o&CO(6Q-1KPX0n6TrU&u<)@6X*3S5cXF0jlQaYMO)|pjuyj*~{hSfWD@p{yB zC>t1q-K+mv8KJLF1_RIGr;KxY4xZ@am&|WdP4+gcx&BPP-^FDw2tOFAIXN{y-^mrw z`CY`md{$3hcTQE(oWF!2uVV*Nx07E}-*1lZl|B4$r7I9ZpOAh@M~ScDH~ zZ_hz}P_S)jpN3s&q^52rC0P?UM+G4Cr3qNGJ^9%c5FDn+yi(^cOdZLggRaeOPL*_m z0eKY(5UP*?w$aSCY>$v}Seu(YwZ?2=oz;wT+vCL|#HcQFh}Ph|7Rae_q^UqSGD;9? zFK}BoWmeA%(C2GQ9foX@qK)|`P9;eugUEGik~}Mc1@>nr1Ad*R?@n#Ik0D>D(W&?$ zRDIAqY+i^9;fQj_QyF@7xl$XdNmq%%ff|E*OSGB99fwO_;W5}Wh=xCMdVwW}8y-v# z#R;~M7FA)3RYZkow*eqmRrKEW>+o^j-!rssM$w8JQfhLJWCXX)UnS2tY5nlyr7{Xv z%H9cMuO*YX(qwnWG$T>yz&z8HrXvQVn)HzGPN(F_9QxzdGue+vNb)CU<-E+tnH2Lf zpLLlJx=ttJ{MBq?xe$a*e{&KZ!?bkx#n*g@MeBWezYuKsn44rt3|U78>V0*#J*J!k z5bv|-iRpgF(nng^E*&@7MyXfr5395`yLEUsezFU)b8StvgH7WTNz6_?vVh?Q3I6r% z9hPZ}NLvbs>UmN<$v{-6CQ)~8j>I>nk1k2V0W5&aJ?lNnw!c>7yy<3-BeFZgf`C*=?@5lfE%SfLpxa z$@FXNC*G^3qHj?d4MxfY9nOM4su+1;(nS~Tr`84paFQl$_YW|%36!WT?&MYJn*%u& z$_O)M5=UwpNwAk;Up>V*zQ9_e%2>Wl?3{Kz^fVnWLJ2wF`08+#LAfVdfcx8(W>+8X1|~KeK~E z;&1HB`xb^?0WF1%skPO`sh|T0CO`>g>BZm^+g*MXv;Zk>1ObQ(I}2MV2G&ptfK~u1 zsiP?=fJjpNDyS)y=ln?2+}oR*T;J$uh^DBjkXiv12|ZC502N};2@0xeA752KAif9e z#h??_J{x~)9}eHxbmV13B}J74Vw0|%FvQuhC@&qeI748-isf8hC0ouS18s%wK$4~};MEnvUHG0YPt!E@#Q z4fvD3dy@cYLK_oIAilFp?7ZrWWy4eP2k|}2{lRa5Bg*+ZYyC~*9zcNn)-(MJi~Ady zqN11pvVox$kP}ECoOmBzoLrdPL^QaMzdeRzDSSs4AP^YY96Yp#?fhfo{O)J^gVo)5 zJ)Ux@;q8Uh{d*dL5uA&|`x~46>$5S|PACp7PCo8u0s<^UTN~&<$NkI81U<*k%}MA< z>Bwk;%Domp4m>}x;oksU)iY+W12S`#w0}zxPAh@!*#g7_RbzNU-?i+HRaD}k`xWPML^$fX7sxqfqq zD_JpXT`EEWmlx5{>fQ^=EYCxnSFTKv@?KXO_^B4l_)5s3LF~V_XUuwFf6FT1nTsDt zVz#LUb&E`|H3EJRYZa`my7H@LG+C*>C_mxVp+ra*6<2t`zE~zw2*uUe{!ct+W5^<< zs07x%LVb+Lyb((RA1Q|%&JGsg*F>##uhzbS9dIoZGX2@j>V<2UTWJG+my$lN-!7+^ za&l78J4*9%L`8tt!;~t%5T8+u^-J0zZMi^USsN$-8$t=ZT&ycGvZX-R-y=y}x^e}1 z5bQ>rX8^huU7&NrGCClvI2du<;fyeE7i^;p15va{m<%XrchsY&&X5O0U%uDRLXYi9 zE-BLJL^3JI@j=OZ78eZr!0X(1a-pN6*`5F1SuMtVn zUPK~#nDA}wSq&+})joDjd%7<&CV|v_aD#EIVcko_UU?|&-^KlejHGaj+i z9*@#JxI0G7^-C~t$}`c-{dL{^modo=1j$G(1@t+(c0#Tl>7fpC+kG2?!37GR(!HG7az9orZ+FzEmD{ zeTRqP{#!*;0`0MABw6vc~j%E|0))5w&qc^vo6Vne$;p0l!U-L7l?T5miMOI5)!zMN#tqSegh_XN6L=c zH%ya4H0N**zjN%vN}WjoJ%tdmY?Ps^SyZvKsE!Q{Qrzk(yX{loqw)z{xp1@2fur1Q zFuW#LU)fM3JiE8UqfDG;a)M(gZ;--xgsAj2U_dUVb;GXx2qUa1_4Vh!ihFI#+fjy0 zi2dN#6u+~CrHM8?!s^E(&ZjX!df6$`-hJ=2>%Pj91R{4^lv^}cRv*rH}iq@bd0BzKWkkwB{P*z$`CNB+*b zNmw|6P1-^sB4)RH(i9yA?lcquee0=mt(H2%#T|k$JRDOk_I(r@@bQ=kjz_cUjpsGU zyfX!wA~_JdNh_*Hnx8BEfMFY>-E8qYyGjWsnIp=E>C`F--Z7-F00h z0~lk)Y}j^`^m_j?7W&{MB6o`(EXF|+#nd1T#&##!-bO!dq~Ap{3}5GKR=1Q~-^uCJ z2z+TBRFTs+7h7UI>v6gTyDIwS7Ro>44C}12lV{Ni`!SzUIoW~xAMj=BMGPMj-f9dC z`6N+DBC6^cr1vJdy@y{TQmnA9?sNOl5Dmtlir)y4sK-Djv9w-M4V4C71LE2P) zU@nz6T=&lN+Ic*>1luZx4y$#*u|RKOjq(_C|1h-(ZIS1Bp$Zv0xMXk>Lu2Qp=%^~% zU5x3#H?ys7j{zo4TEhSu{`CAy*pw~g2$_>cAJ*iyLQ}LK(f6Ssyl&5rAwB)t71WzA z+-FQ7^5x9^w*;{d!#{n9V@b*Gh@2Wof=r_`H>K@=MmD%iAQ2}OEw0^Alb_z;!$wUl zFR?GN_;AJE)cBd)oMe%r+K!MX)F|9dVD<)G7D8zW;(4x~q*ORjwqP6G0^)ec z*7a&-mXb_fuTQ8zW1qfUy2exrp?wsJV@%|T{K`Le_8+IXwiF&Sr4D`#@3gO?o5B>+ zyIFrAD&}U&U~sL;0oqp3YbLmtB#h`tk}T`IQj1KORi5b=2srbh&Y}r=OZ}s2e}irR z6gNE~^XG}wu>Ljic^7&v#eDWA6Voq<={0nVWjv46Q`d9M``|M6(edNBrp#~(`ZqT( zC`KH`3PMrjZ`Q@|@ecRya-jHGjk4G$PJpd))i(IHP3mb|-Ggh_<#-lJz^Rqs^zY5k zeOJ#2HzQ%tgn@$}iY*RHG;QLp)sV)X2JcQjkBC*&p!t)$vca~-gpY?Oze3fq3 zuW+2i53V4U+GDVo3wsY|L@r)7br#v|E6zVV42@Yhvl^-On=77rSVBBA*D-J35f5&H zoJ98}@RRv+l@2yj5b892W(;JT+JrtwIStObRHN|EVY9P&aTt`iyR^`t|C9KON1KODA)@(c4TIES^9S6uAC5L~2Tg^Ez zWwsiR0Kpa%)VANZ?(^5v(^*TfMxKV@hl(HHY#>ek$8ouQiWs;bVVJ{4OK^4~MPaPF zrxnqju*mA%$Jzy{zF#gx%zxombvj0ne9PMtb=^Emt}!eX3#WRdNEUz2hUC&G)pPlx zDAG2E0962`Zk1?;t6&wGGT3k-A@2jzr=A{alHR6feA!Fx5b<)+f9EW1IqyC2H-)(xnHd!H zc3hm~FGYU7??9s+fhuJ;9qT|rOtf!e-{oon7S&CQF4+ata-XFEc3W80B680iy~+Y! zL0?#)2SOuXT{wrhvkk!kBmWz(Hns)2)#;H3ROi zZ{xKg(|PZxWEz`>e$#Cw<%M+Ar{-ZeeDnPCpee3J4*ZG=Kl5nrE7M`5L>VpF8$8ZT7nz; zC+po4{it+dE^m{QYz_&QhKofLc}i9*T2wy2Rk(97#u+UC=F-V|g`r+TT}VxTcNlIz zmQhoGB|>y;#Ir?T_2Nz@QaY1{Pf1bP%D@_c*+C^Hl18o|N=a5}}B2C**l!V!)mk1!OgUm9t*!%7-cZ?>7o8bA&c8%>kd=;o&6^gc1}Kt#QuBwno( z3g-F7fB+>aM5Ol@NM4A16%gK%NoG$q0qWM5FmdVBCO%BdQWd$<5V&<$a6!YGY5Q99 z?b>^OH5KdtRM?%*1l~uLHpGZ};PkM4ZVEZkhN{K0!Un`kzDFSeTFOyq#*_4nyYe#^ zzFHk(oO9@_9}%}=C+x7M8%2kh8**n!;NKyU1<0Z@(Pk13rJY@ov0Jw3kRZL+is8i9 za0c1%By!R7*pZA|Vzn$PZxmz%tEmBd6Lxtsc7O?mmX!PB>j0EFulT6Mc?VBgOGY z7SXtrpsOdY+75>X?m)&79w)@Mh-fBLq$aXoJ^NXVJ5mu?bT$E0lYPqpDi4DuluEaI z7DdzdrJ9BGsYArc@vNrI?7bX}qdQ1p#4$q>`|{itn2E9yS<^*gL=Q^gd&-zI)C;b~_fpotxxkjkx8|vis_s9vI_K#I){cA8F9-ie*wMyn5M4 z9!emw$MYvDEzif6)Hk+ufY~){{G{b=1$p^N5##3W-A-#ZkUcJGt9f;+%Mrj)*FsW3 z!+B1|JN(#A=e|U{M1!a^;I$y95Li(>JCnmzirIdxZD6vl3;bpv0u=uI@H$I{Jn4X1 z!r5`pS><925)+9J@*khSf#dy@T-(nuE#{0@WM;{G9_< z8`?hVDZxS`Jmvul^pNGIZPhg=N=uy9Oj2uqAliAdPZ%v4{J0*QI2%XzS?l9D&ZvP@o9ss*%^b0o<}rcfe&jvh&3k)|vHKN65L9cZfT7`1yE-Ofd^L zFu#kLpSnpP^Z`Kb$-1^ z9?%3^m8M>sh3>GRTxSk4xWW^S^YKu?3ed?#PU2p}xtFEcA}c2H=KacVQ(|%l_P&6s zb`qT^cMjUbSeft2G!Wwa*)FzU9V8}O1Ps?ehdf3oOHl0pSgKxE z2TCga0pPSb#ObgqGtS|dJ?{ff>XO=$?LdZn<}%PM(}{Sa0%t(i6e-lQH;8w1c4?m#Z9q4p1P1io}nM+m% zJ-;n0P#l`NBj(oCwre%FcNHcQJQD=*Vtr7(wf?~s3Ai@g=j$GH02>Du)3$8 z3|0TQVX2iUrD-dJpb4MUFd2D>dtndri5f}FAzX0p_F*j`zq=T@M*X_Cvi(*e85>a+ z9JA2Hs|H?THPzv9?<qxYNb3d1SL)7YOfB(mOnD!@aY9Ep?(1CoO2U@Ld&n$Jj}>b zHD4eHB_3bB(~rgyO&`BSAQ-oD<5o(3Y|N`tm{A4t7`IiVA`-m*Q$_B=p_2>r+6=rH zmJXt3wK@uO?y;Qp8mUZuM0G|tR8kOlB!zG6Ticnrs1vS)%);s?aI_B05$%}g-y=^q zQ%V}F^lCegd6$CPj~BnYI11p>H2+($(9BGybBT@Xxpl@7l;*c!on6QS1?h>Bj#N_u zU^+)fs7{&7v$_#;d=d!1G>jOMoO~r8KAZfMPP8Zq3e9vpsm0+?Jg(MH5H6z*=1sT; z^)t`-zm)|_Oe^vp`Z4j*EuY%Eq+K1!8q~-XK35I%H*EjiFOtrYu}OYZ1%Z4nKk?pXn)&u=wA>kZmy2{U$|nr0v42c?o!-d zbMW#IjMa3yYx~n+PmYh?te);dqyzM*c3h`g^`q5QrzuoI3#B{DID@tTkw0nXSQ zwq!piPU9D0#C9pZNi%={vQrm4sC*hV4#!6JS2x%5#x3(1AEIX&6r1=h&9!;$BbZhO z=ubyl5mFS@Tw-DYu1B7zWA@XvZ&!QhGOrGRh)TIZm@?t!F45H34E3*%Oql}i0yE}5 zG;Mrd=ba$5+Rye!gcyN-PCA!xuKI8gaSI%u`Uig6 z$}PWt;;O7Ho73q}Tn?(Z$z2pjVYEEM??si#fDWKq5i{?}R9W;BS)h03j zaO{WSX<^?})GA==P92-N!2szk*4@Wk>h(+ktNdUjx69UmcpOnVrv#B6eNj1F2F~_Z zwRXy3c&g(T=>yHXHC5tGgM7HDwRRY@szI9(SOb=An8_xeaypLm(=UcTPADVp_H|2x z>ucRDC3=uF3f-gCo4xf&psO;UZn}9}@2EGeM!TNI21y1~!%!%+cX*4*Ci-mi74pfO zRLIcR-|2Ay<~U4o$|8?)H;ul3>Mpcj2l^wvbK;K)996%SJMkH0e9%e1=tQZfAy)j0 zn8C-(!LioiFTHcJ*0%`lc{Qb7sEsU5?3$Jj&oDwslCZpg*)&0BrGl!2v?&AH&U}Ht zwLU+LqirYR%(`8rU;+`Y;}-8AiC*ra@&!w)u2->cxfMJ>9|h}1aLyZO=-3rI0n}kx ze*$#LghQ@&=I`e;OcY)`NC!|No*b;kd(q=)5bs~v(tn+!@o0>NkzuJ@*s|WOCz}dd zLl5b=Sjl!K>#v1a^Ot1IalH5VeI+rEDDy@{JY z+hLC|uPu^FQ0|x&=K+C*CdQh**QlzHk6N#LG+I&;b|JF$c}3Y;{pt!KQv!5>DAXJgy6E-apRj8 z%JeLgc7XMg0ZVy9LNu)I&W$;d7FvZFjjkD)-9ThPO)Cw9_4kG=bqW>(tK-FSk{xfX z?l)VI>XKs0cU_T!@v+txQH#&B+snf81ve+^Yhu-Y-m;_R#+D>vg+UoTMR zgbiZlQF<(8B7S*B-?6azFaZ*yQ@qS9Gmkz@mq02u4eeIfF6T#scm12N%%C^s2nkZ z6z8mlmdpE-y6=f3bb)#x@IA!>qqvhf%Q(7vmQVkDh?U-2S1tuSrYNWoj9=_WG{)e zNj*!bG*|;U?rmnG?`C|xq6D7dAz5bY6x1@Fii^h+(4c8uO~=PNzED=-?Jg(ATL}za zlZp^%4$13*RY*T&yT4W#Md#S_Zzt!wN+58dBt97SW2!euGI!9r_((?F9tuhIcas=X zTGsmm80ilBiH5u$XNGMm#!j2J>R;J>zl1P7Ov0qbJioFuW8`O#r0!uv1!k<822P8OZaTC&IpNb#%*p?@?V zEsLY~#ngLoc4AlT?wa{5?lyO|L9toeKzPc`{?fbv13fs||H>AM z1c&Aai?P@vo@C~&YwZs3&rYyqrXG60$MAXh42vLEDHNrNh~%nNR*6#wZVmY9xGX&l zEti>_w4Oyr0(IjcH@G!D{s9P>_J|HeE|S;3_IbZ6SvlnG?XK|+910J?YEfB|#53iz zA729%NIh)M0579$tEk7wEVbVuN}m?zpVAOqx1em6({lBPzv z4_!MeNb(it)Lz~5dk5Y#nenp9e=e3nz{_@XyWAd#hh315V!Ry z*V#@O82el6(Eu?Gh`mTz$^5XHon}Km#i!NZ3PLkdhex0yDzEE+ERomyi#3IY@D%%% zyBqvS&pZ{5T>qFnS5b88vSbL#kNAdGu+K71D{b(yLmNsPVxlfE`Ms6PJli-;Bqr1p ziJ_?<^+VaO@cRx-?|v=gEo-=4Z6Cx9EEv@+vB;l z2#@rz5ih=u6W0@~336Yw{ml2VH#&>X;|)6*I3n3%#@>y#Zf8O7WLWdJkvj-PCUltkSTK)y_?C5y)Jw1U%y z0yKzNZ=Z>d5YP}Ht`KuX&~I;lHU|d$%g-LlwtCR*=FR>CAjXUQ+eq?I zE&t7|%P5Q_=)S`HAfCEW81W4>|5j{PW2Evqk&WV?Rs$wL1qoo?$zt+^d}Z=-`f9y+ ziM&KW#CD<`q`0^vp`L=ip})2Tdzi2t^m?nf$JIBpG66KXFP{H0Q*&&+PMNW+NP%C} zBP=(1!PNAZCD9N0$$9(DZ#Yq2#$G$2u;)^^CKh^WCm10cGAyZMYYe20Mq_5NnpID| zcgRUVz7O}ut0)RL-cT~AZmyAHWE|s;GFj0)m*qtJ5orE_6dTv3bm`CV<>J9b5fF}q zZ~MHdh`eN6EqGb5eQ`}M@1~(hU0hF{cD#Z9gx@cbB#^mhFYb43X<;QGVR|xYPwTJp zRA09g+P_(4CF^8nfrxRNZqJn4Lx<%aK>r(x&(!p3%8gO!%?=jRGuy^IV|S%;F})6} zuMvrLO3h1if%ZnC*fB}q57Myl&jw;sCb4j#d!1K~5=r4Bjy?HgrrX0kAZ-Zx!U9Cp z|Nb~>!#$q9I?$Y=E?>rpYL0yxR^P;>_SA(dn}e9t6knWN2Y*GdwGsS>QYaV}@$vZ&^PQ2f^oLl- zDU&OM2=hXQ7IHd<3^S298{+~}x7xgmGJQ%k8%X+VK^@_+Bt<$O-olM?;_dt}}i7(rjJlFP)ehy52<3J}Zhb;IF zF77#_az3Ux<{<<^i4NJw@H*S8`S#^Y1$eDa@-9nWa;oWuw4`7%FgJIM5~R1ZL^okh zxZ3eda=ps3k`!7=RCoK}BDR8Q#drO{rGAjb?f=o2A zi~?|OBu`qxKCb{g;Uv0pe5}URo9?P;7N*SpqpjCsEqSpakIurp1zly9`D2>~UQqK}WrZ>pjRcvlGh_Vgp(iI;gMYU$EZ z=df%n`P`o!Obn5ey7ivI0=^W@UPGFmr{2%^zd@le<)C87C5G5}z)j#7wrU*_e-!SHDLih6)NgTg(eb3vJ!pg+upgxAlA)QO@{L_Wm@K`xa^Bdi2 zcu#F*SO%GO)^J2ck|OnK4SA)e&i$CzZ$uCar60e_sZ!{V9vbf^jv2Js4KI7ODa{^? zpixm-t?gD@HXMAdR*f+3qS*6;$b#*}NsE6b4cYUoi@?ynI*bx#R9i2i$-{KhoCzZH zSOhO*mc7Iffvry-bt7w8mlk7XJ`sDVup}l~T83@(kHkR5hO& zrWwv+gOgs5$z<|zYH;Kbm8O@&MvCRI!}97kz`n*Av^L`^w*7L;trO@M&jyBDa;BcY z7i)yRw5MP{%O*)rs}s$*qRZgJYkKeSMr_aCk1{;NShi@PBO#Mw2c*pS21;{nbX6-c7%y7^+Un~?C+ms{k~z7PR>&p$4#mW`NAXBD zQZuCD#(@fKx(&o{2ub$1ZWxC% zi(s<~pK5k_I4veQx5Z#Rub?{87yv?=A~Vz*>apnf$`hQyeUCOhc8p=M2mJJP(;`uu z=CmG*V0x@;9?P3<;LBo*g>EI8g?{rjwC9x}VoLt(`=GSl5sX`x=>JwB5PS#&h?U74 z(^<05pxGNiQ$O11y>CD;+XUt2ttj2x$ku|$c`m&^ zD8rMykzJRZmi;GFoQ+D(V|P1DQG)+?n$PDDx|j8;rnLVxlDjx@fDbEpG{u>6LNXM| z0{d^_nWy;ZNRcxXN;tJ9T~n%1@h;s@&%-MMmA4>eWG>w%oq_gZ_fJG29PGl%vc7v2 z+ggKLVVlE`!%;jHnmXvx_0if_?rftLYMAO$o5B=zl?>x;Yt3NyrX3sloaH*34M2$I zaPw$$e#5j&&E^56|G^P+u8!DF+{5t@@TK&tkBDj&So#~@*#Va1@X7C zMkO7mV`igV>$0V-0V`)S$laG3PD3wSAUECXQevSSYh=@ z#xbjq7MkyXLjvG~qSJ|8^$glHbQs86)v0GoOQ$FSh|GR#nmgNR>#ub>Gfi_0`$vNU z`rpMow{>wk!Kh=ON|+>MTEEr8EgdqXnnlOT;3++bx3}Pu`txFCk^Y&v`1>~gquew=f?cUz(?MRtoA|Yd?@>GU==y^gY63d` zFhZ7i3fSzPv98q8@M^b#UeLwi@x1MIpJ88dNP}V>SB}4PC($HbP^Px;S{B6JR?Yw= ze6fjoZQ}qgH;RFR@nN3fkEm$#Re%uBnsh(S_>6C#zv%Dxa12KVz8-{kSK&ka2Nd)Z zP~&u>`oB0QJt1|{OlM`>dD|N{bl4C|JA%SpsTJjtoGz1%y;#xkeb}>TZQBWxy$*!hPelLg9g$?5t=s zUiFA!a#p7#`4*pSi7nP;_fUG(2(XcuqM-gFeq2MJDDz!PfL@wPo~(BAfZ4}OfoJ~d zcbj_7E(e+`7=_Q0a6hfEoDw7g3y_!?=j4=-L%*??L;*JkT7SIIg*dp2K(^`1p3RFu z(Ne=7o{qYr{~!*VNyFhiawtDglZT?gg%(BgNc3*@>5W!Iyj7wkHWMG=MxK{88GI&? z&rXDMZQ!pm07;hkspoq`9dmx?gCRw279z-M6Kja|gm&VPz;|PHdmg1QYg2;3+(pDuM$loGF>U%y{*g?IhVy5Xuf!b(Hi%I}IWUf|f zE2s8>ox4_+4(D$RzZYcsfQ12TCZbSXWqE<&;QLZ~9%k`(+h7mjD8($N1}TZGlcdj%fkfUv|g}Ek3uOo=o`2kr_#1sYf}7MPu&_(7H$Q+u9*wmh^YgKSQt~ zAREBAUNj3EXJ?LF;X?oOlDxEgXjyb@m5rPvSI&~ReEHX=;pU3HG!-fzBR#%a1#U?* z%ysYQ8>n?TJa6J8duf z`T2ds4CQ|@b`C+JFj13j+qP}nwr$(CZQHhO+csa@cK3Y$ZX#wTVrp?$RZ+XU$jChT zPd&8Z0)TBDo$Av$1E!~79)CRTYJ3;DNy!fZ1^HUk5=aeMC>|&tp8Ac$&nPL5O$9p> zYG?YOd32f#56<0|C~nzm7T%vN1t)2MNUQiqrCZs>CAn?^7+J9n1qQ{amL4dkdGuVP z(*BMArR6~pyov~2UyV09{*c0kv!Ko;=fd&~xx9t3@uMY38qCO`?WTS@Q?DWux_i^t zQ=Lj1>RN(mUDtcg0o!)it39YxW%-S(U6Yg8`leh-OsuH^Lb+nCTq&ZMipScCS@HeT9)_9BKZDDidtcKA#D3V$dG}=C8;#b52i>vmTBz34#mg(z;siwyJ#$D zmDnW@l<7$+)t{6o)78-7W%RY8l}ASc!5@hUTLi=Bn+x`m$BtMqN_x3!tnOu%9?gY> z_(4OI0TV0SLV&X=*bHw|>6gzdXk?J!lKOYNS)0}TxD_snky^e+B6@Ac!@80e=77sJ7DnM( z(6V*1@C#wX)h;U*M4a;Ih4FLSMq{D5N(UEk|uf|rU}_2S4b@nlUv#XI_#CboKk=H zjPuqLCC0v1}e(n6XW^HzKFsHwc)n)Q*?f%;&|r4>USV;6$JDhc}EBy!2H z@`3gF-m`tM{;6QJ+Mypc>p9cZ?}OI|aaPk_fvR>W7Z#U(qO4#zh-H_GU;Xp@GD}Uj zEFeMzBK19W9o!_Ew+ zrPm5rT>!w)=}i*Nru|Ji)B56cljXUXq#RS(44>0n)RvHWmNzh4&*ip~WNGSrl=0e* zU^8>vEzM`o_F%7dd4Fl(N^_~@C#!hl-b#|Rr}b9r;|Q@pCkjVwbR&6WKG+cfSp#oV zXD+_?s1wpZvr0C#+z|gA{>Hp8ev?QzBNAci=2NUF8lL8m3zL#|i<9fu!a!Z<-|s~4 z>MpJ~j+U7IX!>>K=e)PGbbOnyXd=1N?181S@t)8#>SUZ(B?4ZiAH**z&ACixUN<>) zNy@{kXA7B=^{#PPFw_W#SM)BES5_pf=~;U{gOiz^r|-rWmm#$3l6)Cag1 zY}Aj7YBY?9edWVQQh>$HtDB%+3~&{>*&Qt|Uo>P+A9WFEg(*-f*k`Y3ex0u>&0e{HZ??A|llXKmXXf=e7fF{5vS7=|&7Fme0sk zCm}Ym_-rSM7d<2PTEdS$9sIWzQU_oJh;8XRYUXb; zMYeE<0Xg?%)#l-TXxKr58Ath9ERs8obJwRG@s3|FS0%EadpV%*F5=GY5B4OXV6VQX=v6{FN3viBbO z=FI;U-ZbN`9fZX|^t>fw1#S9|I`HhsP~Z@FiBa*TYc3Ro6Kmhy^z~^m=3)6^mn1sb z)XJ_Bt9zh)DF2Q9fjUf}dAP3NRlYD#u=c@lCu7gU5SBO@Ko}4Js#ZN!u`i$^c1K|D zn|n_u3wI_wW2m3l;_-a_%iOPn=LwIgnZel3vRu5&)(ohOO#1bsc`tM=D^1chIjisq zFA^}sdmc3hq`b&JhktlmC_Geudc0jSmG*e%uHM?oy)SHP5ked3Ogwk8)l^0X2V;hr zdY)$gD+;7Zd9SC&Fr~=X=e|O%Zfy56R6W_BZ8P8Kg*m^46m2P~(+Q9uoSrp%+;YOQ ztvC3bYqOS>1vO!1uPFuO-^c&BpWw{4UN(<0-5=DHyhfAYQ}yA0V@b;0<_Bf!80&&I zrt}KH@JDoUCLmpqf(CSRxGB_?*}MxWwPX<*=8HwjLrv?v6z{}~*;4mIT%jmk(b6(n zn&X2r!6;eknjc8&N=`!a(3G{k_MV-Lku2Sl%O2cQXlk=QlQx*&SYw_PES(SGXjpK3yx zJePu86pnp;+TS5pqSCP@Zj+5<7^{sqM=-RGe_WqVEADVzbAa^h$=cCW&F!_`H8hJh z?nq)IP~o0aP$x0JUuC>at?Ur<{6{#i#8j%;s0}h5D3{S*cw%$w#IrQpJ!5B>IuSV1 zt75!LfJg4|cK?PFpow!6s(3@=>e8P_4!WN^M|C?(#X?oDU&NW48%?0vS}Q(r&Eb-cEYFdz~uM$;LY18^|zEY z{18@a0`p%611M6Jk2sTlBI#@;t6;U+6->9o$Da@5HUAV1v*!`7|}k#*h-I>8q9c->@~1i&qusp!PK*^6{_W~HS7jhNQHM2S~N+N zEfvga`a9}A_q6(b95A*O)Jf@_-O!7g=TH_{f~}?Te$Ln-)WJ0az-j|c9YZHH9*?6# z_ll~R7^-N^gY60(6lnS=#S8rKC3QICp(~vHGmT3x;vGXVA_Kbr?Ro zctM<2LPV-7hU%l23++#ImyU%z8K@bju5GYk6>!2%M4c^X1&uyVooLd5GR)1BrDH7R zq#ud4Cv(Dszgans$-I~f^*LmmLPH9HfIwI3`?mzKW~gzpxPqpUp8BX`a0Mh5h0ELgL0RI9kJ=!^pL`-_G#bKfR8|0JwcA?6X$J z^UK)#7Km(uXdWzO(zQAl2L~N*d_gp)wwfqu#L9)FJ9$C)g^+f}#$;fR(zCFi!qazo zZjW4};F_2aF)%cN0Q@)0LR3*O6d zlokhOsGN1qs1#>1r;*2|uiWBSddln&u>+pLWboVCq1UIU zs-&69D{h^0YjZpa5+Aehc!@rReo-8TjItmJ{q8F<61E9;k&10&TO`}sE1*_g|11j) zfmv%5nO^}yBhqyngwnsg%2}b_X9|O9-zpRbW`2-TAK8`V2W=zwPlE!I!kxSx@4Bh( zzI(6kwl#tUPo=N6ZPOu=2|;Hw&-$j&^$epHzgLw?ca*(gl6GtLzv^@eF`I1JTeoND zfAt4vwa+3Cv$Gy_!&JByG><+%={&|K-;T1|*4M|Z^{#R`kBetdFZ`R{6SyOGDvmML!mExbUH%U0oHC^<+jm+_mb$Lj5~@4S4oH_cha zVkm{ICH^Bot_LyVMN92uEpdcl{{?z(G%oH6_xm=-$z@h`EL@tRMOWej{fkiNZ539X z8oGlD(AR$*`3_jk8jMiaF0uitAAL+>=ZR9Q!2x1@;78idp<~Zcg#)-gM`)E;P3Q<<0sx6Z;B6&dzS?hX@ zF&0-ty}Pk!DWVDPDN#gp@Dj%aG}K&1*Uz{Q1>xt{51k>woqkWZqxoPQcCK&zF5VNT z4ewCo)==5(SHx)o%Ke#ls83X^H-y0!L5bh)RAHF>jCsY?jjqv4N2exaDDA&ENed!B=h?bMQN zme06EJXw(faGyH8hq}>rjfavNT2{e+7;_F72pJY~)dUjY`ivsi31JJYK!zd)9Cbuo5M<5a#BlIb8*`Xw0KR zLvq^=LTCl7jYb!)Gnr4xp-xkkq2>T$bcckOqUZ_8ba>%*lk%Q*_;JS0baTG{U);>y zd4)KJD@Xkye$T|>XnwfQOAR&(;-nw$XvkBzVWNF6QyfOm@#UxieWs$T5BX#+mB|J! zDz}T6M(kqXY2RzBg!}9=u2$> zar2S!gqp&}qDhI4!3_HJcPwZu)pam)jdvHxLv+Q!8h8qAPxvfDW7J{?Yp<)}ZO3_Z6$v0+y(J zylNlfe5;aruEw&j=B=OvTst?qqCO%+^hGDF80hKgV0q1$zJ|hBQT#dDJ$aNd(uddq z8*0`g41LoAe&lkuY=WEv7EBptOKYo&IdY}b50Ar;;80Kg?fgs$e z1Q-(ZadxJn32Bv7@P=&j92M`RUy_WyuxxYnXw`AYyvuA96|rZ}5#x-k}^@$A}8%(u1Z$ z`YvIkB+VJwdvga_S!l{pW1B5*O|>UCHqQHkCM&7jl@-A~{J}>*RztN0QWkxkoGu}e z-}r5Ui}FY!!^=~f33@Tj*`hXuU&|N$J(wWfPZnQU+CEnW`osNky}K?~kgE9OY@TIx z7bMktx-+3Yl|4Os0~`N-;i86YK}g5QL(b*lpFf}k6#FZx8T|fa2i9##g8Pp14(yyC z%5j*;JQN6@nT(gz$1$m4)3Lda+_&@WOS#Q38}Lh7@`||VKUlF2Xrv3KIc|k}^lU&z zgrSHX4Djx+_j*qMV|n8q{8p-)>E8ZXA(=w^DhXXHb-5gC56;o`5V4D(u=8r| zp3#aC#*OtJbiZUs`7=NpdripL5Rfu50sOS5Y@`%jiFk#oXrt!qyEgkG2^6HL73Qef z%(cz0_1;5#)MbFmv+cOTlr0Stl$fFXDLC}&Y~@>kleAUiT7cn#WVA?!$Y;y2L*X(4 zOb{@EyiBiEW<%$8VFqwNaQU_m(9m(6RlIGYmMS)pgZ7SjW5URmHYf#E75>&tK^@}| z$K(5!9;ReQBIn+!au&B4vSNDnGt7Zx{0^tW1c_@3j|4ngoKub;*s@df5%7tx?h_6q zc;xKJ7lq?zd85^~i<>KFSy-p`ozzy(6QTmr-UUTYsOdxEbtSKd0`zWUgjmY@Bo>6f z=$Z67nRBQW)Lk4YiM0E~G~N2D-}7qc(@?ZlN9kl4DG$6yREb5e(Q_B=)6aa0<>1)O z#-H61xR4?%t+;U-?-#`WIoQ8(f(f46`ya|c<+L|r1b1#PDax9w;LQ;XXoyud%yCP% zhO0lR&4+9m#4n-KO7N{sAjiO6&)oVYkeF+dFx>5qZA%VYa;by(TIcW8`0!N}b&5ga za6)dX0(B@29{MbeR=9EBG?@qUFSLKz{oYfzm_IN!-7K7w1v1;HjRwOasW+ky`QQoMT?CgDT78e^ELUmfd2xkYIt?QmhuQ{nD| z+wt14(_cAJsTo?Jj;CHqgWUQ5f~pk|twDyg{q*Zs*{wVYs zL|k42Vuywq7el&oh`O&SWBeM(iMC=R1x}>JpdyqF#~0oFe|MIM}}%Nn);o~rW< zh+#c=BLCouCIkxJ;F+EmxG!cuajTUw>{3JB1$vbLkbmer3kRGpfv~#IB}7O zY!`?H>kDY9IhQtp1^p3NC%G$cP<)W+?;pRtCZIEUp0rj-eS1f-rw3Y^RPqu5f&N%c1L?S@ORq$NV!`*x}@&?$M+!jh)eZ6)pM-&T& zs{D7!3D*P4J%Bq`_Uab5;WgA1uq^_|tZWSP<`V0>9R1w@ZfDEvoF$*N;i^i)O6WGP zC14g6PM@Ya=Un%AO~ah_#US;KGodKM{T8|LfhN6DAr^3Pp1-gG&*zx}((w)KN^EbN@_Xj2e<(X`t@GBXC5<{}cbG z%v)x&nP>uxZf?y}SO;U!bEY#YQYMZQkWv*B&kETzMyq4}ohuNO&Ok^qSYAvUuj%$S zmDdEY=QE*Bz+e_XqSnkT?{<<1>anv@<2SgW?Q0o~#%`Uu+*J~#3)^Z$i8-TGf)4g3 z0VuPSa)FQe&gTQKA%q3PAS@ew7(i=?6hK_g{vdAfOcg{d?RjI6!)6L@_9y|EIs z_tfvOS8bgWflx|4`e80hVZQ*hU)s&ctEB{=Q(eeodtN3zgkPjOW`ao<9Sfo^XPbBS z$~n>GgR>8Us9&P#;RpXO1t;f73RfmXVy{w1g>IZLbddpt$Zl4#LMzx z)T^CY_~pj}rw)JbS!WNz1X-Jaw%;=t91)~%uqo4Ku*0rb!Go-hN0M92R|bP_N4Q-T zYZfuv#P#lh52@m&_@HJ;xDlKwplTF29AFQ~h*xR(TR4l~<|;{qs&a0QEzTf$=l%_d zMlSa2VcpAE-X-17W^L!+qcvoIP%fdx?2tFO4;G&ZFs#6ndX0a~DvaTzvB>v`0BH3a zJI4qNF2*jQnI=zx3zHKLG_Blckrkcl)o>F)}YIf{qz)i3W$zQ~zNUU>`r^ zr$w=Fo(_q(Mw|THHO`%d346yF8)GdXkk~7$pc5vP&+6X_Bc>1fk=%aia2SeVhyivk zpBfC~Fn7Wq5Ehei-8^WUq*8bRR6$S;hK{0ya6cP*a<4zQGm74E!*PcpMID)>K+n~V)&cPq~K6-~=wMMQ=IFI^B8jCiP2nmde52Ls6 zTrlqFfWJTlw#sCOA!)>P+an%~42&Q;n+4?!#HcFL@62$e9gm3LsVCfVIsp&m#j5Zx zmv4=C6?J5WyS-$GcR2#z$-Z&MS_6Tf>;U;fAu$S%>i?q3aA=8f!%3oQ6qgp4*c=0r zWvY}k6@B!tU95?DiwRn7yvbY&SRSHX@yH&G=9OFQWk+;A3NgRQ&R@gf_O%CB{T;TC6w~J)QeM|gVF_tgTU+a{kg=mVY#d{Je7`qByZ86 z6sor>QV~zt@yt{7dZ!=586z>F^w@=2!{hq7}_!$8yEmgQ2RZJtfc5jC{({X&wyksC5B#s{@1?Cr=y@fARwxGw^0ol2`ja@Ijnr;l^U(L)v(TvxW zXx;T|S6So&P?T{la~R)7I{?#gShh7&L6?iK(}FWWE}N20{e@eeyXJat-yIG2B(05Q zKeH_U9-(?!Jl5t}?m9$uh;iYAw?cBqwEH*-t}_xcfHu5-(O*dVZ`Xd=xJS?2*MV2v zU{?DZ1f$^O)El`|?Z_~(k={@2fnU)SWY(#IUI{J9p*tD zn3jQ8%w{cob|U-XtT1CoH98-!KtYP$0jRN&k4n;9^PkDNR?ufIUO8?kq(?Ko-ka-+ zz13l~;7*5cRPJTaUL0_Z0m1+R#gFWe`2fkm50z+9Vj}g%Dt^pz z&4Dh33VXH$ym?&T;)dZWd)}*W-KpQJ0YZ!$vSrp-23}6;Ny*^plEwpkT<&hDk*O$A z4@4!?nUNjU_vvBK65z@6_}n$|_)|MkqdXzQ9GAZlKq$cfT2JKSOpA^CG2Hx^^mT+-4pbLf>v=LnHmLHzWw&y8eW9&r~A+ME~K7xne4h zIO*(ULRt;XCAFMY8=D+`y7*mGBpQD3qsMxYRMWWa#b~7xL*4qgmUa!v*(yl!Y1eKi z{94|b%dJ%jTRjQLb)b*d#!3T%**)Ww(XXG>dn#Tc+7U$xLuvsj2;v2r$Y*MS*{AFao2!1tHc} zVO6He#0ZwFn>T7vLxF9xSM;$#uo_LpVIp9Hlt&k}g+AZY5(G-EbsvwgNiUAc=isA{ zyjP-Z1V6<+BjeQ)v7?`wv$fN8Izw-Kn%16mknFU%^3H~`e9<&PMVZ=5LZNni)!_xQ z^oWi+e*u|Ajqa)Qs&p>4a)SZ-lnMOdlp~N1zOW+`*+MB3L)3$uV~PKVBK+wB+}*tc zBs~_>m0X3#`yI`EDadHIy=WAr(vnYtNkRQy*;*g{SPOhm`z4N!+%<|!Ne=-F=_%yh zxBlMah;jEG@R4A8cLQC3UBmRL$0@iAe;}Z;@;%PzN(097SKt6s?_)( z8uMafAsgUPF8ITq?s*=;{sfo+yhP z9#(Lo1)JzgvV|E{L``Mf1^c&tKyFl~Hc8C--PsAsvQ_}}KvC>mv5ljeO&4pEXw=Kj zjN1YheUZx`AJk~gntooV+OzyRB)K3-3;ivyv2UF#9hBL`nIKx-8!?imPu?F)KKZNK z8n&Qb+UB25>C$?hKw$xEa%nX84CUo@C0{Id9SR-;D29~a1%Y^eE#SH&*I+^C1~fxT zT{c^p_dyImb5}T>wT)KK#EWJ$H-%$no~HC1nKSrl8R_O?aN&5XR6WD&l6%RwZdEVRiy6W5=1}F zX1U-VJhf{JV)9j)yNpVcedFj!?33_jBm zI*}a&;IL<8LNf}r0@aWN6>0@ufB2jJ%IDlTdrTiO*={9s=DLvnf?n*FGSWTRU~Zgs z;2x=@=U6PvKl6cV|1q<%?CgKf{pqsXI;5OCu#z=-SmsSaKZ;INHG-|xkE2ZM<}UQE zW(^(koFC*DdfF@%atW`Ds=dy}FsC7!dB4Sd4QZwYuOt~fHEFf^?x=#%ALhYhF7_y+xDJAQf@(1DG<$Esp-ehxDYt z7lv#tn@ejJvQ~L(3Y5G_lzup(G7d+|iOE*OrR=xFdupO_Mddt!MN>(X?bnN&@fJ3i znB9e9K!El**#wGwD3{BRA<%zj{O_Npw^`r{D7DU9{QfbCiy-g8?p}-7@4#Ucrs_ik z$rvKnh!`t?`t%X{N?Ma)d2jFRdiYq@fP^ToGsh+`2a8V_t~PA2p-zv0#|+^!W!n9F zBJEG#p%#Z+MUx(D`RteuM0;m@g4|Cd&!TuPHo(+vPd3Q98S5+r#c zP77K(ub?QABwSx=7gr>9j1_DVaujW;YCI3TSU z0C0Mwf^!>touPRqf@?DaKm#KqW7BYQQZz4UtWFK?&1j@f03W!pf>dB}{gH41YhYk> z98Llz349eegD=M%7=b2$cVSg~aspuju88>ql7geFdnOyB!?!Ew4Nc`u_#_rx8C;y+ zoElnsk%v|D`s7djHHl^bFo}`1#o^JhsR0;!H3Pr|^8^bB3LXz%gc3M0fM)>I!rIOP z#+d=M5}*y}s_KZM5>OIV)Rj!lIxKFe$}TPre)@AnR8>_@F9D8#q^^hr4#08&kaSgT z<-fBM_=5jMa{&Nth5yUv!w>XdIvHIJS#3!j$;ABI0S>@CfOBzc{9JzZ?@;t%0REP} zAzt0t9K5Xu1E7*tR~N?<6N86`2ZLruR|f+(hXxbh?tib;)anG_!O5`&z*jdfpuMjj z?AXlqjagnrdanoaPXi$^H-&0(2mF;xa(GW)zBNw4m&A8l|3mn~2)WA7YWsr%?*It= zTOC{4IH}j+Bqikx$ZbsRrC!+E*n1{kSlw8i1vPk1zc~iC5c{AH5(ucsZhp$gd;D|b z{41aBuZ-sK*~!eSjn8i!|EtSr?9GmT<2QTSZ@0ZPxHCFAx_qpU5(wx_rM-vuPUp8a zGyO}K)zc8v64FtP)w>n;czu3^!(VPRcX)C6q5hQTHDu((13*Ux56H}h9e5^Ga`a|a z1n2ICU!!aAA0sz=VI)@vH>V%_Q*4dx4G!;}|I?bSy`G)`zzv&B*-A;1@eMJ3c>vU|?@^1^m>^1m>q};G-M6 z6$q$q#$K2|%a8jLYnaIaV5U}HhfnmkLD}f9lHShd2I%-R{WW;BU;m@!U)9e}`7{pC z;@sHw0-OmrZHR$_tHU=c`1~`E|Bh)}k6X=Wh8CEg znf&GNgQv~f+5pzo0l>lW?+n0ae#h^_5B?iQ8>k?pEGw#%_~Li^pqAL#TN#|0+5;LG znge8Tb7OlAGW}ctfac~N2>(%|m!BLT?K)%giHgPUXcM?(V69iaFg zf1w`%JV5ctegwi0^$YI!A5oU{PmBgYtg?SmyyV*-0UbF&@xuNYvGS4K2$Z4fH?{*X zX6sLI2VXVjPfQ0uEct)#UHBt>h0m`Hz8W|EwJ|XJvao)Ee;Yu5gMSkk{=&Zx4jthq zzz$e^z`rvhSNjjZYq9+W;LBY9hRxvn{s{(u6xR>?6Kspu1pn}RF!;fJ>Q?2D$}(b2($@ch^$ zV1fCEhO5~t{N#VZJ*Uqf^}7Ec!r%RInejjO_TOhcpa6k+0?HdFqTtGV3Rf}php4|t zq|B)Z|8`)=S2KXLm?RITH<@!iHjF~GNdyL$3hV zD_NJ>&!mL5#r~Z9JRppQjzhCxtOPs%O9!IvmaKp`g^FtBZ=OAAA#X(nS{aWQ>E!mt zJJu-MQ>9H;$VY z;Cg)_utwHNCQC%3UDq*z`PB7!y2%@YE<$*!A7!|@`Wo{1McqdzE>nz@L6-8fB9^T+ z@T^pxrVV(zP?zqI3(ScHGN+|6Kk|!%*)IU+3cBS^Q1b>4386PJB+i0i(qK#yH}x3( z@zH!5m-=bc1BTL*_^2FP0!!^6-jlv6-0I%rnQ1_;I2^^^B<1|%TJn5{ET9V=dLvrd zh?Rae>&}v&n)BiZq+z7Lg|$}_*4fsYLK}X42zN!W!NjebH;NT z!o7cvd!+I>BDUHe^}Scw7(n)Q1R#0Rj+=;s;Fp+_Dfe2}iA$ptkT)@XlD~U;(XHe) z1MNvWX}lE@Zwmd&Fs8G%#dO}?2a0*_Qx`N)H)j$e09r?jum5fr4mPy|D0@V#eq47U zic+eUz_Y}C91lh3-Rnc+a}G|)kHpgQwLGX$+uauE5~075gPzBes+;KjD?Ul)KClB9 z6xdkBCObKMkzO-L54UnX-#hL&h<4sTBCT|u)PZy?%0wt_p#wh4uslgW*m+y%TQ}rg z9QRH$Yi(z;=(}@PzjVwzAbtfEB&ohg>~T6#7BTa5y;hADZL8Ln#o(uo5Vc%CBg_SY zD`~OVI03l0WLi)v5DgoVXb6XrgSj6oUK4h3TG=mpWyQPf$c^rtZc&8BrIWTByM@DH zn;dpMTc!=QWLp|b1XUJk-lVoPohp2}Xf-({OIi~}C!`qK{6}DblA?bs{5|QoMbsz# zgGrq=_ph}8f~vUG;|5bYEx}?|@hm8)I*;=-qSOztd668}ho$N+v~uLL zY7ux|Nqz5+by~(mONsZ_Xx#_&5N4H~-}%KM#eMwMuF$Zw!!7+#1Mw1&#%HBdw0Kfw z6IUxF$)Lsg+4*M1dlHhx9oS=;#~ly9@yX{;*6{-iqn|Yh7JOJ%3KJh6k6b&@DSVI} zZ7y+1f=;LACad-;PK)iHtFWma5f&+qLju5HrbT(Oz8j>Ka1ycS?OEtwCj|KPMIo!)VveOa+5oQ>=NLn_F$dkr{Nv4vGUM4q-?o4ZbO09-67c#?A9r64#v z3E^;;OVs;-QpV(YtTp!Zl*VvpEO5ELGua~f!kViwx|6YuP`3VM^_QBp7O${Qj7IaM z_p#^U8|`EQf-(HQ@}sGvY~U%As@ko`UMHWW?#c@%r31vEIxr|_uPqc-u;Vd_H2S`+ ztmoTND{tHfaU!TlMi{hlR{o*Czh1(DKv{Z?l#MYBDWWW@#nxLpZUpyD!Qa}=VW!dX zd&}fPL+Z$to%RmjeM#MVpA)O!x>n3b5)ovf7twVGmYE>80 z(rdA6zA}-XukDXEIV*_83%OiB`SgJHV+4n_U(P_HCWLDyq;JI~u@H!qvuL;Z*INXK zhBUF`flU0i^$ger@_3D=HT%aguvFy-ipO6{T=)>1DobXW>J{bkG%^$A_0a|(4EEkl zUEmt4YLN3O5dmi=BGig#=`uU6tJe-Ywt712*cGmiX}&_k$|VyR(>`ayB+FRk zC+p{YI%APhyo}~o6WGic;- z+Y8DL1maEV$?QU%%pH(1$A2ZD$6wQVP;DqFB->-o^;;UE(>$lCvsOzvyq(~vzHd1A zqiTJd0d6TqWRTh>^fslU^z?p7Xdna5nRniz~*2*1)A`*2{VDGa*IPUsboPiV5Y zGr61c+;qKsG!AcI`IAb#%pSCYGpGT?s6A<9HeHqyrh#{MQQ1*dtM1k>InE*x(%(^t zSW_Afx>psIkHAIxCxa_Duq zI?U51cJ3T@uD1dJVi_I6tuwKYS$-kA+PWre}$ zOhv^W*vO&)M~ZWR2Faw5v8M>2-s9vcrO|3Z*Z>*W|*9kukn+NOh3BE|6Fff{dz@D2+&#hXt~{=kwqwh#&B zw-(mDuiTUmU*P;USI#V7dA7u^8fQXT{4*;yJFy%;1r`{kDG@Cfw#Uj@eTubIkB>f1 zx)qmh66p;)S;~aSri%98YKS`t_zS#!o$~T+Vq7s-&BmxJ`0;Y)Fj8nAaB$rg5&CBm z`=wUw*X`iUobF4%x~(9-XO|^D$v|@!o2sLY1}*>Tg_Y9nL|+dE578Yw8$_LRgfXG{ zU7TK2@bw=}79((my}7;h_#PwJDt^;G*=L|>ji=-rvfI$O1TX(OmM&&Xa3B%Uk*}_` zz0_1wU>wx#rX(z!u6LKt#7(On@Mo+JkZZ(hBXv}D6VsAYyca6zr5?_iZyjZRpDKua z1npLWT66O{M_k)YD0l%3o&z}PHjho{$cG5}&a<*0&Jx&ylA}^AkE#o>U8L3|8xU3O z6ifxtq0zr~*y~jO&g2&FN6gEaM7C}{?xgZ0Yku8HBAt^6o9yE3u}^dG9ZBMui57J; zrxr@fm_-;`6u}(Ox$7F2PYH|Fwd-xdj}@J6j7S%sy?4`A zWpR38lm)fJoYe#B0e;ZVT1ZkOdb3782Q$by{+_@?28UL=u_Sfm#Xso&pdNY<=M>)4 ztwcqq1dW;ea$Y-hxtdRRc@Th|l6IuJ?!Q50ZnRb0%N-Hiar4u=e1$QgCpxoa6W$>5 zEMQrD{Y#?1Yjco_*cO^CcPkKfD{TO>Q|6t#Ux}`@#Zi2ReZ_tzX;W5uzc_HdUj3Ts zvFJ(yu#12L6Ox5&O)lU!6gen?M8=X#Tv1D>`Y5{=3u)G>>IKQ=={mp!Q!h<#$WZ9t z^APS8&gPwq_6lBQLmnUISH`Mp)UPK!v_)b8eK|S}%C>Bvs=VdY`QCx!Gpp@CCFws} zU-@G*f=Wau>bogDv~cBEqgp{}E|{+LdCZZ@&t5RkGi-RzaVNtY6aCjEhT-hd4tW}7 zX6bYELkWMaQ7zer&$kvOdUv`S3}hq*2ZP$&d(3DpnX2RjgX~Bx0=(QDhU&1#(|+3Z zGE;#4n_$91hh|H{@i;RvtOo3af@3H;S<@Rp1)#LYxB>5GL^vvwHX}^aM_$WCEoGr) zqcF^~7by*ZyB^wI`)~n9O=|QDQrt)ue_PY{)j87cTyoETKsP+1U7y%S$;0v?fSK1% zG?$=V9{w&6z@z$FHz-@=>!h0g^u6}cJv~>qrp9Xq{72(c=&BSZkxV{ZrVj)8p@mT{ zf`cyfSSH;^QfBT9V@I+46A=cGEQ_Hm)?6+HEXTZ>YTvs%QtW^Q2Z)dmtVr_%t#mG6 z4p3$>!Do8U2SuV&MQ@~DqGIofi+Zx$^lzP#6b?$J#+*Bfe5N$2t?LBOj-kpGnL2P) z2X}O7J|whok<#qO42i!_RbE$V9?RIbTw)e^XITBl4r;3;l2t%pY&U!zL>Rc9uJAH= zNduFl(*jkQc#A=z39aF#F~alf7ki7QF#Hz00e4+mm+;k#9cy^^gDCW)O;&hFsqn1Z z3u!;CXEtbajd9qt=;TBX_n}-RG5s1d7+uyv`mdyiUc=QuV_6 zFuaci;gb)~v3GEZf#v-BQo z^U=?+dPV%C<^js?s38seF+DD%dzgCh0~SqThcfv(NX0xei~UNP06y#>+TY9DA#|Go zPGQx7^(;%ruXQI(uX=FC+iWMJo_xVP9Eradm(LkO<$gxnaX#_py{g9V?BaeWZ8NCU zvJL&`eeC^vtP;r}7PTPU5Dl!YRw1&})j%Vk`HFol5$EqputP?H=Si+1#I_;%>nJNX<;}-%FEsc2GNqW*9=y3fw`PHnS@VV!yr(0S?x)u$ zqPPkt3Vx_7CD_DexVRz?Mve-SM58Syt)80TSmVF)9v+FkFjWmKZ*isd_ak; zA8GvnQ8&m1OEO)V*7yu;W3L;X{L&8&T;PgpQC6l6GtnDrn(Q$q9Hjm!r))k$1dvA>#_WRFo0uT_X*v7cR*si)lzWUij7X4Dq zuw*;UB90>hrkjufem473^G`MAWx6q#+{CLNa`cG~I`mLo(adxydJ<4Frr!{acI;6L zY&2Ix$76n~4z$NnuW^hnsQcmfbNzb6W7{N}3(~A#Lbh5gM%k&d)??l^2kE>xdqDOaNR84ZuQJ!?w32+3XWXLDC~@uoEZt^Que0=sT32FpGb806l z!C88}KqLB`Qp+;*Ipan+s4wbtRm4F)4IZrtzqQ$Gu>OP(Z$^XjQuC>_qh?D|Qfuyj z2$W+E71@dfI^c#A0iWCn-Sbsubc1)NI7C=m41Vt!L`SqR95<2m)VD;Mx@A@EMa`+vZ{k8 z>p}G(^4-DTtaVGrUisXKqcjadvcDgHQ3a>UKPKkIe`C;EOSU^TKK84zwpZhGfsPtY zYU$(328T`WLN9TpLds|k-%#9*Yt1q@i+K=SA&j12k8E`>ypAiOmC@lWf+JGDY4V5M|P?}|~($4$0v}YNoXkwUIsX8$ z(QAtJHdWBdJ;4T@I=CgMiiLpE{uXZ}ufT-8>z<1HBVj9FqQq36W}6!#<;DNXV0S1% zLUJq=q^Y272F^2Zf2F}Rx476&yXJyYohj%k_Y`z5M-C$HNHH{EGk2-TV4Hy6$1ct7 zM%`pU`}?M~xiZg4J4Hv$)ceLBT~z3-Jk`p$_g8Xu-bJsp%T#ejp@Ps@3W<{{${Ih8 z=wKuGMcZps$H+IH)&A_q#vnm~Af|0@<2UUPL*iA`cH)SzisL*nt5tuUB;gu5je)e; z1~uBXJKX9q_>N&3s-x`v>XY|p9un)FE#=F11yn2fJ=!!j{H5o2eD=TsIP1FCT(}M7 zheJ4ayickCFPW$VIkszB2?GFHG86y!7S=^2dE`vE3RS{>wVU?R)DW5@v9pYQ2nJqH zO4o#SF~HzG;tg?NAE#?5wA|bxZpi#H}@L4ustkO?br2yf&7VhhW5{S zO+$y5;l_Z}bbV}@$H>fu@vbXMYeiRshF5bEq#(s#{FgkRv{D2kH3`&X+zf^K7Jlu= zd1eLJA*qwv*tAL@LY7*lam|*6bbA+6#>GsRPY!N|)AqGrY_P7+Y(HzN*ydqKNX7U= zRB6ZGIHS>7sYJM3#FeR#0Lx-EexFvz7^W~z(JkA4274dH^mXKS?@pbfJG#X50f{9} zU_`N;Dy;gf(98l94v=1GL@eF%sF*lM_yLJoUKd~c<1^f4&{4XUEI8G+#Yt+l)fhu6 zMS2VD$h;4Pi__&%PC7G5k*;|It@G%D+J(|gE)f%IqWRi|0fyisWJ0Mr_W7crkAhhei;=bem8oVI zB~)s36?%|TW`Qhs!jezF|BaLRdSk!V*G1vupZ*j0<_pdUzfOBM!h9h7KIJqI3^S$d zl!--rks;a$whPE&u@=VAdvVfGku)B4G%0Nt)Kjt4tOK#|o__&~@P;YJoBQ=(L3=6cH1 zKppNpb)7$_5ffX$b+r^pTVk2Q9%}q92vhOsnx!x2t5DOI-*bv4Z>xtqSV7rnt3^@$ zBAQJGbSR`Ed%On$IKdTo@>P>)-JddAo&r;;xrX_8pNU{k%k+>hFF))7}FInC{^3ZK( z81o$?NVXlZZ(`Xotk+fDZ0j0v@9B2!7+FhR7G5(QA%y^2VTJE7Sw9p$hVT)j0dMR9 z$rv_A6U>#|jYd>LKkT=FihM$X)oXI`W?N$DuC5-zzD+a;m;WJGXmUh!13_)Jn{UKz z@bWvOPR70Gn;T^AHU-n@5 zlKTBYOj)fG?y>x$mrP5mW072DDaWRF0#qp%(>Ky>G&Di2z(UHQ#He3_bDH~zB`?X? zmD%M-G<=g`%9FTXk0xGRHVbp;)PF&Ke18jHM)urIDSN;XGz-C6g9e-*gHG=DgzjD* zjB3*8g#Sv(uk5OH`5_S%MB+7O*jjpjAhf2s3XvKBiC0hze|6qO0^%re8!Mul?MGJH z2wu{H+sb8kLV%%4ohTZq*VA(ulQ2gepyP8?zesMyXiQ>WA#lq+0!V|e?(=>XE`kFu zZ=iLRIi^YKTgu`UUGdJsJHN4w{_1^1ruS(pteLKkx0l-EYKV5L7Q?5|1lqQap-2|q z?O=DwA5nBJC0sa9Zl@Arw(UZlN%1#?u!&6e&fEp}!Y#-5p2CoJ+rae!^%9fv zW%^&2?5*(VK7V6Wm-|-E%*rGo+E(USn9N`ON1*aTovj@dRodtu#t6N-|B#-6m{u z@*U7H#?@mWy_hNd%SYTmCdCJAO(%90)U=$m&D>eyh0FvQLb9;;HkRr8?@;iEQ|D>g zm&G2Hr>fnnE6PF&N70eoc5Z+rGprX-fl2K`clYs{0$mm4GAaKo&`rML!0~Oo7g#S97FZ<>b!tU~w( znDi9VjDE?qFkz1RQziVPc%1Pb-=v?iPxTCr1gNWS!=harV*GuZvo+8@45Hv0yEWZ} z_3VRmkUlUY=ZZ}Y9B+2m$VC!U^I|bKsxQl>m#I%?2?f0wSZr=nG3lnByq9huTZrhU%V=$?(pTToy{Yjr>9a^aGKy*986+Gx ziDX@`w9EE39g3*>E|>J$efC7{P&iD^%{>bqij~4tmO&d4!&ElX%`N+tfS-(N8PB$T zF*r`jWW?mE$k+8U+{qeghBxWLJ_pfzX}orV3?LSwR*N48yXum>l9CiEnzOvEth(Nb zC0GYH2%O%a66I{=oJ_yzU&CT2cNzQSA*JZ=N(<6D#z^!tHepIDo)9|-ZX5*nIRVme z{NdbYXPYXqQ-N6q@mJ?m{ndbiqc$!s`mznZ5V4)i&}G)?r{p9=4AmT{=`XQ?Ka(lj z@Q$smj61a`1cop?MBmK{+?ewFCik`3!CmQ@d%xzb3AaZ&dk2Q6lNtL#Nqd)B*g^-# z`QD~XO)o3=*v%1~^+7*$w};-sq|j)1HB6X+jtm^;NQqu8ic)P})eMGn>4Ha!Lf4LL zQ@+@!i8}AKN4+VS{?r)Bs~Ty&m(Bdi6=db7R(;Lr;$>_Og>#^-$RA!%1JRW# zme4+6jKeiq@AGCPpS;8X^n#@)NwrY-Z}apvKmVxu!A7&2A-QvMX1Jt82Mx+x|0xxhyd6JiED$Cb~#FsUF@M5o?I zZ0}QS@lr#Fd-UJ$Y%zbfkGuwxi(Cf@-uv=>m?WaVGc^=M2j$DcqvfIDZ`)X#!gLp5 z-ahvDfhUqp!(qhh6C)UGjq!9r>=U0>?9XhV6$r}4Ft@;3ux#O91o$_{_jZ(hdBCO) zTGkc8QsKY;iJ~)nMB1T%F^4Jz>gUD)@3GRTAoJBey$>g%SuzbE-)DfljYo)5Xww>jPT{9Uchw*IR0 z$Jn<=0``o#mR=08>Cgjyy4z;L(94x4Lk5Q89iQ3=r{$sDL$oc$RQZd2Tp}J!qkNxj z!cJQgWbNVf@l~19MWHHxLf5sDbbgje&Wf9WEMJkxO8nRr13rhS8d)%yz}S*Qs7y%H zpGz?^#hsEFmQA#|5P7`%<15nnr$fm|oS7;tOQXgYFTUh!I@8<1wJYDj4b6G@JmLUf zRTlaYhfS_rFeYg6o8OA)aXwo(58tGp(=+JU?Zwrx<5rHS8A z`R*s}W)0{HZ3SafW|$K$8i6WFVAFo$INtj{-DE zi=AB&qtTI(O5y|vh@>qj`}HJ!AGtq2-6_X_2=NzK|5}! zVfw`_uZb|~UT6!wJuTo#ym-cR3o`gT;X&(hB;v(5$S6O~Stt1=^>9abwk!qPPgNkd2htYJ z`dMU4mvWGxctU(At1lzm1Q*h$#A;$nx}Ny zIU+nmcw|@1f~=X%$y`_sT;{ttZoMvA+~nG8{4bX44ZN5-i`!%cYETxi6u*c9^eqPG zC=)wpE)T4nfTCTHjP3fapTs&WwrnvChfz+&ZkReGjUL*HbZ~SKCt+C+z9zB|;^OBn z#|88R(9B0kXqh<07>9Sz-83fF>BM7v)|dd-XGls69a%_R3Xgh039O%4RiExp&q1+3 z({sg|9%Op+4a96ZsUJHb!{3XmkYQ`UCO5%m1@7`;_G(3If4;i(B0GID z@7%W=p62*eleK3V*QZcCBP+VEAS0)FDSCNUeL1<<0fMOE{|UM4@bvMg$k{ucn|_@t zfyOn-I$U=p)d8P3Dk=Pm%r;?uO9*gxCt$3Pl$evs zj&>$ZA)7^q7s~-@Uhk`+l31Vbq?5a6i&^-q#Eo!LhXbskuDJGFjYi%#`p!~CBD3d| zfh0E4KOqpr>Y%7#=z^>zf4tB@wU4K85@9a)+*d0+Srh!(|oTsYrW#U#p@@~e(#SHwTcb)Q92!Bp#my*^{|$H)yX-M3W}XqTB5IBKA9e*U@joAft->vDBn z5dY$+#82;OZywtoAd3!bFQ7oo*J7>u{U$Oha^9pBAa80R1vFq7JPcoz61>3W3$_y3PSCf5&vQ{4AwiBTd*gh9i-U6T*j3Iu-9 z?vqgSKLEw!(p}udtddh+ifM*asJ$rQ)sHs8wdx$CStrH44P^ON0rAvv7&RujI|Z|c z1kD{GiapAePo1DH;WWLq9}3+#?=AiX&i>0uis_)VeI$@jPyQuy*=je-IN~@2OVKr< zK+oPcUwn|%2mR52e|g%&YzJ?BgU@L%yv;^lar~RP-}-=6T2(0*7zJ4r0bAhg+N?{v zZn#m?{ZwC5yv7Rdw}$n3F%{NA{@QP_@E}P|mGw>ty$GLppGj+e?nau9gPyX<2H2m9m__dXOvP7be^{ajD+@x+ZB#GICAQfbGg>e3Q-1HuTtbtB1n z6y<(PG8$DWC>89tOohO)nCYTlIXR>d1r|6}SidnD9lSzYLU;eoo2M?MT(mMR2IVv& z$$8zQ>R%ZeYbL z&mC2Q{b-60TDRde3KSiU7c!svf-aF)o?WzRmwoGB9C8NJu}CynCt?(9|7klT4q^4z zMpZ^?Q#vN5Zh^D2YQ-RtI<09G%(h~%DAs|yRo1M3bqp06J?;0JOM<3DZz6&G%z8iz>zn;spGZ8IiM#3zmfvNjhl7@%Q3gQF8$M zYNKN$U@`>04H<1wK=oaeJz0rGeO%(9XI&*bNOiQr)N4$AN9>qtpQLp>iTM++Mc`!XX`SYGp2c`TF1e|y*_Y)?> zIA8-IQi#i>nY+MPIhZHs<{6oRSuJ;6btL08iOVk>GE&$@H3~kVhCHW2$FDyd!S|GC zy-RC`(=aGhpbhQUxf0sZVvnp}fCz8wOIvlF=ec7UILdz8SB~?YDUXDywR3lsWrP9& z*NCizTqe`F0&W>%jp#Br8-VjKbS?R(6V7)Q19yDei>Dss_al3$#6VF;5KbT-5p%VE zLp_<%jk@oWq@!fgapL1l4x1zgZN~J1RA7QvUFB1>+bZDYym{;W9z-KCEQ)<|n6%e5Gonp&! z?^~(6?z)FWLo=%Q@2i+jeOF2#9m2rSfk*f#$2_)>q3ghQL2qUjVF4Ga1W8KQX~|FF zL_Y(=lo{*_tOhuiQ|SG>sc<*+R2Fb)5Ob(S3$D(ucwl?KQBAR`m&tyV6AfT=!^Ke7 z!vh#xl?%Z6e5)g>mp)%!wdrQN%wYsyyCfxyR)_NwN>?|!RBxO z)E#l}_Ou{iQ4QoP+&=w&6Fypd#7%55wP62k8|zyE(@ZeOR_=BgDy@b^s|1(lTaURR zBL_E5gV3a1+etn}vpIBmw{UPQ@71frD;rPm*98u6tG0tE50NGpPzkQ-l*ToCyqXiy z=Pi;|TGTLBv`Usw!tET3k%{Q*&_a+DI&kTY7{v~Rj%X4-x;;<#TmmFJ zfn@+gM7Bz?V2s58tWY5$TI;}Uqjg{8A4&nx#sO&YopU)kmpt)dUwr^N2CP_Q?C%!P zcz24+Hj#78R0MM|Ctp_FB6(||TXSn<9|yDtg0hm(u`~zJ)kn+0_I0gy<+fb4A4 zxHVDdOnuXug)CPO$QyD{2m$13a`DeVcMv{}w=zvQX}-$ZMu@nJx)zH|;j(6QSMS?` zAwNcQHHQKmQnpp+0eDX#j8}e$#0n&Qh5o*5;t0P_Hbh$ImT`IC^>9|EM+prI^wr~K zI5>u^GW2V9V{yZ~7-c=OWwCdJqjmx9BK1z_AzvChUR#@MrYbY)dO_%gFd5`fi7skR zaa-Y*yKVKWEQ?-lP}F`XEa5k{D1%W`C;PSS0$%dY91mZ>vg>+>%?{*9S}s@6a01+O$km@`HDbI6bDxSAQe0shT7+x2qM(R+L{tfoNRgn z@BA}!uq!W8IA;^=#E7FCs_4u>GvCSy@7eZ?IofSi^(RSxeH{6lm!Ajz&@+3$hHv!+ z*$<|7pyfLyGNmkJ>_b5+NlXacTVtPxa%W*$dV0`K_RKbf=;6}{@uM2&TRXdtMlj&e zYoHO(M@-C>utdKZCKesoQq+HMevWhWjG^H(SwYoi(oMRf<6u;e_Ra7>8R=;2C(7bv zaaFvuHk5|-(hVh$f!c`!`^{LC*xtrse_pUxW@@IO-O{6i&>w0V(5nBlQ@BWzKggcVC=eh@n4!av1 zQl4~Wd@V$Nw3Abg=CX|*C%l17!mq2TT54_u!_eT03|q9Ws5gQpI|BGSo~Ic>!bAChpKiHz(PXF z(K|DXTe}{k)yMfX~B3OIj_ju;N7Nk3OWEE}D2_wGjZ#RP8)>Z6J=lVIt2lC_|`?+{;GIYgX4l zHztuBQF~69R906#7)W=l77260Rn}d)kB<=GtKzly{;OvGNbL>wxn%V$xsqiuj+HB& z`f>nn=L>TrLD(qCG89u)*D4a-El*$6cTU{Gjg=KFP6A`w-iTjc!W3lCoQ)A**u!5- zwWEahf5rJMpdz$T(aRnjolD?2D1pAY*3>Z?7TI z_KOGRNk7$eKGjMx?6DRck!gnDCRa3jSK6>fDcsm1b>2HE5~wro2>m=AKAx-$0w{-qW4BYd%r>6@jT^~#qeLbP2rUq`!LCDlYB6UAy?(irY zi}Lkv64!>Cy!C5J8+TmbTGhoOp{;mv%IO^4BNZPbHr3g=L(HQ|KQIR!vfzb5BzFe+ zJ1R(5j6kowYKauGK)5VSu!^!I@2T6z+*q1`0J@;KUl_gsPPKhlewL% zh@%i5;?;JqVLSuV43kb^Bos9oo?i|JbZyRdO3POtdO?oKI9}4rs~ts^Z{7Qn2GVOu zCW`ansd>E=zK;rD(gQNq$)FBqin71}#J@w-xV0(=;$P&?f^S9kQu(JyIT9uBT7-0^ zr(Za5M_HqrTd~J-ZqzAN`S)Yi8-h})ev$W9B9FMXQh2)2uS$}INW06bSo4S1NWxNW zcx^gg!z|Zb=B5M{PB`ka?4uw?Hwfc%T12c)h@aLFAZr}_u&CR2NS7^e@|laMkb4|Y z?P4?-ADd@f2p*A9jWEdvi_+6gH75kypa!S0wN}yk9zVa&^bobNSKbrWB<3q4%Rwx9 zi##SrVdklIDYC|eG-7Y7HLA~9kion7+Olk{_$KMNH7iW@NviYrZ;hBK*^EBa^GgSFB-zy zBSsqRTsf)~rq9Z2ozyYC71=}b$YfW1clOHVkwk?2XM zDs|(%+0MazP7-wz^xCZ1C|GLSrFa4v)WGfHluH%iP@QhApVs4DwTjnnWQ~4wz>-m= zfI4qES>?(^j`6s}Yd>;w=^hSVh-eHkk7QwzDki@Ye1WigX)l+@j}yi(juI%kjv=3F zn63ha!LFei^1Ve0Qs0dk7lT6VlWQeNRSnAF0E17Z#8@yq2v5GZ zYI&gigroK~CjgwxVQevLsSys1K?c7qb9Sm&mLmNCUec|*_@-4(_88t3iSMT#Yn6DDR^qNaf4 zz5cooNFFEV_1kp9J5>+mUUp0kSD;Xv-`1nY*ANJhsyC z5{5<{R4PDMuMr6y3jbpHBtA$`*_}4(fAIsD(RNVE0hc?o{7sDFE*oLlb-Fwt^#o_& zgr(9+0SJ2)nryX(w{vu7Q!kd1%7{K%psDrFM2C!~$MC}OdBz<>S)Evh+`@8HLft!) z&y810;~UnoBkJ&iHuso_Uct31Mp`i<3HPaTH&)+Fs_ax}a)FKjm;k>*uDTZ+M?*H~ zxx<%VeVy=I`f4v@y+)Nu@oTnPGV5()*T~8XVFJv}d-Y;JwPi;n`rJbV-c~9TGXBdEQgNQFa zmwK4H>pe}t{61fiV}sFJ$cB$m5YuiQPU^8L6%4SmK}f@)^Pod5xFlry|JpbQtx6aM z0A{z@ZnJIMwsmi|YqM*!jm@@g+qk(lZ+89v1AI?0=ggTio`!D^zFWf-ODq-f)%OCs zfwrmKg{fj%R zsln)mGQj_M36>TcDW!|~2R?bMqO52k`iFlxZu_+>Uf7qhB;AofJ>!DqcIxzr*#^B& zM4@i1I{O7f_VgbdKjrKA)#8!+@KYQ#e@8g(^c1@4z0(^dN)jX&HwxR!)GS&;{&f#N z{f)m$Ux=T8L%FRQ?|a9`FK%7I{$^-?^TJ18k>%6ocVG6aHH~CAw-e|(20MH!6e1L% zZZMJkR_n)$5)oq6CmQnsRCNi_o{&x=UXFN=S!lrzrJ1-Z}B?aw5fDi0~Dv5CWQ$?~6yWH%%|s@Ga_u zwco<}wn+Wlg#$(eE!k66iy&Qfa+er9^dNewpqW@gIVQ<|4a%7w7e7yz#uEMelh;#K z@+^Q-z9%0!aecwqj;sjV!f=MUvAG+byM7=`Z&J<7VKbRGTetpvbnIZ zMiIiG%NAsVn?LY=zaMl5Ph?!Rf-Hl%u``W%KC7YQ=NBsfV34G*@bh-`?V5V%Tzx1l ze`aPV8!@?wJ)31QV4@zwwR;I@bP!hdMr?^xuh*#D4-vRtpTP%LXeN{jO`G*CVWlW4 zWjH0dLd_M)D`R47Kq4XW0`f+}EAJTtn|$ zcts~S6`pQVc88Qy)O@)upTIDYF-)h8ar->MGD@{*lFEz}&1$8|bz`L;VPvTgDD}9P? zhXfiY2F@@1g)_)$qa$sx^KxUt-PkOG1Xs>i5G?$y9a)Vl_@*weu0A1_iZieFL-ilF zp1E|XfeJY_+e{u$g?wm*lSk!7@oqa)3mO#$pm(itAd;fhhn+zzL?|EoFyRW+NmLl2 z_}@n-EKATStLS|1A?7bBYo^)TZmlzBkb040a;hc40zfGCX(U~Fno?JF&u|ZEr`Ut9 z?#`Nws`+jG?uf-#6*Gq6!EvDNu0i%<;G&*^U7Q>CW5oX#_%Z4V?Bz5b92*r7o`xtG zNY(F%J5(+viW4?4=#*qKh3lk16naWk9CU~;mOw7vE?9w7{|Kp*s~8CQ%N6`l@EfP* z4_n%Yl@p=ZUv&_Tqr$ZMcpY{1XXg1bNB5v-aiEbS%2)HSUJ&^Vm4FF0 z;$?n$XP+ARgT#h@AcrDz(rn2|JCEAhB6*4Twl@4~jAJphHKkcOQ20aG0~TMSKNDwG zLDSZtN8z4h$QmbiLl-xickU-O*KNM0(vIBZV;HLMO36T!CC+`;aLFZ^XY}9|31ZZN z=OaU(n{2S3^(FFMiBzxs!pdA5X`ofb@C>oPLop#&bK;TH+cau+4-IN^l;s98_{2(eVEF-w`9 zEd@tJt)4}07@`A(@xr@g(S2CCe5*Y5rnZgmIwum1NwTs2T%yYTPj{#xRWkA>j#q*^}fp~V)PQfQ+OtIulo9(m{h zAB>7B@JYuU|4&{XO}=xIaaoo|*o+^J1$n4EBV*@J2l935TFD|o0Cog_TF#lArN&~} zDv#u5>GOf!Cet{rRpGJM|&|GM%tlM$;&eK z@L-mA4rhSk>8WQFeo)5fHZQUXV-(>vkNEy)vbcpn(b|*4b5IB7M?Q8(VY=shI_LSO zzcWVPnx~Wjv{bu%KQh~w4#^Uo zr&pxMTa|XjOp&1e2$&%8oiy`w# z*DQsdM`h>IZK)5##r}!Rs>@ZAIs-@heea7~-86%QIV}=;&Pt#UA0a%t%M_OPB!m=f zxaawp{>qqt8Vj!z6tHkd;(k-3D)5z#{^tpaL}9)dXMbS^;g&W2!>8cellORV)1)F-(@WYPl?* zD6?cxH1V(W6@J7=DOJoX1Be4q%mMD(sL^CDliAk?S#?PE4Gt=O$ng=snZLQl3l!y5 zxfbWHnLaeJ#qWsGX)i3$;6m9H9LxNKm7xMY{K8o;>0I4weGiz2A2dG%$MHVDLnZMu zNjoH(mq;}#&_{m}yIt-0q~iAH{+gy@ldtZGhDAGy4R%RO4&vRYYjj_-rt}x9v^AK& zrAPsciTcwrJ!jICRQJ&(W)|RDZUEL%|zNQM36xrr=CE}M`t-ArUQbY-mM!SaBju0~oUU+DGzaj|6 zf0*gUj6AJ>iKcwDjw3=$sqZuF%fR|otNaemcK4Gs-!1U$hu?6XY^wMExtOJHbaKoVDu1HDIPS7Hm6hv)gGgUjOB`!+--75y z2MV^>)&BH`z>`QQ9uR;t0AS7$AIOV)j^yt3xpkJQ^CKd9 zxb_}!y(R>*t@+CoI7WHa&4EP}nV8|e@x}}5n(IIMr-uOhN*;!ECgrJ682w>Q>C%nn zekl*xgd^rp+Kg;E-wEW6@788V0%>bwc)MCA;#@fF1sO;fn$>c)(S8cEl}_4}o6)Le zJep9}#B4nlRQu|3+V+_lnbWa7BRq!2jF2}zT5_m13Pt#i}*pIF}vc1Q#Kv5cPHP)%K~;y0 zlOPu-)t_R)#J2@q9mrws4qa(25ST&dN}(E&ZD~QVTRYnmpvZ@gQ3^g^1632MG=s`& z+9|0r@ro}$@#Il8Z*X~##~>NZq`1u}XMU{MV4>U4-YX+jta*!WG1T^{Nx_eog~z<^ zW2$;vQ{;wvn#KH(Xtm1U_abV4#FPoVQ!8B5$6v??(4h`ROxG;cm5|s8E0;vtxHYKp zkn>jN?VyzKC~S~DtvaHBd(a0)$Ie_bI&IBMyrLn<1| z@I(k15TL)mmTg=HC7=I~tP(xQ_}D5M1}#JL9@URZznJL{&!oao*#=e0u3twOU$mx} zP~MVJzNrGl8C6(;DW1-{Pi2c>rK{MQ@M`(qPQuc2Pk|L4R%^ogP179FBg}F%7(T6( zJaLCsKyV=7W7PGUOSyRCBzYP&@;3f$PROzRsgFGL@ZVeU_ zX{6^Lh3_)#?5e}d<`Wt$b=^Xg>ZF`4-cBw499i$DO3L}DkoLx49+Lvf9CqmQ>W}Ef z=)yj|I+R~y%>pqb(nb@aZzew&a(8#aRI%$;9SR+jG<(fZe)zj+LB(pW#!+t^y)7LW zfsyWT>RJZJnoR#j!bzHa1zW5FcODL*+dNNx7VrVUbSL_TqOiy@WaW~F<)RjUWU@8I zQ38WU;|0qS&gyF(R&GM^xmdwi<0Yr|yYL;5<_OmkgO?gPo7F$l)A2S}*Y)T7KO(762rvUkUD2_HUl2@2*X<$nvX~l9zGG1xd#ck_g zDq_W*xaqkSasq2$i>%N#L_yQySu2DEdjIZ~=s29I&S$t?NVMjYy?Hc) z?zt4s+9YvoMz@KXo$00Yr8?KKWGE*Z^9_r~Gx3s&3X8lN&&p1RX02p!t&ca~8Hl`i z;cB$5=M>JG%E!?;_KpQKd=br^HH|9PJ%EYXD_z_JS z%1p+YbkV9>AiEuBB<^%cQb90*wMayPL+W48AZ-;8vShF`ug}^c@bWeEOhw~dH1snb z{!9$eBQ=qlsoGdkD#ifGk)ufz+t+wQ30Zx2kmrqplvc_&{qowuhGmq_i|)1GUrKuH zOFg*Xdhh^y8}F(9g@O9%VYIU${KS`aDgKsQWe~G_oFZ}*zKk3Jzfh)-eg1-9e4G$p&4=?_P9BW zUxM!xU{b)&P|h#ZOEack>;os?8|z@ZRF9in80Se3CrkgDZVTmo{N7~B=iR9B}c31F6dR{#x&ZCU&)^FTq`;tm%TY0?o1ht^UjyS zMM@#z`|UHJ`H?t|l%>w)(Obv3cAwmTyLBNeA{Rds92J1=Y=ORr;p3 zaHiTgR9->62zcX3Rg()S#yb=y|3T*;!lVOuC`9Y0y{{nRv8;X7&9r!oytMRhC?5% zMN6gIEHnx>Xm3K^j?iRf*R3jT=xNj@|yU5wv11I(D1h18Vx*=F6im(b6n6l5P_ zxg92udO*R%E$=xjo8m-c>!2S0dEgT`e{`=owq7TehN{#R^AuS(LgeVv;v|%#jgy+)KPNB# z;VLZT)lN0(wIc0fim>^7;gvYjR@7T7zW81cE+qP}nwr$(Iz2J{7xvV?sr;`r4s=j)M6a@61 z+=*~$qqklmffC@qRe|+_6UA$#rNM(zfm(`>$W?1WrT=b1G5na-7E zq%TSeHP7(7Xh`R;Z-|7X6@hUDVA4FU&6$4V7MT?hFGHl_ra8VzIa;DWR~BT9GBdv* zc<-)aLYog=6|5Ct^w&hM7aR<`;&QVx)I%B=4Gu5V)d0t>@A}m$9wSlDH)!{6*_)+q zTC*;CI)*&o|55=roe5SbG>k+4Y?CK93z9QJ$pDpo&OUQbVfvfT;W%p$FfNevgnAIrEExbCQM4 zp&ML#)KH5pfczHR*TOmWhg)jo?%Yc6Z=Pf0;<`h6ehM-YN|=uXoo@fbfTu5rM4O=N z^`(9Ll4aS%A2)vZL}t0B02 z%4NX3@c1pDM~yRj{UvSXit6M!OUc*;W+_PbUZ+GsxFB(rpy zJWs|>81+H|rt=LmIC3CkW9p)?LLK+SC9Kn1ICYfeyrP&=T>)N-&~jZ*PuV21H65?R zqPW4{8%M_~%3JhWQ;J`Yge5Vz277C@h5JzP5rB$_*huqrfrG_>4uiZJw)yGi@3`%8 zCqF*iC^2v$Y%O4P?git+NLQ*BNCG4fEAj3PdNrvHxO1o4AAi1!3V3widbvZk5K9mpWMtqtqu#_(oE_5hhYF69~qGQd{8};gim*u2tI|6&KxtJ1!79gcP;vv++t}xGV zU7Rv+?eH2)a;tP+93>@V7ivF zXIi&G$+j4(t`(V!mfRhSO5vS=pz1bEwMVeFDOVddKG5?-RS?NhGLt+K@VXH-aDEej zP~03*ql{;hF1U9gj9;_lwat$h7TeBXCL$ zwRCy1yAL}CmC;QG7Uf~7Y9RY=WQSEnJ(iG85vx#yI~9p6F>k;db+|GN5JUaOYrbP?k;Rzu)3?2`g0FKyN& zakh3|qj1|$lpdL>xYC#ORcvGJt$%V1nxz|PlpM858qV7B^XHhfoCQ^p2GkoitqNCS z-pbvi-mjWt;a?bF5Z<$i4M~j+qfCakrCS&Ds}qdEJI>N|MizH8W*%xW^Y1$wSxu3E z<~kT!LBSbxl~b~g2l31|Ma7A}5c`)| zen1?!CShW)S8~3!n@Z)Sp=&|_a&PyaGLZrCmaiReU#}EpTrPcm2dAu^FAzcHkkaU zgG`L8TT6u=DlEXqU7E#NMI0OY@Dvu9`MfH%22RNia5I`2jhQ{L)lN@iEmVzy16wSW zMDp8FUuSc48&o5GbCdvDlQVb5(SLpl0wRrfrNn-CKVRpg75F)i1a(}ji>=tI20TmD z&PY<=@|XoF>${~2KaNE^DF~KwhC4?h38gzUP$xx=di6pa#f|%od(@ju0>jbdy5m*u zkSQ2SZA)3J_re?S{(x1_^q9YAMaN`1DePERB%wxl?8n^}zNhS0wXB80B*wruWTpD> zXZQR6`dB52v*#Ss9$Z{EIYxu*RfGkGc=b25HJW+EmOPeujN0qT+)v76`51svwNJSL zzg9asFMDt^dR6C!4;piOS&l5NNs8DvZ6mA)YdavU&AMQ!12b=1vyEQ)av`K%yNl>u zkz93qW^q$z+{lMb5ru)z(|~_;nJg1}byW}%o`2l8A=7(RYtR?57pwZ;LI;1{s!DN{ zVou|Tx?m`c1E3cp){u3Q#NULZVlbzln|=E?#~5IfT1-A4czT?HWziKMmN6ivfW{Fk zBF`c#klVq{p(U51`0_OV5bA+Q6oh#8@C2Urb=g`^ z`9@Z}#ja-NzBes-33C%oR9$2j{uhsk*%HNjv7j76m^0)95j)`InY4!lW&DpM6tC%6m zf~NZ%W4ZU$I&|`W23ozJ2si_o&Z;};EK3UCMWoFXZ_p;um>p21mx7&7h}RhrVOd$Z z#n(c>`lbz3gV`_@7$@WM2@Cd@;RCH(Z?cMDoH#3x7ON!zfLeDj>F-+3n6;lfZm8*2 z&wU9$$TplEaE;JVCh53H65{oxlFV9YJ$N~`Z+xRo*J=H=7vlmr{J@*T+aNnK#%t4t zLx$}xs&%J;;SB@NH44Uwg1x+&DOoo*+O<zs0=}{$z$8|Z$O{KKlB^EvZg9G9%jkO zS>BNhC|O|gyuZM2Y_a(N7Zt+FKtNAmYhVGz%?(8-ZER!eWcCjg!b;Ed-;R-hiJp;> zEA*!)roCk8)$)G zTRgSsiK%Dy6o=>Z^YgX6O|NZL$G&#WbJo%?KuJAWU}$LugTfr#!9-8r=md1^k5^qI z3s5~hlNUfn=NbkFkQ!!s`g%MC#So6a=uCJ(wo?1_t1@bigG) z4FG55aQF&AC;%Xu^{$vQ-}n=gEWJwf*07xPCtXFp_wH(h<|sH~s* zWL1?E@HH&;fGmFk;y6gP`Sr2&VMx8VSeP$Z=Hd@@0RsN1;rV-4m|$Nzjvw-=f3%Pd zf2PIe`v(Sx&)pbIf2}O;-->D9QfmWnhXyAHmyc92g1)H*u(!)jvWp<{Qzt$&~nG&~moCO2_X{t^S1mKMDgS@GUU zavR8+#K!DG@Wo%QwG{#`wz$>b(SUT)v85~5YZ>jU^d*55I9h(p`(H)y$he=eDgPM& z6M#8D04KJ3y${^GT=#bxlXn_0^nuaIof$km6Qe_rr+Q}K0NuE*tSBvi08=sayx7%U z=-=o#wKjl~6tbDUzu>m$MZR5=ep6Qf+`mYH>0h4bzaf4fxz&=uJEbKq1laI?lyflQ z67w^d+(=8mzw73|$WjZ#Bck(TXyV_$OHaP2(N;9p+&`MnzDmsgz3U2pHaIr0*1u*m ziW7^9x#pDC7uJUkbc?@JwMNz3U)Bm^}?wZ)QdesME>pGf%0TuM?190C4-llP6y z|7*1l$K-~t!odNcL%Y+<1ls<^L<2x39-G0m0;+She*sKe=OFsFp&$qMlQ?QU(+xxJ zBl!Tg2TU9KA+P~ReEY`;Ve*rBfHwf7m-rT_0w6o*3xn+=c?9<-9{Uy8gMj)+3(?D+ z@I_dYeTFvxr!W5&&|A6iMS%B{_~Z)%zgj$l2Wn*f3GCyJ#_dl8b7}$1;P{Pi{1*GE zP5*ucg63LlWpJDD^Q8;aIuuF%br-NoyZ0D}&X>-l#me={UZ~m%-ZEj%u z*ZTchz;R9fO8j=R?8yup!8M6S2GG1mHtC;BFUdBfKF%L6;7 zjvsYf$bLJ{e%n?0#CCh{Z#B^XKstbB;!=7x)9*kvi&=wH-X&3_lLr;<=yQ|~AS}ko z`lyYioe}mTPHhqagQ0*mzL+SXuaAn|pS==+`G+-7`qxWOY4{OLWYDI&o6*hCnA#*yC?egN zRj7m2h;F`A;i|AHQ8cjIhvkioLrw1Dr7 ziUvv?0%C5_82_pzxvf-pHU=#I_?IJ*|M3PdUpeYJZ&Ap>XB3*=49?gFjoW@pFee4L zPj??}c!r1CI!u3JqwQtQy@YqNY95W#X+iX1p~WN;CPNHljP1nCAAu6golP@}Cvj7h z{}v+u?w$qJIHrt_f^S}!UIV?U<4K>J`&z8kmoDQ%dMM2Zlg-5r{lw+mwxgUhOrVork8RT zN|qp1clMwlf=fj$(^T{K>Tazz;P(Dd3qmt)LGQ1c8~ENE#(^w* z8>Y=?=zZnC$+0>L9n~JR+mg`C+V&i-i*yo^t8jnf^D0Ybkku{(463R~Bp=nOqSq?M z#NJ?M@KixK@`~^k#K%x&EHRFMe|)MN<11AF*YfYMF;t@^9{ zeIU9G-vhq-C_>8aCDSi2z`@1Uy!+fX(EWmdkh)uO;5EhgNjE?V<0qo-U(vFIRXov? z%5PIAY*nZzK1gq?VagxptT7`%BlkYhhKxl&<+)bZzQBhYM%m%boN-!3n&Gu&%-Vu> zrMkNS5il7?5313JnC=~sT&CrAKoyyGB+y_xbzdN)P@}7Z#K2YWe--2>@vT8YyU_;v zkC1$MAo$al+!Nf;s<&$AZG^AUA3$gaP`ML}hrJ61d>xyh$SUyu&-OwvGhU>M|177C3iv2IYpx$rYS(d%`#SH3-h$wP8%O0lRU zDSEFh24M)q&KVw!wZtjEcjBEf#7^r_BqLa!Fv}Bp(Twx?@cM@wROIBr{dMW@>V${| zt#Ee*xk4rGQtHDx2*2V#0AL4K<}gkTXH2jqF&5CO9;9%$uo5(E1sBncqY(LdxIkUhRsPB4xQxXLYaR$$R)mr9^Hk66* z%wbPm*Z$6k^^`v#t?=tzF9$h8aZ~_wO+M^Amm5q!fTPeNSnVYHTAnTR9uq4)y(PF` zkUZs!^E37pE^z89vAE1V4fx(WU?C5ZF(o_aktso))dNQ1RLuGyavFC%7tClDria4msUsEyFfq9OHr*BES_8cPT9LSdG=)aNk3%r#i^qPoJbP zYy^QY`ih6r%@D0aLSbLRXO;Fg8M_3JvvHh|5c|R~IqIxW?#IP@IO$<6p$kg0K5WM` zF4V=t-LuOcgrDqeBS~zOvKizWcq0aSDMj2?uHl|4-n;i7tm9^cjVra2`dL9sN@JcW z(7G@+|9jAaKl(*Md+0tNEiPOK)9o;q_b)N4nPIC^C0WS+iZthCl!Uvpf5ER@8^ROx z9~AzEMSJ&*WG80Jm2DrM(QDfay}$+YxpNioR?S4L@5UST)aBR7>MNzoo!6kv>iqM~ zAKNoV+udGrHhjzUszr)u3GKd*uy~G-Qeo(#N^%$vlE(iE@zH22Go}{ebGDyLi8t6MahGoZh2k_t)cEMa_$E zNgh`KG%W}WIfKl5D@SY-Io~`sO}bJIpcg$$HM{1s6ey~p1#^gPa?NhMO_sP!957AP zO2P}Viipq>G>l{G#@azDw3=Da&&zHbTe$Iy;CIm`*QlMtX*>z?t}INV33gT|3O>4_ zIVDWc%keWd339yy~#zj*CIa7{gp7=5TjQsJ&rhof;V;M&%@W`kNhD;-=isG+e6+M<{(jPXsB+Vu)PmP$m8p~?1L=T6 z?c$;Pi;lu+3JbC`1nmwcC2TsDcSz1?EsYUcw)6UiXP^>Mm>5JL4&v>=<%sW$3W1yd z0VTv;-_AEfF1$^sYZ+g{Ot98yKL8%}Vcc_KcJfxT8nQ3@DSDq#DOCkRqASRU%(0Yz zphwruY|D#jVnqNRE-`WP!657uj#QB{(-i{3w^)J(m28jVgtNQWoTI^)vm*=LOGwnR zkmV~`Xe7U@wnS+RJ`L*mT(&Jjxbtw zC{Q$iuPL!ZSdRt_)M82a48A?iKY)WXvgjYTM%IfApJwTtWBm=k@&2{$0b4LOhFVJ} zfZy6iS9*ev=L(;tWm8;>gMc(#_oqh>WD0dT=$U8o5>#W>L~lb}jts9Q%KbpAw#Ah; zNXZTNz0jQ+J-8agkVX2g^2I}iW)JSNJVFQo!NcU>$qoG z(-x-~iZ;L2u^GoaTqH`!Nv0uYC~j0%2p>(S_es*h%?D@%0xSZN!V_gUC1I4FK zmesa2o7}6JANZ}Zvfhw*u#{73!*d~1r{QoCA7IYf z7&1Y;@X%s>4WOK0zxSNHG2ZY9VcEf%cNMfc>PP$@(!qiG@WGq+jP6PYe*KNotQ(D$ zS`U+J+1Ly7Y1!XSE&C7+>rfM?PGJ5E1IT?Hx_)S^u^8BMOb_O2-iqLD)rbTHLWiz$ zlP$lRaX&~EP0*#>8P|;!sW|DDfT&Z6Q?G?)met80RpIc}VDxSl--RU3oX79ujk`60`s6B!a#!a&sx{whg&e#Xh}Ui6xs*WY%Q2<4{4 zV8Tcg3tU}O2S+Gw3EHS87|^*UxJHq8&{RJL&w@C#Tou|1zB^T2LC4Jn8WRLzla~

zN@$@aSl*`3ORD6|VwAbXbNgw*JHlMqvrj;jjEv!bZt9@R z7_8>xD_Iq-uAmVJkKZmc>g=qRMAclcTWWm&E@I$iR)wb)6hiJg9ui767OSM0;e>d2 z67{3wE;W6J5mz{>{csmu*V6YgY(l~Q+9WJn6h;sc`hHJJlHY>l&&*e4s$vVgx&F6|tc>?HF7-{Sejv0a0;c#`oz=B%A$VliX?nIq&Iwr{)SM z-~dBIjH%QinRY^&YY^<0k}sp{`NYj4sh48efRafOYfX!9!h|zrXLv>i;Sd#reF^&f zX1?WlcE!UBJI?3FODl9AHYsy}2>>5)NhYJGOz7_HhN_-~_ceT%#^(`KZW zcu3ZXB^QLTd>5@$GEV5j4=&?Uq@L2!eb~h@wwb+MigaI zH4}$v#WtxCuj0?$n!2;P!WwtF$tiIA2f{ftTIlL}@&0V=|5Z+GVFiorlyH8D z-K_j+R7bP@zJh$fX=T^YT$vKmdNnUuKXr-vh*Nb@jk(uXiK@VeyJ<1jILgLav&n)a zHf0#;VOz$UGsCUafU5)Tx1V1f2mzctRa zM!O6X$I~Z8`9#71h%TC4gQ&w*{))^F{3oSd@Yz-_-8&{v9JZ5Khr1xIsr;6Vq?#)g zSdit7hk1S1Qo|cTuMyH+_lJgCe%cYD7BkZ|3R;uwS-v(^u?FZKRVVFO#!R4zut{t5 z0E(BDB6ADFPyGe%Yiqywx`2yhs{bXoG*^&xxwr^WMYK3pbp4pT`pc(Qn*Ek!pQ`N2 zoDromdv|2Zc{)olgcs=zT0?@_+~4Jl2|RJN$2588C3VePO+$Ww1>OHP=hos0em+j^Muv_{Y02j!9UFe7KEMOFW9hHv0&GU~TPntjV~QNCoA zFovLcdu49c)fQ8lJ=mctoffH|pea?1*N!+f@0-YZ9@|)POBBt>Rt9hZh$-qpsqL%f zc##3pp}J0M2A@FtfWO>GtoBJn?N)Cfnb1nJG$YrVh|?9gWyS-y6(D!^=*RAA)0dsR zU~RYdxO{f8_Av9Ouz2(Yf{}=Sb&~};-i3;u2V4!Ah)XX+G1%Nf)vVC$;eQpBP6H(8 zWU&P0<)%?e9#5ILN52`q2Hq0r`GM`fJ~nQwpa{z>zJQxrisi}2%aTf_X+m*j|COo0 z5X&ZRWhT?>r{Bx+ghZ$)BrHFcySZFYuglERI`yGMSS5?~xHrL_M4UrmuL-dG5|EA5 z0|bQLf=~aRaosPwoX*Nbpcr_(-ov@vz!4_ae~LuKOiwnC;&Z6H0;Hw_1Wc|w*ILcf zksc|T%u8CDj0IYgkrbKp_8&EA5V7k2jwQ7b(4OBP{=pAbM=Q2tfJ&8@uS*Kx^hcX| zr<&M=jyb~^3Nv%{YS~${J`L;AIke8so{0RHNxHY+q~WGwB#edjm^v9Ms$GSy4_iF` zPA^9#aRmvqN5rKcV9PbyJgr#&Ba}4^r~Du+Tf^sFCw`G)?3+Q!qDn9|^p|h1)<7)1 zd2+pRN?9|asIN;5Zs28cOG(zbdX(Nod|*Vrs@FroE6SRng#{z3=Hi&!ljPPae&zH% zh4tjD;|*)}Zf}9GVY-i}G@I_hkx*0A@?y{=5;|KhJs@vQ`pes_LQKIlR)hiY;>%Ly zZywiSKX!kmRS2`POly+)Ous)F5vuui!)*Ds@RUL~rMQuogzAmKu@M(;K}K%S!c|1U zyny*L)+TdRI@&bqH8Y`opHR&`F41GH0$e%Xq_q@4pGI04FRcZ)RgqYMnNFi2}q*E6?HTb}j`hf2vy* zH4S1}ig9VB?Wo2IgJr}nm0c_c2faR$$reQugSUq}xEpz3R2D~HdZjFf780igdz7@~ z)lulM*&EXu4W|1|YbJ|v{UTQBttvvpf1iCjWWDPB{r(^YIiLRbi|K>5q-`f%tAAq~ zO`Q};ex>`;I<~7K($~$Y=@;?e!5k5uw@PT{PmRUT}b1ml~qJOK5yUD+79mp{tn z7y;RZFvQc$jNx-m87)6Zs}`QWKYzyi`@Ux8!^9^)z*RB_w~;elJ-$|*dIDvZWhE)EQ{j=z$(A*j zH;!Yr!fX)GVGHet7B#$G4;YfjSnH^FOnlBvnv~UOBL}DiX@7<{4JzFSqp5TRRu35m zYhytroIKSs4(Sx8xr%GYx+E?yzl&Q68QjDq^|&Y0Hf*Ymt8Ul2FqtPoIec4`YR0?YAanY`-;l4r|t>etWumXVy;HUlYA+^s5`2bk1XrYLt!C{kdY29`LdI9y;|Mh8Hfao z>ZN33iZgb&=%Ph?&DPZwGE+_=`e!i5VJPSL#&@AV>i3_)17TAqdWx=ee=Foa!p__o zfDrMwNw5v!bd99udDGqpQA9@Pp4Tu)3WDq(t7Z7#5s3|?4b9}p$1+G1%0OWsKq z&|I;JqXx;cPQ&-57})h30{tm<<@Mo;ZB|@aLV)7c8y;EKaL1iKzH!|w-9UAHMs;282{?C;+2u5Az=my2_WA`wrDS-jxCUurD4<>xR9LvldHB4 z)D9f4xydZs&=~OyYu@&1NsDKnUCQI;>umf9zjT0dg&=FNp}0)rc&tKJB|6Mg14yYH z3s*wh*5cfzjU*6UYC*lfUrBB%9#NCfARqrs+H*)2g85wvuoA z7h3qSNQWjEA8zaSP+S`*ui84b z!E7{PxLr5@m6lPcD!h0+OXMU-q)LXO9jb^34G7^TiKoa%mBE&ZN~=g{#hxc zj&vm*vd+|6;^ov@);oG6m}qa-Tfb0g9}@|wSMw?J0%be%hu@;fX*wdoRF9~u^}*XY zMi`5_9z-$J`JB&LIx$)bN|mI1_Fy%J3??BTR)$IQ$L2iZwWZyS71*s_e)K5oykT_q zQ}G=EyfX$7@E9Y6oqtYk`2Mu`%eGGP!DCOe{S(6VRX$t%bII3YJ(6H&LV>qa3-Cv+ znFb_*O?J9nRmp_Qs?1|_Mut2O?JRbvK=0b{>Ek;9(SumyR@5(*M0A}7)c-7GGXl;w zz&_6NAS6PmGc`$S!zu|U5TcemKZp?W^D-#)J1?2Kwm0%n^oOgb06K%niqRx#sd9||K&rIGjDy9@$2R)zPmJPL zFdsBRsp!3+Z85_4ESBaLHC5Pyv(==nE4Fk{&XA~Ur(~AfoI`#IxyWZ}OzBy_B|ZRX zpXWwqhDA#;ybjb;1Cg9l#=YZC3f5slm1Wu}CPEA=uD)k*!@PA}U3Z)S66#$rod25= zNl2WdDp9rWT7jG~;GjJ;??^SW?)h-!g<7$h?(I)U*vqnnumj~jo1E)YIr#yHu3Xbd zH^{HX*!KQnv%%Bf_qXv%kpvl^AI5Pq_Rp{H%+1x>j!~tCZ;3_a$*# zlQ7sva#VbjEQ=gvi@GbFopzr8*IdUYw|eGN`eWjWl((Z9Xoigua+gHEQgVM{OPz$S^ zG(;dYi}Grk1rYu;#bdcp`;*|Gku~o({9+E z-h((5!f?z0`R76&d~n=jrOU)x=&Jv{eFG)u1O*bQ-MWOj<5~laK$|hhezOXLPdgvY z9I^VS#(HE>Cy*CX!^8P**9F_f?3Zd1CRfQztFtvqNyb0^Qkdx1jYNPRJ(?$bNh=8$ z!GgRmDfYg$NRqQ!^}1$(tDB%O*V*$I!%CA$>QE7Zfb=txpQlZw`V6NyzgY9G!PQLU z@Lh`}Cjq@-W85*o#WLq}|L_!lq+)aou5+Kko9`iV&yp&1(NNP+KW@Cn>oDeYYDFi+8|=)5@|s?)LNDN<-|{YilT#oW)EMXCBX%c4_r} z9%D-v5kNcFkUb|3kdguw}YXfTcQ~YMo zHdmvPL0b4zX&FRq@c9N~tw2~fFo*lqYp?UP!mEoki;h-_+5W9%YBuzifp|AMa#QNM zT8Tx~)D7?n63w-EWhN8BeA%RqRykqWgHK&&%hziDXBpklMs`_qJFHblgpos3cbWO? zoV&N<^YLPMNnH9upEGsdW#WRdz!oEG*T%kW8ldxI&hOy#1Ut}u(P+XGE?Z1DIG24h zV(bYhIrSP!5+jhVz`)ZK;m+3f)`H<0BRoX{oK0X=Imyntoo_J?HP5omQ|faE~>{5i$r1BEdu-X&O zI!;(c-`O%+u_Ma_=;EMiv00zjWK$yVlj3+ci;cOl2O`fMu1m%GfUV=7!M9xBwcJ4P zxPwUGE#YD?m?X<9*an7)V^0Tys61hj){TW_=gd!mkU@)M2N^xSgyEn5F^b{^DN5O) z1F1%Q#Xx`Hi|$EGC??AfM@`RbEDyJtO zWqfvJV0(Y)n&LlS*WkXWt3$4mUE9A|^R zslVO6KJuDI`Lg(iYDSDFl5aNB4_QFpH*HfMG746qD*;vXbX|umzY|m?2XRZNaaw2g z12A3_<4@RQH#+=1DXU~5fPqEg_V<9&L&T=?znNn1D9N0*BaAI^#tpJV>)m~}-?kH* zc?_B+wY!NCn6TcoyUWVOl z#XyXY4I#Mfa_c|BM6a=wsQca_birrd#n_y$K2MX~Oa8R)#R6-AY@l3}jmK z{nA1i6FbzX#oFJdnhDTY72Fdk4B@dqXRZs^TxbH#=E{r~8=yR4avvsq3g$x{l=_^v z>xYL-cDCJYpB4FiH8CSbD1=8;#TzNayk>DoczdqQ9LO5dZ+GisSs!}Z(Lr^+C^OVP z6@7ZKg79Zt{aQ?f=pTc4fsuJqz0gNX(zKt4 zE}=vY{x;-#+~_bI5u%1m5{_NnK+|~lJW_i@M}77M|i03I9dGNr;9z_OAab8R!}4LC^V8`tW` zD9kxob)105P4gDU*o#TTfW)fOg>G!BPy~>Th9M2`M31m*3_ve-<+_UoccOF>WN!j_ zdv7FsLmlH=kpJBy>I|T7LJ?L?%sP^OTNKCKy+@Sh68v=S)m}{zWbQG>o%_sJMdGVi zZeaJx#-V_%*XU!h-?@qErK4{w=O{v2vq(B?OkY7ZXYD3-B9>Y-pL%S*)cn&|(rv(_ z@fY97`sry(E>+CPWlN%oh@{OTZj|uZ{m37OVXnep_zHLYe3~tTumWH6rzxj~bUWOV zMj@XRhw9iV&%UNvazVgMS7%l?#K%qYPk|kyX*qPImO%SLBwVt~RS8uOQ(Rl+zpR&S zxX6*_>S%KeEapdDZ?9?&`H0?xbBLCxTOOQ^CtO}KZm zRK(HCCDv9r6mfl@86(z%H|XJ*jH72mE`BI|NUe5l3Ng5Gvg^SIC-}yUfEd&>uHqt_ z_pYBqd+4B_ki_ zSJ3rl76vofSzEpBTu}5i`Jmb%Jk#6Bz9dVa)~&%Bbm1t=p=yu!YhgZwO=0ptVCG{< z-D{g{nlB12!7$d*PUtPD^(2)2xo0K4)l>ll+q6Xl8WDG!{vX!QHSf3)3Aa#A@7W75 zrCKKLf=|@VI~fu5?BA+y!ME{+kG_o@6pMWhaE@Q`_7zRKSo>K?Ryi)oDKJJKP{13{ z;PmDv;Z!jPKqusfpwg(wG=QHYq&thg(5fC|wl6j++57nrcb~=#45M zC5w+SuXGn)$m~2#HWDJB#IRQoRAtQa`kf30`k=4jhJ+lSWHn($Bo%B^Tl^nOsNk~~ zI27KArmc3w7wKo6osYaRh+?0k5y>R4@wS3vUIm&iKbZrT;X!Q6KV>_U6Mgd@U9yp7 zx2DLFTv2eEpx{DkAd+RYPY)`&p+vECuwi*1c8v(HJK8Y?Z+2QEH<66)=m>WeV>iN> z^ayFOkGols1Uv}DT@`K3Tx4q&qKknzj|N!=6nMbSPPz6e zRD6e;>>B*$b7WV3UB0Km&Yah~}1O_^&#k*Jyl>25h%Q^>pZL7$4A>#sh6j=9(|2AeJ= zOGoPxCOdygpk;$$iBxe%f1 zmv6(VXaB~=j-X`H>zLs=x}?n|^cZJa3f8#|u&^&&@S~39dSNaZZ%4h8)xGCL)O{OV zP8w_+f0?}`x#l$(P0TJpDrv7e?Dyv#@7Qm>uvf7oL8-SAn+kbocC>@~%HZhcSSNkh zpSIF2Cz)cCi1;y#-IVNU| zO|cXnlBYr2U7%G!9;Ve7CX;@3IbVnk)*UtAe+7DC5X3xoa&z4=>Nep`*_fe&$$j^V zA71-xk<1a#8pS<3j6g9Sc%H|;;Eswb!$ljW=E$q_vNJey@s+dbIymeSzDFIbG)>@! z4d(C4Z%n`$Yi+>8XokLTz0Fx>W5@Exqf_2Jn-HzlpSC3A2iXuer{95uSo{Fi{5*ow zRlc%ro`iuZExckFcGZRt2Pn9Uca>Oe^L)N2AO#Te(=Zh$8i(y&;vvDLqenXwS?6il z&%ra!{1TE6tkCHj#|NLy-22dw#wR6zvlWoLr8ji5NR z50$Gw!n>DH(txmvzW;4O45m!J5x!2qpVY7-)Hxf6Ho~nY9E#KN5|&x7zdnPQmDZ{RTPNA1paJ4D*MY{?PR5#5 zvz#n`j%IpRK3&r}b2M2zLv{4`GXl2uJ55gvrTM%mO!;;15!|f0A`;KM%kSaq?+>%b z*Gf!pD=geca$gJtUu7KMu`hgUYErw=!DG{9E8fMu)@onNU0NC)E>)>%zJHg`%c>4RpTb%`3O zV*JmfIYdM?9@^W(Yv|~$Q@g-+kr3*$Puffxs$q*W@61B#ib4^qN8UJKF;>rNHBQT! zu9GYBCfEr+On>QC?>bK(7I#4>7uA%;UEJPb-hNSmYQEkORJviv!Tb6C*7s+c{X}m$ znM6(nF7Jmc=AX|4>>6qd{R5)2iZ?srrFfH-{je85%8C*KK=FkK>6^_}tlP9=jN6#M zy!EgxKsObNoCfKIrlsHNy8^v$C8(V?zFc8Eh*fKm`+k6s-F>1Ave^on<2sXxjC-O> zlS?li^@aB;`X*r*05N=c(w@!6S|`Ae{!a9vp26$}J76SSx2otp`d6`JovBu#L}*E- zlh-v=7K%!&!oz5LVFssyE6D-b8#qW>I`V4#EIdn zpY?XP+tVK2+#AfPzDj_(AT81{?DLtx9Ib{+40X-><)#QXmOe-~&}+1UoVBNd5AMNz zdCXRPknPT+vbr{tb1TWhT}<13^3O%P*k0N^B1p&|#S;t3det=7mMRYP9CE}>?j4K* zG1pLvRl40m`d=;^KHQc)`E}5UusqFWZ94Qj`HHu0qKf$mtUg?`q^d4Ke8yJGLDG}R zIxmm}E%6i`w8(K7F=10!*34JBZWK;2cjF$h_=K=QxYsCUJ=o;L}ivGVi1t z4R;+U-S<1heV?U5=aghQIM`6ZO*)82^&GB5e7>s*FoA~q?u%4~TH#JENDv^$U0HyW z3wh4ojNUHyCL$t&XDd+$Rc|BNEJGyuBYlUNeOy|~_s_DhCs;FnViSh2dFdI+jV*-? zKiaW^!-Cv@4lxeaJ(l8e?mdc+cRJ;S9-F-|DZ$mo?Qvz`Z-nSrpO27!iLiy)BS_%0 z5pnEpNj_e+|H9TR!qPaxv4C>8?jr1Y#1Z_H>t=!Uct69KCAE8e`snjOBt*MiK+J24_waThjJ+`<2iK*;0s^>n*IEI@a z6B_XdViky7wEvX6z6j*pPrHXgy+N(?iN|##^A7D-F5JJ^a?imJjS^p|B=yGLntf*y=R5&@Mmv4#qsxLC z{;~=r_!?b*=AT-KUWnW|2Qw@{EwGkYD@J!xEu_-0+FN4@`TT8rry84q<3mfp0YLpc zlYmX9i~C+(G2@3)!1C1(+cicE^p;Ly)=0eW$y^kg(2ZE;`c%t=d9zVjocFMU4<+#T z`r+9e-$zNwdL)O%Cc<*Mw|}e(h>eZp2XPuQ!%-)TBN8$@Z%R5{oJV)AyCZEF%Hai{ zGytL*26dj4aUSXAcNh|~5+eLn4|=eL1 z7}ts}?>LU3zI7DWUyX9b;mFr%NpeBL|H7bz{AtePb$;}E0Xor+AXjuOk5pJF7niD= zE_9tj|K*l$Em=GC$&e_+`; z^?}ej3V*u3mr_DLT8LQn^yHT{yff+&#@S++NcNCKPyzz#h4Ive5E~BE3tDeWdeUo+ zARkPfoNx@$bZJ@X0Nc3$BvhNA+Rbw0j?^FG4RJ|b-iJyUnU%}O zg{juohxwDB{8v=hq`EfADq?ur&efW}?jdb(@3#P0R<_1n&-KjmG1GtVC~96O;gdSNcuKfB`<|@{$UR?mu$6f7t0L*cdr242Ce8?tLQ~|@dpLg=`X`{FeRInl$iKL!u?;C{N-cSLSKv*s1*PGkD!$5 z>hAB^$>m&qn}H-3&2Rw+HDg*p{c$7jOND)?WHZZ;FuO231<+hy*-tFA<#yvAfNt|& zJrSC&*tYu4D*BM%5n?SGkCi4|%40M%b9ak|c9&7Zb{+#CZZkSY1JLu{P16LdV>@xc z5bMzuXn%Co{V_W_u0~o@nwMXeu(0ctr{x?nDK0fB4c&4Mc=D8_J~Z#k+fC;E24!nDyXBs~ z3O;LH8-n1uU=gfFf6=?%789$=qOU8#ukWW-^XFH83K{jnds9x3h#04rlaZl?7yggzm)bp&!i`*jF8{rFW)ryInSW8yqb9g^$wyliD>a z?`PYkZWL~QYiG@C1Z-n^81H+)Q94HqyGtqEual~8XZ{l=wKk0)0mqcy(#HP5lU@4A zf_S&>i!taS3TNz%w#;z=g9ACT>*^22(n?w{rr@8EY)*ptBd50X`WPYy%Z~klmvxWM z)!zk-j0XqbdMr0Q)96ubd;+%L^H#|8@3U#oobc={9-gowRLO_yx&E>{o>A9q(M5Avj*vEL>A@Bs zw!sW#E#f-->R{wox9ysd)ww=m_Obgb(aK|6JC52z(F6dxD`weh#IRsw1a)+sVq~=w z0Y>A8w;4Rnff!F>iObfUMf+Jbvt}#(l6XD3UIX=<&#_XLt~My{cnQr84$DQZl+t82 zJ;i>vipEO@sdTeGK7`6y@U24JzBKE?Gk%&AL6eW`8GUP(X-{#DrDzc$!cz?WaP#8o z*kVLZrE7dj94|(%@O7J%^hih*Ub&BySKvny-Pfi}-&VdGcwA`LG|m#Jy-8$6v>=bg zVV06Wo$VnEHq!|?gp5?Xw5{3-gu5i3uKpo>V|e$abR>Xrkyrgla|^PbtWL{T?J5h& z0(4(t66jDrWBv(s%E@JgqGhnBqM+fe;fmdxU865#0h4^J?@Lk>+SIGz$*x2njxZwB z1;W(D3seHi$wm)f>c#~ROk|z(xLEl%Ok9`V3Ee5V9|7;>hkFQVLNxY@`w;=l3);a2ZOAcvF z&KO#Sm2=hmDZSMQ4s&6kHorwHG^nVJw=2H0#lTpGN`K2I|;hl{u)Pj zcCn02!J44JG6cqc{f6@1qE`ragz55_g|1IIh|Yvt8(CN;#5g(c(?cIY*ElciC;luD zyg|B4+hqPFo*WCGwt-wZ%M{1Js?vdmxr2r3{F7?ZaI35(()?=s&sRI%CT1V~A=0E@ z6DkP~v^8A=f7O18r12RaUs)%M@sgk!$YoyRC#FygM4rVKkLb#XnhfBNc8K&z@o9?b zNz5x}_xHLbnH~TK*y@{I^~GFa>0^--k<%+Ffje526`q>ZD2u1DU?Xe|iW!QdUf9a| z3nK8EtX|zTF=Zb^MT$be#?$;w5u|dFMz+C`0qmVL&<}Ag(P@Kw8N%mc`9?b9X%c<> zjeMN<>p*{??6*NFC@DB6$u_$j>_LT;G=4VA2$8)_gTS>MrEEaHWEK1+MikA$!{PgB-i2I2W!1k}>cIO`btD z#0z+N8mf^X2_wW4eF*!m5df8!3p-3~d-mFkDt5ea=V!ZPHr*RreTlNT;C#b0bFZv( z(p(6@#6hC1C17V&1|;DOw+1^Ct?Mi4Jgq#%+)+@@xSJ6A*W9k_n<_g=KAZSR`yv)n zaa?K=XN2xDJiJ$(8qT8Bh#zv$h$^kxD##!r$FFz%qg z-nal)e#%<&Lld&;rAx{ zb*3c)tWlZ($oCk8luWx9U8Jt?*N#rcuPaR7mar1h@Rs|7#T{m&>t8h;qD!g>^!P5y zD1Hr)uUPl2N9OblfLP!=Wf3*PE5<-LU!IHhq7g0J5!POgn*bMm<%+BEnkGSU1sr%c*c z$l-4(?NP&dXcAPkhYrNOZ2+n>iMQ*fFKgFYMN+v|90)9h-##OIe9ECoLV@;hFp8#W zl`vpULbR7NG|s(0;)X=G89oiS;;aWdc7Vq?d_VnbW!o`fCNaG<;aMw)5Xgiz#zaX| zQltJmy`=>pjIEnokQYOUBmKc|=gh~BsV_l}H0CM5qtKG@7-6dlp2H7e=@4z;X@)y{ z`~1{kfd(g|5%zIsU!22pL_tkXjbZ?0JnVGq#`vp>-{4oEqwqr!01awE8^DHhOibN3 zuoHO+ODLoEZN&qLHNFV3yNZ|r5C8`Ob1T&Tf zEMxP1tjPeB+8+EDWRJ=b?M-<)Y;(Z8%xCLltnQI9ECb(tYPj16rOtaz6w^c2jABO1 zc&TW=ibpo{C>2w+>kS@wIwxyxm;}bRg~m|m^;*){s>=ZH@;Go-7_rfHUhQ~HuZDYb zBB!~el>y!_VqETek(O97(aIKA1SO{}t8N^LBWuBrlIs1#W2T83C;|R0kx!HzeS5C2 z4%d4p`HflpDp2k&wj|Tak~&>OtXxaig{WIEwOpG)3%PEJ07*-@O-dz@iI^v=u*6Wg zV+BqekF_BoMtq!nVR5LoW9P?>9zD$T`JJSI_ZZH4v3KV)uE84J$8t zky+(8Gla#my>`DbF4kwYZlSt+d|&DHeNed)2#%`L!ICwD|%MmAR@gA z5Z#*DcPYas7HmsJDZY2F1tB*JoWNyVA=e{9Dcd9l^L;gp2g9eE_ClH_m!6_B2$o-A zX+Ak$yxR$v;tO4P|97(Llhnb(f0Trv$!&=yjDnAGHegt*te!C{AOG+~d4wCtY9A(6 z*CmzER;{Vs`=|LRmg;*an`1lBK9u)&ERpZHm3T~95rsELBRe`7ebIJUA5u3Om*qjU z1wBdFZZs*qOvHEIuE{mpww1*+B|(Zv3)+uu*6xu@7suAQvNWdLGrcO;HuJI$K92briGupn)5G7KlVF(v<% z9Mqp~O;IxT`Gk_gSM~bH@w|wcNd8ouk;Q?%VDw;{$wG2TZjiU4ES5VOpNp#al5V<` zWyw!)`oWOC2W^?R2$bo6)tL;VxF^^^cXQ+vxo#Fp0%6#j*nX>X#aR69&`IJ>$mv_H zRv=K134o?-$V9akc~?U5&l2eyubfkuk6cQ^M*PCxT9dG2ioNkZVhWcj8ssymjIND4 zQGB+2Mm?}dWx`R>;hKH!d)L!mte>?9ihnCXr=T@?sBVT)abe(o7i|>wlDr;4#G7en zfEZ^*0pY|WE&QW=i$E>49B5?T0Uc*8q;l-TAdxs_0a^F_SZNAF#O~t!q4NdjD0}ws zX`Vc=NAE3G*ERK>%R}{VZ_6L= z{uTldRuDkUH4||_+?_``>Y$ayAR=Pa5~psk`eGDQJ?PW-K!WIGc}2XJJU_6&#=XN| ztS+QJktLk`s%ho|t*7qSQM z%@TEggv(341*;JE;-{T9E?|>KqoUw7Vwdv+i7P|Xq>eWRltb>Tf;i6wvvr$jv+NIP zXGbuQ*FSqB5{RFN>Shv{2%7CoJzK@tSs#Dw0y%WbJ5k@_ZzuFV6T|y*t*9Tn{l;r# z&X5ksj1u^!8?^&ZBU=9!X&ap*)m%9MGCg$taDiBz;nw?ccER zD&4^VFu2O0(eN#}CrLxrk^4&v7h9xP;P zdzG@f?uU``0LzBkS}s%t9*Z|mW4V#dkdLOhT&*RmROO}rMK1U9iK=qk0rt(;5z^$_ zS{k7KBDd}8hxg|m`(CdF96yG?gyk3`UH}v;-%BAf9{2GIRs`qpFaHl5R>wSWeBam3 zv7IG_T7sT7o+q_Nk?+3{HRul+5+&rfy4Sc};lXJ?`$Nn=zZblG&Q$J(RjPq=ff-=9 zW~aplczKn#bx&VNP!uQs5KK=InNN7;VebWag6wvIlzyH2g-L4cxH5SB64Tx<$NF#nR?4)ncej6~MGm8bcoOjWbYxsayRM zS)YZI^AK~u-+*4sl<_S4fqfQ+r$~I<#C_6))vvGZn~AJNs(oX>n&xNXd0_pG6>6c z6LfcL$V)t&*t?LJ=o;9}w_Y?phQV;1N>Zi-|854>4v*l6Q8`KP6BnBbJSF^01J8J> z{ImL-TEsSjPYpZ8lYca_1zG#sCR{~{6yGYfN&sW~`G;7()tteg9V|R`XTHmmM}}KS z38qcg-ykCTe=pW9Ogy0L4UVnxiNR}NSz`?ixa zFf}(eVMZUS!u{6to!3Z=fVsUacmu2DfSVS9wUBt8-MQ=e69%Cc{Wb*GbwfN%jFXv} zXP!Tzw)sSx40cgDfck!myIZ=i&pmXeV&yNU%ilS|{lW^YrNb zSE<};{Rgjs0QMevS$cxFFK#40{^jMihEL!&#)je=C*5RoteU%uP44uDM#4!@u*=spE*avGAhUjmk#7($!d+RQ^H~K&{CZWoL_%; z;UFUyEP4BpSJW$w0nS*1Zz7lX-9FqfBCEoZ6WI*{ScpXFqp zQOF_?VHW}>w{(p9aJ!kxVZ^{f}I{1aa1#Q`~ zUnLO9Q&FqlqWow(+ytMq?l#Nb{Sy5FWGM`;1Lw;TccpIy+<$lKXpUE|uFr6_-pjWg zab7X$-|ORmQrb=LO=OODCv-o3r;F1Za$9$2tdiUVC0gFu+FZ~~G3jd7*iC9FjFhH9H<|E>|&7xGe1y`y7^wh8M7 z&b`9{tuPscA~fZbBL)x{2v~^^7hBet1T1RL*lLNPS(1<-1Jx zMd+W(w$KcQI_jpA5Eb5Jpfmo+hwJ^C?;txsviB264Qn>^-YtD(~tDYbu+U z?&KkP$dUIq>Fi23+Sy?d4w4`&M;d+5F8q-)KDZ@;0Dqz=iqLN+Fh(tP(l*Kv?>VQl z1Xn$r9hYi)jCjlb{x#wfT(Iz)jM_yKuJ>fCk|*g|xH1&Pf6-B2FsQT91c3`Hjbn9k zkQ^e3ImL-hNeI8w#SWbqulPTck;K@9oOi*s*>_6RWXB`ny%BHArv)0Lv0u8Rsl0Vp zt9o2x!7>)tIFlsqyUqBsLsT(v@)i@24*S)*brFVG!Bh2BDY~#Z;BxKaGUkOP2WV|g zow>xG76}{vIdDo?Q`U#64aqFk5*Qa92TN|MuI@m>Gy|$J`uTl&b(PM!w591kCsmbk zG0!H`M1ahN#cv*3Qcmb9e6^dm2_0z%qf_{na!i@7+P7hd*Myff?!)wrO8&`@StSff zIxCUr&7HMpzX0VofjCW&NtWG1B%2@GMpm}tpQW1{O2ha`3~~LOzc%}#>sOE!?C9sJ zf7EP~!Fqv;>dx;3531r*n`K|89MAF^_$#Q8GSAr($fuu1`bJG+t%bMxMxvG$RFdnq zCOGC{00ALBtwV_yXf8b_g$a>EhH(Msg+h91EZ9GX_%)cfbwQ}Z^-AQXUrsORw$SlI zJ>(MuKY-AY&)g}$G)1v9J|v$IG5!l|FwNI4U(YrZn-CUn9@72p8AGP5;$@Ba`g3w+ zpBJnEj+rBbk2N#5)h}q8=DP)EcsPo5s1IDkUZAZx79N1gLgN_xf+QQ6AO(fF=Gd8j zmYJ&2qjL~kZ#OU6hXnG;MWO$?zLvp6G>9Udbn&2QRqGuY zVwV<5>ETIdQr=q>eB^9qp*d>_i}u8SyISSmgZ)h1v`JwH0LLnl{@bINv}Dm4YFR|& zg(|Y6<`#OXjo17u$`6gmkx%tq^ho3jU*JxY0<-%AljNDMJD@$VC2QVsDBtC!eSpi&OefH9$9eJ|Li z>0LhBB#a-Vs@V`sj5bx-yC)I&UPVdj&4ytg0Dch{K;8asBPdkto7XlR9PIdqB(j4P zM3e{yAWFXk!*A|U1DY-t3P~Aw3f5q^oP5hIkH+(NcHQChe|dGT5;+&B=Ew37(#>tKO1ezQQJ5@;r3jcf9oIZy_s207YS z)^M;Y9O^GzVg%bCwYF4bLiE+i3}7E0q}UkqS8VB_aZl-;eG7Y;`J0xc6u3x8$x(BBfU- z1m8%M<_nciDs8dpLaNRO*Y_b_HQ__Yp zrbY8{45(rB+(ty%6R(q7l|L@ z&?BsP(Vm?T`%MwAHf#tOMN4WP?z;&U8>)MUoJD{1zW?w+xhrNSz|-@1G!)I$*yOld z4C@38H-TSydf-Rz!;fvJ&dP}Ts)AqK8>m>xtC{LY6fWD5qDGK1BVMvKk5&v?PloJD zJX!#M7pN1*zJZSHINF+1drw$GSxw3*V@Va#R9LM3(~u-Ou&O59KMs9=Cu7)|rhtG3G(sn*xUa{5iVZNryyS76Sc+kygjG zmGN#F#3r6Ph=U7PhSTm|5y|mZuT8Mr{_WDm%4$rA;FqEpa#R4+B@shUXONrmXM=GL zsNnZ{ynKEoNb`hSnh-hNeT;|kq}!TZTv26mhOEHH<)?~@GJqTjzz&yx?@r9o7qGfB zNir<-Z^Q~JkDuzyK*=E*WW2I&hC4Bl)05Me7fiOv&TM$>_4Mau)7c7RKN9nuRv81VHl{Bhse?yTxLJ{QWqFedUb=W_lO=oS z4yF}BkYHwWNgQBoCY_z^qU)&nwbck?qDjI34XT7d8eDnQlk|9@Gk|f3U2;@H>u0%X zb2X3Q*{rjaZ{lpNC2M)iXL~MKW*F;02$}4G6DSHXnOvb_Zd1pzc@tZ{HV$ly(Eb?0 zN#S1$pAaKa*#4bye91oV75=`05>F-pHi2w!1?07wZkmfAo$bBfi?d>?IK-%CN2bYo zdFt`G&}{o>P5D3n*J#RAbupCh77Yn&V%Vo5=Nrqm&%wpjC0-}o39*wU0f``IVt{-_ z+j11jo-thSmY4>2@^;in7UDpNVz)zTGUw76cM|+`a@61~a){p@V`;~UG`#1B%z7FX z)4EabmfC{|WRo+LjGIddxQ5|sG{9!vI-OySQ+v)zs#lvV4{B;gykUp8Ru|hjtb<8b zl}Ic6&75=rJpZP_WX2y0uNl3@wJ4BzCkE_veu2<}l1){$^t(67UM;M##W}LC5O)zb zF%^^2y+cuxC;Luo8&w_qS;se^N`>JoH|>okM{j?jMS_#WE4Qer8=6Jg70XoD7jyeC zpNG-7^e1ddtztWO!{Sj>+QkYH=135gS;zJo6TqpK9kY!JK~kKLgE${qH8 z6P=}(A~yMpgq9B;vP%%1O+#_?v9Lu|$HKAXD4LChU7?lU?~abv-8>^T-{b2y*?3+m zs}5QjOcb)^VJ8)y&5geAwb0favR0I&KBOrl1^_U*Z|1XiXnTvM@deZr#RSi4Mjc_! zh#~{R3=*iF3cUtdQu2IYC@%0nqhZ;bnndf_si4z&HeH=HTg5iQ;q34h)^-XYj#PjG9fOBwx^Hn+kI zND^_6hpWT~Iou56LkT7)$+r--y+JC_=vk9sQte7Eau}h%gw=7`-Lg$JO-3Nt#O zu{^?=D4696y^TifAm=gmn#ovGr6-wB#+ND!a{l7N_4&*4QCk4m4MD1F`J#GpKcRDv zHKb0iVYUY>I!MP2g?XZT-;so)QN8xpC?J0*cWBt{1i}zF-J%wq<@SFvCGp*YO zF6?w^S7qgqAL(d zZy?|*?grZ~fD~mdti(zP3Ipo?-_M)~1C~zpC9M3qt)4;YrYtUA8B?8KpeIlo^{z?> z%EOJg1;R`+`fu05TqPbnvD-?+_?et`#v8Q*io$zFe?M+#AuJHH1$6K;XGr$oR@dq$ zAut15HAY2W&_OM{`3l#wVH}w{NKVX$n>>@$fOVq_EKU}yvJ{#e4H(!)4`G3t)xX5l zQRR<6`i{gJ?S&mxovTU@yR=<^N5Il!8<1Xp@L8}ZweQO4l!Dq2X=l+`VvZk@8P$dH zJwkgdYBg4jL_E?)-P=|*>+S#Q_KQ*1uxg~<_T#M>ic63nCI8DbQ-l04D|L8Ry9EQtrd>!gF zWQKA#(Rfr`)OrL^$UfhJfM5A-? z0D^6zsGmXAA#z%q!Jx|1iX?zR|Fo*yE5d)e-|)*(rp=FGMF!SKlj>H`pPf2aWWssYi z%}p6q{Cv7l?Js8brz!-K-AG}xFl+}FD$l8e->nYm^9}c=s7fG9gAB zwZTq+EWCC#!d091gx>CaY~?Fr!d@4%*O@zW(i^y$uFBu+|0O*oeT<~Q=u6YF9&d$R?S{)mu^ zMc;wnFZxeBO+IJYwJDu!-|qU27B0#um=0F6&M0T|Qyx*3gA~Z+iVU$5n&_sq7b9e_ ze*?GNL;$&?y?c27f$<_n?gPYY4niR}Y-`F=+^%61oSca{@)CAkeQ1ecc@W9IF;#lO z?a1oqe8ck{PS^U3PlD5@tk#aXV5>MA(NnASx07|>4Fm+V{j*5Y+hhD1KDOejch)$R z2XnlJ%;8Sl@$(vS{0NH_A&Vw;VfkGb$@6m`SDPF8%{P1kB)(-b>r(~6(95^wcsg8X ztfgt(AqZ^a$Z?U9p+TGgy!a;X!C@T=2o1Q5zs%ChjYr^YV4U&yaoc!g zQk6%lU@_sLKL4mVto-I-$&7IE5QzZJaB=pA%%SBd1>i2W-eNLG(h=Uqbnj|T2J~oZ zgx+zqk00=w&{Ez)M_PJ@WkQ@wnZvG4UR#waHEpWeWw7i}U?uW+_@l+V$m|htLt^?@ zKEtHWZ(ZM-VM%36dTJNydBBmeFWsbTJpqBUYnWS*btfEknQ*WZ{4+7w_}@aS0yZm zA!A6^_t|UBB8W0SfFA#6MIGA`tZ1l5cI`>?6_2FQS6C5u7_3~`b4$EzpK~39@oP5l z4i_WGZf-a$B^Vz1$h1#g0(^8guf!R$=Y@W}^}122ko||IAatlKSHD?c`rq`FKpQXL zezZbvjazb|cpgz4IRBD=H#`8WYEk)@CH`8oS3yfjUr4+z>s0$vy&f2nmbTtzvM&#u ztQ~Lhm$8(eYm2?-Qi9CI%`F|wNZJ1MhjJXRwj9jFI;*P;P+GQOpHED5u0V)_Wc)}j z`fgC&1t%kFu)Zf4yrdc5p&=#0GJ%2&1l#rKgSLwLpjR9kkR9E=J%Zi&WKx{eGC+_M zWV_J3Qs~C3hM-a{N_5%PTh=`=^JGb9fW505jKY6kI{)*T19JXO@wemmBorVnCj88x zUr5FEu8?{eXw%G;^a!Nq$lZ4!phYj400Jh_Ev`93@a5n3nt#CIuktU={z81~N!qFe z61Uj~IL$nzc8a?8%e;Dld><4LR;#HGTq_clT9PT}5QHdn7pP-XvMcvcmb;#7)29{G z&#?yI<(!QDyX8mq37W+XeyxcZVktJxsx?kjOj465E?rdZ-0bW4{v#?Qv0cqJNicdW zi?uC_ysLJJI%}^|S~fYbeefNPW<)*kGG1z|9C6DtEYZwS$Rsk+%|aYD{q--N7eyu_(U2VchWUa0E!Y?LBRG{GA;UXCU^4orI$OaY zVvF}Z$HS2Og;1s7PQAl%=F5QK^6wzXxD|_+a}Ci^cv2*?yrDa-ATPYOmTqp~Hobrh zu7caW=G1H?mFe2@TyDc_&FeH!E$3{)Fk^B=S0|#Yfiv4B;{?mm?dGyot*4UTwDj7W z6@FlfjCy932U|`gj@+_1XFx!R9jFKNS{w#*_?r5PiD7R(-1c$lCxWIKyX$ zX?C4=K70{7ZKa#T2oP`cn@?*I)!R&*C(mMW-;fI9c zoD@-455v(+tIW9I6Lv-R8s)dP!tgh3*)E}Q`C%(HVsQCa5!;>}{GX65*hYeHTK7sP zP3NG|UU!35+0Fk^X3cw}OaU`cWX%3IjtS%i998hWS($Dl9W7pw`Fj~*A62En^2>Ft zEm9-K?e1H=zR&}KOos*EilePz3W;qzzlYl2R$kj5M~dFb*A)$Q`Vc!~O)hf?78KEb z(okYr-d6rgq?KWhUsW=rdiipVQBa`gls-5TdfT?% z%qehZA|CU1ONPQ>M*N;`p1`M`!k!EpYmcX=M7Jc%vj(6jk(C2caWzDw&uu-E5A2G_ zV5Lvc6eT4{PZATQzv}x)Vp^F89zJl zaP113UudV#QW9)YVaxe4s3zTRHRQiJ$s=qQvL%+AoW`yEk{=HoT?aL{et$v6pp~P3 z@h&7TFnynXC3Cf=v(ng9S9+T!y+ww+oCv?+;aO5SRwb85J*3jMbf3X^Q%` zl;X<_wnN<+p8MPx;9X9q^0zGCwho5Z&JiJBe$`MY?4wnIozp>mO9l`+!_>`~hx!?s zNc&oq>#y8taiYaVw9M>W;82pyKLC9mt}SjnnK^of2#g>fx+niyq1VB#pzB<*Kx>9Z zz+0V{JdHBFnEKQ1aEE8Ez9?MuSyP-ZZZFbkz3Dmq4<{1BR>nHu)d_EHvZ0C{jaNf%WkC)B8 zOpK_QVJOrCrc2||xVSjQ(u}ch#kFYUiO7csoCYvGlh>!ssO0A1wM3-s&%pnPE1AMF zLi{Zv%mne)^hOO^ZIH?C>)@02_3Idk`49o=L{5N4l^wHEc%zL$O|tI8@aU)Tz(_r5 z5PsxY>udxOz2ocvl9LEqYbbJHYRondDYqaZa2gE;JdF%y~&T-UB+{KjAHQG?_f_|Z@J(8uT>ju{kLRV zS}bT(D12tKDY7W;L*~qwhj(?$YoL>R*t8b2Sv~Y_bm^hvmg>A^n@wm^n-a4PrO9}W z(pVW#6Rvd(_>u;2emgXs#&2w}>V zz8lA9MC{gENczj4bU*v-hh!fk0x}s3cVtzlQ8N-~aaBIqlD=}lW3uviDRY7eirZ{i zDWJ@X6*iH#Twv|iBtSdjAWB+)atSz2P==`I$Or}jXu z^anFlfd(Zi3!D|!N)7L^w4ZSU8Zcw9Wq-reVuzc3e+Oka8KV^|%u=Ba%D|KJO({Pa z=bw3SYrN;d{BfM-10v7}p)ag`Ud&vtuBE8@=t$*Pbi?7>821;G$Djp%b)D`;LV(*Gb$21$W?EGx|a1d4NSt^)GBcViLf?A~11{&H3$mmM%gc|5cEUU5!WsyWW; z5Du61)Hx0l^3OA&f}(|iCNJkiHMy2e7#VoEsh*0$Vs!y66!w*p>tjJ;JbdR{337|ut4 zCeSr7UZj}Y4nk^x4CYGCW)p4>%+!vU2WBk=hc;kjNDf38(8{nT3vSwj1$AZ4kT1yY z{m%|j8x3APVY83MZwNTKjgQvX#<5U`-(y9xvBCh)eVxP^ZPqD~^Nn!lPGIxUU}LP1 z!Vme)>63aCRx4Sv%-CrzM=zXxxyNw51m|B(-(;0o&#~Xn7`~*lT85HAL}i2A`mW`3 zoO5(K)l=D8Vv~!8XPTliFn+2Hp9#j$nTV)-Y#HQ!#n9f5{=Q;ngE1R!I(q8;o!KW9 z4ax*FG^%1jzthaWE#0+ZXd9heZ9eUN2Bi_Z`I+slRCBEQVvjt^gH{_829D0r1&osS z-@RZNpo!z~WNypbAQ70(vF)!+MvqDA7))mgxfeuB5CI=L+2Xho=6fOO@iz1pki|TP zCl40w3n)07owNDUP>h|^vM5>*7`JWPwr$(CZQD58wr$(CZQHhT^N>`kl85}ns#-JC z-Ip0Va{Lz@GdKKIYhUjD|2*mDM~AJa6pBu`zDc(yF`@t(BJJTsVNBO4JLbnzp%@A_ z1ef(ZxXb|~jc=96=hFm!n3kD*{!zC~;cEJuV!moQ%R8*jTug{5ge=zOv3w9^2>Vpj z`J)IwDA6aHh;R*g^O(@Q`1_Lhh0lBpKL1ua{qbH3I*R4SV?3k28bGI?kOtgdc_pUHTr6n7af{pb(!~vusJb4tihG?a-Xh`S z+``L_Ln7@RKA9jXNh9RvW?(1R9?=m8=(2rWIzAz&;LSB?7s)^U3%#$J`R@HSQH6#V zhY?}{-?N~r+3$282kPtf!#>#Q_`xZUO$?)D{5QVl>Iz(}<$XI$m@e$VE<@oW!u7$M zP-6f^7W>~RBbUA1KV>BABb&T;h^}M_CqVviQSF%7;#xpK-xZI)Zit{+e~2zG;x2GB zL!e-YWa0HXQx-WCczeb_Zbsca7=;fcQiGEYH8yEG^q%#X&G-Qg=(iFGN=6FmTk5aU zwElW4$SJ&@Fhxb26*BdO#tKOXBDF)l)V58)yEY>(W+VEvnG+nhKZpO4Z(wxUOwekt zoXWi}JrnLA9Qxg6H2aLIU_JgayF>py$M#!Y2iE(xO#^D4(n)@DW#|`+6XbhF)1+O| zc1K~ZHlR9bIIL3sB<$vyZh9s%xxj=1PhhTlL{nqMMqL7U{@MmrSF+aadQYLFB}W}c za>0hH(y)3;Y_Pms!Xacu@DPW3YR?rtB>&#<8i9wPl^TK5W)GnJ-iHi~`@(?8FP%W2 zUKFxf|GU@@gB|YOX;gju57v4;p=?E;BVu8uB;7t623t#t;iCqICFp$FQ79{Mk|(TA zyEU8Z4cQ04RQ4ll#Nxm_P&Z#1*q8~2=9BgD+%w|}O#~a$p~4bSFtyS$ zL+pVEyt9oJq%VIDGpN6f1t}c&Xi8`yy%uV`M9s1EGFo(r)*=@niR?$nNV~VZbdY(5 zW|12GSn1o)sROFnfESImRs1%5Ep2hl7qA+}d~u=OY(8hcSrG4i=^gB{?r2Lgt%@>= z7!UwoPV~2qHy)@F8LA}CKSI>jmrNxGjg~yDppK00v3%3}X}CG7PVm$l!9J;A>sf4B zT!!wSwPd}j=Q8W1`xo&$IjOSrP32zZ*U?lY@fJ|rQz%%S=G^dK%Ho|1X|bvd%Qwl7 zo-O_n_pVMD0&*On;_9Jy`bcPoRcqd`sU^ZZaEI*%xDjYd-%80Lp~U?R-kLGeMb)o^ zG;oRFU%dS~vQfS>TW#BJ)2{i1^Y6MRPfJ%Y`yZRc$2$o!TqCb`*q(ZVY5}a<#>&8M z5c^^M%lSJ7$YSwO+*EQDoPzB}lyqP?519pQ=U1f%wPLFIh+Ldbm$v|#X2Q>vDn<^$ zMuWwHUUrtgeQZ)~ij_Mw7W?6zf|{3QwX(u~+jjA`n}fo2D%i zaN8@L^tSUID4JkHte8#sWKK-PkByb4qK>>SBNBD8gX9#!U}8X)KxXH-gmr8RkS~Nt zH+MI6pYTBUU%byCf>ZxY)uLfZn^v|KbCjTT@H9Nya0(1VO0P>ZBc*}id0++at)5%} zRKri|LI>9a)>}TxB%uxtZ;INgaa?ZeORNUDwD@Rr|JK{V|8>M6DxzCBp^r*CTwsE1 zr3$Eq60rD&X7|n5L8_-6L;vDNG2Bgjhv<~D68l#s^n8`TnK%Mup%v0`=v5P$}G zME}n7od}j5zZZKB3H*j=;e!B{|69i!L~elUICJ($4wXk9)BLEfVi?f&(X9X++HDaS zx*I5W_06}<>%`6X#_GaA&Rs1qW~gB8=O~VBJXORAaT5tozy4<9n`Q>J9p&kt47|zM z$x|Y;ON(`parF=bZE{R;VQk?In3;B(_x|S4h{~zUPPaPzBf<_=``4Yu&~~v-NygDg zOzg3KW-bUmvzP$1@84-leLS+6d$`fH*N9XS9(wFxm=V|3k9%G@n7I@L4Me`)|xeorpf7fkOJ^MuZD zX@sG7bxr`G6YFcjXE$&kPo`VIjf>Zzzvom&e8+@$rgTjra%ivpm?-^yyRj6e4>zgw z$mh<1qzZ;Mf4l?Lk3GDs%SKya(7IsL_#cl*VmqNvUk+bGNf1FP1d!^<{TzQ={(&ho zEu>WV$2>}#A18uD2KlGFKi~qX{=@%AWBk8p`2W=yXJq5#_+KS)CIY7aj(Puga{T{n zj59H^vHgEH#ygE&ig#MUuv~1oE7si>8#Z8ml)>;IMQ{krk? z-BTpCqWzSizWI{%Bz287WLXc8FvIce7_ zULNQCm$U2#$c7sEh^-pn3s-M75Xl*GmOl0~_#4<^sTZBLB0;lNaW{ zV>*%?`k%~kI34q^7O)@20c?{y{YUl-zeUkU4eUK{kxw_~#$-$()f!(ma<-3f}RGAq(AS$K@bpOc;z8U0q34`34IG?wF0p-08z~3F1z}O6; z*$v>=6{+nbZSk@xB~LD|{pe47-JBTbZ?yS4jk6yR;#=QR<^}inAw^Lk9#G>VOOqR@ z=A{Of9~SooHwOp6>(3xhJ|FR<4+;cGB>Vd2?==3$KIP{x>$FcAz4b#UqqQVFt#``* zHodncDfp}J<z?fNjfA13a_ckNrg+IMIK0CR6-QCnHLw-_o zX=>BQZ7g$OVdVk;s_!90#MA>ITgL_f%?u7;iJW(&I!45%x|*-bE#PO1PqAr^k8^Xq zi}9DdfVSFJr<(VdzrG=(CE;PO7n^f~sZt|rgA=es#{e%8{1B4!48(TY|p|=Eherg)y-81mTVcs1K2$48y*SCJ4d1mMO-|*X{*f6wJ0dQ~t^eKL- zeRJgBqTRXKzZ$fDiYmGnS>cdZe#ob|l=PN@n1qZL$birs;Cy@gx;wC`=LsM*H@5)r zC-R(_fZD%#kp6K4+oSgjpn6xQXCREN^`YM#DKj!MfT9Kc>pl@P0OI>S5j23JhrWnw z0I{Y�>rz+h17|V1~^<;@`Vaif`Qi+UAqb{M27{OE>r*T}3@Hpb9h7;|tGwvL-LM zH#0gd>3sju7!m$_zh61Ce^LU!{7Db?yMu#lwRq1q*ERrNYGiZ(> z`e)Pp)4$t+f2n_PoB$9mpjd;oG@BTYV3}p#Fmz97)ai8L1ZQR(Rl~>XX|Z38iF`YO z;k21u!ayh#&(;q!`4r6w3C9Z$a*s`PYUJ?4^*(zxvelWLtV$@`D{rYUqrxc2SW}9o z3J-Jfy05f%L4=Zq4r%4xgSh;73dTe(-ymz|r!P*xjbkN+H(%X|trts=sjZ9Y$1HhJQeUhF zg(z2VGc3j;igEsWZ~b&qz_V7E9UPVjZ@VwJB2cpGB>Z^A+u12RRj%tB5^z!^eBz!Z z_#x_Uz|{}eylt9boRt=i__am8R@9oPEWcE%+lNR#(Lj(ZLV<-gSPEZ6h$&Ee z&@{-cK2wOCN+fOj)M~(R73US6{Y$Vw|$Bpd+6fDUjjFm)h{tH5Jb60B?Tnh|U!61XvN zTIXha&`G{ML5#Mez?qiBKuoZ1C(b--tB8xv2ZVrVJD%50ahm5@DB}WFxmN)0kQ*$)i zP(~aeVJwkGA$i6NTLI#9c5q)u4-}Yw{06XuW1q;sTY&1A=)oQ1^1^L8l*k~Fg3cgg z5JJgJIBIu0`^*BTG>VyfJt>(2IcYjl$t|q_-DUk zu|B3Wk*^}N0 z)R;#dHN^x46whwo%;Vcftp6>^s6*M@CfvOo);~pd98}NTC$Xpmki1q;8`O3pYUU~e zaO^$_$RcE?i>HVpG6V`xaVjzsMcWy9zl{3ncw3#!n8MkxW;K8D>FzUD;qp8BZJ-Td z)10JAW8ci#v`8p(s2Cs&pvf++c4z;WWhh?F)oy9wAk6NAhL8wsdU0 z>uXSMn`=-&x>66q*e*O2FzZ0E;xtT6yq2OV^j zW85{UmChnVad0zI{&X@uJ#}E^)@WXwA(O#Lo6VW1Bt_C)$Wo!P^I*wxqQX* zYCuWdr+kfh6u&rtLgZ&ZEC!1XsV8I9UY(F0H`V;b^7CxokL9!;)-l0#w8#W)MaGGO zHY_azy$dJN5S7BwEWZmmE8j#rIjW_$67US9Hq6ty023A^vGfR=ox1S;eUCMu6fz1q z{tlr+WygX{<4|Ho3`1*1Ku8lcPLK>4dFbk5`=J?BrrJ#weU2+F=dBBD1huzuwrV*| zRd8Fq+{MsCuue=-_!nw87LPSyp=2V83eUgB-W0-oA$qF|tNkr*M7OKPB}T_poMyvQ zatRyld5ToLs~8`?T4cx73)L>&M1_cq77M9_ZmX$g)M1PcLpe!qUMboP?ahVD!jj#) z`A?bACN|DTBk?6uwZy3SDaes8nTYyZt;PDSCw@#)yENpqMeo<*NJTP|nv5mheO8~z zyB=kXGqUXM^TC@-o!w9~Of9lNVaWyJGWRoECGw09ybMYvRy%_Ym6V-&8qUZX8mi5? zgkKI&q0bqF*v1NU$p4KniRVDU!2q*${rD4CyoVen@V^}y?@qp2ntz^VSkCk@EFu6! zz{PYaHi@hevUg*}))!tx(BRQ+dRNBF|T&C~mP;C%}I~uQQc# z3s}?!0OJOz-z?JN=AqoU>M!Hl&n4oq=yib>%fs7O)!+_qlp?ZV6`FPQ2{_CL3g{7R zC8`KU4)VjFiN*LhL2QQis4W07D{msJeC5~5uXbsaciL&QqH$o2Xk~4z8C>)yPf=D3fb4u=6ql*6Ah(>^&CS-c8vSbH0II(W40 z>!gg{fkC!1eYcZ19^8zA37Vqp#&(h27H{eIyfsEI$!EweR6=2Kuppgvv{~yrNh-oN zONNo{jCSB9$dw6p3@g%mBcyW{jkh<=ot^V6EF=c7HyU8BFRl)m+-yQz87C#*+SopB z^~`eP4e1`mMKsG2I-tHh7l7#RA<5)Bm7$`>f}Ilf2lKeG`e#!^WP|dsEN0K%O4o9o zTl4UR0K+yA{weeJIpYc(_ta!hg>d{{#2GO=7Q2Y6=G4{1Hg>Wm6+KVX{$BQ^ZureZ z_Vfe447jF&79?((_&0>O80+Cu(;G4hKidI@{HONfgX@$oU-ZTZON$XagHcLQ$uTs(MN8^7)U7#vC&I{Rj2TVDUN6~NG`z9EKbJW00m3& z$mSJ7j#a4wfh5c-?RzIME$I(8-?UC?NaM+HB#QbJQ0l!8p_2vd!NiBV?vC|UPouuk6fN#Q#xQ;z@2 z@?>Y+Ow5d#3czBpU}+NNhtf2^)NaLYOE%bYvMnZ4pKWMZWy;gBno*=BvNc{o3Ho(A zeq7U^>Nipujs&h#wO1Dkdc(b)hEpwd@{?9J+Rl&|Ry-929a1bJjtA4ha4TK5F)kkU zHaP7d*2R~bH9~uRB*{>VSkX$^Ase;myB0;Jt*(=OdN=dW9tk%xI2tf5C1!0;@OKdy zzkrHvsBM8p#u)5;|0GW2TQjWCoR?Bdpf{A9F3a`HoKQ(xI%}4Z2LY!P@l+R|wO;wT zH(JNsrRS`KdBJ+jt&|yr2R2-x!j}y}VhIIKwehsz ziccfm#jCQ|#0vzKlAbEb;-Xe^s%x4;aa=PH<(@-RXy(H0(;2Sa{SgRIe^ zsxAttp;u_;nq?ikB~*6XVRi9{5^kD)5QeIgW{&G(k2Nac?Cqvc7uLU4JVj0zHd8Vv z+$1np^|gbmegzV6JjqatP#(|E$TlJS)KxkV7_A)URy=uKlP61=qwYoAVMnRy9{E#J zX6rM%D2H1n)a2ATu357bH*uA#=I?$n!WPJF{W`?c#GAq$on`4tG5V15k(9L~Sc#)6 ziaGNoQfzD2&>V>YD_h7|Ac%f}O)Ka&d-=}>MmEN_OR7@Ix{UW#UP_HaTwF}-+5R_H zk#{S#3=2KQcPVR1=hO_FD#T}w6rKAb%?GT=lyo>4DyOuX+4;VDU z;BIji2GJ7wM6b=2lID;EDIvySk*WcC!fbxRQVm&_Of~}>(oq^5F)|GYuTI7ub(evs zzq56dblpxeq0&=*Z}GNpA^#*ykb4xVRq%|Il{s+}5~+}pfb8Dr=R&{!=Jx%5nfTw( zMoIYr`IM}pSmtp{L>>MaZ@;DH689}($LO*9NlAW*&RdwTVK)ai8Z{Ixm35#!(#?ux*I^CeF0(v1>1dlas0Sli+4e(CKsHei`_fj^Dq=3pl)>%8=J9WZy z;uy}91n-KFzYP?NEU6%XCNpS9AyM3K0?{3^f#0ZXdF~8b)=X9D9l$^^y1Ic91 zcvf-6?VmPXph)g^s`fZAIn*^u>?JHs?e;KIp0dxp`I zA~l(Y6|{SdxK{bz_amjzoji_W(#}XG&JMzLPuRC)D;~uz!$ntbrVUD^T^mJJoUR>9 z^(XSo1HM#^3{6&QN~%ZW16GBhVR;3e1NtH7@M_EN0pHJMhVBl-9RcS)Dn295yi>V( z&VYi{i!-0K#|I-#qkN>gMo55JS~gcDsgxiG@eBBc3x7aDW z#TdbG%Xdb#-eG&mEov}ZZQ7azb(pH;-p1wH%0j&e1S}KGuzH{2!&SJ6)GqtD&}zY} ze*0_@(0n#ejYvuod1FkA=_NK9RG{u5c8PzGaFJR@KrjP{-C;%A;6Xzt82%5vdKwE?M+75?ce$?@x3iXc*5DDFyzD+&&Oy2`!Zetd ztGq!g@O&~p*kc3Fe+*Q&`8lffhDASv^z^2Ge|of-&y3%=X#(2h9XFgRSoMKT-d(Zj zJgv5=su+ZXGK7SZPf7KHD8LhffAeuolbdB|LP&3)&;s4!<9Z($EPflkO<0srI16$Y z1gC-@&EXFfXv+gL2|k*g2NzfGG$@vB6xyA4>FI`w&=`spOjhh-w;+P?t1JJF`y)iu z)}WFqu{_#0!jtnFrDgekaJTU-}Ka%xoJoveA}K(FD7KJ7$kbw-M-_ zjb5lPmcF|%N&hUWdGkE(KJV*Jh`=~kpzY)q$MV{EEh+}1Pjrhmw|zdx0N;M^TEid6rh^iit#Y_kh}Ym+J&% zH!8Afg;0!Oq7^HAji)$(_i!M~FJQJKBu@&vzQWj%#VI?Pk)$r)pHj6!7H=P|a8GK$ zFR<}-=d%sfr18eAy?C5Cl0Fww-g)E?roMK8KXMTK`}2SETtkkdfsFc@OS!JFIlXsJ&(+Kwq@1P5j>H^ zsI_t#f#igW<;kJ{=nk3juJ4H%nI-JSp4z;TK>iQsF&^chuP&$deEpowI>9I7DFf_| zp9!>m1qUN@J5icoL@6T&RPrrE*r-_w-yam2#Iq=}qjBQ*Bsr|*Z zp8jUbB50YWY^o_Wzc1s(q7iLKmx2uci+Ft2>Z0Md+koqL$Xm?2A`*89Zt6I=FNt3b z18n8qr&%uww(z6QBwqicl2>H@j`+rw_oFr8%C#IJ&!p;MIGbuv4Pj?URU?G9B(;>9 z#FLU5>N^2YutB4 ^i0!+K9VHrar?0-xk+pPAVAuO8C*LL3Y2b@UB#XCI!*Nab4# zk!W&pEgsoM_i>{75P@aTU7z~BZ${r;KA8q)2lU6y#dp{w@&oc58Wj-gNwnQ1l z7<8^|4L)M(?KX|R0ej~X_^*3vPM`Id(gxBgym)NCy1sm% zdb$V1j5g>o*DukvQ|=Y=Zj3jR!X7DJOxbN(Fxge7#XRs{eIo!lGmXfm4MgAx2s=Zj zZHhCK59JGO&q0U(K*(gzSM+tbW&qKUs{Iui9ayKKN*WpSoHa1s;3)f_5s%vLA37yz ze>?1Y0pTqINus=QS0K{&r$S6`j$nfQloGTQsI8swK}r}wg_cNb0-@73d@2*n;Yk#k z)j0F+3PikRq3lA4@IC2nX{iMJ;P`^Q6!`C?aIRensqy*k&qNod$3w;G%W3W&Qr0~$ zS@<>&Q9W$&)_RWo;qL}b!%D*2Yn|YaO{%@uXCv5ev`v;VbD_WGv8b*+wkR3cPp?V? zD!*Z6hdW`Ll71bld5ElHa&2VSB`ERXu5@UPQXvmTRhQnO2sy#m z5sQIW8oc^^GLYa=YoJAtE^zqtY>^|kH4M#I(a0UuCYKYvG_SVrC>$tcr2tn8r(k0Qv%(0IZlP20~v_8fi5eSlQs znEREii(sNUAe~^wf!;8uqvOK{P(*dQLlJ+n^ETOk3N5l-D^_MMXLvio4yT%VP+j3f=RzgAQ9tG`LFn4#HbMUsa8##S{>Dyk~=lK7Y zSuEHnilEf%DOB$JwQeH>nk_(&Qou$bm)1bX5Bu!7R z1igoZH#qE;U9K3%CiNGMyW#1@7F02=(rPGeC3pgp9v#|9XHHfRzdx-chjK2iT>*8W zY%tdo6wCEKS5m%tkmR1?O)nCw#T-Jkmv|ivZ-X4J>V4~GHo}F;6bWhfRaW$ISkm|> zJwgEu|Fu;5+~5*3`W~=~1nbcczxsU{Se6S}mcku97?aHN$H79bno;Ac8;RGFP+>B- zfE5*Ek~tZ>zqrE&;m{axhWVS}9O=D@fK1_U$MX6X!?F`-J(=xhLp zCpBOG-NiD4Yjx)uzUm3rKo6FA=OfH}6E0wQxHH9^R_rIWdkn<_2eFEMx)Ri8rM9Y)}Db$RkhgFm^;GLH^NfEx|7IedmRT zPJd_`uvf1I@27#Lo8z@Z%nwf=`Wms0$y!FzD@rI`)w#^P!N&HbV;Dii zxF<>i%iq;*d+aBLJ>>Pq>FH9Um;5r?8?f@Dd?`GPjq(g{Os(&utw*N}t>?-6u1{PCFy5ncKwRyMz5u2SFd z!8t^cRB%lSO!Ua~W|t4kDT|jdKKdORx^M22#e^*R_UFo0v(kU&bL(Cji+^iFN<9(= z#^Gi+OFSefWFEa{WpPPbw?d3VXal1AV2{qlq@V6lCqZrd&549TQSedA0%}>Icnq?N zZL4rgtP+Xl_7aJ}QYF!<5N+DsuJ*kRKx_UlXN9*Y8SLY+Em@6oznDjX>{$X{*}_^& zGZx{nfoC^=27~cLU9RIGgJS%%PCtjbEVAJ$+Gt3TE`J-7;wk8!lHcir_bsbwbTPQB zH*e$6N3d_9zPnjtVVT?~3h3lGLYibv*EmBA7*sA48Hb#52~W8g@aNV*~e&u>pFWq}dpZ4)7q^ykXV>U@n$#X`SR z3BMi?grvlQ@MS$2!uAgc>-Lfyp z91SgO-UgZd;z(+TsrxQd{;1B#Pj5Vn2<;R)T71MkM`cK>g^LZwF3Mc&E%kgwgLtVw zn-w^ccyQL_H8_d}e})P9#Hi9ktI*m_01=y5DzR%yj{r*>WcS z;R`H97>d)6O^ZmC1v`MSLyNNILOIWLU%CHkT%hb(&**ETOmA3CQKJg<@k{BK3Ptqy z$3FV|aBRh+9m^78)euvHlrQ{8Mj1g?o=xem$S6Ea1QVDcnMr6FtV#eM9(-pIAbHv( z$MU__;peK)&h}79z<}pjr~=a;O#2uEi2hKvV{B+Xm?v{sk1|e8d1yrii zyWI2Z9feNnz~Q>HJ=ndj%^NVn&Tus=2CTmfn5u-^c^1w$=sTUPz%`gQskl@-&uS>@ zy-S&oE^pyn`qyY}H`CH?q;Rtm_@c_wc*1Z(Lj{eD^%q!CVJ}M6GuOScu@oI;g+HdK0mFaf&*hBourDH)ZDDYd$ia zD`J%d=l;`dluyU!x^yPxdiwKar$;c4|0~fRvIf^0ssZ5k2a;7iEc0Pf*7jvulgAFB ztU52QD4lBW)1FR3Bj4 zBvepFDL{BFEuo`zec{WxpFN}~k4$h77A|$OZD?r{=i&Q!fbF>gadqmt(v;OXKK23R zi$7zR)yKhE%RYVhSa5NCZ;s?d1uRWT|8JP0D;m?l)qcK}7g1IaGB5&a>1T3-&hfLIHmQnj$whQ~+;f#EtRHsh8m`jMs! zIRjW9HuIZ50CIcoU?B=Ncn$VsVz@kiqV%wW!?#<0@Gb?3%A%J`CI-e>(QY)V#TMRZ zG0q4a_)%9HACHae`yB_NX=Z=ss`%IeL~$%K!~aV$iZLBLmil~?wU}nGY6axqYHE<& zqU$ap{aiho;N=&wl&Pf#gI$y&aQ_InKZ^!N?g9U6Myfz0-AxoRQV0Q%@fiy=gUx1h zqAt9_*}7R!N+JS!)N3=&{ZyY$JC7grPg$rItL6^3Of7~PCv{ww#%9=xj(nNK3MrRx zmjABM;hIr4)n`uQ>d@RQjyxb#?+uwjm+ii)HnN@@mE=R_Q_%um8H7>@C0gcH3}cu; z>HEy@y3NeRZg3e}@rO&!tctO0Xr+nA6d2d4{4TDf5UmKtb^{)g8$*L!bBa_B5A?l* zT92{BucttmK{rTqube<87EMdOaH!64!*XuV#-Pstpf;MPj7}1|&qZ;@GgVHqSLbB9 z5+SbO0m93$m^FP`yK!cRR71+OjoZ@g3@7+Z*alp3&Uw5UML?pPM^Ih3HjIjz&&a3U zZzp6W#CQo-!J-2e!_QP(0d1iMU<^BU1eQDR*cOu%mKbnN;|?tw1u^B+Wh7GPlVZT& zyS6?5zYl!W;l$c1QOotzz44*9@vy-S+&B$zAOp%^hTezfw|CaIg#oM}nVfg%Qv>wb z;BBjsi=Itg$qCCjx%7|OyCKLbj zkXY#6PX^`CUC0DqslCh{xqJM$9WO%u`v%L^Cj=pnW<9k1BfGdb96;c#K{{!hAoAkD zzFgm!`s9ji_qeBj*$?i-7ps*q%GUrl6_P58?fVGkd|aDK!*V>4qW3hI;BjLvrC=Ad zWYqYo^;q7OA_NUid;8sfIb(ha zqT5A!x?iV9hHM+Xlsh<7TgFlEmELc-5WJ<4SWEaM(xu;PM#9}xRN-G){z&GDn5U9u zY(!mD)B_3U%Zr5(BNnqlcxI#TX1cSPs|ImdS0P+wsrgpZuY|0Q+Lj-_CVjex2>D!SNlrA5>V*Zs?Zo;VeB&_3Vp_ zyW<;y#6hd}wXMycM^_TDxZboWk6H%J&{_{9ab?vQ_Z&&h5~aRQtV&C%u&_<9zjC(d zsWMg3F&>Gi{@RhLCcW`tY}PGjC1IVVy4l;Zc8)}67_5$z22+Kn_{d2h%Z?TwzbyL6 z7R)pIETT_!KCk;IY(NpuJ zF0OUQv_)fV`XSewT`nv-_kch=EXhD(%R^bIkCeh&+}_(T8aY^?8%R5uHVHHE6P(Wq zWSuRRDsPV7iMJxQC>GaI%!mq-dAb5jOkCrQ`dl}KuXmrg2UW_d1Yx!><}yr*ZH{HUT>Vbl+{8Dr8nr!pW2dpM znSg+3)H0>u;HQK}@(!DMo`ef-V~tP|PY!s4AY2OXGmMZh=f^OT5)&R-7N>t?4K1?f zJfu~_ey`meAWgKOX@F#4P`#jX^rlkbZ0FMFOF3Ixg8iOkzxp37DWw7{`V)SvB z=8mNK-A_^P z-HS^d+1HLoVFn+Bd^C_-)@fk>?FXO(-ayLdbC|TN2}v7YvbmIf(V6Kq)C`Vk2s;v% z(?U14B_Q`hN~_ABDotsAB}x|-hO)%UWq&ExWk6%g{ipYpxPLyrZ_8B<+Jv8pQaZ4nk-j=JARA4>xocN+VNWH}-9zQIdca6op>BJ=C zAuiW0+YzZPk4CMGq!HeTg(gST4C!eEAa+IKp0X{p&k z^zSp@^~~&xALDq}d>xI|yDA10m#ribQd={s;px@z(dFg-%?PNr#s2p0;{zHLBNELy z1OMH}+G}bk^+1h`l&#-g5z?jeOVvW?x*JHL$JY0Io%=Ypt8<${DgNP&$cw{pW1SIU zJb`eFqrez5Y@?jb*YO)vLTEp+&hjSae0(d#Pide^eG>HKQvMiF%PoTAeY+cB-oP>H z%mvxJ;dw-AIAy>{g|t+92u8u4{6>T9o)nYA#di!YZ=+B=cl~jploNUezF zF4|;$i47vzdcfWTtshm_u{~tYy=!qY0Ew;=R9<6voP~eb?6AdLae}`x9%v>cH=_k$ zf?bS+Hy>#a@rkuj1KFA+9x=}*0T3~?65VHJ9|!p9=D~04NBChzN!Cf!vyAo!*O_%? zWiv?|I=FoFc*=FME$6p`gBlvPT8zPd>5!t8UYP0<)6ve0{BR`0g|g*twLih zpkZC6O-&c=+3OZk$_**@rc)?^rNU7M>_uzb-j;@d)MP!C*NdQ78^O{Et&{>^&(o_L zs@enY)Of{WXM$1fi-7R5gw#B4h`$n?J7P13RK>EaDgM*A>qltpeTv5u!I4G!Or_rPDMRxCiNP;$V@C7R4<<$QV@&9BG6WgW3NpAmnRJ!AkBD`u~tAJ zdx4EMD1(ftk;0fGb34!rETjK2G#n%t0p7{nmqleMdJ0cGn<{fWz^HD9RS2L(ka*GO zG>P8_rvlZuT>&VbtvM#qb{l$RaGK>eA;ZJ~v)0^QgPs^{%7bVHJw9Ni)(V|KdBV%d zj{X^V_$4Dh^N3{RkXzZ*0>aV%TS4ODmVf148FQdUVVNM#ka=2;*N7}%T(`03q4zMq zQjvCrPlCoTWOyPZ)Sp+L+>5j}yuxba`%g-dh`c}u4;~zi!5;#I(DGa{3*zEM z6c%!Jta|fp#UmtgL)MjZhN#69(&|-Q_e8XsApp;m7C@$SWzOdK18dgP(a5W2ag+6i zMJb+Yu=<(-&D8`!Y4us!_-75xW%%rBe{WIOhjmC;rR=RVhsPTnfpU{*Nxn*5reF&f zOYGl1&fuW|`Qml-qY4%GwK04&kwMqyiuG047Ym@p6(>id|N6~%QO}o=ZvhaGpJ_7s zyCA-etFwL*O-TGx@}{YHJ}_uE=}U~Uwza%-0vAk4qLczPh62X>47DDlO*!bHBZMfb zml%?Hp7F60_I(&dwhqez6Jp+ed)=3CUA?buB|RquYlfO`4Xe`B%K4WzO6+NT*aZV(#=*(u-^v*<6LNFy4+cX=Nf;sK^x9_!n$Oo z{_RqAHMFzAtb=Y&-1)$C^pK)@TQ3hZ}iZJ;DcTy^$nn6(K*$V;omqWt@4` zoP}j=YDl_1dsSuU%~D@>s}>KcIpOirQ24dxoC~R)xAjy7NjD$R7@<$=+A=Tk`3|nW zwDN3DHN8)-ljc@!k=-a4fy&J>FIwO7_vuvBh}6)SY=2v-vkvEhO;VBQ?RfQF3QDS&m3XHOqC)N^Mh3fTFl=m64jx z&scZ#+>}sMRZ#P}sm4aN>}#NA?V}V2=OwZZKv>+SeJD!!+*hYQ8EkE<@hF%|Y$HFW z<}xvBl()ntZ}aSLx|kEK%KH*KccbgCR9Wwn%I8oW#k1A3pew5tX`FkIJ;){cRh<>_ zD4RPERsILskcivUQb4i--?eqN|5vYmMXRwCBFO2E##k@TTPBV~(ZDi91r=$jYEX-XVSYgmd)^pOl`r@5-qqPA>5YQc!wOQA~AKBt7XN{une9x zN_VTBa9>$XhSGv+=P_LfB^Z;jf9PD%O2CvLmA*iF0-kk@p9g&>@(^EJ*fk;w1k@wF zqTU@Gs@GeV^t=Ww{yFLnN7T!=uXMq%e#4sqxl`wRl}wwFetfwGQB^o4@3lIGIaF(; ziAxgE)i*IGz2$NGfZ6G-53qIL0uWM2&HbSP z2ve=_KgtR(W#z`>7Xz~+z#q~c2_~Cd zc1TT(o)NBa@sR_p^`y+Wd{(#BdCoWP)hPZzaGm6DoZyDhtbEL8NA+ukU*EgTWoc_- zOde=upA&BTx%c~vV}rp@tx2bT>woVzA9k~Hv*PmWNVK0K(U6L(Xr${~Sqk2CP+7jx z52$VosVwH+BJ>kSGmEjciIJE>MmP6l<|CU!3t}_ke=HDx*&MVe!rMdX3G7hLbH|L4 z1CZImJlA= z?R+;x5>iPKBYQCLG(~2~p(c4>+N*UIlJebpr@7Is`{v$CH?&T6Ij25QqV#m2HKC(( z@*>Ib&4r|lVe8w#^!v|Ns`OI)r0ebMVE_1jj=I;~Yp%7w&{gT6)k>q74OcLDS{NZ~ z!j|oeTyL-@DKaPdaaozLsCpYZ)c9pJ==|En`qL^kAC{@$E|bvPI`LUcOe8^OA#Z@< zD_9Y@xnE;iD=CMGDVHO^i0Zi5%^roJ-O<4&8-9inGIsGf!sttXONE2r`(|1bXcDfL56PEtr*H4Bn~0kFx#|f%mo!n2 z@StL{q6yqy&wut4*OOAOwZ4r`%7#t#Ik6dPcc@aHmE0X2$Hq9K%R8Y5!zH?N?%}mJ z`3ZxYj0NV8A<7B$jaH$B&zhzs%rtjVMJiffQkz$I_6oIXt&K_x=5RuLO2)w4qR+Y~rny za*M@#-1Vc9HN@R$>qV5-c-jVoa8l3pKu4uBoe%gE^3=aXYU(}K>wU~4^ni5Z+jBXH zSG`0|O_Ms0hvl?04~x3)a*GKCM~(qwzrm4yh;CNN30Swej9;&h*~oD;$PxcrZ)Xx` z?SjHU(2DU|v1>gFk^A%0H@M_pibY8i;XDjZ}9O6qfrDUmLH5ZaxWT#7&1CqP$#{3U!Z{U*-8QlpdXC*Psa zfDXC|3>VJBgjx>GUbKrS8eZ}VTP)WqJ#g_OPfhr#EN}$J^S_s$xmrc6ejgU}m{lUmjtvz){A^=!bTO`z$ zD~oVg36mq9I&|%lZwMlL8xZx0@`38$HWY8$cIts*wr2dLBrC2LM1$rwaKb-g9wwN_ zaZRTahPTtXALyZZ5Qvej=Rb;n-2~@E7G;Q;tWq0vJBw=jxiC`cRBTR#eEHnpn>%FV z>9mJ7*$0t{EApYz6n#da6Nq2J8u@Z$wwoKyQTHHSoYp|nRJn!(H?BzFIn5XlVStc} z=09V!f#UzF`*id0#fA~IMv?qy7;xBGQ0$}=j73%BjQZR@se+qP}nwr$(CZQHhO&wY!jnZ?U0scciJ#pz|gN_*6dVMl;Ne3S+WV(mHC&?_& z&+r#naNm*y(M@Y5iP28x?;*Sd#Clqw_2nE1=u(0Jkd;lK5wq2vg&qJv4TXCM|{Z6%YIin3PB7lE^grR7;p8Bby1&kQ&(My zZesP0-9)5aw5j&a&aIf+RVg|Ms~{i^q7pYFw20m+cG zmarZs8~0DY;t)l0;cihJ}$Y?-Wv~3Yd4X0F#q7N;$0oh@nxHTB=Mtn3A7+D6^a3zJ;cQpG`MQg z(+B1)cFScTsLBq{(KjZw9iLv!7Z}b}na$iL=G;hZk}Y`MKT-ZHdl)x13zh&|J{mn- z^pwjFK@G986f6W3@9RG6nU*Pmw0VIjo7}ZM3Z${+!(Cinf9B69^W8R2@Lzr+k~<}(#*-Z${HC^5z>m(BXcm&^IAG( zFXv6q4?C^8&GWl40HTB*@@QOjD&A>#rNFVGbrXzZgc|Z6n%UZzOlcgbMwWs^nS0^n zU_loRLFy>XE}$G%-ZRaL&z1xsY11R# z8A!cW@MlvV>AY=L;AYa7d&TqT28hh2&Xl4R{rASRQPTQG;S#-At%-}2#@hDZ;uqpS z`rgZH9;Cf)hSY$^8hU*(hXP*}15$n5e!2nPul&mynII9*d8u}k)W?1u_gD=y<5p^Z zd*Pci@nF5=-)J#cbdEC&YN>&8i`PFePqiGj4uY);g2=Vv4PCai5~D+R`zFLyQ+T=- zCM8mQgGOECUHlSxB*Z3LS27Jj4mv@Vw*Y@^k#*MO8!UaZXS#pM(?TkE-Hp5M+ zw;5$W3<~4~uX+u8DCffBt7q-n+Wdv6u;33Rn3xZrH#Vnf4QP8Q`0TS6l#2gkFg2YXgVreF=YM~ zSw&(*bTU!dt$2(LP#+XGx)DD5Jw>kHytY9q)l}W^Yh|MBTvE+V*+rt!!8m^+?J_}; zy-br84D|r9!WRlYOTn&6IF1bLlNntwcKOO%@*LAc&hSRlO7F;>_2}f{E~S#|i*@UW zbKMhhAHa1m#PTbQ0#!`*CUq*egwQ^~@+$Uo z-_x3zrUwL!4FhvCq;}&?FXIaL5(8)sqh_J<5LVPH@mJ!W$4^)u9DtB+GwR~TLNg4{ zOK^jH%5M)!G% zH%*DyI8kJMEb}IFSDk|sj1b~rvyGD)Sj89NtQVv zD@p)vQs={aG6gWeN!WJx?4^U3R$C5!vt*Ds<_NiZWAziR5SRodUSp&GLAH5 zkF{2lt~cEJy-T(yIfw@FX#!nX{eGWC_55qwJPRU1dg7{XHEqs}_(vUri14ROCEW2C zj!np}rH3aw4I$+QSpO8$<4gu&@=7HK&kRBs|MC$lQ1EWUxD!N#nD&aCc;DXak3O* zcPc8!pO|TV-P%n#M0wKuL&yK_Tyjhimw7U@f%jKB-(ya7HH0B6E->7RJxDxt;c%=A z;aNyR-#iUM;pr6SFo2KL3fSBV9?~IV;QC3dE?wrdl6hjh0He1!P=*VqG_=@9hea84 z8ocZv8n8fVgk8;4uO*M2`16{OE4L)ZB)%%mzL&i}XO{c>(RIi<)pn!x`a}p$cJ`$* zo5jv^=g#$47C;X<_o}pz$nCLa>e8pa-^D=R=Fid+OghV7Cg|L3!h0J&bP1N!L9?U1 z3^)SS(2-?ea98*UjgBaB6AE86?lzj^c`(Jo0zVfLMQ{AXh@*#~Fv4C?O*G z<;iM>sfh>q@`s^WE+ESO(p4*wYY|1r&uMMBc{QyxY$v*8t0tyteygQ2LA|X^n@Lc? z;y+AAyP15}CH4o`;|Kk}vN`f7mY6KXb9Z|aUmY*mA~b`B8XGGPBRz!Nif(Hk&Wp;q z7vA9#az2#C3LJzC2AsU)$a%b26eA#eIFtf5UW0IwE@_|g0Hi}#VVvIZ<@|S7zX1xK zY+2~1^E_EY@a<3{V&7RVS7eWMeTCSH^|Hc$vlNJ{QF>xA{T6ev0&V2J|M37v=1~0f zr~WRJq#_+PyC}Y|(d(tJrO+Vbj<<_2h9E{D5EMu_fB<-JB(y7;CqLx1m>d-1ovY=1OmGrb4^)D80;V z0Y1c~M6iMH1~!4~FU`(l*B7Zfeq(unmN{=%75-MY_Jj)ARvU}WJv;^Y+;l9w--Y8T zGx`r7Dm1MQ^_zx+k#W#m&wuQW8@mwx;Ivk!UaDW{+;qKCRL5O+7}*W0Pmfy3#I8H? zv4=Ltyl=Rkn18zh0AJs>N@h+B!Kh`qUq`!~xB-;~O}`k=iOc3SxGog5`VdC@P?ErpTX5j-&hprrWHpQ5wbf} zn5Ogd9^QyvhA?f)o0z7hK5P-lZN0RoLL~Ut*1P5Hqo$D+lyxk@xWZ{pAn*917rs6t zvH`MN`hs#=FK2H?e%g&D$+y@+eg7{1dgZRB>2pw^uZqy#+&}WP<@yJjcahQsclt+r~!(rZW9&-m*cNt$+4#%7~OF57_eL| zODJSed_5p6tS(r%dn`buBYZqJ&F~x3JKquN;y7eYvj{+R$(2P9f4v8M^Z3s>LE2>0 zyRjkHl4Q7e`nJl64B^iI@hA1%i-ekpnd!-yVBc(k}KGJ)TboS(Z{KF3d>;Kvr0YK(Ed$XGq0vnzUaacRB7Z+jpS{79W ziXc8*QDj=ZZ56~&6F>Nk2utN5Ka6hb&XqVnmeC9T1-&eWaeuY9gEFn;0o{`oI+kr> zD`(O~8`?H#EQnI97*k*_!mwA*m|yO2RM!t;8B^&@J1mYcv)YC|1h5@+G0p2}KfzWZ zv;=e)F-A}UH41RR>c4buXqL(DIeWn9tc^|b$Dhk>PAwu9K>uk%b8tMt9oM54QQB#Y ziAf^Vjt!A<$1O3}H4WcXu=-pa74bkxD|5L5a7+$G+>`_Ifd|P$G1|Wpj3{P!w6+j*r8sLzEm3z}L*hl#Mq1OH*OG z)xMQPhg&Gc=1`h1j4tD%Kb<#nWd{;IF%GeEe;{fivC&Csy)8Y(?;}ir`8%H3r>|RG ze-~=$45pguTZ!9ntD}P#Os9|v085;IKgJ6$yNn-*`m$x^qyDS-@2LQRA6ch+^+rHk z1NX%d(o{NkqIgbK24K+as6wLF`$z;r7Uedxr41VNCgX>4*3tRX>AwrF?j>eKheGY- zCrxr|R_m1xUUg|h{|Ig?9D{L17COgYFBj6U+TcKDNq z3Fg*Gz}ET;s%*9GTkE?9K~md%z}`+q5pD~Y+FVgqWYI9?3-fVDlv33Ej4q^MGLw}S zNNKQP(R4|TXjMXI?d8R*z)5kn(x(9xXtH!UX7g?jk@vCntH2Ag$#G(m`m8nBZPLaT zd4LL5Nhxm{+AYZ6PV9b4QT1Lm#LbPEcolp4Vl*7s+;v5^0~B48WF-DKoEz_;JS!v7 zdmpQO^Zs9a;W6sGABxVfWv#L#m|j$3QnIAtT$${feV57&A7gsS>w$l*ly&?Z%-huW2!= z0gD@>DX;H6lc0z|)V0r z$4T;C6jHIx8y?FJml>*R$~Doh(XyxBC^^@oL6K@**zq5}s#=@HHSD?+vZTokhF?m% z2F#~&q6cJAr7T3bAt&&K&yq)p zpS`b^tR2E$0qitgk5|))>A`lP#>GU+6%(S1NAoQ5HQ~KS6wSAJoVL} zAj7~j%jvkaFNslbW=plb`;tkiA!1cj%g6FkC6O(j*09x41L{QZ z!Zur^CdW}ekQnd!#CsBIVd6-DrKQJV!r+O}bN!UcR|M_5^8`(F;VxAj&%i$Y@$m$i zWX+a@l@bNeq_-OI`NjyJxz>Na(s2P%4+z0plDg_q1`prD9JpLZgeB zHoL#%FoD2P{K`e0)=I)E$v9bf!!XtDvXljAy4qIUqS6+~)O)4Bq#_2RrJ@MLYuh<` z@)C}e(U%9H5?2ONw*VOqSn!5$k!Z>_0I+tFCuM1K1855GNyNZE!}k9IrM4PDFm}#w_K$UusCh7?=ShHP&@}t9O3!;?p5&O{S!CXZikcV z1tTRc9kyBgroWB=BE0*|Gz2ogDGs3E>(HbpUII8+v1^|#FfCaqc=!u>g?CMY7Pq=5 zlvyjs2AWwGl3kk71=JeKlPiLv99M|_>>DXYQ~4f6HLd#_gkc4`*|S!{6w$_D+5iet zK3{?wLyL^q64|vR>z(gA*ZCC-80+B~d<%QbhWT%zi}y+rcwbu)yH{f(P`xIAA-rLMIAu3^K{#7Rhu7ljA!XE}k=j1^_77O64}9lGP-vV2*M4 z&EH_w>pgRT)W?<(&rb3WRpAGS^rs@q?&_LFpRZ$JfP|3EHldDr(L1bXx4f5_7!9nA zY&%UoE&-3FQ-H(t$Fz$9g}Z|#KDA;ZuD9=!UR%V-Xt9C{Tw7+Y+qxNG7!58O< z1^^#SV}PRS#dZ6amZ0~^DX=>X*&tg5P9x^rpYk;ru9rh?*MaTuNUlm@mxW`-K=ftk zp@1!W%Fve-qZ!TWAY-2-I%2_y=T1hgc<6t56t_Qv+WuK&g{nGQqI^|m<_+hXO9>U# z;X_VH3w0oPgCe$J)ugD6kZkc(FdWua#!!cB!*5E_3WK#kd{_Xwf0rQ(x@#h?(6h%P zTf}zOvI6eec5(6+2Dq^B7zQn3O4EP*iJWd|5-nz8fGPb49iq)~U8_Q7=g6;A!S}b5?rK!ZT2`HvK37a1)?3k@pQf4Q9aPN3FJ&aA9FNlI0|SPJ z({hgWAdN>@fzvwnhsVd|q;fyUM@j}XO;j_#f^#-B;J)8thK#=OF>g%WNnXQwt0eO! z6z!K&fbN0nolbN6w%2e1D8ySA>DuY z@c&Wju~NN_qNEXlhp?eX0*JErlZd7zY4+=_%%x7rx;j)=#8{~k*XU7zW|r2cmIfXa zP}?3_^usW=9v-vs#nwo%aBaN)a-#jrM}9A%)dF56^@6n>h*V-)`NhP&;noq%_NG!V z84-+zuOA83lZI@adt!@|Y!{q7l9eMa>poW&w-Y;F#(yj-cc}B!u`c%oSO5}xQ*9*2 zuNG&PEiDx*#T)Oqx{wms$3&9a&{h=$Pl(XvzHXwQu#0(HPSL{kJy#eIT8*6ah>NJo zznM6xRV&zmEL+|ElM}-;1M+cjaa4@Xu?A~fX7ow-kajx@9IsVB!p{$qfbj(ss`}zY zP7ZnF{zERD)+WxI9>0?~@iPo8DT|oVO1IxcZh;~!0B6Vxe_q~G=`Ngx^vbQ=>yy*O z2q83S@u*Evj%dUo_LL_k|3NP!wo~14I zD8W)ju@~G?e<$-{^|-H?Tl|#TZBaVtBOgt_)+{RBhGcl}4rr|3$8{z%G6n7Tk7bxf zZ3XXH|4=zFDVTDNMQl%7H=h!NLhAY9@O?)mopiT+xu!EdQCJ> zMR^ndGk-*iS9Z?a!At&=D6aA%zu3=9!Tsb|Pj?k+s_Nr&}IarLJ14(5AZHR|8UfCeiC| zPQwN}0zeekXdSqEa2d9CgmrrX*f`!HiImCo!d;Nes89}-hStNG_hr$`EiKfaD@>y) zHyU!chgky=Cxn9V1+FYgbuQM|eLGh?G7eAG?Tz-BM)tu{%2hj5a<@W_d(B*n`JbX} z%;cP9Aty$9j!o;Dzq6r~%i&6#Q4moO0$Ut8Dc_!etXCVz=#**m2s{-d`jvlg{IST= z*20VZzW`kiF_5+!&pWO(>M0+?rr!}tH86XS{f@ABVBF)lNIu6FQfMbdEnWBZpzdSXBrKO1E}uD&eGEZsWBsyCtd(#b7HeggJEdZYG}=a zm6EKejD>!B+wpF+2#tZ57x=dG_y>`x#o5%69hwUte$k6l^-co9M2K;0Tb%@Vd(vS~ z^Qs?|B1?v7Y*2F1V%tTt%Cj4;HHc>IpMiyel>z@h1BzC}+{($=0iRaHO5e#?$k@=<$QX);2g=dO z!C2oK%59^m!&pgsvV}PU#1#UPU)9d_-*^ok93W|v{5OZVq3I9k1cki0L2#q|-12L-g6B8lZgJ80223!e6XZdddz?nk(1%xxQp zXC9j#-$2qcGKGpONi75)3*71xkpv(agVTd)qHCZB_L~Bz;FI>th)8RI;uBEkmzEEX zz|~S49^Sz{dgIj+Qc_wbHUlgWc&IP{$m5{q6Oa~uo>f)O!%6F9r{>c^x%yoM$9^N# zmXT5xQevXxnpIvt4Jprd#MEjkbpKd^|S#4V`XLa za6}_D^@uBE@9v(~{ZgW)$_B8(7qtX<>m-^3drPIGaVgPb&!+bKrMNRy1ZYV4%g0H& zmrHCv_pe<}kGx-8uk*1t+&|dvM{)5)cOQTh|BiP<4W0cvnX&tIOFIi7#CN?fmrpUm7x5n!RSO&E_bKjEH}$Z0^NZG5AHmJIgC%E2#OAl#P{f5s zM)xOw+LvY%#OT!M{OIbbHiFMDy%_cm-m{d~)5QEWRa!<&M?y7#)xg9AIvHC}3Zids6WKM;2HMAG3+@0`3UH;J)eUHZFFM4VIX(lXhwx$e0c}qQgg*2e-Npt0Bl0Wq54;G% zLmHDJv*H_S2ZSE=D=?DD{4QN-!Onps$al$Kp2%@zdhk^=}?@hos-#?-)nG5y-!vnAn>n3 z7GyX6yxz`0Lw=V&-?sjL%wCs3$Lr`m*HhoH*?;8LmEWWs6sbkruJpX#VI$Dmt(}o$ zTL-GWX3jRhKG(u~-ap1bn!ovI$ZqexrFH(V9$&x# z&R-9#rgp`&e51bfZ?l5sZ>Q-`tIm^eujJpkq0hQ%!|RAfuuFAJpuJ^ZL_#wVT<@qx0XM6>jMM$uCg2|bxKZDde3ZXX+JPLov*${8DE$6@2kwP zJ<>mDzwRc0etfQZkOvPyV?m^26vgC4Q=(OR71$wBkv~Nbz`Z3-Tb;*G<#1fs^IOyg zd3^c4k-A{+1Y#-)nzN+iyESTwR1ljd&fU!Sn@7>*341N^v03oA6=`#3F&y0*d?8y2 zy6$N7%lS?+Jt~lwQX-mKSOk35Phvrpg`e~h?gy}k6zOWMUDu|#QJVAEijSV6SW>Sf zPFpfv#pN<$r>&!}^zkCz0+UOPA@2luBE|aGo}Kf}<_ZtW%&{I^ikdW(A`qtotWD~O zjOuQY6`+mDvv(0ui;xn|Jtk;+f*i-9Y8z^$G8hvy${2i%QEn;7<-{xrwPef`$ml#} z2n80AZx@2tETB+*xfdGVEydKA&y5@nCiB?`usFtpbA?qZH!=)&#?pA_iWBEEQ1uTN z<$9(l?vyY?)8(iG{di6%Y5EJ}DB(p!klAWal^8PlS1MlViL-EC6{!fet}06Z9NfQ!5?4MR>8_Fx;_fGqssQOV@5@ulFCP z3_jIav_yg=UmX<97(2A9!qf1o0=-`vvW}}~TRAE7IHn>f43>xlLA(V&YmB8ys2FT< z9%|okN>Uwr8r!~vYo7_rG03fck1kIpKNYxY;#`l1&|}d{zhA1Obc*IX2)nM&z|z>E zvMsa0%)3m-x`C0^jmKKNgZ6?fQrBb+LL(h0pYN5v2xLn=%)<02h&yJ!dIJs-;bYXw zvo~n}wAAmK`Z0TDMeCvg6KX1g#9k|l<5iwtKv{Xav-6HO+m-x*=}pM0hj?~&uj(;| z?u{0#Xn;Qs#)32^9&R@ew*#32^_*LYfK;gL>Qjfx$+Sc(Bsji~r%4T-y+MIzJ0(8V@tIU5Se8q99wA)-5 zL1}nAYQC%11cNM)c?Kq=3%HkNSMC98z{V-LsS}~yZrxUW@-Si!3aB+i{H^UTtdSYy zQv*}|D#Bt0Hd4pAaJ^FwiqG$6|2SqaK2zxOdZZ^IpN`TAO6%6b2_F;bc;@@^&{Ymw z#C>Z|l^1CtW*aTz@QU;Nfg!ZBvjXc<-HSCFr-y&@MO$G{y>_GyCGc#`*T|$rmykwV)5A=SE;8dg0vFI52(1Fou@A zA-b-vTZom5etf?4t2ONB?bgjAlv%DVcHI$7)E~0|-GoH04m%pIvWLWVYz4o@)yH4C z!w=OQJbUq2gU)Hka~M?8ubgxC2HUo2o5C*``kXO{^IUlh->Z|9{=UuSTvP{?-{F{4-JbgMuW=yd>53ZXFr{$>rU1=;m~wN zN`gd73nw6R=-eiJHca6_xbQ_Q6zJ`~X!NCbf%U1M)41v1%xdMLJB>1!a$5IuEK@-% z0j8;6K$Tb94O_Qr8ST(N>WPb>5D!ycg=?RL#Wa^N5hz3Y-S5GpSw`ZH9!#k4kO}@$ zo_Wp~63+>68Jj!3OsOq`SZ#RX##}7NyiY^Dhm72k84A_0a2f7XIHZW}j6qJS;@dWo zipdr?DDgP~j?@Q)h;4B*Ahxj@<;eu|f*hTJ_#S35rNtGns6 z2kt7@YH?f6LElrzU|exx2U*6;gK;G#N0-|RaVPHMUaaFl=`6<$5~A(M_huE0x5{yb z;|i&*MK=~Ljcs>O?qsxl@ysZ9(R$VEYBwI>L6x=!m6nL5X(% zXSdK(u)f^ZnX9aLYt4(3eeYP+dp{yiQS8WJhUkgg+bc%kJq>GzpJ>9Hih(*(#^jHA z6)gR@*kL)Pv~=^!w^01R-DO&GhF+Bk4?Vq7o&g5HuEsGY=_lnEPmF{Ih^O9LP5od# ziI0hh-T*{?yVcPIKPk*h?L{5#P<4Oa@eQKuOmot`^<02VuA#fx)%e zsN3-{ryvwk6}44}1zv#3Sg3WqckpU9C6aA+^N<3LKc|;ABW^OCYc->mV|>dqNKgVE zEB&HtaOGMy_Yv=gh1M9YPm$EWrwjCY3KFz)i%xfAomhty0Jps;&66@TrdZo_gwL-F z(TF=ih@XBNy{hr<)vC>>ac~^esuzi68=7MZS~pi0Pj>9fzzhDsw6pFV_rQ||OyAL2 zLBeQ=p6GGnZgtRR3vy`0R6pXRL&AN@&-O3-tZf;)=sGmT|g+K&EX2gz9jCs_KzW02iM4p))dE%MMpEY=uvzwP5&fS5E2jt=u*f^ zXtD`rrqWarDasDNm&Ib^EFg$9pMuyMJhiIq%H9%s=jSKQix$H>xAi>9x?mF9uQ?^- zf!NzC6BgIct1(h{E86})dw7VqxKz3ypR2DvH{muUS`na+6@2GMZqG^(jAFcC(S)02 zwx96iSU-l+$~kE)2Gp|z5JuV`SY%m>y;&+zZF`@3Hqa(apz*X;Hx~)ppz=J#U zmu!N$xO}to&cR^oS~ZCZ+|BqoxwI!dJ0YO8Y_mye2@Ya5W7)Dx|K>E$Mi?LK`n>9; zL&Di)#4@s1BSpq+@-mQ6g^d%A6XOH{Nwqu}rpva*F|xljbkWbedoc9GpDOcN(i5|( zC|2QQ^ETxjOng(hcj+h2w|j6T`?rD3-|VD~WCxdx)KJR@*}J7lJpNtCa}$*pR;3Bl zgSr~pk7;%oF0BkPz^H`bgl=0W6o{q8n$KO?<_Q&(N8C;RnJFV{82X;d(ig;kQ z-Ac$)OR5VR(baUJ-Q|QVyo&)+ zGuRT*w!-ql6O{{08(Hnj;BzGWwh`U9oOcA3*P+Jzf2lPH*sZg6?V!2KqH%;!@*Am0 zZ{!Y+cZwT5+lnSS5Fjm~Dh*llDo)V#j-!;`DLK=lD!$*95G%#wuQ|qZE%dY{1g0v= z@7UJ&E-Jb-NRPNAp73O%s#JGw@$j}(B9&C_C32+x_n#*8p?rz6?0}P|6l}XeOMEPj z+DsAX?3$a8;CD@v7`kZ)-xGOgphr2h1WBUKI^P!=%rE=jy21BwW`atY;0j)dQgXKv zG)f}hTSMW+t+&1S6cu2pR3|iXu$0C|A2H)ZCbYe*wy>1C6TVCeEa85sv={Bew1_E^ z`LY#*@Nt=Rr@tZq#oIyyHDi1a2Y{yag}0 zVj#+KK27?sx$v@s-*Q4!Y9KP9M-p;_DWaIQBZh{ABhHQ| zdPh3Nr0$;U*<5{&9)^#^s~liluqEO4j%8`dKufZ4=&4>PK9`E;y5KxQdotglby-*W z?Z`=&%Wzvpl9Ocz=Qjo}d`qo4AiB`%Qmgw+AvEgLaW=Vk-dW|-6+NYnc%(H-QLoq9 zU`$C}I+AVj@@E-RN>fQ<)?y49>Q8scst7HTN}&qai-J%~j^FYB^^#(cijpRuzZFBM z7xrXO&L3M59Y|KGm-)nh{QZlUw2OWFW$y1thBzoK;NGN~bu#Mrp;A@pmsR(Qp&JclK|E$2!6rFHs2rTAXMsXezWEN#InE_9h$p zgK9BkT}o|J=##m57Fb@qy!$x31-BxgMY~!rELgSrKL_0hQgSM|De%qq?>IG^_>1?O zdgVUr-eZLvQb;G1>j}>er{q?*WHM4ax6Km`sFKdIcu5oTJCDbvi+3E+>g?xmplfP( zn-W6o179V74)@6}+c+6?K1_x7VsOB0LkO6ajUruYbSKUzz6Y_LPdnY%XRIp;DmS$R z=Oj7AB|Dqi5b(l2w)9G<%{55>0dUGghs&>EzgRz@$YfiIE&9FY-$IF@J4}N8#z0xN z=Wl|?8~o0Z_bFrC&2`R2Y_Ea|e3J(OQGBquUdFcg`b8;cl!c4nnyjoeMAFoj8{dcp z%mJK%CtSNU*H!w|)3A#X97S>^=P|g0sh$mr{c^^e!}f)2-t-N6rH`lnZhKMr+O);S z)U??^s+jIxmZIrlm?}aUq+bh$K3C2w6cko8t{Td*9c2(y*|V3tJ>&3Cx0Fwf(Kge^ zW12uj5lwsnOH>oMrH^O$Fev?=gk57qOTo%&`p&f$VH&7zGg@U4`J->cd@<-bhRFeM z`W-$DEHM<@roVO;Rga}qt6Hq*tT>ZQ55P)UA(>|>-EKUfZ%l8iD>&-7o$1P~Bp3l6 z$)$AJr5Py-lMjT@_p&#?vCcyxHJHP+=fm>~TAcD&f=9C$~Ebx*&SJ{0aQRIxhnIV~C)-BQZwyuyotY zi3QVi`nIx;8V2PUQ*Y(Ir~34hb#bF?B(N;iCRN1g>w4BZkDd|#*qLy#mU-No)q6Gj zpg+Q_iHU?VHZ>OG7^9@%tNu4smz#5Z0GobtcmIpa2a3$TR{)qbFa>g%R17q-fNV<% zDiuwlW#TVAGamD??h_iq?S&T3o2{S&Q#szJ$rXi!wP2FkJ^puEK_uRduyP0n%-GE( zWG+WNv~bkp9M50t4kBoItc`{zwqi_p%_x3eoFh2JvepQo7#1t1l#M8jbM}9B0?%h~ z*^O&@j*cg%{m^9T$P01nv^X|P=?MXuz`FrFVYcd_C`fw6UjFS9%}z&N5|Twyhn~B< z#3LBjb|TbGyIVxiNmkOqTZTR`xbGp^rCix$2?iXikE;&%X?gTRR(EI^iz6z0nG3qL z-xvm=r-1p33xLbpu(Z_%!MLC_MM3jy3oSbI|c5UJx5>5Q0ki7{*p<%F~=_WboL>{ z>%tT5P}N%)#4Ph_*o4qGbmQCHGlw?CAw{klJ0}hdy%sT!rb4R0Vhq%I4@HrP*~vsc z8sz(vKP%`_FXtR)lb1jKVjz@rz8lx_eYAR1lG5j*&10e;5yj>CZLW;HSr%U_dUHLg z;LBJ z;U_@hu@7^xY@U;W;G&_2?r-EBk?i30Hqw%>6Uzi4=_^gnS1(uoWraPt)&ch(O3(Gf zICz$7^RA87T}7g%2rjgRH%b`vMLj;cP_io$dhgoHD*bKbvzI*!5szh88KuGG!{W*= zHG(6p4qSBl;t$p7#4&u|tj@Fat!TPl280lVm|GDnb_x# z?Fj`K(@mnAay4E8XD_vNm76?Y%SB9}&%so3AT154Nc%81fA9UT!PYT=m-@bvK40YD zK^W;1T0}Mg|3Wnv)ypZbNZld(r*non13Gi&*(NrmDzP>9#G@PtoV}}%2EDu_j?uQg z;ec$?XGfTAQxFf0&-bs$?;D{pKp3RWWL9PGfZfk?L;Ksp4$F=58}qu(gbw&(`da4m zD_#Dx@N-&5Lr+xCuV1R-8J|B9;|VQe+Vf>rP~2{M!zRg=$Px8DsxP1_$gn|knBL3_ z*Ft#D6^)j2?J334A06OuQ4WXc%klvK=FH){8P2tWk4`z;%;VY;jpNZ#>Lvwxga*`8 z9;Q?J*XTU9i{{bzh8VB_^VAjn81;*qcA%-{iH*zvwEauf^BySWLZMyy%QhnGHo~pw zJ=&Mg=6%k6Ua@9P3_a2L@e^IqXJqo|6Zks%IJl(9Rh#(HM7iYG;*Cb+0y&)VU9Qb? zvOtqm%KA^>BX`w-GXz~{Ha7GF9SO>BN~4Tzw;Tds??0R?@g2trQd0Dw#+K020Wr|7 zlBs|Nt2k6H)@gIQE43vFEjrSulmS~(l49&A2usIwy09VCc`=3+ltR-%UUiuFw*D8O zB)*J*;Q~k7g}!ujf-*2SL%q?p&#Nida&6OnkZr+yV!@wnB~y!H<&o`a@w5tmMWR@< zCSj3VJU&GoGQ8YV^w4ovpn=-YEBox2qf`bn?#&DFgsf-gb8<3R{eHveM-i=Tx~0tZ z=_CQ(!CKW(+}44DQzhniBVn%S`?!=b72P&fee1v;RqvShv35+&ifX-A+!K7@9aox9 zLGQ9{V>}ig>4M?SL^okD_5yY0t5}X*TmTnteNC9*J};R+dZ!;_R;i?o>Zkumy-~wY zM$F{P5zg1^u9_dWr7eSnh`~duo2**4=!6v60$w>u-TYTJ^v`!Wiwn26dC3Uw;6-;# zY`>#_jLyG6zG@&1%Gp#;`2B-HUYxaE^9-5#&vLALyQ^Kuh zH8t2zn08w$O!-F6x2)82VoHPT5ZG;1!On2tP;EEG`QGGnKwkY-T3JT~XISziS?fin zZ!RQ?yWoUf2E+Kn7nVBE`L&zDQAD)_+Lq#IH+3{oT&_eCuN9$^e2zn%L-J79LP)m( zy!tbP-jmt>w_f96H#8q=D zw`Wf{P22{~hh8)-xJ+6(Ws(OMRQ;BZ_hi$9U{3R5T;|sAgIn-3yeTb~$p)|)Y&c&6 z4dK5pH53uuo%$<-3p3@yU<#r1AL{RG^W8;ii!yxIqs!59A)i>Pl*#ps#=iB5+K;V_ zajGKvQOFF=MJbzWWr8{n;4a-aHN>tFf8Cj_jen|g%zA53eEMmZ+k^WE6^f1HGaq&> zVU^v>=5ITK+FqW}n9@hft_?vqYlFx|cYot12aQR}!= zFP+KLNY~njs*HolP_>u>Mm>D8Qswg{Jru+yOr!lTQW{g1r@UV4gKhYD`Eu%MHhSpn%N%Pj$HRgBu8ZQZOS7Z ztF1U-8^{Jjtkdb4+>hd&Ni#IS2(&Q`@2cM3&MWR$JE_|@+5f>M1LcxJr|KRbxZ5+J~AYnp4KJf-TMcSH4ANf1DjOjO3y2nS_Up zG_-6S5NYrKnjRA&+Ig~yOgeC{mh%;&RO@No3PNGKz?44wv-nj0?Nx}iPw{r8%nQTJ zzoyl%TM|+FXkC@{8-6m7%mPPLyFq5j$Cru8-u2xe(CGV2r>!S*Fqy*;#2E$hFnDWp zc8Bz0P`p1VcY6tZ5fnH)zgRqC4=1nF9~v%BL@sBR@TI!

zI?M10)blB+XYvk^NW z6>Q75*G?}X0+RW@(PE{?eALBsEw^k=IQsU9@E-&^Et=Wspluxul(}jhZV+CCkNTC9 z+FS&=Y7=iYv@MaHTSDxyozb8flV*!A?x4F=I#D*8d&~r(ht|y7g(X<@bK5s{<%o{< zP+Vm3m~-6-iNpn_rYw`hd|x?)UZkJ|2z9B35!%TiK89;l8y3~02l08~!6E2^r*7i^ z05Cw$zj+~nY2sr+3<%Cl@<#HG*QQlQUGdago~HltMdSN|zSxzjNDPSx^e;W%+7aTn zyL`wRX?)J`R$$&u7Uo;{@8`y;d=%0`TF(8#zM<_)mB=7h3=|Kk%J&D^ZVlhhe**l+ zWF-wpdw;T0z#_-t?5knhz>H2o(oFQNa-rpSPm3TgS37h&m?5IG^kw47c$7d?e5q2mha zPA_~v!XaX$ZH4WFb`KIZWv8gzbj|t7op~9~M-0m)vus8pMWLUNap6$VerRQrvvv($ zJ9&pr*BZW;l6B+xt;mgGqJlT3;~G>NlvN@mHeoB~Wi+v64F~OC+^E{)N`WQpSnCOimRWJ z4oMi}Zz&f_-j(~*8P-y%;67EOvujNTb>3Sh^S?gDCD7aLec;W1&R4pEO{H`7`)1kDFKmXS;G*sXF{e}BI z+7CYslI>5a0ZsvyLWkwc?{allbu6jxoYU%QSDuiG?akAT*H|-~?($JR?3}lrG^HT% z7C{e^X{IGXF@x8Wg)+WMb6UMFk~)6H12}wQWc9}JREPb{Lkf2o0EzUMX$*!**fxH) z(A!HR0Rx!za7^+*h)ER;c%JZKTmNc`&e4?)4P}uCk<2z4)@4jIxsPZ>fe?+|Q8GJ7 zkg)LZDMa5D&@TJtEh{B+_clL}Fy(3KFEbuO;T`db^T(7n=$CzNeVryjJye7*D}vKw zh+KTCsR#K#+7RO?I*FG_XO$+n%zo(|L4`;7zwB3h8INT#E!u z5<4NH1a;4bbb@jnfiS=RWB;j!H%=B3UwK)B(5+BJ@>)DemST`0$|q&^)+4vZ088l7 z`_qK}zE%l^?I_+i)7kAD2O^VS+5tCn zm1^CMY`!GP4>t%K*8;|h0gOk#C~-YQgx?FrEnu}g?g9RPguTK2lG542_ns z_*IE*Pa`w(`EZAyPM7k=NcnRM09NwuHr44K-iPq5wvRaa$= zRj&LqhmxYLmmXED9{pP+#%tRoPYfiP(Hq-dF#GV6wQjK>8d$}3y1+!mFtxhx@rDk%b8Si1Dv z>6@~PhhU)tFVGkT;oJA_kP zU=L3xk3Fjnbr{(%WPO_tDphwkl+SdCd?c`*L%s6)m3v9oO5>AL2$R1#c}lygj%@I1 zx$L7a$EywBSIAElkF!&wzd^-5#bb+t%6dwRSYtr*#6#S0>ta!^+2Zdj9cJNWACvM| zx?<@o#L}yuOBD00q|)Y#(vJ2*IcM!ft)$WFAfTr>mrl}Zp%%jqm2Fz0<2 zEk>jn_sbS_=qXKvsAj%Zr{Rnptem`m!8*CVeT$RwnIUJ^0uZGHYN9v_?jwK#H)r-3 z@DcWYW@eYD5qSC`!%blex{^SVE zm0WVD1hJYb`OHA#tabqSj7?dNI+tR9Ed@7N6IeiQr!ign(F&T5ab#eM0>x61i5q=#Fag(XM)RD`g9=#uQGwVEU6TLMBbt z7EC@XjON?N7MfAWSh|3nW9!}-lY;xS2^aW+x{_R)zNFv>kNE02h?zU%P3ll>&|dq8 zeD@q=*?m7t7p`EZ1)Wz(0`iF~ztEfQ&><8ll`z;C2_eTQ*+ht{b`*Y>=;`xLCuGBDs9tohS``8vsEe z?>;+mtc;VDe+%$v|Nb)DxXKZRyze?RujN@KCjn6;LB0%2E*o%AwYXCmaAY?{ve1c7 zTax+B7wxd^;T}D_|4|m-bAFCV9FSJuf6gD>R z7(I@P1W!j#KKAHw#NCpxtX!T%!ef@ZlqmDgtZ(i#V$ZpEmS6Ty5Jk>;aidS?R=T3_ z{$)UoGYGnrN`u$K^s9iuPv(!<)!|$Oz@J^^bj2;}Cv4TU{Fvs_D)qV6&=A*{{5ma_ zy>Ls-#6szrMA2O=NdD3cq{~rj%9i_1RZUenKE(~W>20vKRVN=>L&eSm1Vtr=edgd` z^9xu~mAY&*&w4rYsf(0(EcFJcK3REOYE7a*mg+gwJg@{UKd$hJdo5C=KsrgDGDKn& z#LDydqC2sWmA-3$vD!7fRMK>rvpsFpTRp#NY&Z!lFu?ZT@mgHX_~kCgnor(jZg^wp zro0j2*2DGXajNa@W2qC6JcB;hXP%i8o~1oX?0qNsg+vH{-n8pjy9VaIjL;tDd4N9CqGe}_tfjjOd6?@k-ujv5|T^Ri)`|!1ALFpnoHs) zmDk?qNqqqDP;U{5x0ZowVy3P!q_O%+N4u2LV>AZsX!H$~OVUP^k+C*b|YT*JAN|-#fE^^?+rh!&UE>T|1_Y=RabU=t?^yuri zDK=I8*u;dno%k2ao)UIXGkT2_j#GrvfNEz6y(U-7$lr;{JqCNqRp{H%ZFD?gjlubb zKPCCA9d`-<>1iLNA>Tf{{}P-LP)He=zAaTGp{tuEf;r9t4e33p9w+JSZ_Q^gx$k8e zEKxE}Xr*+87K&q>-Jaj}i@lRE?G-aE0h8k{BT|*P_GiGGYpe_@X2$vS5jEZM57B_8`7)2lpd)QEbx)J((m#YWU9s;`y(V=hMN1 zN?X$71c<)R0m20_h&bjfrOI6KqusW@Prt6q+b)e1^Gulk9G7=`qzp-Lw65Kp&FVi> zg*}A~YZ;~;DRd}Oluuo|Sq;=hhL2M)dcPgXo}6NOZQWKKfqBW)9}<(F66*GKkB@#>QjUzVZLWt~1d#Gw;M$0H1RiXP2zycH3lizi zBFd7vUs&w@pdqrYug>e1!SIo}O9>D4`-Fz21+ySi1MRzwS#R|_sPn&pU%_( zo_xLOx`_-(`^k(_dvc=M3-kp&tZqb8v|b|jnM5IM#28oY-O$Er2fR5!K7DMTlKa+a zt4hKkH!f|Fb*BRt$iw*b5NFKwj1Bwr{eUsZOTxK|0@1gkv{Yyng^HFC_<^R>PX_@wvg-PHz{!qVYy2PC08TZHg#!^@I;7(Ib4N1`deU%QRjkQ<1Yq- zZgK^;Ul8L^%c5VHas`E23UoShjUoda_I>kh_vDe^e zp8o`!n=3YNv+Aik-8$Vrq#c2h3VZiyvL| z7$%AMo>R0C){2{K!|~@X3~GEfqen9a1z(9%92Rlr>1{vggw!?uKmPWsP;5x!PQ^ux z!iInFF?&z3W_bVx*8UrE(+3?cVRw=E>RwS1E;D7-i5F^tY(?*p)D-FYG%X*4lFG5| zx~YOhGrx`Bp`eW_9;G>Jvvjzlz3s0RC-@}3I`L{Fwp&q6=A&a3#^2u5UKDSzbx=!;xpE0ecyovO zcKcG#hKp+&JA5??X{WuZi>=lUmBQzk)|$e56T=w)j#(N^=8xf)!x$_sNyp7in8__{ zJ@hJQ7yd@}TF3gs=S@((L~5;9_ltUED4g?%BgBOv?no)sUgS#>a=LEi=r)o5SFfI= zY>o2Z7oJY!5!fUlfpz#mk8@(KeXj-Wt%PM(-{f$`_4(Gm&oy$U>6okdwuW+)rN@k4 z2sGhrAJ=~ko5s?HF2>WVj|VuAPUYNrXfq|C#0iTz=F7j3Y6v%Q9)-onL0ZjfK_QFe z!q`*W==fPImsc$ABXiMJYRiWS$UaN=KJACg?QP;nPA*{a>=?wn*}@7)XpDHAQGMVo z*Pp_H4Z;)KIQQ9^bBYzXGVCP?a+vPTvT+7(N-N@#%;!G z_=U{Fnqd zBsXLu4ixNhvDFH$ZpDpMEYY(@+~bdt&)YsBhR6pSwr>XpR!HVl_>EM95KLx{KVqTO zr4~T6_H^IlRGGr0#A(>-?{D<+b)q0^GCy)I9oyk#IKN_$xD6$bR32<8=^Np&t$M7dGtbl*e~rGaQicv~0bJ1B=D@ z2YUphK@9%f=P{E)s@qE&8_@Sm8rtu}E^8iO93g<7Z1{oIL8q93*o>2fIEg< zd3k|ow}ld|W7x+o)Jbd?XJ<{!>r-3u<2GDnOf!T&{$oACng|0}Jeo+Pe90~dSss!` z=RLMzZQ&I1QMqF?t!Tly#9RB`d!vWrNa@!h9}VyC)bPtmrCo}tOPP}%bRDS6StsW% z>u326HEm@T-JvUm^kLmJez&H@E(GlC9}A?V1s*kIMyq#Hcv)%&+7mc1ZlufDY$Cs> z7lF1_hP_jwv#5lB{N_#qQe%g9VHuMP>#1TSE5mRi;b_(KyQR=AW_$LJ> zcHf>9ufF>0yu4E`T*=b=9Vr$nCwI0A5}K_u1w4wx1|gD2WMQZJ78+yjfgeb!T}@d0 zq#a0}i8FngpZ5x%Vm1|b~BvvL|$vN#NBi`J$x^wlba=^*S=WKPn?41lCHmugU}PA+Tcq>BZlNQD z%h9~$e%AwDfLu}WOdY;gLqh$d@6=1Uu?k`;hxLpN7o1*pZ&_%UbI&h%DW9l{ZRjg} z^d=2Zx_8rbyip;g)Pq>Ie0 zC3;Il-n^MVFt~*pDV86^)n?1D>!HWZ+E!ccbv7HFL~`OxXb%Kwro#C0t9zW-e-jeZ z66mLZXZzwoh~Bp%2rPhSfPO7mpSA8f!oQ-f%${S^;-Ne$5u4429~&b4#ry$!YlTAV z`#cUBg>`q6HXEF|r~Xi=@axNt%-um1@iD>k!9>}uJ)G2+B6Az~fGDrqgo(*zrEc39 zJjct{&lbuS;tQeEa9HgSDQ$z_q-SQy>G0>HvA6h}-g+A1Hwg)%Q9k!*I8GmW6Q_p4Qh|i^RCbe zlOp}L^_au*ZTVlFTWq|#wUSGHT31LP^tJY#WXtg9#?~f~%JANLNnafm(8DG+r43lw zJi8bdZ_`$wJK=rwqO9Yz=rrEYj0}9Bvb?&Fj%=T^8C67-7a-M{zwixf_7lJL(x7-M zveq9z4u0>1-k-9l?VpfnK)U(%E#zG1EI?Nv7|Cu2rc>#G1&)#lSdb>xi3_)S|v}BW4*E(?dGi-FbS_!;g z<(1@Wgpdf|LnNJmPh96~_lHv!(H!SqLEnZg)cy)Y5691xE=p@6D}(*CNs{E6*^PkS zk6L<8_O)N^1{y;i@?g-lEfF6Npnh2ND`xORo@Dc}es z61qzU9xrXLh=gn7hG{ITq$|>|rEWrL{=B6*#W?JUgkE+3DM=yg0{mX+b`(wvCKi>k zbGHm*h`e{608_6H24RL(L<^EM$FXZ>)3CMr`D*Kc;GG7G4VKXV$A9K-#Br<)yOc+% zYvk?}X;vVWF~Vu2Fdr>RCF&nWc@c59p-kJ;tdGM4Oyt4w^jj5R+R!FQ;i=*5p>Z)2%;I+J|O>B8uP zy`I-9rL~F98hemTrW1BYd^cg`oajk+s$UmuHMuH3l%1=CH4B?LPncsTS z<-FnYx!@?E+CGJ-vzho!7`Qwse&Tj^qXkVAm!jVHK#&?!V{$G6-J}-(QgBCPcNM(d z)e#w95usf!AegWXc@eKbvK!@US{tcQ)O}FYhwh*JiG=a+2i9`G$K71FMZ}NCyD#it zmgTcXBm(rEuaKQc@}YipHNqR;;|%dSZWP6gWP2t!fAvK2Hh@9$%VZZ*@eetiD$H5w zjy7nnEUe*HR?vC}08tGmoFuDbho?%}@DeDKz-})*j$|iGm7#S*6BNdrf6hFesHSnN z=z9GYmOOaDf@rg^S7g*=P%SFy0FN%^Ijbpi`^qE436o2^)cF4e;v^m0A}M3rs>{%E z!*Cw!flmfwUz=Oubmw9`btJl^MT7LiSXR4}g(wGTY-`#|1$^5llnFdaD;53>2)U`4 zX=|0b*GrEi62G^w+Pvm~|`5Eg@ z-Rdc36F>tzl;^oNqo7jyj#Yv)D*CNDJ6zTl>s3t^@nY#YJOyRSvTZ7ND)=v$@IFSw zzR@MTt6b{3gAA!Eo=9fo(BdnPn9a&|L0qMsTTqEaFjxm>i*6rLQP8!B^8ac~IYD`1 z;NF*p8rLoFn(Y;xKD10qS)JRqO;&w>7!>7DyBObwzJ5Xf1cdjdb^##$^AtzN=+ux< zgvchZI$CXc)v`$Pfh#nVXY2SqejhVk(G{Vh)z3AGk7UgZqGYNBcFl;{ek(GaVytfY zr8N9cfy>FfX&&4`?2x6W2?0eF)gRS)W* zO`+^M!qh&Q*-nFkcF(1h^CCPV#ROXdhXBZN4sSwLv?OG}WNnP%m27ypWvzor5y7E( z;>6)(Sb8mVLpG1rd0Z;IF43669E&RTpB^wK2c5C-NR{6J43FdM!G5WfY`?DufG(!1 z7_kyc*@Y`T2AG0GM@`XBP`*5kNZN63vd|$bn6Qnr%1#YAXg}i}`cg_T+i8A;;O)U8 zc1zD#0Lrf_P^oK+F-IFO(za~f73PN8Arx#YK=nvoQvl$hQ~zRYHRL`*qz98oV~N;J zZo0iGjC`~XaES6;0Gm5Yfzp`VFAm8xz$zMJg z*{s*IW2*05q}mUOhU*&b7xThv3hxTa%ShU05#a5e5HH19hmz(aPm5n#)>calFFRRW zpNony2CRVFh0bO%S*|zR(ww-TNXj>qKy0;tAm(`AW^V4-M-#DJfTDIzyVfFii9;cQ z63vA*1){#~1tq~2tl~vkZA`>MtAlNC!Jgve@da80K$J_fb^A&~TLprA<1P()K2-OE{ys}Hb!K~;8i+Wp2QhY6 z1q)D6aH+)JnA2|8rtLf}3mcZ#@L}rB zo8iwldYr2zmVzT{AM1acvnRpnlAGnQ<+41i~J^ChW76Us3jq#T6njAj=G{orpMT<5-|HvO-T-^nYa z=}I$a7X~15z^oCU?kvmt<0!`53U(iL>`nGNQf%?-R0&acgD)SoTjEmsM;E&5otu%T{g09hp&U zFN0I|pZ~SjHuzn5pO+yjB1}pcn0WFJ_s|Er&4%0{vr^lLrU5sZag3!6rk}|_U1$t- zQSYlDHnO&4{7k&!JdAQ#q(-CDe!7af%I)2>14w|}I3DPhyXUKd3HO*7d+1wN4?3`! z*WZddp}voLvD>nI*uRSqe&;Hk7zyCdw2Rm^MBeQ6Axt3u zMKgxpY{yHn9azp6K9q6sSBiJCBFj-Vj83`<;n``wJ>@6vrh z7e&C?=%-D#+4&n{{q4~Op=qoMmjK>-Src~=RuyAU&h0iPp7X#NraC<38yVsy%-OHN zj4>qA$O9`u9~+6Xb=X^S3vt7axGeUHP^VQcZ5mU*w<(jeZZa1$YEb7A4cJ`MlW5mb z7t}C5zA|#tTum}lMuqzOjxV4tft-~%SGKYWlz+jH59wD{IM-!2APp&A;GgtgEe8sJ z^*I`;yC4Dohj2#)A^R%9hgdb9lJ@2Fm>Cy}m|XX=uX6g}%in<88};WqGon8G9TMv0 zYC_?3GAN+yW}T zXNrF=DT;rsmU=Fy+(LeT!;U$KJ&tC}53oPVy(~Yq0`{%+dXkcIbf+?bI!UB+?YHTO zYivbeR)(&jPB@#T);^2mMO!uHo70-Z@Cy=RR%UI*{L}_5Y%?t&ukby{5vWi$0lv8D z{oa?<%dLNp=$lH;7`)8X@00Jl6!bOz6!Ocpi6Zb&GC|IW_u_!JoSd5iI$J=XzL1t& zEHuO?m%41$+dJ$%f1H2o7&=xhdW@Ip>fz5b4utFajb5hXMcxg)7}|wd*JCRijtY`J zI(WHquyQMjXFXV_O9Dq=UCL-(WgCPK9f46&=gHiww(}tPx7BOYHp6Zt$wf(r*{J{L zHrh9b#gW7Es=AX6mO^GZh?uWKwGn2(zfv7RO_dj+tgbplJgk-B!7cOmw+L4EjV87* zL)rwFl2!C?m|w7!NT8Au6PMcx4EwV4NaUlt#>Qdsh^&>_eq`audxj!|M8Qbr(;oxo| z0w_5Gg!9Hl2w%lgN1JhSV+z$@)i@(%VD_n%esMX3)fRD@Q|rAL|F0o$T`^$0bK56a zL+^kvDH5y^ZG%a(KmW5@*A=@B&ATl%wHGkUjLFj}kr>TsM`1N95MDYDrhF*vO0v)1 z+0+Gj_6FLB1fZgs2yGSN7f8HCj>|0;qCj8@CB++s(NkP>cvPEL-{Dvf*u14UZ`i_8 znK17RBtE;>VS5O4>8I^tS)+?mfB4w$E8yg0YV#B603onGn*s=9*Ry)whrl{5(aufI z)5bzPT=P}i)Xl;rxdK&09+6eUZE_gv2icDhkivf*KRb1ZA|`afx>bK;pDyXZ`j&ow zvx7O_=v?L*5S_)a%c$z6F)6I`t(ih3eX|wY;xdama29)#jMTA-_`B%6uj_+C!goC5 zjvm7kDk!mB2VJRA{p^gp2oki$^}9k&M8u5EStpVOok*s6nD|wY^fqH%y=dLuiI(~# z19@7q5tom%@YPuW)%A}zxQ1=$^93LW`4CT>lhS}LGQkbY%%fPzqXBnvgTRjA98cPv zFE!oJG6`1F6phEi*YD_A^d8Xlcu7R@$INbi<FlDLB+vZN^At z%Tfytpus0pk61Y_6;3pSnxj$btvhSmcYqsM;nMHzY{sq4-oY?t%vNy(tsK#0MG_|o zUr!eF8v>Xpm4XVieGBVdTfSv?+IY@n{^)Cy#aMZ$>H;KmP%9*m3s+khJBC92f}(PS zYg5I~Jq(Aak0wf#gXWkSW(UBjiyCT+z*GLUC2g_2nG9Q0Or5~LDzmXty4sTvS zvn>bzp!_Ts(gD8eVLp@Qo;Y%jtN`7o2)SrGGhPSmnCQhhXcIcf(hpOU7RM&+9xBZa zv*ZiD{Z{m%>t&x08PDbfYs+k|_zc48oHwsp!VZ?O4j-2i`zn}CsdJ%e3&1euxtUK5 zxuU%Lfm_DgY9@Cx?%`b8U4@rPJ%U>l!v1+{$FC62ZrT5FfZ}(@al6P$Fb9;T?%spD z+cq1lAgEp*n}K7pG(bgrh+`BVIn1CvVvfH0`)3m@vx58rs~K2c28&Tp0{(QcD?JQr zn;;xI#mzAnvXKV*t~v`d>}ZWtfc%n-ttRF0AQE0Lk!nAg^fD-jTMf(moQjp0S_N_D zdQ!nVm_J1tP0v(B9WE_fIQZ268JEUVNs;d=KY2rdq3yrPAQ#erIIjauh=%Wr-^dn# z8r$I(;OP!wnYPBWSx!4}0XoSz9aBnUxD2kW@#E@@dY;9@>(dfxqfUrIZNxJ<8nz-( z*TlyHpwwMM5FV%SzIy+@ae%Vp-{ih5B63>72A4nA?&TcIiw%u~I*f`cF-hiW9H%UY zEmB;iBy#+!-+re9KQ;nLlY4A_s^`%>L@OI4h_n-eU5*`f^)RQIHxwQxfgEcyX-hwt zim=J;2IGR41k0YhIz2aYBK(<6s>CCtv;$Y$@M1%`PBVvqHJ}imbGhj2QN=J;!rDy; z!zQ!E;M~^M(!}F4eytSd-s3$9ZyS6%A#5POlpArV*tkveLen5Fae7d;2S+N#8aw0J zB+u9c%M@+`nvP{;Mhw19Uf^sb#PtH?>tH52(d^&;T%Xgzvn zJbm8)xK_bR!7XvYbsyKm&mLXJz+W7QceW&FQ007!Kcc=pv@xR;W+>Bf!qHfvx@E}H z)O_jEUCXR(GPWfw(2JEtsN(iiQ}RU0_Di&db&E=(Wb=N1l4AUfXCJdQP}9u88F8+I zMmdysqQA4(=3E5s1yevR7r#Ff$9V_*G(lmd3nJEkMRn9ip@u@>aTpEWNmns7RXy*D zy%s-><_bl5Mdy=}a}#(O*{I~Dc@$6U|r}($Ax6>Yhxq%dC*fO2srx z7Fi>vsbKF@GBN`KOonkA-R`ESgbvO@Bf5cFQG^t(DKsD!(Zhk^AzuhdyX%$H{}O4z zF*cU4Ndl(^e3+n51Q#$05ROrW7f?~xMX1IX;r_ajQB!+e*Vhv+!)z?cxusDr1`A0XO7u#Cy$V#L{}`BxBYhY*S4clJt3*rjaIiK1hM5;T($!xlLUn$xXCWGAwH$Q*;oOmu_yMADHPy%OzHa* zXXuv+%6h7~t~)0^w&zjx3d)9~undxTy96kg9La%?$w%0iQvu^!^4ig=#zB6TqezJB zM)4O~86!Qe7xlC40pHbu>GSrD#`H~4JR>-jxyV}A_drtcpHr}hqva?|96^Pc2*P}Q z{Lit@r>nb^R^^o{1&O9XzUbh>WG5en$6gez`IKbF;#UwAP9Llp&+5Ra|FU1Ha73f@=9bS)W@o` zS>&xrweAZ3J7j380jtRG{hJGK8mIWXtllAj0!_`S#w4K~<+00#IpIK-U{bBHUALQH zT?&-!CouX^k;!p0sn--(7`ORPQFQ+8r!0zyEb?wbMk4fCYm^@DLGrg#Sc#b!RfYH* zo2{k{E9=Q(mZYNib7FFDRkn8%ve?*q?@F8}vXCKc9uh$->)&$nMJq1@DvmDCaVn^DQwzEWdb z$A=xf^0>I90eF&Q2_wy59+u}K+Z*IEn0jIS3y;RYU~sXb71Gm5G3jYg=srPj@=p0?#icPUFU88#>-S>&Zq-c5W%e3>~GOkuLf>IhOf^h1kPf?Hd zHEjh$(ZPG8$ZX7rc?ffGXbbW18{gk{My9bO5H3D>|F_?M1sE=?iX~FjYNNz_&5Rvx zW1ex)4f8nrap$Y%@9QdIa?7y$P!lC)-xB!*g?Q7&x&;g1JIc;7UwHf@Ba_}<7GZjA z#?od(nRKEw@6B6o4wY4hVjTSd!)ZhiVm#=a0TD>r?yE(l8Pd+rm4@@#iB9MJn{tD% z<8%a5krkJXodHl_rM%~sUR0#TXHgPJ5exhwi(r@^Z_N_=`<7>^mMqZR#IXHwAn?H9 z(#a$$qY7)FWKa{DkW+H~6tMFsDF+W46e2~57(ULFgG#-l1aU=5iH z8>yh&TAurHFz#~eGZRq`+a2_cjzwEcM5vTb3pq);v2*`25zWu#^n>MM=`wZ`yR3{g zspG~3Q9fR#e#yx|Jq8`PxKChxQz8hUS8+=~5tVS=R{BsLPa3t$~B;fb=I<25Lu7{_V^2jA6J_R^XybAWz+CH}qVF zozf~cRk@T9(7R~3W%J~_eDlo!+Kx#bH$NhdZPC!xs96uDZ{vwznn$T5WXQ7KJU`Q5 zrMX+59RdPY+WAFEhAyQdi{(>?4Ka8M=$g8CEd|clB{N5k&TQ+Rwh=HzyX0K8Kqw}~ zDM`AOicU^f@$l9?RO+~AhR9VRCs!(Z0LiB4d0LntW3SfRUkrQjm*iQLAq0b|k-{$| z8}4I(cDJOe*rWg|sOwoiUJw&=d@OUfJJ#bkHy(qkT=N5f1KV9Jfq(xxr3b|3kO9iL zy&id^T9v<#*SAgSZ#SV-K3SP+jJBAlg!%BWj-p7IDU5`0ZCOVb8pP>Tv@^4*pm#@r zk=hmiS(&Jy4`UFYSyc+-4!3EH)Qw97R<3zKQ*_|XUlfqLJVjH&%y9SO&vtc0qCKJk z9mOJ4;GAx^lwm!Vr>1MiZOTq&BMWjN`~qpYasOSMS`Jflj`5bv=hk3#+cq;VJNN^; zHJMpZ%K)V)oC6_1Z%+p(IZXX*JY=Dx)^D1t%M0~`;M#DAqTVRI$UfTr#&~3D3aldqLe6rWT(eW`v|~Y#?^a3ydb_mWx+wX?u7T zu`4)OYRMB}^}wVAH$6I}<0|r!xI@7=UML~QO{z*1_z_lhH`(@NM#WZ#Q?lbsUO5o* zVqG4eRZdUe19h}_p;i^#hy@Y5$j$T1v~DOw7HU137PVAN4Z<>8IXX>VD~s&k%kZJ_6~7pJ?nuPih;!e4`%MR{ zOHT)TsxzC{bQKmu#F3Sgir;eE%0&XZERJ~Q)LiZDSreKN;8o9@hyM;X$i}+s_9Z(V zMCP+$#mg(!n>0l(C7?_GuRiFp+5(rg9)0d|Qap-S1eWR*8gDmZR~FUh@P(ccM)7tK z)=BC`&UOXb-!XQ?rkGYQlDBv09>9_$gs?8TnpF;?-qw#hXCRm{*qD@pYLUl}#tkIf zH!&FEgvYC1?TmbW6yPDUc@@h=Ln{utANtyL1m~sHZ zFrLx#5*EB1z*G8LOw*WUdIW}IAKT!Wq0Ag+PPUZ(X#a_df4M_KnZCwif>6|v2=nv6piwO4v(+mW-Yh#Zq znn%E`ei1icc`*A-?_<#5*01aNUB8}v2O2hVg;+e1k-|Is*+v2BGM}OGQK5W`Tv%Mq zx_vVLt>R(u8VV1E;1_>Hee1S0d8uPy}g;pJTqrz#w%X1H_6;h3E~3LDmW{%m;aL-@-K=;f@? zUXvO~MBUiT&zmT=%ZuKiSAk3c%mF`9w%a2@^obpYA-EdiTO;5_*fF~WrMT|fcX)4^ z)rc=+dF?ehDm0@u3`p>vXEnPFIvU}Uo&<5+;Gh(hQ_G0}C%A{)prG~*n8x=EL@xQ4 zL0fAJFQeQf;)di+Ss(UG*0ZewdZf|I&<6%7M`GQ6vIv;N)IW4J(L2Az4fT@YVw7xt z5%*Y4!3QmFb7YT&KB~y6yysw(Mxl5Ivbw=gjlDdQ9Ox5;@}SiX zpUHRG1NGR($_GEO#!Qp;jinSfC*5Zud=B8x29x=fxHBr_^4n!Bf%<@bAw zH{=Wj7Lpf#i5-KJ%8zrlVIMDcF}Ae z{b@z8h^ih`f>QkS1&+Cs`CO!WdeBcQLO@onnxDw~Sift{AWXmFpOQ`OVHsk-JO-hg z^4a9H$|m4|WC4kk)H&-9vu2#g_7W_u*FSi|B}ERhiR;*IO`o#S!7c0axVNM6&Z4PU zD&cUD!5qa{_WLn@pYCu!dsVgxesjrR>PUfxqMVWuat%AB-t!GhpnS`WL%;8@tK*dN zQW}=!!Ud#E#4Weu0~dV?U(_LTo?|>-G(4J3hXA;sfBdp?HM`*jvrVJf0g-4yY71Bg zPOAD0j}x!6Nb3Q%(DHHnf&1r-=zFtvz=lh$*q7y0%YasWf7sA_dV~#Xas`#Kje>WP zI1zx5QRVi1(1PDF``e4`Kni0NXU}^s-x+;lTOIvR!*zB1rY@2CYh&`}pco8MJ1vtR z0={P&mGBGV-5R811wTr7Hyk|l`4fkR&0^vvzNUC%k#=DP=C>ZK8fK5wbQ zucl<$tvaZ)fEX-K2-$r^{0wo6hkT8?nZSw&jiwr&xQ=axsVhFw!u zirLPvNb#$!Gm=s0Dfb8uT?!7>1}`+Jc_{C}*7tn=UASv30*GY(Q8wB$xk%H12mg4R zrmBs$b_#7R>%ked#yoVI`r2sWL(QASt0Is^k~qB=kh+kiix@D#e+6yt?}jOfq_D=B zBvdC6k=6(T!yerB9HCh-Sn3-b8`Vq1dTpAkXFumFqw`ioid}A+zW&b{ilxGh3BF9c zOyccddBwJV%|QEF8K`X_U`rW?w&l}pNaN&@x#89r+VO6$?d&6G@##x3)AYUd?K)r1z;a+pbo z#Oll*P+XZrxJ~23JL+E3XBSaiw<=W zOp%maQ7CZRB|JfqK!^-SUB6856`?TbWtl?N8D~-qU7W7geVEm^c~q%!Cok;SKwPce z3GjdfJXh;*h+fxZK` z2Ud$r<7a}R%k;``w+0}=t7U-#@!vOgkVrIPd2OGlu!Th8wyTd2`j;=8P;3jX113KA zdFlF$h^4GP`My{2kqO}?@nJ;2%A3+IIPs?vs146+t0)wUVkVq9ww4E70yM*ZuxO+x z&j>P!a<5Z}9T&3t9qwg?)uh>+aADRa8Dd6}o23jkDp&;Fw6b|p!*s`+MyjH^9bQQL zu!I$i3U;kw`P;$)e+ZH z3$OVLW5etudf_Uvzm=%*DQ8KiFMA>&|CeF#EE9i0+b{6jFt&;9vaMTO%SG<4kqU1z zobysFff6&X*II$I`0tAcMkEm^p2wA?*@U#T1Wh;4p0CBPvuqA-2HBe`PQ(N+Gmlz^e)RnO8pWrOJknYgGrrWm7-PUf~+-)1XZGCOqwr$(CZQI6fWBNZ+r|L}2xtLreE0tBL z%DPDM!}~s~ts#zd#Ukl9rk^$xM4uNefA3fP3Qdp~af14%O48vhsIQG&bxyeWh}mn; z5#Kn4M5dORI5i*~77g)bT0VtVH={tjtZE=I{&f%nU{#xZYE{4KG8H>iB3(b7B_G>g zMEk4>%w9ggr1-rwiWKRGVV|WnwB#5lhLOCF4LFS-6s#Sdst;X42o>k1OE=dhOu;e5jM-C^!quG$RtzkQXa)iWAgyI&M7e|4iB-{0T*?8u`_}P zh7SM0g0o}vaSpmPtm|Bk(%UA$Y|%b@6ZW9#(9l8-8T()x*CaWF4T9nsnHChlyJJnz zGHc7I=__T0yCt;uL+pU+xWUGNQW6(NfrK3mN159}pirWBz%ZhO=f- z^%aaJ-jgzhD3FR7z!smbH}uAnS>Wyt|D?vrezKic?a8dTs$W2mYr9gBRvoU)S}cI>XerSAd3t9<1fPo$7Jf=I0k#D$7C9_)GXtf?;B+^ zMe~xY>x4tA-n$qmklt)%{GVTg2up6mHB1AEGxn!H#2ByA$pNd!8vA8hv|SIZ=u4i7_I`u)ecq3 zw>ND=O{VaSyY@-wkVugFNTko84Yr<>kc}-v{|7D(`~Sqn`OV1mU-TO$B1RV0->m<2 z`ro-YjI8Xeod2I(oThda(r@3H$P@xY%tY6>GBXo@G&(3vaC~ia zcpj{Gd~|XWK~9i(ABqLg+z1+m!wKpJtUZkZ1de+0EG-U1!_17x11tJaKMyrbO4zAW&^mpfC?Tb6A?{$gokEzg%lLE1wG{8Si!;hP=AhL zn1D&(rm1hVacXFRL<$rGnTv6X`F3V)bj+a$SVMwffzm}yDkhWj6Y_iY#R{-u2H*>E~SyxFq=la^$5|DuP-lf&d z#Ngra@vzzT{^6wAiP^N7?E@yi3ak``Z$?%D>+a_UvKi=yG0vS-VuMzezbA_%6O^vdwW$mmrL zw+R@){{Cab>dR$~+?j|%io&$SM+FRIlGfVSXO;VNjah0|kByVqjmn-8T?JyzWzP@m z@N&BZ)WFf%<%{M`j+aQ}5WUO`vP8K+=9Dx6=2{(>H+JukVw@ z&w`ZJh^VN}IJ($3Lex*w)VQjmsF825FVg>jaj2U*vkI!>ruhGFFi!9b!$S`jwBMw) zFAxNVvzQ6n4~NEW@hg)u!{w}7k3ZSRrpASzVuj!Hrkte?0yrxZD3iyh&G)9^1@h_{ z{zVw4_ZLZJ9zBKBv)tAPIvE609di>{8mKxa2e4Rlv>3td2E^XUY2>e&XaE2-RpdMMOGtYXi2UxGgdQki(+7#2+xO2u{?L;x z(?`8W-JzGBP{>}Rd&0r~58cH_+gR{kun$}#@ZV{l@b$o{liv_K8JZvP0H9RbpW(*( z2YmN)(q*HrsGv6mY42h$l z;>;+SD#K33Yu(kHk`%oXXQ`E{4D+QdCC`e1$>Z`P@131`hw5j!3wOzqEmfJl>jE9A z*7wf)@0kT0cl-OVg!M2jKwp16`66RlFH$s0a@(q7g|pJbTkmW{<;$d~mX^B}r+`!V z_FY^B5k^Dn> zR?vLYLT(l^lsyp$RyGeh0W*%l8YNb8e-^#uVX--wU-%ZMGVT~XloFke^H7Zx)ssfS zM>g;`1UaokQQoFchd23Jjkzar#@*0A6k9rQ$l%%iqZFoEfCw;eP-Kfm{NSK0Ty z#IHnPgW%~u1;1A?pQTPsCg?FF-GTmV9MKeT95z}H% zz30|{;?R)fG>+Lpj}er*KH;nBtBIWxt(ar;0@A3BE9R#Y(F)dD{6Nc^h(wkSZHo_3 zjcZ(rsjjIK2nqLSJgMrt+xE-HZjQf+j(a~OmD877iP3TGXrrPv=nzxW)Qpg;+{TdN zlKD2yXPJ`5gG&O-RBw!peM5eK#OjvRlEzT81f}kivC?#lXA(bXCT!LL4$NDYFjq18))IbA~4mR;lfk0OMs~U4Tuw=quHiV5c__;j~a* z#$+Yl`I{-28R`@RkuN16=Y!GS9<`7WAQ>Wt+GL&Iw$)vlPv|SIVJw;zWRiCh@CrSR z1eTH6xWc5pn6xJF;h!quG;dsp$1sR?I$9C%&lN0idJ zpmS%T4tw!QWCoe%RRol5?-KO%G>1L!Px?=e?}=5igAuWh=pof!?E>bN5%439z=hx= zWl3M}4F}K$Fm52&m7I(PX|^e%EN>|p)vCz}u2mzD1huzFjq3@)Vd%_woeQay5Vfy-8pA-3os!&I4qniizg zl8L%GjTVGGoT4>=zWu+{5{!XJQPUJd)zi|4218`57oUpBgE|zUn;Z=iY7KX_q%lL( z^^C3T3_BJvM>g;z3p?T!)i9b0OrIXZfY4vPst=$&F806H-gPORa=QnHO3QsH&5zdw z!h2tJeAlf+qtFR}lF-7M`B7objUNA&3{z;FQ!;v~Gf0vflY8IOVZbp=nhy1TWn^+q z>k@{uN8S|D6zwCI|5WkW7jxF9D>Wr1|cx0QGEoBeY2FyW5tb7CfQf+u6;`lmF|&Z7b?fSVl3K}Ah|`vP+$#9ZO+ zv7#G|(yLEg+?B1&TPrhc1!Gc0q~3wQaiD)Q3) zs(cZ@=+9HCUk*rAr8Rq^ptkT52`gry?Gm*leyTvl(-U&($wdAk<#7ENfJdOuJ#0j5 zT{}?Dq(v!IoS0ynQJPWe>KCZg{Mzcr!_JMr5CiMiSu&5mU~k6tq{fF|#I#g&slI5& zHhqyOn|&x5o8j~Jh(R+rCg|>hr^n`Lri(ok)=e(DCXGmF&|Z>S)8K=`>t6jaeHjc` z6AWP;RDV@4V58up$d=cfGy@h6G1^;F(a{-u+31LMQKZ0%TLdc^i_%}avJ>6nigZm3 zceyr*Dp{7j*4oNsg)GEdQ~K4?iWsW?rsnmkszLu(ScQX2DAnc~>t$c6a+AcCyYghH ztyC(DXs|3N;PU!f96bA*(=+!ggzQm-Q56r8 zLhOZZs=!P!X7?r8K@2lt^!U6aWgv7rDd3QU;;1gFZ`CZyMX^K%w#obmlTs zt>&3KoNPps*y1;g!Iw8!i zln*-2O=Vc`scQ++KnMVzi5T3|iRe#{AXcZo#@O=+rOCZ-#C4)%;~<+xr<_`H@1Qo$NOsV367bBG%;Ojz#qFJNT)}u zo1xcKK)b{%s#_3~%M6x-lDlm5)|kWhZ4e2cg+OQD#L^*#iF0~6_t|=m={DL zuzQ?ZJFRxd!N%GJ-8p{GTi_0DY1>o07gPEaV_G%D@|%a@$mpJX3-&hRN_=dcf!`~+ z&t2vZ-KQi=R=D$SiXn7UTp_@L1gr!0p&Axa3%35{r18`Y+U-U2lEtP|P&xC+ccPfUG|p|=%ysA!ckH7L%5y!#U&YK-KzXE| zagi2c@%|uP`i01Bv?hNtc@=I+;};3FT9pJyhKv@@FXPss#vp4jx-Y1ifj$}Z8$?Ze zg*kHpM1xpoy?g;j{Db@h%sC1zTtK1glX0*X#$ImLF^@HwC;1(gok?4n zm5ZUFDaf5`?5Vt%C-cJLL%L|F_6wn8&LmBb7&@hJO0%mmriT2=?Zn^c@Rom>0$s?= zJis4ZnP=u*YzNGRP7#&7%MO`-;~9{s=rVv!*9kO6b4mC!?;>)tQje-iC$dFymtE~^ zK5k{Ftj)tx5s?j3NEjVH6mE?hJ$e0;BWzM%2Vs)a?0zBO(X|R}gOnmY81h1PcUFGz zWVqPxg(-+^N1|F>e3B8hMQ23s?5hT;TFDq1fyI0sQ>b6nykRghlnEygsk3 zhEb~}2Laen!TzY6{G~TlWythi6vt`mS}_HDu|=~*jl7)$qq`pjYwL(_`|0)l!gx0a zGpt_X%OQ}@X!?4b5bekd;dE-hVbG8`IWxDscl=Q_pg{{KZ^X9eRe~yJ)rxRdDyX4s zliX=G3jtnG*>l728&CC4ir!<)Z*(2|bI`NzPkYsaq;tHwW%$7i*;v3P`qwxyO4z5~ zTt)%ShsiAf-Bm_9jBk>)iqa`7tV6qmJE7BB=A}bLaEfaFor(hvk8ytsIxHKK8UA75ZM))vW?3X4T%UQC6n^m{?pZ*KEi-SHBH=>L0ip#Pry4OEhZ) z0l(+U%V|D+3kR-%2bqXrL#Oxp5mUeW04r=*8E>utxC%Q5k@jNdyZ9=>-F99@lBVt* zT%Qxnw87bJ79~?Em6#>9qlr|AeA1f;E9k1&*c`$w+!RF3ptF}4!-bJ%P2Q=(jOm;% zkUZ_)AvFroWjJeg=$Wz!m0YX3qulsGJAz!!yWYtkf@WfZ*4PfKSo zAy-ZXmE+>lpU!4&L%5)BQvp*R`IcZ+3m83&{Bwz{P@;HnsyQV!KUN zkNEo&HT+4wf&eFjBV&A$>AT#uaTv});64Y}=$QXL6` z%d+p|G_*KxbdkKL39hkIVj#C#e*e*srsjXX_Wh2m#s|KiT3E*smXcBAhu$_BA-f|z zuoL!;K6bKS>=j~c7*q2w=Qh3H<4?y@PSRoJOUdLY9GYN$!nc{nX_EWJJQbNX zUAuE3v%pgTeEOkuzK(LJSYIRdPv?bFC_s45fX)>`7g%uKsOJz=4GT)V`Cqze7K{X# zO0zCGQ=Wc7;7G$xupb8m?u+SM0IvpWK@Tk1l#iP7MIvcxe5xb zJy$T=*f@=s){F0IA?QRcn{1Y#hs>1*Dr4X0TL~-K{P`f_VW+(8`BGG|A^?tKC*7Hc&1By@j#5*uP~`Ab zmTQanPwH4xRDE-+6TX1--5SC<39ieXT1?X>&)6Xpq+Hd=T*FfIGU8ucf+d%IRhtB zyHmY}nyTX#owzA12WG3(tJxSJGpvF&$Q6C~KJ=NC9 ze-itbVzr)<0SCqjFBZk)YdfrEzl5*GFM)aZQwCzP&0KdU3Z8d+_2TMb;p z_6_p;fDRVD&Al08;2&sPC6lqEXGXg(&vugg(<)tM6YTc;?-im=ggJDj48479RV2l^ zb9?JEZ4)c;3rpBL{(#~uYm-D&#_QUkTBcj1z;_~7x?;n!9pSkhh$O~R=-)(khL_r6 zav@Hf{R;7$kra=_1R)ftUYq zdZHXcK%_T}Q^F(tF`I9FXl+>KkB>j?*bs3^_I2}!Ra45LtoeoifUZcdtH7jkD#7^? z%|%gfQ*-2BcrQ|j)qRh%C@Cme+O^Fnc7uE<6?0O1yAmGj{RtZ@h!)OvfEG65+LL_xqMvk49{p zqcNgc>Bq06V0DNpDgSfBt)lg)ewW|;4r3Mp_!JLFc%WG7 z5A-TFmgs`foIq!1hlRRfhL48aZ)J6W#a8PP33IM8R`a!nqppL=YvQqGil`ZB*8JkX zk9&ER^HHei7xlKWxFvGUW=c?t1fw9D9yr-U4cg2DG;E_N4Uy!y#MQ^99S0gpwoaH6 zdp%k76OQ63>PsDHBLIuc3-6YNCB~|Ms7N9oi|1me26@yp?tA#y=^DT69Nh%*TC-j;hC7rJ7pNWh+nEfjiE92VG@*IuCc3$&;zEb%F$5WTMDX#boAN z7+?N{IIEkzJp**c^AsK%v0~X`D%(KU-1UY6E%*8wTnZwBv2mo1T|(ZOcms_vu|2<% zYBR_4R~#>id8!bEpwfF+gxZ)LXbDINS4kDRBxg&!-PaPaTFeDmvs-DrsT^XwTLpVi z9C7+>!G`ti@P!XZ;)6y+_>}BB*UhAM&^^NjZ|0N5Up}0BRSit+X;bWjnd#<`POj9K zCkyBJH1_8Ab>|@e%wC;T%^&qdGZ!woHkw20E^I`yL;sS*{Q~oe^QIQXN)Zheh>vlU zQaSqs?yrWyn}2|@59MZpGx}>-?QW0Oj-VkzCq|WiiLRCy8Gj|)`riDUw3Al%MXvdD zk#Ps(3nG@_?{#;-eyG6qXhdd`$3~{E@=M09)(;?BXXNk?&LhP2t)m>yNg_CUI9z3m zg@j;>6WJRJ!C!SSsViuqbi+2GASjUB2iR?yGe`!^)`qEuehKS)Wl91&Xo%gI>b;WX(T_do&ZUJS?;qy{FpS7*=K z=kFiI#$xA`KX;>%^ipTW5K zS#a7)CZ8knP;~O{5k}Dml9*<&9C1j>+=v-mY^z%<%**L{H&S)X&FWN>T}FrFB7t=Wda~_y3ME&Vb3CSkWl<;zj9}W^CVMD?b^@bIB~o{hP+D0LV>S zwX_b*s0iw9#Z>JrDItCyI*gAx-17kA)+l)Nt6myDAz<}0Xr`}FiRmlE?Qt>~sJW~d z(u|GRolv+HvR_OQ*;$5aES2l(nK(L#Ph(NoaPwfZf7XD;2EotYaLkGlqI6RG$pwUN zT8_71mck^EZDswMK3wUDIJht$&WKsx%T{f_V~l43p1nXa;186SQtcm(zlLWpNDj&>62n10RiXOD8lSMKFuU(@aDLf^@d^;GU zvk#VOnU;4Vk7yhuZ|!6}+mhykGJgZo&eFVpBJ$G#YbZ8uR|>fSHDg^k=t&Wax~sBp8BJhRi8n~N=tnHaKN#OVjy6N6bjaQquu7O82=Z@zG` z44n;}*O*90G6`RO#E^I_5CD+!rhHf*Nm;b+K%4#dCBRYTy&Rq#Ojip592OI0-%$`nXU z0jJfyjELS{Ly6@uoRC0KI&R=r)O$_PVsh(&F$SXf#k~Png^DhdgJ=VyWl!3p~>@n@OX(vn0{(Dtw zN@m!h>=Djuit++MpW=D2*%hyn?|nX7Or40AI%VXd$)_A5&_ECM+Z8qFB^-bD1yyP* z-?Gljzp+-=^!+JHm`NP~8CCIkXZjrQ8eD;FGpr_hVs=3Cl;LJ#>>)boUh6iwvw?KQ zA%Q(>U;nlk8gp0cp5Z=i60!mo|4a4c{lUyGE1OxwY1uUEZSK#E02c@OQx?VVD z*elNw`?&~kr6FpS&kwwyNo7lp3&~QqJ!Z_{mXegJ+0!t*{mwi~66!?fmNt-4%E0-GP;qp)8_k> zPOjsbrzp=NYA?@E6_FD?Ti1UvnIiDXdLExi4$fBP7iOB|C%~2_ue`YYdXbYS2b@ah zSvQszwTAmPrg2|?eXMaW>m+t4PK!*sHc^JGt<%L5utPB|jKDZZKA@4DD@O9)nJ1vF z4-lH3WG@)*OQlUR*0D8^ja-@u;!Kl0^I89%@}36*M@ zYuZ>Ju=h`~P*v{ZE`JE=V^-tu0Dw2_p26VXnkguHJ-)u4SWJgsV(@p-mD(;q5qSIng2PU;H*1zh+c3}PD!WtxQS#E+Ueu6Qk_%b)*9(gMFlMv`tAxT` z!xU9lN&Z*rD;3wtZ>vmlL8W4{frZ6jmqdYsM1`5e7yEHnAJ zzl6eY8s$&(e~$@wqO8U2Nl+wK9*23;tpkcJ_*BBb}J7^RM47v4uXyo zG>14+J&(QDZfhvG=znUPIL$_(FZ%hp+FqO6X=Z;R(=1c28tR9IyE*rmimXhiT(HsSEoB}ggROm`91qn$%;jc#6P7uq z5=nWa37#>;@}2eQwsb)|6KiH($c!GMc%{%6I4$nkqIUN@!Q2AtF%!_W6x6D0!xA}M zC{i`I_>&dx!+vSK{+pZ;Rl|f*?K2{ieu6mZDT3EvaN;79Y$s{eJM3w5jLF^#wl?{?vE@&vO=XjlrpncY`)eBy?@@&bPk-8$M_2I82uvX|H1bP3|Dx4sxpnBW+z- znZ(6CK+LzY_wZFVMTZlY<$wPjbv1k_RAdhwe=lF9)TYuwT1to1S zV~zKni)|2SpNU3QnlpRMe1UcRom1}4yHuL173hN`0QG8zT4HlCm6D5hJGiF!OwXbN z4h1PWn^J?hbJ^#`llTp;WNx0u*qY=y)%?yG{@Ys8qNgPSN<2f6$r+mWE!Zn;#kd|V z0Ie9r$TjHG969=;6RCwPvr$B^R|v~pBm+Pr@m&9rutK@rpZA=YvOU=LSE`j31yvnL z2I~s4S#(U3F&Et{RsU+nOd5V>T43HW;QZ*=KMYC;3(u*c0L*7Mz68$bib7#PZ%4=$ z08&p>vlc3^1+5RQc(b^K$?xXjA9C4*K6wf;AM8q%4}0gFK>S)QsYOd-vQLR+7WB7V zVjzwkh|EM~W>yxRa5lkSy&-`aQ?)cz!$nMzT%XB9H&AUmkU1FWy69D))vSnM@P*W0 z(MNpxvl^=QlI3zBc|@QbZ-=$hZJQcS7*i}pN}mR!>n>g3 zWx^9_!46SpPILP0F$UPx(HL+%PN5O8tNG>d@Tj|O1g$ulP+;jf z{5<1fSXHa(x6}JpXOy%+V@^~%mFO<(K^;?6$t zGpNjD7^bAziHnjV%cM{rMzr{&8MG(i()p40mkhjP7FKI@y&i){TXU}h#Ivcef5GPJ z;W*lmesY><6H{x9@ubQ?(A+!r8-u>MKSAIh%O~b!o0Y%Mz%%2U{0VSWS_inI17+yM zZT$6Tyfxlvi&r(%egQaYPW&>cZZECXzQ|2a5}36rP4~hd3htB99}nT)UG>e({yrXP z{6x=%jjg(=W8en`N$bwJOx}Z?)op)?&o;JAmECd8_=-EZS?Th~R4*`V-?xA**I2IV z>}MpbB@qid*d2;Hs2HQR@JpNMQ8XnE8IKh+V)aXosWM);^#|X?m}G;j?jtA8*w7Js9G8l=K?(Sl4>tBxXftBaj&X!q9= ztl5=aw{dPv;s3Ce9n68>og6;C*YMY-e^f`2J=4IPe}1kA+Qx>tFd#(URWr92@#s^Y*SezWhJFEQ3L zKru4%V_Mmo4!xw)mOtDPy_11FF(`yXIMFmW65}?n8 z4;P4BjD&B(4$bOl%6Vt!8bAcZqAa*OOcC%o4Vt9jWl-<}f=unU6RL_bh(Yn) zUiK3uy!x#OP!T(QaEtV|l z^NjDLk3%p77%aSaKo;X($;=e-t|-2>9faYSs1F1-1Yx1L&@|Qj@N*RkXrXg!Y)Fsei;tO)#F=WKux_}9R zfO#>RixOk=nPO?763+24q(V31$y>a$Gk=%0!J1y|s#!Xj$bWsoO5Z7J z2(8ngGhip3|1QDJE<5#%6#DB3EM4acNxIW*<+$pp_kdk&?B5I9ip8EepyFj%HJOm* zEp{@Hg*Q=9WF2=ng2bRl&XUaWB(M2p%`jE4 ziA6VjA7SG)j32Sf?#Qj#{-WFo2#PnYuR5xqF`(ceSzsax$|h|H!ftlDZKkqNdVmXt zGt{O^;FzJntHi;GjG?EFDhW!?XJVeKnXS&NYMb3d`Tn{PgxP77oNO^Slhw#f<5HlY z%7>n_O?*{$KhHWKW#QdCX-Uz=N_j!*8XD07&OkWAk}AXegX>lqaP`3|*ERu1GyCX{ z-LzjJ8WpOS<1HB-3xDsF9k=3g=<>0At|QJ{I2FcPqLI^l6>uf`yU~{YD4UR%JSFz? z*r2vp?l=FeCyt(`Jz77Lv#6n+mzWvmD)67I8-ae?d%M)=3;x$~yfHRAS_&Jxmq%bd z06G8(UV+5uQ>h<4F-e|I8lIN=ge0?&>#nAN&rBF0X08PYUr&Ph&1RJQ$aL$XhU z^s8asya0ZuK^OJBHwK3Ui}2*^yAWx_5^Kcx&K&ORW+F}z@?dG*3thQWbd~wev;VxH zrt_~jc8wQ9w(!q;S0~ks1I7bkf4mzxEgn|Br_CmZ*tN}&M&OPF+F7Sojm;@J8-zzX z|92?ag@W!dV|de>Jk;YsTfs1*LU@TSZJFSS8-sNz++HohW-S$r4fNT`2RA8PH_oU^ ztSVph(HvLOQ)_ruGEG(iz^g|BglXJp|A5BkDh>7-ds|#4F}891zP%a#TULXu5PCnk zX`ApxPbfQzYlm|IL0B^=NT}TH(;M|5WX~e;6AgHR>=qmnm$>=BVGiZ>=XFewe(SY| z%Dx;9Ur}y$sK28X^&JN+zs3VZrR(jnea z(5@j+&~DBYRj~UEAt21UJ?6xNfQIcz2+vVo?8rOVmGxk1^ZWeMFbp*Z^!A&xLD483riMmq;D zC)t)}6<-4`r)*d^coOdfRs0eXxZ;39Ke$-@Tzp^@vS~($$%75fUvEK;X>32hoJ)MV z>5`5ceyaqnjlh}`;&aj8NP0H?kVryi!yEd~C6dPqR}4XQE`t=jQ=Cy9izexhM@yDa zw(>JhN)PIkJbaK_ofu2(Ze<;3GZ8mbTfPy3Fspc#6&-Lv?S0ius!mJ(^B>FDXc(Wt z-U{i?y$)S0V~A8~*gzq#rDS+EH}%Dg5-&SziEV?KNn{&F8-HFUsNA17*Ss}IS=i0{ zKtlzEY!*uzM3=$YHj2jWfJTiQ22ug0Nn0KodQVl$$$;=*8s(ChwgQ{kAW)kf0pq2r z%hnd7WVq|8O1~(><*{=V6^C1YWncXqlAKHfn)(vkNmqssAt3f|BU{e1mO#yY<%`^d zTp?@BF^B~34o8zv+H&lKkPuG93W@zEoEz86J z^x6fh%A?77^VE|$%*FT|_aV33$t_?JcjhYIq~5PB zw|?NlCBKB{H42d zXMBD5ovXAJCY-F)#-OJdCgM4H%9SrdKuL`Q&?fV!EX#5BZ7lCFR-Hyn@*0?RzBN0 zt~^fNii->Sr%R=E2VbA@0eJq#0LeOXXbc(W%TgyOCMItAjv8m&`rD5?bpW&5d!s}+ zw6nIgPdb;QxPxu+Ba|b&tF#+mb#Iqlu0>gPB__?nLDxipQ`RlIcNUc0n{DC6*BsId z^$zPGA%B5h`7~E^B8B&6nRH}~{tvbY(~i_IRhTv+J%aZ1Gg5hR z4p(=$yvv8(KjpTCXl_%2AS=m7poRVAnon?w9fZP#-SzV;k6_**Y*f;#7KW|dQaXi< zA^5ls3MIriQdxJ*)tY%;5fr1B4PHJCJc;@=8#+es!qTdzBN}pxvT!vRX7gl0B$L+l z`(&J4mSOg{cJ+Z%Yv7oSQ)e>0RFTk07FsVuctw>X)P5)hR-8SA)J~b}iWPLd)5kzv zPGJb{Nhm(04I4NQPU}`G@7VjK_$FIbv3d7Jm<(#&SL3`2<=ysQQtTi6W#m1bgtI~nFQjTT-|nlo`P0{$9f{GU@7#i~ug7bVOYB%f<1(dryW z1Ezh4vTk6KzR095&-$it%S6Hb&=;wavcawOCe`aEVHUlVgcWHh^QU{efqzLrEz#s{pdEGEB0$U^?~T#b0h=dP%x{!paM7%ur}@Do zlvX557z6j?_hb&=Rm}Z_7hX!n?Izmk;v5y-fxA{%<2~tyU;Z*3jtY~^pK&AESbxO% zl&ontXx)Hku>M*K_Yyuv)+VMV99awZ5c)A)FckU`9(V^kYv=u4lY2?ZYURX}Hymz}9ReMqX7>(D93G>sOaNgE$4b=w^sP7~ zHb{{L7~!0vlH3m>n=OAJ$#MloAFQo0Xl%cc8jA0~=*Dc2{DBk=+8V&72?fiXR%_MSO$6^*&UfMOLh;Yv63B}~Gdj6`8( z!+i>eu~Om(_8|5eQJ78my}x@QV)jllC@2OtbBsPlw^f5saQA6c2JWdpHd>W$(Gxk6 zZ#7iBy%aRiXTEFFm5|q8)Mid_!ciS~dTU^ub;z5ZjZ--i&zInRq$Pp?jaO0*p3Srv z_?x3rwS!(!O_=wc2gLiAuj5x)h%U?;()eFVwAH-!Kr!(VqRXPg#px4>CVngJ!< z8mrmpb3IVN!mH%87M+*EC+n8pjz$ZL+EHJ{3|#2?`U8TmY4tSQIzAwER_Hz?%(!x1 zvlWA)UgA^;26J(njcMYx$E)QglRgjXnOSLyzwGATP!0z=CHVdLyrGH3RGvk<=UV?z zGP)B78-&NW56lI-a3%wvY|q=hu*^P#dx!LbN||@xWv~TLNz;8HC|UehS6B-St@;c z29(BY$Vv$Sqc@nEU)of*{}SFrmShIAnMN&rTC7le{xg^nEIqJs6`WV6GPI;XOv_6@ zkdi1LjEUb~6RpexaStN;Vqb=PXs11#D3tPu%*B34zhlHI1xNT{MYF1^qI&JNz$TiJ zm&?QDN7pWQmuH~oP}!phsg$g2Da1^@~8z|V^qs?BlLau(2j5s zKIVy9%0oOmr1aDnGj#{DuMLDaKb#A07Cu&Tuv zuL6N+Kp0ecQFHCskj6qMx*jABr5*SIEN{>EubYi}<6Ve8n0!={I-aktW+{ta*&>UI z-@Gq0`?p&o7)Ge%kSwz{wYTu(u2x=Af?j=>HEJkX^i$1d5EiPqjCU+cnNT{l+x@czWu6a-mzw8}L*1=sdmqt8IKLNbN}yaoG;01lOM05iKfRmrDrLgr)3t#ANxif< z{1vvgf2q2=x$9zr{YC$9hd4Chn4!SsKQ0m#%1-{tnrbweGm*ntXlP(IC0A@YG z)lXG~di;{gufhho4b1fy?1PydA%;NvcTO*vdpKww+ea6l6e2C;yvt$)th4a4(XKR@ z+OdgG-_gh;@k+d~6DY^UTA+8f-Mp5B_iNwa6=?T2Y~jwtnPI!PVmN?fIQ4{XX=(iA z+pT?f6FnGL2FWVgm(!MP^e&~Ie$guR1pzC5yibGQ2Ih&i@U23uX+3}30>^=MVq^OZ+sIGEur!083@Lnh;9#%S-52F$3-(NYm2>V*J zbkwdnTQ;k+M;JP}v17mXZoCd!?<{zEth^~~)nD7StB2Hym5$qsKvlXM%_9WSGug(48avgPBb8#osNBE!-(;5k3() z@EF8H*oKT?UN`i~dM{Ty3)Ug+2na!Mp`pK=h7AhRnw5oApE=Fz z;)A8%hQUv;^|U7Mt4lcz+Oq}~3V+)pfpw?7mR5js@;5nj>gO=W2wo`;S{tNtG%zUF z{`&?xOdD$Ze`Vn8tp800F6iWFW(%OCVo)}Bv@-rVDmfWg7#lkNjD!Kk`i?)>r1h1J z)rnlp9nFZ$-0X}20Amv(JAFe-eN$s926=$3k&~e@fYQ#$paj&i&)L_j#WCEcc(^z31GYiX+qFN|48YS0Y2u!NC|05Ey~>55)k$mPjLeWyB4C ze>ZesASMK)fb{kW1u@5;Ih+I8VnRZh;XqJjRW((0O$ba~38D=9D>tS+vmP^_FbwEd zB8nitU^Lb{6oUqZ{mT!g3{i$C0(5kKH804`7XbOUjv<*n0D&#sd~L7+0I)d-rUn2b zd_qF8AT_wgUti}z5H*O(AK&FczqJ0$hg@d*{>=IfG44Sgm|uH`VwnvN0N|#-I68#R zzy)I`p0~R{EiAU2Nb9iPaNmV>^e(vPgN@2DFwGc=n?;qJsriD*?B_N)4h;2#NPn39RM+KLW$ zSbglP+~t(gG80X^1x=81sU=eLQnl&BiSZuE$z-t=py@y&THkjnv!YJ~DPm^}CAhk| zKPr&t=4g7Cep^}Sd>MIJimbNc%y#88q^sa)j8wp|>#oc_QDpX|`6mU}+(gT=l}86e z@dIU#k&+Q9E`Snr^s?ox7@9r?*m$> z!YuK$<)g_Ru@CcVH7X};3{>OUq!_Y@J(8{$15Z2gJ(oyx->ou9#$;Rke0L&h{h>?b z@S)|bNe(>Bw?$zJH?4gzX>c;aX78v$5 z4dSt{j{QX4?J#iW^;K%qmNKj6aUv^M4(0~F3H0eT(#A|DwVoEoii;BWQ(}Lpx(E3s z)$(b@*}a?+?B)x}A7}gg^jK0?8*7HKT&>eZ62)lRndREg=YE2^QkN0y8MMr*=F<;` z^Y`evK`*jsX=L~pT5+^v*;uWs2>`TC>#yrMv5m%);5)?fV*lWrIEV@g#9_3_r5Z(mJnYrm!sT=n>2A~V+M5%Pk7jIj1|&bQt*uN&G)qCT*F+6wD6 zuXB`*$!*N@MrG}YXSgQsvi}a*>P+3~)uLYZwP*QFUD(H->M z9lu^*pHt#x3#eqYOJv5I_Z!D-PUV4qObUk=p_EstjzAOs^mHcE=Sn!j7C0kkHsHW^K1%P#LltZExh)I*BMk;WH(fg_) z?EGp}wN%t17n7VhRcbtDc{NMc>@Eo39x$I^IO*HGw%ts?Vp88M+Vsk6EzZubt@16| z3sOZKDQv~5(*kszYIP#zS!GXCQCS5GZ%b`LaY)jtH79_;l~W`L2E$9#Px5kov;6T$ zk+(`FfP`F7s#10zsB&1(Rxf7v+zGfsUKJG6-t=AfAy=;~@nLZkX)QRuLa2is+$i_k zN?N;KcUj`Kht5|73~u1wb;X{)(Kd$xoAD3qyucQ-E}hqnuUioMSu?%d=A z?&0i1?)1X~wnVqIi;5UT`rr>bT?ZpsmrbsD@dm&{0Mn=5A!T{bVrc0Nb{P`V@ekId*k zV<6XF{Jc=yrsb!xtYH=t^5xb!m+u;f`B+l;h-oZ)o{NP`>%qbAK=y~LI91Vs29^>j z{E<&kB`(kun}Y8fA@w;!r}UZt*DQO^f8^xjg0F2s`A2|pZ)R-;s6(7@EPOOgzxH8b z%rodp#|nHL4B!Kj5=VjQ;i8AF1+e*WIBS&a*S5ipF~!XT4FM9CNoKe@VKL{Y$2gbg z^*tqSS3+Sl`J9j)((X-GeNj9 z%cn2xdW~Mfxt$m=0Ht2_(0XNZ>3f#t1-*7Yyx={sj`nX6pHDxGy~Zo2v#GEKOZ<>% zFO=@k^440#xQk3toQV%p|Ek@vyrcHjT~ZAIj(^j@~5|}kM!J*S(;c?k;IIt z)(vcIST`nqBxCH-il-kgQgknvI#!OVT$Pq8C0N=}FGm z)53@mBj^xTC7TEL6(l2qHzAn|J#}Vfa3-xU=K5Q({kemex0>%P6uErk+Cl%3b;}`16Fh2r`p5CgrEGJ{{5TfSgPOa z@=X^V^V$e2YY;g{LiZGskN7i^5d%B(mFM-^X31rv)giaV+6c4C0I&3CwlGJ^rS5?0 z67RZNUA;=<-9upKr>AbUl{x;xP+h&6>Rc+`F%qSux9`w@_x1Q?d0oBdUuNFp-NI0+ zr4944u{WK^={j8o3yrkm~8)!-2X2;Fy$BP}`4fNa?Tp9Dyo7Fk7b5Psuf+;RK zY<7-mTR7A6 zvMj;soTyPV!3s&+ITgcBh;+sW*9*5eIw<`$~SeryVU_-JO+q9Scgk)xDakdyfO}7{ow@qp+7h z)=?y=xA(*LW9_Q334=-Wd9Nu0+h=A|a?K8?>X-?Iw|LhU}qY92rh)hThTQ*>O;e*nd>zd<~EYm6rVY#Qi+!GWAqKpGm(0I-cuB!(#g zz;+-fRS*=U%3QM!3c`Xk)R_L(zsx;@Kz|_YUlT)+(*-CLhCm=xpoUDu3)R##f@;7u zj5HBYga!nmhk&ZXG%> zj?Uc@GSQc02*e9F>j~(m-?oD%L3=LpcL*dX5Gzx>yxz9oio(U8U+Dl|tw=l!pbL`b zkCO_HMD#qxozLypI|Z;z^lMBK%qnC<5#BdcQ!CEBv{fgxC8C_ah5Aq|onsu-DIBp? zj+<--B4iOc71SOrib0Q7;*{_g&i(sQg<{bmSRC^f0#H>~RfB5+WMz#k5P*LImscfc literal 0 HcmV?d00001 diff --git a/ch98rubrics/PackagingTreasure.html b/ch98rubrics/PackagingTreasure.html new file mode 100644 index 000000000..a5a7c3af8 --- /dev/null +++ b/ch98rubrics/PackagingTreasure.html @@ -0,0 +1,826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

An Adventure In Packaging: An exercise in research software engineering.

+
+
+
+
+
+
+

In this exercise, you will convert the already provided solution to the programming challenge defined in this Jupyter notebook, into a proper Python package.

+
+
+
+
+
+
+

The code to actually solve the problem is already given, but as roughly sketched out code in a notebook.

+
+
+
+
+
+
+

Your job will be to convert the code into a formally structured package, with unit tests, a command line interface, and demonstrating your ability to use git version control.

+
+
+
+
+
+
+

The exercise will be semi-automatically marked, so it is very important that you adhere in your solution to the correct file and folder structure, as defined in the rubric below. An otherwise valid solution which doesn't work with our marking tool will not be given credit.

+
+
+
+
+
+
+

First, we set out the problem we are solving, and it's informal solution. Next, we specify in detail the target for your tidy solution. Finally, to assist you in creating a good solution, we state the marks scheme we will use.

+
+
+
+
+
+
+

Treasure Hunting for Beginners: an AI testbed

+
+
+
+
+
+
+

We are going to look at a simple game, a modified version of one with a long history. Games of this kind have been used as test-beds for development of artificial intelligence.

+

A dungeon is a network of connected rooms. One or more rooms contain treasure. Your character, the adventurer, moves between rooms, looking for the treasure. +A troll is also in the dungeon. The troll moves between rooms at random. If the troll catches the adventurer, you lose. If you find treasure before being eaten, you win. (In this simple version, we do not consider the need to leave the dungeon.)

+

The starting rooms for the adventurer and troll are given in the definition of the dungeon.

+

The way the adventurer moves is called a strategy. Different strategies are more or less likely to succeed.

+

We will consider only one strategy this time - the adventurer will also move at random.

+

We want to calculate the probability that this strategy will be successful for a given dungeon.

+

We will use a "monte carlo" approach - simply executing the random strategy many times, and counting the proportion of times the adventurer wins.

+
+
+
+
+
+
+

Our data structure for a dungeon will be somewhat familiar from the Maze example:

+
+
+
+
+
+
In [1]:
+
+
+
dungeon1 = {
+    'treasure' : [1], # Room 1 contains treasure
+    'adventurer': 0, # The adventurer starts in room 0 
+    'troll': 2, # The troll starts in room 2
+    'network': [[1], #Room zero connects to room 1
+                [0,2], #Room one connects to rooms 0 and 2
+                [1] ] #Room 2 connects to room 1
+}
+
+
+
+
+
+
+
+
+

So this example shows a 3-room linear corridor: with the adventurer at one end, the troll at the other, and the treasure in the middle.

+
+
+
+
+
+
+

With the adventurer following a random walk strategy, we can define a function to update a dungeon:

+
+
+
+
+
+
In [2]:
+
+
+
import random
+
+def random_move(network, current_loc):
+    targets=network[current_loc]
+    return random.choice(targets)
+
+
+
+
+
+
+
+
In [3]:
+
+
+
def update_dungeon(dungeon):
+    dungeon['adventurer']=random_move(dungeon['network'], dungeon['adventurer'])
+    dungeon['troll']=random_move(dungeon['network'], dungeon['troll'])
+
+
+
+
+
+
+
+
In [4]:
+
+
+
update_dungeon(dungeon1)
+
+dungeon1
+
+
+
+
+
+
+
+
Out[4]:
+
+
{'treasure': [1], 'adventurer': 1, 'troll': 1, 'network': [[1], [0, 2], [1]]}
+
+
+
+
+
+
+
+
+

We can also define a function to test if the adventurer has won, died, or if the game continues:

+
+
+
+
+
+
In [5]:
+
+
+
def outcome(dungeon):
+    if dungeon['adventurer']==dungeon['troll']:
+        return -1
+    if dungeon['adventurer'] in dungeon['treasure']:
+        return 1
+    return 0
+
+
+
+
+
+
+
+
In [6]:
+
+
+
outcome(dungeon1)
+
+
+
+
+
+
+
+
Out[6]:
+
+
-1
+
+
+
+
+
+
+
+
+

So we can loop, to determine the outcome of an adventurer in a dungeon:

+
+
+
+
+
+
In [7]:
+
+
+
import copy
+
+def run_to_result(dungeon):
+    dungeon=copy.deepcopy(dungeon)
+    max_steps=1000
+    for _ in range(max_steps):
+        result= outcome(dungeon)
+        if result != 0:
+            return result
+        update_dungeon(dungeon)
+    # don't run forever, return 0 (e.g. if there is no treasure and the troll can't reach the adventurer)
+    return result
+
+
+
+
+
+
+
+
In [8]:
+
+
+
dungeon2 = {
+    'treasure' : [1], # Room 1 contains treasure
+    'adventurer': 0, # The adventurer starts in room 0 
+    'troll': 2, # The troll starts in room 2
+    'network': [[1], #Room zero connects to room 1
+                [0,2], #Room one connects to rooms 0 and 2
+                [1,3], #Room 2 connects to room 1 and 3
+                [2]] # Room 3 connects to room 2
+    
+}
+
+
+
+
+
+
+
+
In [9]:
+
+
+
run_to_result(dungeon2)
+
+
+
+
+
+
+
+
Out[9]:
+
+
-1
+
+
+
+
+
+
+
+
+

Note that we might get a different result sometimes, depending on how the adventurer moves, so we need to run multiple times to get our probability:

+
+
+
+
+
+
In [10]:
+
+
+
def success_chance(dungeon):
+    trials=10000
+    successes=0
+    for _ in range(trials):
+        outcome = run_to_result(dungeon)
+        if outcome == 1:
+            successes+=1
+    success_fraction = successes/trials
+    return success_fraction
+
+
+
+
+
+
+
+
In [11]:
+
+
+
success_chance(dungeon2)
+
+
+
+
+
+
+
+
Out[11]:
+
+
0.504
+
+
+
+
+
+
+
+
+

Make sure you understand why this number should be a half, given a large value for trials.

+
+
+
+
+
+
In [12]:
+
+
+
dungeon3 = {
+    'treasure' : [2], # Room 2 contains treasure
+    'adventurer': 0, # The adventurer starts in room 0 
+    'troll': 4, # The troll starts in room 4
+    'network': [[1], #Room zero connects to room 1
+                [0,2], #Room one connects to rooms 0 and 2
+                [1,3], #Room 2 connects to room 1 and 3
+                [2, 4], # Room 3 connects to room 2 and 4
+                [3]] # Room 4 connects to room 3 
+    
+}
+
+
+
+
+
+
+
+
In [13]:
+
+
+
success_chance(dungeon3)
+
+
+
+
+
+
+
+
Out[13]:
+
+
0.3952
+
+
+
+
+
+
+
+
+

[Not for credit] Do you understand why this number should be 0.4? Hint: The first move is always the same. In the next state, a quarter of the time, you win. 3/8 of the time, you end up back where you were before. The rest of the time, you lose (eventually). You can sum the series: $\frac{1}{4}(1+\frac{3}{8}+(\frac{3}{8})^2+...)=\frac{2}{5}$.

+
+
+
+
+
+
+

Packaging the Treasure: your exercise

+
+
+
+
+
+
+

You must submit your exercise solution to Moodle as a single uploaded Zip format archive. (You must use only the zip tool, not any other archiver, such as .tgz or .rar. If we cannot unzip the archiver with zip, you will receive zero marks.)

+
+
+
+
+
+
+

The folder structure inside your zip archive must have a single top-level folder, whose folder name is your student number, so that on running unzip this folder appears. This top level folder must contain all the parts of your solution. You will lose marks if, on unzip, your archive creates other files or folders at the same level as this folder, as we will be unzipping all the assignments in the same place on our computers when we mark them!

+
+
+
+
+
+
+

Inside your top level folder, you should create a setup.py file to make the code installable. You should also create some other files, per the lectures, that should be present in all research software packages. (Hint, there are three of these.)

+
+
+
+
+
+
+

Your tidied-up version of the solution code should be in a sub-folder called adventure which will be the python package itself. It will contain an init.py file, and the code itself must be in a file called dungeon.py. This should define a class Dungeon: instead of a data structure and associated functions, you must refactor this into a class and methods.

+

Thus, if you run python in your top-level folder, you should be able to from adventure.dungeon import Dungeon. If you cannot do this, you will receive zero marks.

+
+
+
+
+
+
+

You must create a command-line entry point, called hunt. This should use the entry_points facility in setup.py, to point toward a module designed for use as the entry point, in adventure/command.py. This should use the Argparse library. When invoked with hunt mydungeon.yml --samples 500 the command must print on standard output the probability of finding the treasure in the specified dungeon, using the random walk strategy, after the specified number of test runs.

+
+
+
+
+
+
+

The dungeon.yml file should be a yml file containing a structure representing the dungeon state. Use the same structure as the sample code above, even though you'll be building a Dungeon object from this structure rather than using it directly.

+
+
+
+
+
+
+

You must create unit tests which cover a number of examples. These should be defined in adventure/tests/test_dungeon.py. Don't forget to add an init.py file to that folder too, so that at the top of the test file you can " from ..dungeon import Dungeon." If your unit tests use a fixture file to DRY up tests, this must be called adventure/tests/fixtures.yml. For example, this could contain a yaml array of many dungeon structures.

+
+
+
+
+
+
+

You should git init inside your student-number folder, as soon as you create it, and git commit your work regularly as the exercise progresses.

+
+
+
+
+
+
+

Due to our automated marking tool, only work that has a valid git repository, and follows the folder and file structure described above, will receive credit.

+
+
+
+
+
+
+

Due to the need to avoid plagiarism, do not use a public github repository for your work - instead, use git on your local disk (with git commit but not git push), and *ensure the secret .git folder is part of your zipped archive.

+
+
+
+
+
+
+

Marks Scheme

+
+
+
+
+
+
+

Note that because of our automated marking tool, a solution which does not match the standard solution structure defined above, with file and folder names exactly as stated, may not receive marks, even if the solution is otherwise good. "Follow on marks" are not guaranteed in this case.

+
+
+
+
+
+
+
    +
  • Code in dungeon.py, implementing the random walk strategy (5 marks)
      +
    • Which works (1 mark)
    • +
    • Cleanly laid out and formatted - PEP8 (1 mark)
    • +
    • Defining the class Dungeon with a valid object oriented structure (1 mark)
    • +
    • Breaking down the solution sensibly into subunits (1 mark)
    • +
    • Structured so that it could be used as a base for other strategies (1 mark)
    • +
    +
  • +
  • Command line entry point (4 marks)
      +
    • Accepting a dungeon definition text file as input (1 mark)
    • +
    • With an optional parameter to control sample size (1 mark)
    • +
    • Which prints the result to standard out (1 mark)
    • +
    • Which correctly uses the Argparse library (1 mark)
    • +
    • Which is itself cleanly laid out and formatted (1 mark)
    • +
    +
  • +
  • setup.py file (5 marks)
      +
    • Which could be used to pip install the project (1 mark)
    • +
    • With appropriate metadata, including version number and author (1 mark)
    • +
    • Which packages code (but not tests), correctly. (1 mark)
    • +
    • Which specifies library dependencies (1 mark)
    • +
    • Which points to the entry point function (1 mark)
    • +
    +
  • +
  • Three other metadata files: (3 marks)
      +
    • Hint: Who did it, how to reference it, who can copy it.
    • +
    +
  • +
  • Unit tests: (5 marks)
      +
    • Which test some obvious cases (1 mark)
    • +
    • Which correctly handle approximate results within an appropriate tolerance (1 mark)
    • +
    • Which test how the code fails when invoked incorrectly (1 mark)
    • +
    • Which use a fixture file or other approach to avoid overly repetitive test code (1 mark)
    • +
    • Which are themselves cleanly laid out code (1 mark)
    • +
    +
  • +
  • Version control: (2 marks)
      +
    • Sensible commit sizes (1 mark)
    • +
    • Appropriate commit comments (1 mark)
    • +
    +
  • +
+
+
+
+
+
+
+

Total: 25 marks

+
+
+
+
+
+
In [ ]:
+
+
+
 
+
+
+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch98rubrics/PackagingTreasure.ipynb b/ch98rubrics/PackagingTreasure.ipynb new file mode 100644 index 000000000..691f949ea --- /dev/null +++ b/ch98rubrics/PackagingTreasure.ipynb @@ -0,0 +1,500 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "65cc87eb", + "metadata": {}, + "source": [ + "# An Adventure In Packaging: An exercise in research software engineering." + ] + }, + { + "cell_type": "markdown", + "id": "26640a1a", + "metadata": {}, + "source": [ + "In this exercise, you will convert the already provided solution to the programming challenge defined in this Jupyter notebook, into a proper Python package." + ] + }, + { + "cell_type": "markdown", + "id": "2ac425b7", + "metadata": {}, + "source": [ + "The code to actually solve the problem is already given, but as roughly sketched out code in a notebook." + ] + }, + { + "cell_type": "markdown", + "id": "d91ba201", + "metadata": {}, + "source": [ + "Your job will be to convert the code into a formally structured package, with unit tests, a command line interface, and demonstrating your ability to use `git` version control." + ] + }, + { + "cell_type": "markdown", + "id": "748ee515", + "metadata": {}, + "source": [ + "The exercise will be semi-automatically marked, so it is *very* important that you adhere in your solution to the correct file and folder structure, as defined in the rubric below. An otherwise valid solution which doesn't work with our marking tool will **not** be given credit." + ] + }, + { + "cell_type": "markdown", + "id": "14bd7f01", + "metadata": {}, + "source": [ + "First, we set out the problem we are solving, and it's informal solution. Next, we specify in detail the target for your tidy solution. Finally, to assist you in creating a good solution, we state the marks scheme we will use." + ] + }, + { + "cell_type": "markdown", + "id": "b2d03172", + "metadata": {}, + "source": [ + "# Treasure Hunting for Beginners: an AI testbed" + ] + }, + { + "cell_type": "markdown", + "id": "ac2178a8", + "metadata": {}, + "source": [ + "We are going to look at a simple game, a modified version of one with a [long history](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). Games of this kind have been used as test-beds for development of artificial intelligence.\n", + "\n", + "A *dungeon* is a network of connected *rooms*. One or more rooms contain *treasure*. Your character, the *adventurer*, moves between rooms, looking for the treasure.\n", + "A *troll* is also in the dungeon. The troll moves between rooms at random. If the troll catches the adventurer, you lose. If you find treasure before being eaten, you win. (In this simple version, we do not consider the need to leave the dungeon.)\n", + "\n", + "The starting rooms for the adventurer and troll are given in the definition of the dungeon.\n", + "\n", + "The way the adventurer moves is called a *strategy*. Different strategies are more or less likely to succeed.\n", + "\n", + "We will consider only one strategy this time - the adventurer will also move at random.\n", + "\n", + "We want to calculate the probability that this strategy will be successful for a given dungeon.\n", + "\n", + "We will use a \"monte carlo\" approach - simply executing the random strategy many times, and counting the proportion of times the adventurer wins." + ] + }, + { + "cell_type": "markdown", + "id": "dae1434c", + "metadata": {}, + "source": [ + "Our data structure for a dungeon will be somewhat familiar from the Maze example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10a3d280", + "metadata": {}, + "outputs": [], + "source": [ + "dungeon1 = {\n", + " 'treasure' : [1], # Room 1 contains treasure\n", + " 'adventurer': 0, # The adventurer starts in room 0 \n", + " 'troll': 2, # The troll starts in room 2\n", + " 'network': [[1], #Room zero connects to room 1\n", + " [0,2], #Room one connects to rooms 0 and 2\n", + " [1] ] #Room 2 connects to room 1\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "4b70dc43", + "metadata": {}, + "source": [ + "So this example shows a 3-room linear corridor: with the adventurer at one end, the troll at the other, and the treasure in the middle." + ] + }, + { + "cell_type": "markdown", + "id": "acf19f69", + "metadata": {}, + "source": [ + "With the adventurer following a random walk strategy, we can define a function to update a dungeon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92577e3b", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "def random_move(network, current_loc):\n", + " targets=network[current_loc]\n", + " return random.choice(targets)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faea4e0b", + "metadata": {}, + "outputs": [], + "source": [ + "def update_dungeon(dungeon):\n", + " dungeon['adventurer']=random_move(dungeon['network'], dungeon['adventurer'])\n", + " dungeon['troll']=random_move(dungeon['network'], dungeon['troll'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aed1382d", + "metadata": {}, + "outputs": [], + "source": [ + "update_dungeon(dungeon1)\n", + "\n", + "dungeon1" + ] + }, + { + "cell_type": "markdown", + "id": "0bd04e30", + "metadata": {}, + "source": [ + "We can also define a function to test if the adventurer has won, died, or if the game continues:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80681b8e", + "metadata": {}, + "outputs": [], + "source": [ + "def outcome(dungeon):\n", + " if dungeon['adventurer']==dungeon['troll']:\n", + " return -1\n", + " if dungeon['adventurer'] in dungeon['treasure']:\n", + " return 1\n", + " return 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e35d5c26", + "metadata": {}, + "outputs": [], + "source": [ + "outcome(dungeon1)" + ] + }, + { + "cell_type": "markdown", + "id": "64dfc554", + "metadata": {}, + "source": [ + "So we can loop, to determine the outcome of an adventurer in a dungeon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5a76e43", + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "\n", + "def run_to_result(dungeon):\n", + " dungeon=copy.deepcopy(dungeon)\n", + " max_steps=1000\n", + " for _ in range(max_steps):\n", + " result= outcome(dungeon)\n", + " if result != 0:\n", + " return result\n", + " update_dungeon(dungeon)\n", + " # don't run forever, return 0 (e.g. if there is no treasure and the troll can't reach the adventurer)\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04da2677", + "metadata": {}, + "outputs": [], + "source": [ + "dungeon2 = {\n", + " 'treasure' : [1], # Room 1 contains treasure\n", + " 'adventurer': 0, # The adventurer starts in room 0 \n", + " 'troll': 2, # The troll starts in room 2\n", + " 'network': [[1], #Room zero connects to room 1\n", + " [0,2], #Room one connects to rooms 0 and 2\n", + " [1,3], #Room 2 connects to room 1 and 3\n", + " [2]] # Room 3 connects to room 2\n", + " \n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68809b27", + "metadata": {}, + "outputs": [], + "source": [ + "run_to_result(dungeon2)" + ] + }, + { + "cell_type": "markdown", + "id": "89b2115b", + "metadata": {}, + "source": [ + "Note that we might get a different result sometimes, depending on how the adventurer moves, so we need to run multiple times to get our probability:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ae85574", + "metadata": {}, + "outputs": [], + "source": [ + "def success_chance(dungeon):\n", + " trials=10000\n", + " successes=0\n", + " for _ in range(trials):\n", + " outcome = run_to_result(dungeon)\n", + " if outcome == 1:\n", + " successes+=1\n", + " success_fraction = successes/trials\n", + " return success_fraction\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a78f7898", + "metadata": {}, + "outputs": [], + "source": [ + "success_chance(dungeon2)" + ] + }, + { + "cell_type": "markdown", + "id": "d804bb7d", + "metadata": {}, + "source": [ + "Make sure you understand why this number should be a half, given a large value for `trials`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c746c5e", + "metadata": {}, + "outputs": [], + "source": [ + "dungeon3 = {\n", + " 'treasure' : [2], # Room 2 contains treasure\n", + " 'adventurer': 0, # The adventurer starts in room 0 \n", + " 'troll': 4, # The troll starts in room 4\n", + " 'network': [[1], #Room zero connects to room 1\n", + " [0,2], #Room one connects to rooms 0 and 2\n", + " [1,3], #Room 2 connects to room 1 and 3\n", + " [2, 4], # Room 3 connects to room 2 and 4\n", + " [3]] # Room 4 connects to room 3 \n", + " \n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7dd6012", + "metadata": {}, + "outputs": [], + "source": [ + "success_chance(dungeon3)" + ] + }, + { + "cell_type": "markdown", + "id": "6bbacb52", + "metadata": {}, + "source": [ + "[Not for credit] Do you understand why this number should be 0.4? Hint: The first move is always the same. In the next state, a quarter of the time, you win. 3/8 of the time, you end up back where you were before. The rest of the time, you lose (eventually). You can sum the series: $\\frac{1}{4}(1+\\frac{3}{8}+(\\frac{3}{8})^2+...)=\\frac{2}{5}$." + ] + }, + { + "cell_type": "markdown", + "id": "8c736e1f", + "metadata": {}, + "source": [ + "# Packaging the Treasure: your exercise" + ] + }, + { + "cell_type": "markdown", + "id": "a20b8983", + "metadata": {}, + "source": [ + "You must submit your exercise solution to **Moodle** as a single uploaded **Zip** format archive. (You must use only the *zip* tool, **not** any other archiver, such as `.tgz` or `.rar`. If we cannot unzip the archiver with `zip`, you will receive zero marks.)" + ] + }, + { + "cell_type": "markdown", + "id": "2052c72f", + "metadata": {}, + "source": [ + "The folder structure inside your zip archive must have a single top-level folder, whose **folder name is your student number**, so that on running `unzip` this folder appears. This top level folder must contain all the parts of your solution. You will lose marks if, on unzip, your archive creates other files or folders at the same level as this folder, as we will be unzipping all the assignments in the same place on our computers when we mark them!" + ] + }, + { + "cell_type": "markdown", + "id": "b2b7d954", + "metadata": {}, + "source": [ + "Inside your top level folder, you should create a setup.py file to make the code installable. You should also create some other files, per the lectures, that should be present in all research software packages. (Hint, there are three of these.)" + ] + }, + { + "cell_type": "markdown", + "id": "f0d6521b", + "metadata": {}, + "source": [ + "Your tidied-up version of the solution code should be in a sub-folder called `adventure` which will be the python package itself. It will contain an __init__.py file, and the code itself must be in a file called dungeon.py. This should define a class `Dungeon`: instead of a data structure and associated functions, you must refactor this into a class and methods.\n", + "\n", + "Thus, if you run python in your top-level folder, you should be able to `from adventure.dungeon import Dungeon`. If you cannot do this, you will receive zero marks." + ] + }, + { + "cell_type": "markdown", + "id": "af82b62b", + "metadata": {}, + "source": [ + "You must create a command-line entry point, called hunt. This should use the entry_points facility in setup.py, to point toward a module designed for use as the entry point, in adventure/command.py. This should use the `Argparse` library. When invoked with `hunt mydungeon.yml --samples 500` the command must print on standard output the probability of finding the treasure in the specified dungeon, using the random walk strategy, after the specified number of test runs." + ] + }, + { + "cell_type": "markdown", + "id": "516d205f", + "metadata": {}, + "source": [ + "The dungeon.yml file should be a yml file containing a structure representing the dungeon state. Use the same structure as the sample code above, even though you'll be building a `Dungeon` object from this structure rather than using it directly." + ] + }, + { + "cell_type": "markdown", + "id": "3f9fbaf8", + "metadata": {}, + "source": [ + "You must create unit tests which cover a number of examples. These should be defined in `adventure/tests/test_dungeon.py`. Don't forget to add an __init.py__ file to that folder too, so that at the top of the test file you can \" `from ..dungeon import Dungeon`.\" If your unit tests use a fixture file to DRY up tests, this must be called `adventure/tests/fixtures.yml`. For example, this could contain a yaml array of many dungeon structures." + ] + }, + { + "cell_type": "markdown", + "id": "1b4501e3", + "metadata": {}, + "source": [ + "You should `git init` inside your student-number folder, as soon as you create it, and `git commit` your work regularly as the exercise progresses." + ] + }, + { + "cell_type": "markdown", + "id": "7695ed18", + "metadata": {}, + "source": [ + "Due to our automated marking tool, **only** work that has a valid git repository, and follows the folder and file structure described above, will receive credit." + ] + }, + { + "cell_type": "markdown", + "id": "184bc82d", + "metadata": {}, + "source": [ + "Due to the need to avoid plagiarism, do *not* use a public github repository for your work - instead, use git on your local disk (with `git commit` but not `git push`), and *ensure the secret `.git` folder is part of your zipped archive." + ] + }, + { + "cell_type": "markdown", + "id": "b43a04ab", + "metadata": {}, + "source": [ + "# Marks Scheme" + ] + }, + { + "cell_type": "markdown", + "id": "436bc720", + "metadata": {}, + "source": [ + "Note that because of our automated marking tool, a solution which does not match the standard solution structure defined above, with file and folder names exactly as stated, may not receive marks, even if the solution is otherwise good. \"Follow on marks\" are **not** guaranteed in this case." + ] + }, + { + "cell_type": "markdown", + "id": "c11abcae", + "metadata": {}, + "source": [ + "* Code in dungeon.py, implementing the random walk strategy (5 marks)\n", + " * Which works (1 mark)\n", + " * Cleanly laid out and formatted - PEP8 (1 mark)\n", + " * Defining the class `Dungeon` with a valid object oriented structure (1 mark)\n", + " * Breaking down the solution sensibly into subunits (1 mark)\n", + " * Structured so that it could be used as a base for other strategies (1 mark)\n", + "* Command line entry point (4 marks)\n", + " * Accepting a dungeon definition text file as input (1 mark)\n", + " * With an optional parameter to control sample size (1 mark)\n", + " * Which prints the result to standard out (1 mark)\n", + " * Which correctly uses the `Argparse` library (1 mark)\n", + " * Which is itself cleanly laid out and formatted (1 mark)\n", + "* setup.py file (5 marks)\n", + " * Which could be used to `pip install` the project (1 mark)\n", + " * With appropriate metadata, including version number and author (1 mark)\n", + " * Which packages code (but not tests), correctly. (1 mark)\n", + " * Which specifies library dependencies (1 mark)\n", + " * Which points to the entry point function (1 mark)\n", + "* Three other metadata files: (3 marks)\n", + " * Hint: Who did it, how to reference it, who can copy it.\n", + "* Unit tests: (5 marks)\n", + " * Which test some obvious cases (1 mark)\n", + " * Which correctly handle approximate results within an appropriate tolerance (1 mark)\n", + " * Which test how the code fails when invoked incorrectly (1 mark)\n", + " * Which use a fixture file or other approach to avoid overly repetitive test code (1 mark)\n", + " * Which are themselves cleanly laid out code (1 mark)\n", + "* Version control: (2 marks)\n", + " * Sensible commit sizes (1 mark)\n", + " * Appropriate commit comments (1 mark)" + ] + }, + { + "cell_type": "markdown", + "id": "a5163c5a", + "metadata": {}, + "source": [ + "Total: 25 marks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "292d09ad", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch98rubrics/PackagingTreasure.ipynb.py b/ch98rubrics/PackagingTreasure.ipynb.py new file mode 100644 index 000000000..ee991c383 --- /dev/null +++ b/ch98rubrics/PackagingTreasure.ipynb.py @@ -0,0 +1,253 @@ +# --- +# jupyter: +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # An Adventure In Packaging: An exercise in research software engineering. + +# %% [markdown] +# In this exercise, you will convert the already provided solution to the programming challenge defined in this Jupyter notebook, into a proper Python package. + +# %% [markdown] +# The code to actually solve the problem is already given, but as roughly sketched out code in a notebook. + +# %% [markdown] +# Your job will be to convert the code into a formally structured package, with unit tests, a command line interface, and demonstrating your ability to use `git` version control. + +# %% [markdown] +# The exercise will be semi-automatically marked, so it is *very* important that you adhere in your solution to the correct file and folder structure, as defined in the rubric below. An otherwise valid solution which doesn't work with our marking tool will **not** be given credit. + +# %% [markdown] +# First, we set out the problem we are solving, and it's informal solution. Next, we specify in detail the target for your tidy solution. Finally, to assist you in creating a good solution, we state the marks scheme we will use. + +# %% [markdown] +# # Treasure Hunting for Beginners: an AI testbed + +# %% [markdown] +# We are going to look at a simple game, a modified version of one with a [long history](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). Games of this kind have been used as test-beds for development of artificial intelligence. +# +# A *dungeon* is a network of connected *rooms*. One or more rooms contain *treasure*. Your character, the *adventurer*, moves between rooms, looking for the treasure. +# A *troll* is also in the dungeon. The troll moves between rooms at random. If the troll catches the adventurer, you lose. If you find treasure before being eaten, you win. (In this simple version, we do not consider the need to leave the dungeon.) +# +# The starting rooms for the adventurer and troll are given in the definition of the dungeon. +# +# The way the adventurer moves is called a *strategy*. Different strategies are more or less likely to succeed. +# +# We will consider only one strategy this time - the adventurer will also move at random. +# +# We want to calculate the probability that this strategy will be successful for a given dungeon. +# +# We will use a "monte carlo" approach - simply executing the random strategy many times, and counting the proportion of times the adventurer wins. + +# %% [markdown] +# Our data structure for a dungeon will be somewhat familiar from the Maze example: + +# %% +dungeon1 = { + 'treasure' : [1], # Room 1 contains treasure + 'adventurer': 0, # The adventurer starts in room 0 + 'troll': 2, # The troll starts in room 2 + 'network': [[1], #Room zero connects to room 1 + [0,2], #Room one connects to rooms 0 and 2 + [1] ] #Room 2 connects to room 1 +} + +# %% [markdown] +# So this example shows a 3-room linear corridor: with the adventurer at one end, the troll at the other, and the treasure in the middle. + +# %% [markdown] +# With the adventurer following a random walk strategy, we can define a function to update a dungeon: + +# %% +import random + +def random_move(network, current_loc): + targets=network[current_loc] + return random.choice(targets) + + +# %% +def update_dungeon(dungeon): + dungeon['adventurer']=random_move(dungeon['network'], dungeon['adventurer']) + dungeon['troll']=random_move(dungeon['network'], dungeon['troll']) + + +# %% +update_dungeon(dungeon1) + +dungeon1 + + +# %% [markdown] +# We can also define a function to test if the adventurer has won, died, or if the game continues: + +# %% +def outcome(dungeon): + if dungeon['adventurer']==dungeon['troll']: + return -1 + if dungeon['adventurer'] in dungeon['treasure']: + return 1 + return 0 + + +# %% +outcome(dungeon1) + +# %% [markdown] +# So we can loop, to determine the outcome of an adventurer in a dungeon: + +# %% +import copy + +def run_to_result(dungeon): + dungeon=copy.deepcopy(dungeon) + max_steps=1000 + for _ in range(max_steps): + result= outcome(dungeon) + if result != 0: + return result + update_dungeon(dungeon) + # don't run forever, return 0 (e.g. if there is no treasure and the troll can't reach the adventurer) + return result + + +# %% +dungeon2 = { + 'treasure' : [1], # Room 1 contains treasure + 'adventurer': 0, # The adventurer starts in room 0 + 'troll': 2, # The troll starts in room 2 + 'network': [[1], #Room zero connects to room 1 + [0,2], #Room one connects to rooms 0 and 2 + [1,3], #Room 2 connects to room 1 and 3 + [2]] # Room 3 connects to room 2 + +} + +# %% +run_to_result(dungeon2) + + +# %% [markdown] +# Note that we might get a different result sometimes, depending on how the adventurer moves, so we need to run multiple times to get our probability: + +# %% +def success_chance(dungeon): + trials=10000 + successes=0 + for _ in range(trials): + outcome = run_to_result(dungeon) + if outcome == 1: + successes+=1 + success_fraction = successes/trials + return success_fraction + + + +# %% +success_chance(dungeon2) + +# %% [markdown] +# Make sure you understand why this number should be a half, given a large value for `trials`. + +# %% +dungeon3 = { + 'treasure' : [2], # Room 2 contains treasure + 'adventurer': 0, # The adventurer starts in room 0 + 'troll': 4, # The troll starts in room 4 + 'network': [[1], #Room zero connects to room 1 + [0,2], #Room one connects to rooms 0 and 2 + [1,3], #Room 2 connects to room 1 and 3 + [2, 4], # Room 3 connects to room 2 and 4 + [3]] # Room 4 connects to room 3 + +} + +# %% +success_chance(dungeon3) + +# %% [markdown] +# [Not for credit] Do you understand why this number should be 0.4? Hint: The first move is always the same. In the next state, a quarter of the time, you win. 3/8 of the time, you end up back where you were before. The rest of the time, you lose (eventually). You can sum the series: $\frac{1}{4}(1+\frac{3}{8}+(\frac{3}{8})^2+...)=\frac{2}{5}$. + +# %% [markdown] +# # Packaging the Treasure: your exercise + +# %% [markdown] +# You must submit your exercise solution to **Moodle** as a single uploaded **Zip** format archive. (You must use only the *zip* tool, **not** any other archiver, such as `.tgz` or `.rar`. If we cannot unzip the archiver with `zip`, you will receive zero marks.) + +# %% [markdown] +# The folder structure inside your zip archive must have a single top-level folder, whose **folder name is your student number**, so that on running `unzip` this folder appears. This top level folder must contain all the parts of your solution. You will lose marks if, on unzip, your archive creates other files or folders at the same level as this folder, as we will be unzipping all the assignments in the same place on our computers when we mark them! + +# %% [markdown] +# Inside your top level folder, you should create a setup.py file to make the code installable. You should also create some other files, per the lectures, that should be present in all research software packages. (Hint, there are three of these.) + +# %% [markdown] +# Your tidied-up version of the solution code should be in a sub-folder called `adventure` which will be the python package itself. It will contain an __init__.py file, and the code itself must be in a file called dungeon.py. This should define a class `Dungeon`: instead of a data structure and associated functions, you must refactor this into a class and methods. +# +# Thus, if you run python in your top-level folder, you should be able to `from adventure.dungeon import Dungeon`. If you cannot do this, you will receive zero marks. + +# %% [markdown] +# You must create a command-line entry point, called hunt. This should use the entry_points facility in setup.py, to point toward a module designed for use as the entry point, in adventure/command.py. This should use the `Argparse` library. When invoked with `hunt mydungeon.yml --samples 500` the command must print on standard output the probability of finding the treasure in the specified dungeon, using the random walk strategy, after the specified number of test runs. + +# %% [markdown] +# The dungeon.yml file should be a yml file containing a structure representing the dungeon state. Use the same structure as the sample code above, even though you'll be building a `Dungeon` object from this structure rather than using it directly. + +# %% [markdown] +# You must create unit tests which cover a number of examples. These should be defined in `adventure/tests/test_dungeon.py`. Don't forget to add an __init.py__ file to that folder too, so that at the top of the test file you can " `from ..dungeon import Dungeon`." If your unit tests use a fixture file to DRY up tests, this must be called `adventure/tests/fixtures.yml`. For example, this could contain a yaml array of many dungeon structures. + +# %% [markdown] +# You should `git init` inside your student-number folder, as soon as you create it, and `git commit` your work regularly as the exercise progresses. + +# %% [markdown] +# Due to our automated marking tool, **only** work that has a valid git repository, and follows the folder and file structure described above, will receive credit. + +# %% [markdown] +# Due to the need to avoid plagiarism, do *not* use a public github repository for your work - instead, use git on your local disk (with `git commit` but not `git push`), and *ensure the secret `.git` folder is part of your zipped archive. + +# %% [markdown] +# # Marks Scheme + +# %% [markdown] +# Note that because of our automated marking tool, a solution which does not match the standard solution structure defined above, with file and folder names exactly as stated, may not receive marks, even if the solution is otherwise good. "Follow on marks" are **not** guaranteed in this case. + +# %% [markdown] +# * Code in dungeon.py, implementing the random walk strategy (5 marks) +# * Which works (1 mark) +# * Cleanly laid out and formatted - PEP8 (1 mark) +# * Defining the class `Dungeon` with a valid object oriented structure (1 mark) +# * Breaking down the solution sensibly into subunits (1 mark) +# * Structured so that it could be used as a base for other strategies (1 mark) +# * Command line entry point (4 marks) +# * Accepting a dungeon definition text file as input (1 mark) +# * With an optional parameter to control sample size (1 mark) +# * Which prints the result to standard out (1 mark) +# * Which correctly uses the `Argparse` library (1 mark) +# * Which is itself cleanly laid out and formatted (1 mark) +# * setup.py file (5 marks) +# * Which could be used to `pip install` the project (1 mark) +# * With appropriate metadata, including version number and author (1 mark) +# * Which packages code (but not tests), correctly. (1 mark) +# * Which specifies library dependencies (1 mark) +# * Which points to the entry point function (1 mark) +# * Three other metadata files: (3 marks) +# * Hint: Who did it, how to reference it, who can copy it. +# * Unit tests: (5 marks) +# * Which test some obvious cases (1 mark) +# * Which correctly handle approximate results within an appropriate tolerance (1 mark) +# * Which test how the code fails when invoked incorrectly (1 mark) +# * Which use a fixture file or other approach to avoid overly repetitive test code (1 mark) +# * Which are themselves cleanly laid out code (1 mark) +# * Version control: (2 marks) +# * Sensible commit sizes (1 mark) +# * Appropriate commit comments (1 mark) + +# %% [markdown] +# Total: 25 marks + +# %% diff --git a/ch98rubrics/RefactoringTrees.html b/ch98rubrics/RefactoringTrees.html new file mode 100644 index 000000000..d93ffe035 --- /dev/null +++ b/ch98rubrics/RefactoringTrees.html @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + + + +
+
+
+

Refactoring Trees: An exercise in Research Software Engineering

+
+
+
+
+
+
+

In this exercise, you will convert badly written code, provided here, into better-written code.

+
+
+
+
+
+
+

You will do this not through simply writing better code, but by taking a refactoring approach, as discussed in the lectures.

+
+
+
+
+
+
+

As such, your use of git version control, to make a commit after each step of the refactoring, with a commit message which indicates the refactoring you took, will be critical to success.

+
+
+
+
+
+
+

You will also be asked to look at the performance of your code, and to make changes which improve the speed of the code.

+
+
+
+
+
+
+

The script as supplied has its parameters hand-coded within the code. You will be expected, in your refactoring, to make these available as command line parameters to be supplied when the code is invoked.

+
+
+
+
+
+
+

Some terrible code

+
+
+
+
+
+
+

Here's our terrible code:

+
+
+
+
+
+
In [1]:
+
+
+
%matplotlib inline
+
+
+
+
+
+
+
+
In [2]:
+
+
+
from math import sin, cos
+from matplotlib import pyplot as plt
+s=1
+d=[[0,1,0]]
+plt.plot([0,0],[0,1])
+for i in range(5):
+    n=[]
+    for j in range(len(d)):
+        n.append([d[j][0]+s*sin(d[j][2]-0.2), d[j][1]+s*cos(d[j][2]-0.2), d[j][2]-0.2])
+        n.append([d[j][0]+s*sin(d[j][2]+0.2), d[j][1]+s*cos(d[j][2]+0.2), d[j][2]+0.2])
+        plt.plot([d[j][0], n[-2][0]],[d[j][1], n[-2][1]])
+        plt.plot([d[j][0], n[-1][0]],[d[j][1], n[-1][1]])
+    d=n
+    s*=0.6
+plt.savefig('tree.png')
+
+
+
+
+
+
+
+
+
+No description has been provided for this image +
+
+
+
+
+
+
+
+

Rubric and marks scheme

Part one: Refactoring (15 marks)

+
+
+
+
+
+
+
    +
  • Copy the code above into a file tree.py, invoke it with python tree.py, and verify it creates an image tree.png which looks like that above.
  • +
  • Initialise your git repository with the raw state of the code. [1 mark]
  • +
  • Identify a number of simple refactorings which can be used to improve the code, reducing repetition and improving readability. Implement these one by one, with a git commit each time.
      +
    • 1 mark for each refactoring, 1 mark for each git commit, at least five such: ten marks total.
    • +
    +
  • +
  • Do NOT introduce NumPy or other performance improvements yet (see below.)
  • +
+
+
+
+
+
+
+
    +
  • Identify which variables in the code would, more sensibly, be able to be input parameters, and use Argparse to manage these.
      +
    • 4 marks: 1 for each of four arguments identified.
    • +
    +
  • +
+
+
+
+
+
+
+

Part two: performance programming (10 marks)

+
+
+
+
+
+
+
    +
  • For the code as refactored, prepare a figure which plots the time to produce the tree, versus number of iteration steps completed. Your code to produce this figure should run as a script, which you should call perf_plot.py, invoking a function imported from tree.py. The script should produce a figure called perf_plot.png. Comment on your findings in a text file, called comments.md. You should turn off the actual plotting, and run only the mathematical calculation, for your performance measurements. (Add an appropriate flag.)
      +
    • 5 marks: [1] Time to run code identified [1] Figure created [1] Figure correctly formatted [1] Figure auto-generated from script [1] Performance law identified.
    • +
    +
  • +
+
+
+
+
+
+
+
    +
  • The code above makes use of append() which is not appropriate for NumPy. Create a new solution (in a file called tree_np.py) which makes use of NumPy. Compare the performance (again, excluding the plotting from your measurements), and discuss in comments.md
      +
    • 5 marks: [1] NumPy solution uses array-operations to subtract the change angle from all angles in a single minus sign, [1] to take the sine of all angles using np.sin [1] to move on all the positions with a single vector displacement addition [1] Numpy solution uses hstack or similar to create new arrays with twice the length, by composing the left-turned array with the right-turned array [1] Performance comparison recorded
    • +
    +
  • +
+
+
+
+
+
+
+

As with assignment one, to facilitate semi-automated marking, submit your code to moodle as a single Zip file (not .tgz, nor any other zip format), which unzips to produce files in a folder named with your student number.

+
+
+
+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/ch98rubrics/RefactoringTrees.ipynb b/ch98rubrics/RefactoringTrees.ipynb new file mode 100644 index 000000000..a1ceb16d6 --- /dev/null +++ b/ch98rubrics/RefactoringTrees.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7eab9302", + "metadata": {}, + "source": [ + "# Refactoring Trees: An exercise in Research Software Engineering" + ] + }, + { + "cell_type": "markdown", + "id": "079cd03a", + "metadata": {}, + "source": [ + "In this exercise, you will convert badly written code, provided here, into better-written code." + ] + }, + { + "cell_type": "markdown", + "id": "d8639c07", + "metadata": {}, + "source": [ + "You will do this not through simply writing better code, but by taking a refactoring approach, as discussed in the lectures." + ] + }, + { + "cell_type": "markdown", + "id": "bb93b4a0", + "metadata": {}, + "source": [ + "As such, your use of `git` version control, to make a commit after each step of the refactoring, with a commit message which indicates the refactoring you took, will be critical to success.\n" + ] + }, + { + "cell_type": "markdown", + "id": "abbc524b", + "metadata": {}, + "source": [ + "You will also be asked to look at the performance of your code, and to make changes which improve the speed of the code." + ] + }, + { + "cell_type": "markdown", + "id": "2c07eebf", + "metadata": {}, + "source": [ + "The script as supplied has its parameters hand-coded within the code. You will be expected, in your refactoring, to make these available as command line parameters to be supplied when the code is invoked." + ] + }, + { + "cell_type": "markdown", + "id": "af6c1445", + "metadata": {}, + "source": [ + "# Some terrible code" + ] + }, + { + "cell_type": "markdown", + "id": "10c64676", + "metadata": {}, + "source": [ + "Here's our terrible code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aebb52c2", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7316e561", + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos\n", + "from matplotlib import pyplot as plt\n", + "s=1\n", + "d=[[0,1,0]]\n", + "plt.plot([0,0],[0,1])\n", + "for i in range(5):\n", + " n=[]\n", + " for j in range(len(d)):\n", + " n.append([d[j][0]+s*sin(d[j][2]-0.2), d[j][1]+s*cos(d[j][2]-0.2), d[j][2]-0.2])\n", + " n.append([d[j][0]+s*sin(d[j][2]+0.2), d[j][1]+s*cos(d[j][2]+0.2), d[j][2]+0.2])\n", + " plt.plot([d[j][0], n[-2][0]],[d[j][1], n[-2][1]])\n", + " plt.plot([d[j][0], n[-1][0]],[d[j][1], n[-1][1]])\n", + " d=n\n", + " s*=0.6\n", + "plt.savefig('tree.png')" + ] + }, + { + "cell_type": "markdown", + "id": "4c08b994", + "metadata": {}, + "source": [ + "# Rubric and marks scheme\n", + "\n", + "## Part one: Refactoring (15 marks)\n" + ] + }, + { + "cell_type": "markdown", + "id": "f2cbbad2", + "metadata": {}, + "source": [ + "* Copy the code above into a file tree.py, invoke it with python tree.py, and verify it creates an image tree.png which looks like that above.\n", + "* Initialise your git repository with the raw state of the code. [1 mark]\n", + "* Identify a number of simple refactorings which can be used to improve the code, *reducing repetition* and *improving readability*. Implement these one by one, with a git commit each time.\n", + " * 1 mark for each refactoring, 1 mark for each git commit, at least five such: ten marks total.\n", + "* Do NOT introduce NumPy or other performance improvements yet (see below.)" + ] + }, + { + "cell_type": "markdown", + "id": "9698412b", + "metadata": {}, + "source": [ + "* Identify which variables in the code would, more sensibly, be able to be input parameters, and use Argparse to manage these.\n", + " * 4 marks: 1 for each of four arguments identified." + ] + }, + { + "cell_type": "markdown", + "id": "10bc6e27", + "metadata": {}, + "source": [ + "## Part two: performance programming (10 marks)" + ] + }, + { + "cell_type": "markdown", + "id": "dc8f3f45", + "metadata": {}, + "source": [ + "* For the code as refactored, prepare a figure which plots the time to produce the tree, versus number of iteration steps completed. Your code to produce this figure should run as a script, which you should call perf_plot.py, invoking a function imported from tree.py. The script should produce a figure called perf_plot.png. Comment on your findings in a text file, called comments.md. You should turn off the actual plotting, and run only the mathematical calculation, for your performance measurements. (Add an appropriate flag.)\n", + " * 5 marks: [1] Time to run code identified [1] Figure created [1] Figure correctly formatted [1] Figure auto-generated from script [1] Performance law identified." + ] + }, + { + "cell_type": "markdown", + "id": "8f94665e", + "metadata": {}, + "source": [ + "* The code above makes use of `append()` which is not appropriate for NumPy. Create a new solution (in a file called tree_np.py) which makes use of NumPy. Compare the performance (again, excluding the plotting from your measurements), and discuss in comments.md\n", + " * 5 marks: [1] NumPy solution uses array-operations to subtract the change angle from all angles in a single minus sign, [1] to take the sine of all angles using np.sin [1] to move on all the positions with a single vector displacement addition [1] Numpy solution uses `hstack` or similar to create new arrays with twice the length, by composing the left-turned array with the right-turned array [1] Performance comparison recorded" + ] + }, + { + "cell_type": "markdown", + "id": "32a1b776", + "metadata": {}, + "source": [ + "As with assignment one, to facilitate semi-automated marking, submit your code to moodle as a single Zip file (not .tgz, nor any other zip format), which unzips to produce files in a folder named with your **student number**." + ] + } + ], + "metadata": { + "jupytext": { + "main_language": "python", + "notebook_metadata_filter": "-kernelspec,jupytext,jekyll" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ch98rubrics/RefactoringTrees.ipynb.py b/ch98rubrics/RefactoringTrees.ipynb.py new file mode 100644 index 000000000..15193f0f7 --- /dev/null +++ b/ch98rubrics/RefactoringTrees.ipynb.py @@ -0,0 +1,86 @@ +# --- +# jupyter: +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Refactoring Trees: An exercise in Research Software Engineering + +# %% [markdown] +# In this exercise, you will convert badly written code, provided here, into better-written code. + +# %% [markdown] +# You will do this not through simply writing better code, but by taking a refactoring approach, as discussed in the lectures. + +# %% [markdown] +# As such, your use of `git` version control, to make a commit after each step of the refactoring, with a commit message which indicates the refactoring you took, will be critical to success. +# + +# %% [markdown] +# You will also be asked to look at the performance of your code, and to make changes which improve the speed of the code. + +# %% [markdown] +# The script as supplied has its parameters hand-coded within the code. You will be expected, in your refactoring, to make these available as command line parameters to be supplied when the code is invoked. + +# %% [markdown] +# # Some terrible code + +# %% [markdown] +# Here's our terrible code: + +# %% +# %matplotlib inline + +# %% +from math import sin, cos +from matplotlib import pyplot as plt +s=1 +d=[[0,1,0]] +plt.plot([0,0],[0,1]) +for i in range(5): + n=[] + for j in range(len(d)): + n.append([d[j][0]+s*sin(d[j][2]-0.2), d[j][1]+s*cos(d[j][2]-0.2), d[j][2]-0.2]) + n.append([d[j][0]+s*sin(d[j][2]+0.2), d[j][1]+s*cos(d[j][2]+0.2), d[j][2]+0.2]) + plt.plot([d[j][0], n[-2][0]],[d[j][1], n[-2][1]]) + plt.plot([d[j][0], n[-1][0]],[d[j][1], n[-1][1]]) + d=n + s*=0.6 +plt.savefig('tree.png') + +# %% [markdown] +# # Rubric and marks scheme +# +# ## Part one: Refactoring (15 marks) +# + +# %% [markdown] +# * Copy the code above into a file tree.py, invoke it with python tree.py, and verify it creates an image tree.png which looks like that above. +# * Initialise your git repository with the raw state of the code. [1 mark] +# * Identify a number of simple refactorings which can be used to improve the code, *reducing repetition* and *improving readability*. Implement these one by one, with a git commit each time. +# * 1 mark for each refactoring, 1 mark for each git commit, at least five such: ten marks total. +# * Do NOT introduce NumPy or other performance improvements yet (see below.) + +# %% [markdown] +# * Identify which variables in the code would, more sensibly, be able to be input parameters, and use Argparse to manage these. +# * 4 marks: 1 for each of four arguments identified. + +# %% [markdown] +# ## Part two: performance programming (10 marks) + +# %% [markdown] +# * For the code as refactored, prepare a figure which plots the time to produce the tree, versus number of iteration steps completed. Your code to produce this figure should run as a script, which you should call perf_plot.py, invoking a function imported from tree.py. The script should produce a figure called perf_plot.png. Comment on your findings in a text file, called comments.md. You should turn off the actual plotting, and run only the mathematical calculation, for your performance measurements. (Add an appropriate flag.) +# * 5 marks: [1] Time to run code identified [1] Figure created [1] Figure correctly formatted [1] Figure auto-generated from script [1] Performance law identified. + +# %% [markdown] +# * The code above makes use of `append()` which is not appropriate for NumPy. Create a new solution (in a file called tree_np.py) which makes use of NumPy. Compare the performance (again, excluding the plotting from your measurements), and discuss in comments.md +# * 5 marks: [1] NumPy solution uses array-operations to subtract the change angle from all angles in a single minus sign, [1] to take the sine of all angles using np.sin [1] to move on all the positions with a single vector displacement addition [1] Numpy solution uses `hstack` or similar to create new arrays with twice the length, by composing the left-turned array with the right-turned array [1] Performance comparison recorded + +# %% [markdown] +# As with assignment one, to facilitate semi-automated marking, submit your code to moodle as a single Zip file (not .tgz, nor any other zip format), which unzips to produce files in a folder named with your **student number**. diff --git a/ch98rubrics/tree.png b/ch98rubrics/tree.png new file mode 100644 index 0000000000000000000000000000000000000000..220fe5c8b9be98e9207a144dc1946786bcc6ea4f GIT binary patch literal 33359 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 bjep>I6?kdh2w3Yv?sH4=u0pQ7@zehX^K|UT literal 0 HcmV?d00001 diff --git a/dates.md b/dates.md new file mode 100644 index 000000000..97f33e45b --- /dev/null +++ b/dates.md @@ -0,0 +1,3 @@ +Date and time +------------- +Every Thursday from 10am to 1pm, starting 8th of October until 10th December 2015. diff --git a/index.html b/index.html new file mode 100644 index 000000000..1df381d8a --- /dev/null +++ b/index.html @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Research Software Engineering with Python + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Introduction

+ +

In this course, you will move beyond programming, to learn how to construct reliable, readable, +efficient research software in a collaborative environment. The emphasis is on practical techniques, +tips, and technologies to effectively build and maintain complex code. +This is a intensive, practical course.

+ +

Pre-requisites

+ +
    +
  • You need to have taken a formal course in at least one programming language, including variables, control flow, and functions. This could be a semester-long course, or a shorter workshop like Software Carpentry.
  • +
  • You are required to bring your own laptop. +We have also provided setup instructions for you to install the software needed for the course on +your computer.
  • +
  • Eligibility: This course is for UCL post-graduate students.
  • +
+ +

Registration

+ +

Members of doctoral training schools, or Masters courses who offer this module as part of their programme should register through their course organisers.

+ +

Synopsis

+ + + + + + + + + + + + + + + + + + + + +
+

Version Control

+
    +
  • Why use version control
  • +
  • Solo use of version control
  • +
  • Publishing your code to GitHub
  • +
  • Collaborating with others through Git
  • +
  • Branching
  • +
  • Rebasing and Merging
  • +
  • Debugging with GitBisect
  • +
  • Forks, Pull Requests and the GitHub Flow
  • +
+
+

Introduction to Python

+
    +
  • Why use scripting languages?
  • +
  • Python. IPython and the Jupyter notebook.
  • +
  • Data structures: list, dictionaries, and sets.
  • +
  • List comprehensions
  • +
  • Functions in Python
  • +
  • Modules in Python
  • +
  • An introduction to classes
  • +
+
+

Research Data in Python

+
    +
  • Working with files on the disk
  • +
  • Interacting with the internet
  • +
  • JSON and YAML
  • +
  • Plotting with Matplotlib
  • +
  • Animations with Matplotlib
  • +
+
+

Testing your code

+
    +
  • Why test?
  • +
  • Unit testing and regression testing
  • +
  • Negative testing
  • +
  • Mocking
  • +
  • Debugging
  • +
  • Continuous Integration
  • +
+
+

Software Projects

+
    +
  • Turning your code into a package
  • +
  • Releasing code
  • +
  • Choosing an open-source license
  • +
  • Software project management
  • +
  • Organising issues and tasks
  • +
+
+

Construction and Design

+
    +
  • Coding conventions
  • +
  • Comments
  • +
  • Refactoring
  • +
  • Documentation
  • +
  • Object Orientation
  • +
  • Design Patterns
  • +
+
+

Advanced Programming Techniques

+
    +
  • Functional programming
  • +
  • Metaprogramming
  • +
  • Duck typing and exceptions
  • +
  • Operator overloading
  • +
  • Iterators and Generators
  • +
+
+

Programming for Speed

+
    +
  • Optimisation
  • +
  • Profiling
  • +
  • Scaling laws
  • +
  • NumPy
  • +
  • Cython
  • +
+
+ +

Exercises

+ +

Examples and exercises for this course will be provided in Python. +Python will be introduced during this course, but we will assume you can already +program. That means that you may find supplementary python content useful.

+ +

Versions

+ +

You can find the course notes as HTML via the navigation bar to the left.

+ + + + + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/intro.md b/intro.md new file mode 100644 index 000000000..492e339ac --- /dev/null +++ b/intro.md @@ -0,0 +1,121 @@ +## Introduction + +### Purpose + +In this course, you will move beyond programming, to learn how to construct reliable, readable, +efficient research software in a collaborative environment. The emphasis is on practical techniques, +tips, and technologies to effectively build and maintain complex code. +This is a intensive, practical course. + +## Synopsis + +### Lesson 1 + +* Why use scripting languages? +* Python. IPython and the Jupyter notebook. +* Programming with lists. Data structures: arrays, dictionaries, and sets. +* Duck typing. +* Modules. + +### Lesson 2 + +* Collaborating around code. Version control. +* Git. Github. Issue tracking. +* Code review. +* Merging. +* Software licensing and releases. + +### Lesson 3 + +* Testing. +* Unit testing and regression testing. +* Test driven design. +* Exceptions and assertions. +* Mocking. +* Automated and interactive testing. +* Build-and-test servers. +* Negative testing and defensive programming. +* Profiling and debugging. +* Coverage. + +### Lesson 4 + +* Using libraries. +* The Python package index. +* Packaging with setuptools. +* Working with files and the OS. +* Working with the web +* Working with command line arguments +* Brief introduction to functional programming + +### Lesson 5 + +* Best practice in construction. +* Comments. +* Coding conventions. +* Basic object-oriented python +* Refactoring. +* Design and development. +* Documentation with Sphinx. + +### Lesson 6 + +* Further object-oriented python. +* Object oriented design. +* Software as engineering. +* Pragmatic use of diagram languages. +* Requirements engineering. +* Agile and Waterfall. +* Functional and architectural design. + +### Lesson 7 + +* Tricks for not repeating yourself +* Iterables and generators +* Exceptions +* Functional python. +* Operator Overloading +* Map and reduce. +* Context managers and decorators. +* Metaprogramming +* IDEs and editors +* Logging. + +### Lesson 8 + +* Performance programming +* Numpy. +* Container asymptotic performance performance +* Cython and linking C to Python + +### Lesson 9 + +* Further git +* Rebasing +* Branching +* GitHub pages +* Creating servers + +### Lesson 10 + +* Solutions to exercises + +## Course processes + +### Prerequisites + +Prior knowledge of at least one programming language, including variables, control flow, and functions. + +### Exercises + +Examples and exercises for this course will be provided in Python. +Python will be introduced during this course, but we will assume you can already +program. That means that you may find supplementary python content useful. + +### Setup + +You are required to bring your own laptop to the course as the classrooms we are + using do not have desktop computers. + +We have provided [setup](installation) instructions for installing the software needed for the course on +your computer. diff --git a/jekyll_template/conf.json b/jekyll_template/conf.json new file mode 100644 index 000000000..2d3500a32 --- /dev/null +++ b/jekyll_template/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "basic", + "mimetypes": { + "text/html": true + } +} diff --git a/jekyll_template/index.html.j2 b/jekyll_template/index.html.j2 new file mode 100644 index 000000000..7e0f29bce --- /dev/null +++ b/jekyll_template/index.html.j2 @@ -0,0 +1,9 @@ +{%- extends 'basic/index.html.j2' -%} +{%- block header -%} +--- +title: {% if 'jekyll' in nb['metadata'] %} {{nb['metadata']['jekyll']['display_name']}} {% endif %} +nblink: True +--- +{{super()}} +{%- endblock header -%} + diff --git a/latex_template/conf.json b/latex_template/conf.json new file mode 100644 index 000000000..a26896770 --- /dev/null +++ b/latex_template/conf.json @@ -0,0 +1,8 @@ +{ + "base_template": "latex", + "mimetypes": { + "text/latex": true, + "text/tex": true, + "application/pdf": true + } +} diff --git a/latex_template/index.tex.j2 b/latex_template/index.tex.j2 new file mode 100644 index 000000000..71565aaec --- /dev/null +++ b/latex_template/index.tex.j2 @@ -0,0 +1,60 @@ +%===================================== +% Solution about utf8x problem +% https://github.com/jupyter/nbconvert/issues/530#issuecomment-303034557 +%===================================== + +% Default to the notebook output style +((* if not cell_style is defined *)) + ((* set cell_style = 'style_ipython.tex.j2' *)) +((* endif *)) + +% Inherit from the specified cell style. +((* extends cell_style *)) +((* block packages *)) + % Hide [utf8x]{inputenc} as it should not be used with xetex. + % Also hide ucs which conflicts with a bunch of stuff. + % http://tex.stackexchange.com/a/39418 + \makeatletter + \newcommand{\dontusepackage}[2][]{% + \@namedef{ver@#2.sty}{9999/12/31}% + \@namedef{opt@#2.sty}{#1}} + \makeatother + \dontusepackage[utf8x]{inputenc} + \dontusepackage[mathletters]{ucs} +((( super() ))) + \usepackage{unicode-math} + \usepackage{fontspec} + \usepackage[Latin,Greek]{ucharclasses} + \newfontfamily\substitutefont{CMU Serif} + \setTransitionsForGreek{\begingroup\substitutefont}{\endgroup} +((* endblock packages *)) + + +((* block title *)) +\title{An introduction to Python Programming for Research} +((* endblock title *)) + +((* block author *)) +\author{James Hetherington} +((* endblock author *)) + +%=============================================================================== +% Latex Book +%=============================================================================== + +((* block predoc *)) + ((( super() ))) + ((* block tableofcontents *))\tableofcontents((* endblock tableofcontents *)) + + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} +((* endblock predoc *)) + +((* block docclass *)) +\documentclass{report} +((* endblock docclass *)) + +((* block markdowncell scoped *)) +((( cell.source | citation2latex | strip_files_prefix | markdown2latex(extra_args=["--top-level-division=chapter"]) ))) +((* endblock markdowncell *)) diff --git a/nbmerge.py b/nbmerge.py new file mode 100644 index 000000000..0bc8aac77 --- /dev/null +++ b/nbmerge.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# From https://gist.github.com/fperez/e2bbc0a208e82e450f69 +# Note, updated version of +# https://github.com/ipython/ipython-in-depth/blob/master/tools/nbmerge.py +""" +usage: +python nbmerge.py A.ipynb B.ipynb C.ipynb > merged.ipynb +""" +from __future__ import print_function + +import io +import os +import string +import sys + +import nbformat + +def fix_images_paths(cells, filename): + # find parent path + path_filename = filename.split('/') + full_path = '/'.join(path_filename[:-1]) + "/" + + # fix paths + for cell in cells: + if ("![" in cell['source'] and ".svg)" in cell['source']): + source = cell['source'] + new_source = source + # where the link starts + start = source.find("](") + if source[start+2:start+4] == './': + new_source = source[:start+2] + full_path + source[start+4:] + elif source[start+2] in string.ascii_letters: + new_source = source[:start+2] + full_path + source[start+2:] + cell['source'] = new_source + return cells + +def merge_notebooks(filenames, outfile): + merged = None + for fname in filenames: + with io.open(fname, 'r', encoding='utf-8') as f: + nb = nbformat.read(f, as_version=4) + if merged is None: + merged = nb + else: + nb.cells = fix_images_paths(nb.cells, fname) + # TODO: add an optional marker between joined notebooks + # like an horizontal rule, for example, or some other arbitrary + # (user specified) markdown cell) + merged.cells.extend(nb.cells) + if not hasattr(merged.metadata, 'name'): + merged.metadata.name = '' + merged.metadata.name += "_merged" + result=nbformat.writes(merged) + with io.open(outfile, 'w', encoding='utf-8') as out: + out.write(result) + +if __name__ == '__main__': + notebooks = sys.argv[1:-1] + outfile = sys.argv[-1] + if not notebooks: + print(__doc__, file=sys.stderr) + sys.exit(1) + + merge_notebooks(notebooks, outfile) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..bd3141947 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.jupytext] +formats = "ipynb,py:percent" +notebook_metadata_filter="-kernelspec,jupytext,jekyll" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..7e6ce547d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +attrs>=17.4.0 +nbconvert>=6.0.0 +ipython +jupyter +jupytext +numpy +scipy +h5py +pandas +nose +mako +cython +pyzmq +matplotlib>=2.1 +geopy +matplotlib-venn +pypng +sphinx +numpydoc +requests +pyyaml +pytest-cov +coverage +imageio +pycodestyle +pylint +webcolors diff --git a/session99/index.html b/session99/index.html new file mode 100644 index 000000000..330bf1265 --- /dev/null +++ b/session99/index.html @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Installation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Installation Instructions

+ +

Introduction

+ +

This document contains instructions for installation of the packages we’ll be using during the +course. You will be following the training on +your own machines, so please complete these instructions.

+ +

What we’re installing

+ +

For the software engineering session on programming, we’ll be using the language Python and in +particular versions >= 3.7. We will use the Anaconda python distribution which contains a good +collection of the most common Python modules as well as IPython (an improved Python +interpreter) and the Jupyter notebook (a useful web-based user interface that allows you to create +documents that combine text and Python code, executable with the same browser window). We’ll need +pip, the package installer for Python, as well.

+ +

For the session on version control, we’ll be using Git and the Github website.

+ +

Eduroam

+ +

We will be using UCL’s eduroam service to connect +to the internet for this work.

+ +

So you should ensure you have eduroam installed and working.

+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/session99/linux.html b/session99/linux.html new file mode 100644 index 000000000..b521a9a33 --- /dev/null +++ b/session99/linux.html @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Linux

+ +

Package Manager

+ +

Linux users should be able to use their package manager to install all of this software (if you’re +using Linux, we assume you won’t have any trouble with these requirements).

+ +

However note that if you are running an older Linux distribution you may get older versions with +different look and features. A recent Linux distribution is recommended.

+ +

Python via package manager

+ +

Recent versions of Ubuntu pack mostly up to date versions of all needed +packages. The version of IPython might be slightly out of date. Advanced users may wish to upgrade +this using pip or a manual install. On Ubuntu you should ensure that the following packages +are installed using apt-get.

+ +
    +
  • python3-numpy
  • +
  • python3-scipy
  • +
  • python3-pytest
  • +
  • python3-matplotlib
  • +
  • python3-pip
  • +
  • jupyter
  • +
  • ipython3
  • +
  • ipython3-notebook
  • +
+ +

Older distributions may have outdated versions of specific packages. +Other linux distributions most likely also contain the needed python packages but again +they may also be outdated.

+ +

Python via Anaconda

+ +

We recommend you use Anaconda, a complete independent scientific python distribution.

+ +

Download Anaconda for linux with your web browser, choose +the python 3.7 version. Open a terminal window, go to the place where the file was downloaded and type:

+ +
bash Anaconda3-
+
+ +

and then press tab. The name of the file you just downloaded should appear.

+ +

Press enter. You will follow the text-only prompts. To move through the text, +press the space key. Type yes and press enter to approve the license. Press +enter to approve the default location for the files. Type yes and press +enter to prepend Anaconda to your PATH (this makes the Anaconda distribution +the default Python).

+ +

You can test the installation by opening a new terminal and checking that:

+ +
which python
+
+ +

shows a path where you installed anaconda.

+ +

Python via Enthought Canopy

+ +

Alternatively you may install a complete independent scientific python distribution. One of these is +Enthought Canopy.

+ +

The Enthought Canopy python distribution exists in two different versions. A basic free version with +a limited number of packages (Canopy express) and a non free full version. The full version can be +obtained free of charge for academic use. Register with +Enthought Scientific Computing using your UCL +e-mail address for an academic licence.

+ +

You may then use your Enthought user account to sign into the installed Canopy application and +activate the full academic version. Canopy comes with a package manager from where it is possible to +install and update a large number of python packages. The packages installed by default should cover +our needs.

+ +

Git

+ +

If git is not already available on your machine you can try to install it via your distribution +package manager (e.g. apt-get or yum), for example:

+ +
sudo apt-get install git
+
+ +

Editor

+ +

Many different text editors suitable for programming are available. If you don’t already have a +favourite, you could look at Visual Studio Code. +Check their setup page for detailed +instructions.

+ +

For a better git integration we suggest the git +graph +plugin.

+ +

Regardless of which editor you have chosen you should configure git to use it. Executing something +like this in a terminal should work:

+ +
git config --global core.editor NameofYourEditorHere
+
+ +

The default shell is usually bash but if not you can get to bash by opening a terminal and typing +bash.

+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/session99/mac.html b/session99/mac.html new file mode 100644 index 000000000..c923bfa2f --- /dev/null +++ b/session99/mac.html @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mac OSX + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Mac

+ +

Upgrade OSX

+ +

We do not recommend following this training on older versions of OSX without an app store: upgrade +to at least OSX Mavericks.

+ +

Git

+ +

On a terminal you can install git by trying to run it:

+ +
git --version
+
+ +

If that throws you an error message, then most likely you will need to install XCode.

+ +

XCode and Command line tools

+ +

Install the XCode command-line-tools by opening a terminal and run the following.

+ +
xcode-select --install
+
+

And follow the on screen instructions.

+ +

You may also install Xcode from the Mac app store if you wish, but it is not needed.

+ +

Pre Mavericks:

+ +

Install XCode using the Mac app store.

+ +

Then, go to Xcode…Preferences…Downloads… and install the command line tools option.

+ +

Homebrew

+ +

Homebrew is a package manager for OSX which enables the installation of a +lot of software useful for scientific computing. It is required for some of the installations +below. But not essential for Software Carpentry. Homebrew requires the Xcode tools above.

+ +

Install Homebrew via typing this at a terminal:

+ +
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
+
+ +

and then type.

+ +
brew doctor
+
+ +

And read the output to verify that everything is working as expected.

+ +

If you are already running MacPorts or another package manager for OSX we don’t recommend +installing homebrew.

+ +

Python

+ +

We recommend installing a complete scientific python distribution. One of these is +Anaconda.

+ +

Please download and install Anaconda +(Python 3.7 version).

+ +

Python from Homebrew

+ +

Alternatively if you wish to install python manually you can use Homebrew. +OSX ships with python and some packages. However this has known limitations and we do not recommend it. +You can install a new version of python from Homebrew with the following. +Please follow the instructions above to install the Xcode tools and Homebrew before attempting +this.

+ +
brew install python3
+
+ +

In order to ensure that this version of python is selected over the OSX default version you should +execute the following command:

+ +
echo export PATH='/usr/local/bin:$PATH' >> ~/.bash_profile
+
+

and reopen the terminal. Verify that this is correctly installed by executing

+ +
python --version
+
+ +

Which should print:

+ +
Python 3.7.x
+
+

This will result in an installation of python3 and pip3 which you can use to have access to the latest python features which will be taught in this course.

+ +

Then install additional python packages by executing the following.

+ +

brew install [package-name]

+
    +
  • pkg-config
  • +
  • freetype
  • +
  • gcc
  • +
+ +

pip3 install [package-name]

+
    +
  • numpy
  • +
  • scipy
  • +
  • matplotlib
  • +
  • jupyter
  • +
  • ipython[all]
  • +
+ +

The following packages should be installed automatically as dependencies. But we recommend +installing them manually just in case.

+ +
    +
  • tornado
  • +
  • jinja2
  • +
  • pyzmq
  • +
  • pytest
  • +
+ +

Editor and shell

+ +

The default text editor on OS X textedit should be sufficient for our use. Alternatively +we recommend to use Visual Studio Code. +Check their setup page for detailed +instructions.

+ +

For a better git integration we suggest the git +graph +plugin.

+ +

To setup git to use textedit executing the following in a terminal should do.

+ +
git config --global core.editor /Applications/TextEdit.app/Contents/MacOS/TextEdit
+
+ +

For VS Code:

+
git config --global core.editor "code --wait"
+
+ +

The default terminal on OSX should also be sufficient. If you want a more advanced terminal +iTerm2 is an alternative.

+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/session99/windows.html b/session99/windows.html new file mode 100644 index 000000000..ecd65d13d --- /dev/null +++ b/session99/windows.html @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Windows + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +
+ + +
+ +
+ + Menu + +
+ +
+
+ + + +
+ + + + + +

Windows

+ +

Python

+ +

We recommend installing a complete scientific python distribution. One of these is +Anaconda.

+ +

Please download and install Anaconda +(Python 3.7 version).

+ +

Sophos

+ +

To use the Jupyter lab or notebook on a Windows computer with Sophos anti-virus installed it may be necessary to +open additional ports allowing communication between the notebook and its server. +The solution is:

+ +
    +
  • open your Sophos Endpoint Security and Control Panel from your tray or start menu
  • +
  • Select “configure” > “Anti-virus” > “Authorization” from the menu at the top
  • +
  • Select the websites tab
  • +
  • click the “Add” button and add 127.0.0.1 and localhost to the “Authorized websites” list
  • +
  • restart computer (most likely not needed, just restart the Jupyter notebook)
  • +
  • output works now :)
  • +
+ +

Git

+ +

Install the GitHub for Windows client. This comes with both a GUI +client as well as the Git Bash terminal client which we will use during +the course. You should register with Github for an account and sign into the +GUI client with this account. This will automatically set-up +SSH based authentication +for the terminal client. The terminal client comes in 3 different flavours based on Windows CMD +(DOS like), Windows Powershell, and BASH. We will use the BASH client as this most closely resembles the +Linux and OS X terminal used by other students. In order to configure this open the Github +client. Sign in with your credentials and:

+ +
    +
  • Select tools
  • +
  • Options
  • +
  • Default Shell
  • +
  • Git Bash
  • +
  • And Press Update to save.
  • +
+ +

Verify that this is working by opening Git Bash. The Shell window should have a title that +starts with MINGW64.

+ +

Editor

+ +

Unless you already use a specific editor which you are comfortable with we recommend using +Visual Studio Code. +Check their setup page for detailed +instructions.

+ +

Using VSCode to edit text files including code should be straight forward but in addition you +could configure Git Bash and +python prompt.

+ +

For a better git integration we suggest the git +graph +plugin.

+ +

Testing your install

+ +

Check this works by opening the git bash shell. Once you have a terminal open, type

+ +
which code
+
+ +

which should produce readout similar to /c/Program Files (x86)/Code/Code.exe

+ +

Also verify the typing:

+
code
+
+

opens the editor and the close it again.

+ +
which git
+
+ +

which should produce /bin/git. The which +command is used to figure out where a given program is located on disk.

+ +

Telling Git about VS Code

+ +

Now we need to update the default editor used by Git.

+ +
git config --global core.editor "code --wait"
+
+ +

Note that it is not obvious how to copy and paste text in a Windows terminal including Git Bash. +Copy and paste can be found by right clicking on the top bar of the window and selecting the +commands from the drop down menu (in a sub menu).

+ +

Testing python

+ +

Confirm that the Python installation has worked by typing:

+ +
python -V
+
+ +

Which should result in details of your installed python version.

+ +

This should print the installed version of the python and git confirming that both are installed at +working correctly.

+ +

You should now have a working version of git, python, and code, all accessible from your shell.

+ + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/site-styles/ipython.css b/site-styles/ipython.css new file mode 100644 index 000000000..1af9d05af --- /dev/null +++ b/site-styles/ipython.css @@ -0,0 +1,69 @@ + diff --git a/site-styles/local_styles.css b/site-styles/local_styles.css new file mode 100644 index 000000000..57e921593 --- /dev/null +++ b/site-styles/local_styles.css @@ -0,0 +1,7 @@ +li.inactive { + display: none; +} + +div.animation button { + background: rgb(190,190,190); +}
+ + + XClose + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+

COMP0233: Research Software Engineering With Python

+ Home + +
+
+ + + + + + + +
+ + +