L = []
for n in range(12):
    L.append(n ** 2)
L

"""
Explanation: List comprehensions
Q: Why doesn't the list comprehension syntax make any sense to me / how can list comprehension be "more readable" than not using a list comprehension.
List comprehensions are simply a way to compress a list-building for-loop into a single short, readable line.
For example, here is a loop that constructs a list of the first 12 square integers:
End of explanation
"""


L = [n ** 2 for n in range(12)]
L

L = [n ** 0.5 for n in [11, 22, 33]]
L

"""
Explanation: Compare it with this:
End of explanation
"""


[(i, j) for i in range(2) for j in range(3)]

L = []
for i in range(2):
    for j in range(3):
        if i == 0:
            L.append((i, j))
L

"""
Explanation: As with many Python statements, you can almost read-off the meaning of this statement in plain English: "construct a list consisting of the square of n for each n up to 12".
This basic syntax, then, is [expr for var in iterable], where expr is any valid expression, var is a variable name, and iterable is any iterable Python object.
Multiple loops
End of explanation
"""


[val for val in range(20) if val % 3 == 0 and val != 0]

[val if val % 2 else -val
 for val in range(20) if val % 3]

"""
Explanation: Conditionals
End of explanation
"""


d = {k: v for k, v in zip('abc', range(3))}
d

{k: v for k, v in d.items() if k in ['a', 'c']}

"""
Explanation: Dictionary comprehension
End of explanation
"""


def catch_all(*args, **kwargs):
    print("Positional (required) arguments:\t", args)
    print("Keyword (optional) arguments:\t", kwargs)
#     print(kwargs['pi'])

catch_all(1, 2, 3, a=4, b=5)

catch_all('a', keyword=2)

inputs = (1, 2, 3)
keywords = {'pi': 3.14, 'e': 2.71}

catch_all(*inputs, **keywords)

def fun(a=1, pi=3, e=2):
    print(a, pi, e)

fun(**keywords)

class MyClass:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

data = dict(var=123)

a = MyClass(blahblah=456)

a.blahblah

"""
Explanation: Keyword arguments in functions (kwargs)
Q: How to optimise function interface with keyword arguments?
End of explanation
"""


from path import Path
from tempfile import mkdtemp
import uuid

"""
Explanation: Cleaning up after model runs
Q: What is the best way to delete unwanted files within Jupyter notebook workflow? Say I need to run the model multiple times and want to insure that every new model run uses only newly created files.
Method 1:

have a Makefile
run make clean from a terminal
or make clean using subprocess

Example of a Makefile
```
.PHONY: clean
clean:
    -rm *.pyc
```
Method 2:
End of explanation
"""


class MockModel(object):
    """Mock model that generates empty files"""
    def __init__(self, workdir=None):
        """
        Initialise the model
        
        Kwargs:
            workdir: working directory path;
                     defaults to a temporary directory
        """
        if workdir is None:
            self.workdir = Path(mkdtemp())
        else:
            self.workdir = Path(workdir)  # .abspath()
            self.workdir.makedirs_p()

    def run(self, n, extensions=['tmp']):
        """
        Run the model
        
        Args:
            n: int, number of random files to generate
        """
        assert isinstance(n, int), 'n must be an integer!'
        
        self.files = []
        for ext in extensions:
            for _ in range(n):
                f = self.workdir / 'junk_{filename}.{ext}'.format(filename=uuid.uuid4(), ext=ext)
                with f.open('wb') as fout:
                    random_data = Path('/dev/urandom').open('rb').read(1024)
                    fout.write(random_data)
                self.files.append(f)
                    
    def show_results(self, pattern='*'):
        return [x for x in sorted(self.workdir.glob(pattern))
                if x.isfile()]

    def clean_all(self, pattern):
        for f in self.workdir.glob(pattern):
            f.remove()
            
    def purge(self):
        self.workdir.rmtree_p()

"""
Explanation: Aside: what is UUID?
* A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems.
* UUID version 4
End of explanation
"""


m = MockModel('such_folder_wow')

"""
Explanation: Initialise the object m
End of explanation
"""


m.show_results()

"""
Explanation: No results yet:
End of explanation
"""


m.run(2, extensions=['temp', 'pyc', 'out'])

m.show_results()

"""
Explanation: Run the model
End of explanation
"""


m.clean_all('*.pyc')

"""
Explanation: Now, delete all *.pyc files in the working directory
End of explanation
"""


def clean_dir(dirname, pattern):
    d = Path(dirname)
    for file in d.glob(pattern):
        file.remove()

"""
Explanation: Take-home code snippet (using path.py)
End of explanation
"""


import os
from glob import glob

def clean_dir(dirname, pattern):
    absdir = os.path.abspath(dirname)
    p = '{absdir}/{pattern}'.format(absdir=absdir, pattern=pattern)
    for file in glob(p):
        os.remove(file)

clean_dir('such_folder_wow', '*.so')

"""
Explanation: Or using os module:
End of explanation
"""


m.purge()

"""
Explanation: Remove the directory:
End of explanation
"""


import matplotlib.pyplot as plt
import numpy as np

from mpl_toolkits.basemap import Basemap

fig, ax = plt.subplots()
mymap = Basemap(ax=ax, projection='npstere', boundinglat=60,
                round='True', lon_0=0, lat_0=90)
mymap.drawcoastlines()
mymap.drawmeridians(np.arange(-180, 181, 60), latmax=90);
mymap.drawparallels(np.arange(-90, 91, 5));

"""
Explanation: Polar ~~stereoscopic~~ stereographic maps
Q: What is the best way to create polar stereographic plots? I will be creating a lot soon and currently using cf plots to do this. What other ways are there?
Let's have a look at cf-plot!
http://ajheaps.github.io/cf-plot/gallery.html
https://github.com/ajheaps/cf-plot/blob/master/cfplot/cfplot.py#L4803
We can see that under the hood it uses basemap library: https://matplotlib.org/basemap/
End of explanation
"""


import cartopy.crs as ccrs

%matplotlib inline

plt.rcParams['figure.figsize'] = (8, 8)

fig = plt.figure()
ax = fig.add_subplot(111, projection=ccrs.Stereographic(central_latitude=90))

ax.set_extent([-180, 180, 60, 90], ccrs.PlateCarree())
ax.gridlines()

ax.coastlines()

import matplotlib.path as mpath

fig = plt.figure()
ax = fig.add_subplot(111, projection=ccrs.NorthPolarStereo())

ax.coastlines()

ax.set_extent([-180, 180, 60, 90], ccrs.PlateCarree())

ax.gridlines()

# Compute a circle in axes coordinates, which we can use as a boundary
# for the map. We can pan/zoom as much as we like - the boundary will be
# permanently circular.
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)

ax.set_boundary(circle, transform=ax.transAxes)

"""
Explanation: Now, let's try the same North Polar Stereographic projection plot with cartopy.
End of explanation
"""


HTML(html)

"""
Explanation: References

A Whirlwind Tour of Python by Jake VanderPlas (O'Reilly Media, 2016)
Cartopy projections
End of explanation
"""


