import json
import logging
import threading
import time
import psutil
import shutil
import os
import requests
from typing import Optional, Any
from paho.mqtt.client import Client
from multiprocessing.synchronize import Event

from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
from frigate.types import StatsTrackingTypes, CameraMetricsTypes
from frigate.version import VERSION
from frigate.edgetpu import EdgeTPUProcess

logger = logging.getLogger(__name__)

def get_latest_version() -> str:
    try:
        request = requests.get(
            "https://api.github.com/repos/blakeblackshear/frigate/releases/latest",
            timeout=10,
        )
    except:
        return "unknown"

    response = request.json()

    if request.ok and response and "tag_name" in response:
        return str(response.get("tag_name").replace("v", ""))
    else:
        return "unknown"

def stats_init(
    camera_metrics: dict[str, CameraMetricsTypes], detectors: dict[str, EdgeTPUProcess]
) -> StatsTrackingTypes:
    stats_tracking: StatsTrackingTypes = {
        "camera_metrics": camera_metrics,
        "detectors": detectors,
        "started": int(time.time()),
        "latest_frigate_version": get_latest_version(),
    }
    return stats_tracking

def get_fs_type(path: str) -> str:
    bestMatch = ""
    fsType = ""
    for part in psutil.disk_partitions(all=True):
        if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint):
            fsType = part.fstype
            bestMatch = part.mountpoint
    return fsType

def read_temperature(path: str) -> Optional[float]:
    if os.path.isfile(path):
        with open(path) as f:
            line = f.readline().strip()
            return int(line) / 1000
    return None

def get_temperatures() -> dict[str, float]:
    temps = {}

    # Get temperatures for all attached Corals
    base = "/sys/class/apex/"
    if os.path.isdir(base):
        for apex in os.listdir(base):
            temp = read_temperature(os.path.join(base, apex, "temp"))
            if temp is not None:
                temps[apex] = temp

    return temps

def stats_snapshot(stats_tracking: StatsTrackingTypes) -> dict[str, Any]:
    camera_metrics = stats_tracking["camera_metrics"]
    stats: dict[str, Any] = {}

    total_detection_fps = 0

    for name, camera_stats in camera_metrics.items():
        total_detection_fps += camera_stats["detection_fps"].value
        pid = camera_stats["process"].pid if camera_stats["process"] else None
        cpid = (
            camera_stats["capture_process"].pid
            if camera_stats["capture_process"]
            else None
        )
        stats[name] = {
            "camera_fps": round(camera_stats["camera_fps"].value, 2),
            "process_fps": round(camera_stats["process_fps"].value, 2),
            "skipped_fps": round(camera_stats["skipped_fps"].value, 2),
            "detection_fps": round(camera_stats["detection_fps"].value, 2),
            "pid": pid,
            "capture_pid": cpid,
        }

    stats["detectors"] = {}
    for name, detector in stats_tracking["detectors"].items():
        pid = detector.detect_process.pid if detector.detect_process else None
        stats["detectors"][name] = {
            "inference_speed": round(detector.avg_inference_speed.value * 1000, 2),
            "detection_start": detector.detection_start.value,
            "pid": pid,
        }
    stats["detection_fps"] = round(total_detection_fps, 2)

    stats["service"] = {
        "uptime": (int(time.time()) - stats_tracking["started"]),
        "version": VERSION,
        "latest_version": stats_tracking["latest_frigate_version"],
        "storage": {},
        "temperatures": get_temperatures(),
    }

    for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:
        storage_stats = shutil.disk_usage(path)
        stats["service"]["storage"][path] = {
            "total": round(storage_stats.total / 1000000, 1),
            "used": round(storage_stats.used / 1000000, 1),
            "free": round(storage_stats.free / 1000000, 1),
            "mount_type": get_fs_type(path),
        }

    return stats

class StatsEmitter(threading.Thread):
    def __init__(
        self,
        config: FrigateConfig,
        stats_tracking: StatsTrackingTypes,
        mqtt_client: Client,
        topic_prefix: str,
        stop_event: Event,
    ):
        threading.Thread.__init__(self)
        self.name = "frigate_stats_emitter"
        self.config = config
        self.stats_tracking = stats_tracking
        self.mqtt_client = mqtt_client
        self.topic_prefix = topic_prefix
        self.stop_event = stop_event

    def run(self) -> None:
        time.sleep(10)
        while not self.stop_event.wait(self.config.mqtt.stats_interval):
            stats = stats_snapshot(self.stats_tracking)
            self.mqtt_client.publish(
                f"{self.topic_prefix}/stats", json.dumps(stats), retain=False
            )
        logger.info(f"Exiting watchdog...")


import numpy as np
from os.path import splitext, isfile
from collections import OrderedDict

import dbx
import exodus
from _netcdf import NetCDFFile

__all__ = ['File', 'loaddb_single_element', 'read_exodus_legacy']

def File(filename, mode='r'):
    if mode not in 'wr':
        raise ValueError('unknown File mode {0}'.format(mode))
    root, ext = splitext(filename)
    if ext in ('.e', '.exo'):
        return exodus.File(filename, mode=mode)
    elif ext in ('.dbx', '.base_dbx'):
        return dbx.File(filename, mode=mode)
    else:
        return tabular.File(filename, mode=mode)

def loaddb_single_element(filename, variables=None, disp=1, elem_num=1, blk_num=1):
    '''Read all field variables through time for elem_num'''

    root, ext = splitext(filename)
    if ext == '.base_exo':
        # punt and use old reader
        return read_exodus_legacy(filename, variables=variables,
                                  disp=disp, blk_num=blk_num, elem_num=elem_num)

    elif ext in ('.dbx', '.base_dbx'):
        fh = dbx.File(filename, mode='r')
    else:
        fh = exodus.File(filename, mode='r')

    fields = OrderedDict()
    times = []
    for step in fh.steps.values():
        for frame in step.frames:
            if times and abs(times[-1] - frame.value) < 1.e-14:
                continue
            times.append(frame.value)
            for fo in frame.field_outputs.values():
                keys, values = fo.get_data(element=elem_num)
                for (i, key) in enumerate(keys):
                    fields.setdefault(key, []).append(values[i])

    head = ['TIME'] + [x.upper() for x in fields.keys()]
    data = np.column_stack((np.asarray(times), np.array(fields.values()).T))

    if variables is not None:
        variables = [x.upper() for x in variables]
        idx = []
        for name in variables:
            try:
                idx.append(head.index(name))
            except IndexError:
                raise KeyError('{0} not in output database'.format(name))
        head = [head[i] for i in idx]
        data = data[:, idx]

    if disp:
        return head, data

    return data

def read_exodus_legacy(filename, variables=None, disp=1, blk_num=1, elem_num=1):
    '''Read the specified variables from the exodus file in filepath

    '''
    if not isfile(filename):
        raise IOError('{0}: no such file'.format(filename))

    fh = NetCDFFile(filename, 'r')

    # global/element vars and mapping
    num_glo_var = fh.dimensions.get('num_glo_var', 0)
    if num_glo_var:
        name_glo_var = exodus.stringify(fh.variables['name_glo_var'].data)
        gmap = dict(zip(name_glo_var, range(len(name_glo_var))))

    name_elem_var = exodus.stringify(fh.variables['name_elem_var'].data)
    emap = dict(zip(name_elem_var, range(len(name_elem_var))))

    # retrieve the data from the database
    head = ['TIME']
    if num_glo_var:
        head.extend([H.upper() for H in name_glo_var])
    head.extend([H.upper() for H in name_elem_var])

    data = []
    times = fh.variables['time_whole'].data.flatten()
    for (i, time) in enumerate(times):
        row = [time]
        if num_glo_var:
            vals_glo_var = fh.variables['vals_glo_var'].data[i]
            for var in name_glo_var:
                var_num = gmap[var]
                try: row.append(vals_glo_var[var_num])
                except KeyError: continue
        for var in name_elem_var:
            var_num = emap[var]+1
            name = 'vals_elem_var{0}eb{1}'.format(var_num, blk_num)
            row.append(fh.variables[name].data[i, elem_num-1])
        data.append(row)
    fh.close()
    data = np.asarray(data)
    if len(head) != data.shape[1]:
        raise ValueError('inconsistent data')

    data = np.array(data)

    if variables is not None:
        variables = [x.upper() for x in variables]
        idx = []
        for name in variables:
            try:
                idx.append(head.index(name))
            except IndexError:
                raise KeyError('{0} not in output database'.format(name))
        head = [head[i] for i in idx]
        data = data[:, idx]

    if disp:
        return head, data

    return data

def expand_var_names(master, slave):
    mstring = ' '.join(master)
    matches = []
    v = []
    def endsort(item):
        endings = {'_XX': 0, '_YY': 1, '_ZZ': 2,
                   '_XY': 3, '_YZ': 4, '_XZ': 5,
                   '_YX': 6, '_ZY': 7, '_ZX': 8,
                   '_X': 0, '_Y': 1, '_Z': 2}
        for (ending, order) in endings.items():
            if item.endswith(ending):
                return order
        return 9

    for i, name in enumerate(slave):
        if name in master:
            matches.append(name)
            slave[i] = None
            continue
        vt = []
        for match in re.findall(r'(?i)\b{0}_[XYZ]+'.format(name), mstring):
            vt.append(match.strip())
            slave[i] = None
            continue
        vt = sorted(vt, key=lambda x: endsort(x))
        matches.extend(vt)
    return matches


# Tags relating to training sessions primarily.

from django import template
from ..models import Category, Department, Person, Training_Session, Training_Spec

register = template.Library()

@register.simple_tag
def person_trainee(person):
	# Returns a list of training sessions in which the person is a trainee
	sessions = Training_Session.objects.filter(trainee=person).order_by('date').prefetch_related('trainingId').select_related('trainer')
	return sessions

@register.simple_tag
def person_trained(person):
	# Returns a list of training sessions in which the person is a trainer
	trained = Training_Session.objects.filter(trainer=person).order_by('date').prefetch_related('trainingId').prefetch_related('trainee')
	return trained

@register.simple_tag
def id_people(spec):
	# Returns a list of who has trained, and who has been trained (current students only), on a certain training ID
	sessions = Training_Session.objects.filter(trainingId=spec).prefetch_related('trainee')
	trainers = []
	trainees = [] #Set up for population
	for session in sessions:
		session_trainees = session.trainee.filter(status='STU') #Only student trainees
		for t in session_trainees:
			trainees.append(t) #Populate
		trainers.append(session.trainer)

	#Remove duplicates
	trainers = set(trainers)
	trainers = list(trainers)

	trainees = set(trainees)
	trainees = list(trainees)

	return {'trainers': trainers, 'trainees': trainees}

def ordered_set(seq):
	# From https://stackoverflow.com/a/480227 - remove duplicates and maintain order
  seen = set()
  seen_add = seen.add
  return [x for x in seq if not (x in seen or seen_add(x))]

@register.simple_tag
def session_meta(session):
	# Returns the list of categories covered in a given training session
	ids = session.trainingId.order_by('trainingId').select_related('category') #Get points
	session_cat_list = [] #For population later
	session_depts = []
	for trainingid in ids:
		session_cat_list.append(trainingid.category.iconRef) # Get a list of the categories involved
		session_depts.append(trainingid.category.department)
	session_cat_list = ordered_set(session_cat_list) #Preserve numerical order
	session_depts = ordered_set(session_depts)

	allcats = Category.objects.all() #All the training categories
	session_cat_dict = {} #Dictionary for use in templates
	for cat in session_cat_list:
		this_cat_icon = allcats.get(iconRef = cat) # Get the list of icons
		session_cat_dict[cat] = [this_cat_icon]

	return {'session_cat_dict': session_cat_dict, 'session_depts': session_depts}

@register.inclusion_tag('nt_training/template_tags/session-card.html')
def session_cards(sessions=None):
	# Display training sessions as panels
	# (also uses session_meta, above, in the inclusion tag)
	if sessions == None:
		# If no sessions are given, default to use all of them
		sessions = Training_Session.objects.all().prefetch_related('trainingId')

	departments = Department.objects.all()
	if not departments:
		departments = ['no_depts']
	return {'sessions': sessions, 'departments': departments}


import numpy as np
import matplotlib.pyplot as plt
import sqlite3

db_name = "../gaia_dr2_rv_2019-02-21-15-17-54.db"
conn = sqlite3.connect(db_name)
c = conn.cursor()
q = c.execute("SELECT phot_g_mean_mag, distance FROM gaia WHERE phot_g_mean_mag IS NOT NULL").fetchall()

g = list(map(lambda r: r[0], q))
d = list(map(lambda r: r[1], q))
#bp = list(map(lambda r: r[0], qbp))
#n, bins, patches = plt.hist(g, 1000)
plt.plot(g, d, 'o')
#n, bins, patches = plt.hist(rp, 40, facecolor = 'r', alpha=0.5)
#n, bins, patches = plt.hist(bp, 40, facecolor = 'b', alpha=0.5)

plt.xlabel('Gaia G magnitude')
plt.ylabel('Distance (Pc)')
plt.axes((8, 0, 6, 5000))
plt.show()


from django.shortcuts import render

# Create your views here.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from verification.models import phoneModel
from .serializers import ShopperSerializer

class ShopperView(APIView):
    def post(self, request, *args, **kwargs):
        try:
            phone_number = self.kwargs.get('phone')
            phone_obj = phoneModel.objects.get(mobile=phone_number)
        except phoneModel.DoesNotExist:
            return Response("Phone number does not exist", status=status.HTTP_404_NOT_FOUND)

        if not phone_obj.isverified:
            return Response("Phone number is not verified", status=status.HTTP_400_BAD_REQUEST)

        serializer = ShopperSerializer(data=request.data)
        if serializer.is_valid():
            shopper_obj = serializer.save(mobile=phone_obj)
            #shopper_obj = serializer.save()
            phone_obj.shopperid = shopper_obj
            phone_obj.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def get(self, request, *args, **kwargs):
        try:
            phone_number = self.kwargs.get('phone')
            phone_obj = phoneModel.objects.get(mobile=phone_number)
        except phoneModel.DoesNotExist:
            return Response("Phone number does not exist", status=status.HTTP_404_NOT_FOUND)

        if not phone_obj.isverified:
            return Response("Phone number is not verified", status=status.HTTP_400_BAD_REQUEST)

        shopper_obj = phone_obj.shopperid
        serializer = ShopperSerializer(shopper_obj)
        return Response(serializer.data)

#    def delete(self, request, *args, **kwargs):
#        try:
#            phone_number = self.kwargs.get('phone')
#            phone_obj = phoneModel.objects.get(Mobile=phone_number)
#        except phoneModel.DoesNotExist:
#            return Response("Phone number does not exist", status=status.HTTP_404_NOT_FOUND)

#        if not phone_obj.isVerified:
#            return Response("Phone number is not verified", status=status.HTTP_400_BAD_REQUEST)

#        shopper_obj = phone_obj.shopperid
#        if shopper_obj is None:
#            return Response("Shopper record does not exist", status=status.HTTP_404_NOT_FOUND)

#        shopper_obj.delete()
#        phone_obj.shopperid = None
#        phone_obj.save()
#        return Response("Shopper deleted successfully", status=status.HTTP_204_NO_CONTENT)

#old dont use below
#class ShopperViewSet(viewsets.ModelViewSet):
#    queryset = Shopper.objects.all()
#    serializer_class = ShopperSerializer

#    def create(self, request, *args, **kwargs):
#        serializer = self.get_serializer(data=request.data)
#        serializer.is_valid(raise_exception=True)
#        self.perform_create(serializer)
#        headers = self.get_success_headers(serializer.data)
#        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

#    def perform_create(self, serializer):
#        serializer.save()

#    def destroy(self, request, *args, **kwargs):
#        instance = self.get_object()
#        instance.user.delete()  # Delete associated user as well
#        self.perform_destroy(instance)
#        return Response(status=status.HTTP_204_NO_CONTENT)

#    def perform_destroy(self, instance):
#       instance.is_active = False
#       instance.save()


import pytest

@pytest.fixture(params=["watchdog", "fsnotify"])
def remote_fs_observer(request):
    from robocorp_ls_core.remote_fs_observer_impl import RemoteFSObserver

    backend = request.param
    remote_fsobserver = RemoteFSObserver(backend, None)
    try:
        port = remote_fsobserver.start_server()
        assert port
        yield remote_fsobserver
    finally:
        remote_fsobserver.dispose()

def test_remote_fs_observer(remote_fs_observer, tmpdir):
    from robocorp_ls_core import watchdog_wrapper
    from robocorp_ls_core.watchdog_wrapper import PathInfo
    from robocorp_ls_core.unittest_tools.fixtures import wait_for_test_condition
    from robocorp_ls_core.watchdog_wrapper import IFSObserver

    tmpdir.join("dir_not_rec").mkdir()
    tmpdir.join("dir_rec").mkdir()

    found = []

    def on_change(filepath, *args):
        found.append(filepath)
        assert args == ("foo", "bar")

    notifier = watchdog_wrapper.create_notifier(on_change, timeout=0.1)
    observer: IFSObserver = remote_fs_observer

    watch = observer.notify_on_any_change(
        [
            PathInfo(tmpdir.join("dir_not_rec"), False),
            PathInfo(tmpdir.join("dir_rec"), True),
        ],
        notifier.on_change,
        call_args=("foo", "bar"),
    )

    try:
        tmpdir.join("dir_not_rec").join("mya.txt").write("foo")
        tmpdir.join("dir_not_rec").join("mya.libspec").write("foo")

        tmpdir.join("dir_rec").join("myb.txt").write("foo")
        tmpdir.join("dir_rec").join("myb.libspec").write("foo")

        def collect_basenames():
            import os.path

            basenames = [os.path.basename(x) for x in found]
            return set(basenames)

        def check1():
            expected = {"myb.txt", "mya.libspec", "myb.libspec", "mya.txt"}
            return collect_basenames().issuperset(expected)

        wait_for_test_condition(
            check1, msg=lambda: f"Basenames found: {collect_basenames()}"
        )

    finally:
        watch.stop_tracking()
        notifier.dispose()
        observer.dispose()

def test_remote_fs_observer_conflicts(remote_fs_observer, tmpdir):
    from robocorp_ls_core import watchdog_wrapper
    from robocorp_ls_core.watchdog_wrapper import PathInfo
    from robocorp_ls_core.unittest_tools.fixtures import wait_for_test_condition
    from robocorp_ls_core.watchdog_wrapper import IFSObserver
    import os

    tmpdir.join("dir").mkdir()

    found_not_recursive = []
    found_recursive = []
    found_recursive2 = []

    def on_change_not_recursive(filepath, *args):
        found_not_recursive.append(os.path.basename(filepath))

    def on_change_recursive(filepath, *args):
        found_recursive.append(os.path.basename(filepath))

    def on_change_recursive2(filepath, *args):
        found_recursive2.append(os.path.basename(filepath))

    notifier_not_recursive = watchdog_wrapper.create_notifier(
        on_change_not_recursive, timeout=0.1
    )
    notifier_recursive = watchdog_wrapper.create_notifier(
        on_change_recursive, timeout=0.1
    )
    notifier_recursive2 = watchdog_wrapper.create_notifier(
        on_change_recursive2, timeout=0.1
    )

    observer: IFSObserver = remote_fs_observer

    watch_not_recursive = observer.notify_on_any_change(
        [
            PathInfo(tmpdir.join("dir"), False),
        ],
        notifier_not_recursive.on_change,
    )
    watch_recursive = observer.notify_on_any_change(
        [
            PathInfo(tmpdir.join("dir"), True),
        ],
        notifier_recursive.on_change,
    )
    watch_recursive2 = observer.notify_on_any_change(
        [
            PathInfo(tmpdir.join("dir"), True),
        ],
        notifier_recursive2.on_change,
    )

    # This should be almost a no-op.
    observer.notify_on_any_change(
        [
            PathInfo(tmpdir.join("dir"), True),
        ],
        lambda *args, **kwargs: None,
    ).stop_tracking()

    # Sleep a bit because things may take a bit of time to be setup in the
    # other process.
    import time

    time.sleep(2)
    try:
        tmpdir.join("dir").join("mya.txt").write("foo")
        tmpdir.join("dir").join("dir_inside").mkdir()
        tmpdir.join("dir").join("dir_inside").join("myb.txt").write("bar")

        def check_recursive():
            expected = {"mya.txt", "myb.txt"}
            return set(found_recursive).issuperset(expected)

        wait_for_test_condition(
            check_recursive,
            msg=lambda: f"Basenames found: {found_recursive}",
        )

        def check_recursive2():
            expected = {"mya.txt", "myb.txt"}
            return set(found_recursive2).issuperset(expected)

        wait_for_test_condition(
            check_recursive2,
            msg=lambda: f"Basenames found: {found_recursive2}",
        )

        def check_not_recursive():
            expected = {"mya.txt"}
            return (
                set(found_not_recursive).issuperset(expected)
                and "myb.txt" not in found_not_recursive
            )

        wait_for_test_condition(
            check_not_recursive,
            msg=lambda: f"Basenames found: {found_not_recursive}",
        )

    finally:
        watch_not_recursive.stop_tracking()
        watch_recursive.stop_tracking()
        watch_recursive2.stop_tracking()
        notifier_not_recursive.dispose()
        notifier_recursive.dispose()
        notifier_recursive2.dispose()
        observer.dispose()

def test_glob_matches_path():
    from robocorp_ls_core.load_ignored_dirs import glob_matches_path
    import sys

    # Linux
    for sep, altsep in (("\\", "/"), ("/", None)):

        def build(path):
            if sep == "/":
                return path
            else:
                return ("c:" + path).replace("/", "\\")

        assert glob_matches_path(build("/a"), r"*", sep, altsep)

        assert not glob_matches_path(
            build("/a/b/c/some.py"), "/a/**/c/so?.py", sep, altsep
        )

        assert glob_matches_path("/a/b/c", "/a/b/*")
        assert not glob_matches_path("/a/b", "/*")
        assert glob_matches_path("/a/b", "/*/b")
        assert glob_matches_path("/a/b", "**/*")
        assert not glob_matches_path("/a/b", "**/a")

        assert glob_matches_path(build("/a/b/c/d"), "**/d", sep, altsep)
        assert not glob_matches_path(build("/a/b/c/d"), "**/c", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "**/c/d", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "**/b/c/d", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "/*/b/*/d", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "**/c/*", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "/a/**/c/*", sep, altsep)

        # I.e. directories are expected to end with '/', so, it'll match
        # something as **/directory/**
        assert glob_matches_path(build("/a/b/c/"), "**/c/**", sep, altsep)
        assert glob_matches_path(build("/a/b/c/"), "**/c/", sep, altsep)
        # But not something as **/directory (that'd be a file match).
        assert not glob_matches_path(build("/a/b/c/"), "**/c", sep, altsep)
        assert not glob_matches_path(build("/a/b/c"), "**/c/", sep, altsep)

        assert glob_matches_path(build("/a/b/c/d.py"), "/a/**/c/*", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d.py"), "/a/**/c/*.py", sep, altsep)
        assert glob_matches_path(build("/a/b/c/some.py"), "/a/**/c/so*.py", sep, altsep)
        assert glob_matches_path(
            build("/a/b/c/some.py"), "/a/**/c/som?.py", sep, altsep
        )
        assert glob_matches_path(build("/a/b/c/d"), "/**", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d"), "/**/d", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d.py"), "/**/*.py", sep, altsep)
        assert glob_matches_path(build("/a/b/c/d.py"), "**/c/*.py", sep, altsep)

        if sys.platform == "win32":
            assert glob_matches_path(build("/a/b/c/d.py"), "**/C/*.py", sep, altsep)
            assert glob_matches_path(build("/a/b/C/d.py"), "**/c/*.py", sep, altsep)

        # Expected not to match.
        assert not glob_matches_path(build("/a/b/c/d"), "/**/d.py", sep, altsep)
        assert not glob_matches_path(build("/a/b/c/d.pyx"), "/a/**/c/*.py", sep, altsep)
        assert not glob_matches_path(build("/a/b/c/d"), "/*/d", sep, altsep)

        if sep == "/":
            assert not glob_matches_path(
                build("/a/b/c/d"), r"**\d", sep, altsep
            )  # Match with \ doesn't work on linux...
            assert not glob_matches_path(
                build("/a/b/c/d"), r"c:\**\d", sep, altsep
            )  # Match with drive doesn't work on linux...
        else:
            # Works in Windows.
            assert glob_matches_path(build("/a/b/c/d"), r"**\d", sep, altsep)
            assert glob_matches_path(build("/a/b/c/d"), r"c:\**\d", sep, altsep)

        # Corner cases
        assert not glob_matches_path(build("/"), r"", sep, altsep)
        assert glob_matches_path(build(""), r"", sep, altsep)
        assert not glob_matches_path(build(""), r"**", sep, altsep)
        assert glob_matches_path(build("/"), r"**", sep, altsep)
        assert glob_matches_path(build("/"), r"*", sep, altsep)

def test_create_accept_directory_callable():
    from robocorp_ls_core.load_ignored_dirs import create_accept_directory_callable

    accept_directory = create_accept_directory_callable("")
    assert not accept_directory("/my/node_modules")
    assert accept_directory("/my")

    accept_directory = create_accept_directory_callable('["**/bazel_out/**"]')
    assert accept_directory("/my/foobar")
    assert not accept_directory(
        "/Users/ichamberlain/Documents/workspace/bazel_out/execroot/my_org/bazel_out/execroot/my_org/bazel_out/execroot/my_org"
    )


import datetime

from dagster import RunRequest, SkipReason, sensor

from pipelines.job_append_data.job import (
    job_data_aggregation
)

from pipelines.general.resources import (
    BlobStorageConnector,
    # get_list_files_in_subcontainer
)
from pipelines.general.utility import (
    get_missing_elements,
    read_configuration
)

@sensor(job=job_data_aggregation, minimum_interval_seconds=60)
def sensor_data_aggregation(context):

    config = read_configuration(configuration_file_path="./pipelines/job_append_data/aggregation.yaml")

    blob_container = config["blob_container"]
    source_subblob_container = config["source_subcontainer"]
    source_file_name= config["source_file_name"]
    target_subblob_container = config["target_subcontainer"]
    target_file_name = config["target_file_name"]

    DataFetcher = BlobStorageConnector(container_name=blob_container)

    try:
        target_file = DataFetcher.get_parquet_file(
            subcontainer=target_subblob_container, file=target_file_name
        )
    except BaseException:
        target_file = None

    source_file = DataFetcher.get_parquet_file(
        subcontainer=source_subblob_container, file=source_file_name
    )

    actual_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")

    last_source_date=list(source_file.loc[:, "time"])[-1]

    try:
        last_target_date = list(target_file.loc[:,"time"])[-1]
    except BaseException:
        last_target_date = None

    if last_source_date == last_target_date:
        yield SkipReason(skip_message= "data are up-to-date")

    else:

        context.update_cursor(str(actual_time))

        yield RunRequest(
            run_key=f"updated_timestamp_{actual_time}",
            run_config= {
                "ops": {
                    "load_file_from_blobstorage":{
                        "inputs":{
                            "container_name": blob_container,
                            "sub_container": source_subblob_container,
                            "file_name": source_file_name
                        }
                    },
                    "append_blob_data":{
                        "inputs":{
                            "container_name": blob_container,
                            "sub_container": target_subblob_container,
                            "file_name": target_file_name
                        }
                    }
                }
            }
        )


import fontTools.subset
import fontTools.ttLib
import glob
import os
import sys
import string

fontPath = sys.argv[1]
fontSubsetPath = sys.argv[2]
globPath = sys.argv[3:]

charset = set(string.printable)
for g in globPath:
    for file in glob.glob(g, recursive=True):
        with open(file, 'r', encoding='utf-8') as f:
            print('Collecting:', file)
            charset.update(f.read())
print('Charset size:', len(charset))
print(''.join(sorted(charset)))

font = fontTools.ttLib.TTFont(fontPath)
subsetter = fontTools.subset.Subsetter()
subsetter.populate(text=''.join(charset))
subsetter.subset(font)
font.save(fontSubsetPath)

print('Source font size:', os.path.getsize(fontPath))
print('Subsetted font size:', os.path.getsize(fontSubsetPath))
print(f'Reduced to {os.path.getsize(fontSubsetPath) / os.path.getsize(fontPath) * 100:.02f}%')


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import urllib2
import urlparse
import json
import hashlib

class Category:
    def __init__(self):
        pass

    def getTree(self):
        url = 'http://dc.3.cn/category/get'
        request = urllib2.Request(url)
        res = urllib2.urlopen(request).read()
        js = json.loads(unicode(res, 'gb18030').encode('utf8'))

        cats = [self._traverse(i, 0) for i in js['data']]
        return cats

    def _traverse(self, c, depth):
        r = {}
        subjs = []
        r['depth'] = depth
        r['fetchable'] = False
        if depth == 0:
            # 第一层分类使用组合名称
            subs = [self._parseCategory(i['n']) for i in c['s']]
            r['name'] = ' / '.join([t['name'] for t in subs])
            r['url'] = ''
            r['zhuti'] = [self._parseZhuti(ts) for ts in c['t']]
            r['brands'] = [self._parseBrand(bs) for bs in c['b']]
            r['cid'] = self._cacuCid(r['name'])

            if 's' in c and 's' in c['s'][0]:
                subjs = c['s'][0]['s']
        else:
            cat = self._parseCategory(c['n']);
            r['name'] = cat['name']
            url = self._parseLink(cat['url'])
            jdcid = self._getCatId(url)
            r['cid'] = self._cacuCid(r['name'] + url)
            #r['cid'] = self._cacuCid(jdcid if jdcid else url)
            r['url'] = url
            if 's' in c:
                subjs = c['s']

        if len(subjs) > 0:
            r["subs"] = [self._traverse(s, depth + 1) for s in subjs]
        elif jdcid:
            r['fetchable'] = True

        return r

    def _cacuCid(self, data):
        md5 = hashlib.md5()
        md5.update(data)
        return md5.hexdigest()

    def _getCatId(self, url):
        qs = urlparse.parse_qs(urlparse.urlsplit(url).query)
        return qs['cat'][0] if 'cat' in qs else None

    def _parseLink(self, l):
        r = ''
        if '.' in l: r = 'http://' + l
        else: r = 'http://list.jd.com/list.html?cat=' + l.replace('-', ',')
        return r

    def _parseZhuti(self, s):
        # 文本样例：jiadian.jd.com/|家电城||0
        sects = s.split("|")
        return {
            "name": sects[1],
            "url": sects[0]
        }

    def _parseBrand(self, s):
        # 文本样例："//haier.jd.com/|海尔|vclist/jfs/t3211/96/3882705424/5364/76e60d4a/57f9fa89N6ddb14fc.jpg|"
        sects = s.split("|")
        return {
            "name": sects[1],
            "url": sects[0],
            "logo": sects[2]
        }

    def _parseCategory(self, s):
        # 文本样例：channel.jd.com/home.html|家居||0
        sects = s.split("|")
        return {
            "name": sects[1],
            "url": sects[0]
        }


import numpy as np
from numpy import pi
from numpy.random import rand
import matplotlib.pyplot as plt

def InitialState(N):
    state = np.random.random((N,N))*2*pi
    return state

def Delta(theta1, theta0):
    best = None
    for i in (-2*pi, 0, 2*pi):
      for j in (-2*pi, 0, 2*pi):
        r = (theta1+i) - (theta0+j)
        if best is None or abs(r) < abs(best):
            best = r
    return best

def Vortex(matrix, i, j):
    windingnumber = 0.
    theta = matrix[(i+1)%N, j]
    for (di, dj) in [(+1,+1),(+0,+1),(-1,+1),(-1,+0),(-1,-1),(+0,-1),(+1,-1),(+1,0)]:
        theta1 = matrix[(i+di)%N, (j+dj)%N]
        windingnumber += Delta(theta1, theta)
        theta = theta1
    return windingnumber

def Metropolis(T, matrix):
    beta = float(1/T)
    for i in range(N):
        for j in range(N):
                a = np.random.randint(0, N)
                b = np.random.randint(0, N)
                spin =  matrix[a, b]
                for m in range(3):
                    InitialEnergy = -(np.cos(spin-matrix[(a+1)%N,b])+np.cos(spin-matrix[a,(b+1)%N])+np.cos(spin-matrix[(a-1)%N,b])+np.cos(spin-matrix[a,(b-1)%N]))
                    dTheta = pi * (np.random.random()*2-1)
                    TempSpin = spin + dTheta
                    FinalEnergy = -(np.cos(TempSpin-matrix[(a+1)%N,b])+np.cos(TempSpin-matrix[a,(b+1)%N])+np.cos(TempSpin-matrix[(a-1)%N,b])+np.cos(TempSpin-matrix[a,(b-1)%N]))
                    deltaE = FinalEnergy - InitialEnergy
                    if deltaE < 0:
                        spin += dTheta
                    elif rand() < np.exp(-beta*deltaE):
                        spin += dTheta
                    matrix[a, b] = spin
    return matrix

def Plot(matrix, T, i = None):

    X, Y = np.meshgrid(np.arange(0, N), np.arange(0, N))
    U = np.cos(matrix)
    V = np.sin(matrix)
    plt.quiver(X, Y, U, V)

    for a in range(N):
          for b in range(N):
            WindingNumber = Vortex(matrix, a, b)
            if abs(WindingNumber) < 0.01:
                continue
            elif WindingNumber > 0:
                circle = plt.Circle((a,b), 0.6, color="red", alpha=0.4)
                ax=plt.gca()
                ax.add_patch(circle)
            elif WindingNumber < 0:
                circle = plt.Circle((a,b), 0.6, color="blue", alpha=0.4)
                ax=plt.gca()
                ax.add_patch(circle)

    plt.xticks([])
    plt.yticks([])
    plt.axis('scaled')
    plt.axis([-1, N, -1, N])
    plt.title('T = %.2f' %T + ', Size = ' + str(N) + r'$\times$' + str(N) + ', Sweeps = %i' %i)
    plt.savefig('Sweep_'+str(i)+'.png', dpi=200)
    plt.show()
    return

N = 20
T = 0.01
Sweeps = 10

lattice = InitialState(N)

for i in range(Sweeps+1):
    Plot(lattice, T, i)
    lattice = Metropolis(T, lattice)

preliminary-code-results/XY Model/XY Model Development/XY_Model_Graph_2.py


"""
Analyze results from the runs that were tuned
using a neural network (Neural Network Calibration.py)

Michael Itzkin, 3/15/2021
"""

from scipy import io
from scipy.interpolate import interp1d

import matplotlib.pyplot as plt
import netCDF4 as nc
import pandas as pd
import numpy as np
import os

# General settings
font = {'fontname': 'Arial', 'fontweight': 'normal', 'fontsize': 14}
dpi = 300
inches = 3.8
TEXT_DIR = os.path.join('..', 'Text Files')
FIELD_DIR = os.path.join('..', '..', 'Bogue Banks Field Data')
FIGURE_DIR = os.path.join('..', 'Figures', 'Genetic Algorithm Figures')

"""
Create a Windsurf result class
"""

def adjusts(p, tf=2016):
    """
    The cross-shore positions of profiles needs to
    be adjusted. This function just returns the right
    values to adjust by
    """

    if p == 3 and (tf == 2017):
        field_tf_adjust = -2
        tail = 0
        fence = 1
        right = 60
        top = 5
    elif p == 3 and (tf == 2019):
        field_tf_adjust = -11
        tail = 0
        fence = 1
        right = 60
        top = 5
    elif p == 15 and (tf == 2017):
        field_tf_adjust = 6
        tail = 0
        fence = 25
        right = 60
        top = 6
    elif p == 15 and (tf == 2019):
        field_tf_adjust = -3
        tail = 10
        fence = 25
        right = 40
        top = 6
    elif p == 19 and (tf == 2017):
        field_tf_adjust = 0
        tail = 0
        fence = 20
        right = 120
        top = 7
    elif p == 19 and (tf == 2017):
        field_tf_adjust = 0
        tail = 0
        fence = 20
        right = 120
        top = 7
    elif (p == 22) and (tf == 2017):
        field_tf_adjust = 2
        tail = 0
        fence = None
        right = 140
        top = 7
    elif (p == 22) and (tf == 2019):
        field_tf_adjust = -18
        tail = 0
        fence = None
        right = 140
        top = 6

    return {
        'dField': field_tf_adjust,
        'Tail': tail,
        'Fence': fence,
        'Right': right,
        'Top': top,
    }

def find_toe(df):
    """
    Find the toe position on the starting profile
    and adjust the profile mask so that the tail is
    all zeroes, the dune is all 1s and the beach is 2s

    df: DataFrame with the gridded profiles
    """

    # Find the dune crest index
    dhigh_ix = np.argwhere(df['Model Init'] == np.nanmax(df['Model Init']))[0][0]

    # Make a copy of the profile and make a straight line between
    # the dune crest and MHW
    p_copy = df['Model Init'].copy(deep=True)
    p_copy[:dhigh_ix] = np.linspace(start=df['Model Init'].iloc[0],
                                    stop=df['Model Init'].iloc[dhigh_ix],
                                    num=len(p_copy[:dhigh_ix]))

    # Subtract the original from the copy
    diff = p_copy - df['Model Init'].values

    # Take the index of the maximum value of "diff"
    # to set as the dune toe location
    toe_ix = np.argwhere(diff == np.nanmax(diff))[0][0]

    # Set the mask to be 2s up to the toe index
    df['Mask'].iloc[:toe_ix] = 2

    return df

def load_and_grid_profiles(DIR, p, t0, tf):
    """
    Load the field profile and model profiles and interpolate
    all onto the Windsurf X-Grid in order to analyze error metrics.

    Returns a DataFrame with the X-Grid, final and initial gridded
    profiles, and an array to mask out the "smooth" part of the profile

    DIR: Path to the windsurf.nc file
    p: Int with the profile number
    t0: Int with the year the hindcast starts at
    tf: Int with the year the hindcast ends at
    """

    # Adjust the profile to align them properly
    adjust_df = adjusts(p, tf=tf)

    # Load the model and pull out the X-Grid and
    # initial profile. This is the same for every
    # time simulation so just pull from the first
    # for simplicity
    filename = os.path.join(DIR, 'windsurf.nc')
    data = nc.Dataset(filename)
    x_grid = -data['x'][:]
    initial_profile = data['zb'][:, 0]
    final_profile = data['zb'][:, -1]
    data.close()

    # Load the first and last field profile
    columns = ['X', 'Y', 'Z', 'Zsmooth', 'D', 'Lat', 'Lon']
    init_field_fname = os.path.join(FIELD_DIR, f'{t0}', f'BGB{p}_cross.csv')
    final_field_fname = os.path.join(FIELD_DIR, f'{tf}', f'BGB{p}_cross.csv')
    init_field = pd.read_csv(init_field_fname, header=None, delimiter=',', names=columns).dropna()
    final_field = pd.read_csv(final_field_fname, header=None, delimiter=',', names=columns).dropna()

    # Push the field profiles back to 0
    init_field['D'] -= init_field['D'].iloc[0] + adjust_df['Tail']
    final_field['D'] -= final_field['D'].iloc[0] - adjust_df['dField'] + adjust_df['Tail']

    # Cut the windsurf profile off at the seaward-most point
    # of the final field profile
    field_max = np.nanmax(final_field['D'])
    final_profile = final_profile[(x_grid <= field_max) & (x_grid >= 0)]
    initial_profile = initial_profile[(x_grid <= field_max) & (x_grid >= 0)]
    x_grid = x_grid[(x_grid <= field_max) & (x_grid >= 0)]

    # Interpolate the field profiles onto the Windsurf grid
    init_interp = interp1d(init_field['D'], init_field['Zsmooth'], fill_value='extrapolate')
    init_field_grid = init_interp(np.array(x_grid))
    final_interp = interp1d(final_field['D'], final_field['Zsmooth'], fill_value='extrapolate')
    final_field_grid = final_interp(np.array(x_grid))

    # The landward boundary on the Windsurf profile is smoothed
    # out which is not a real feature. This will mess up the error
    # scores if it is included, so ID where it starts so it can be
    # easily ignored when analyzing the run
    init_model_z = initial_profile[-1]
    eps = 0.05
    for ii in range(len(x_grid) - 1, 0, -1):
        if initial_profile[ii] > init_model_z + eps:
            break
        else:
            start_ix = ii

    # Make an array of 0s and 1s with 0s being all points
    # landward of start_ix and 1s being all points
    # seaward of start_ix
    smooth_mask = np.zeros(shape=(len(x_grid), 1))[:]
    smooth_mask[:start_ix] = 1

    # Place the gridded profiles into a DataFrame and
    # add a mask for the beach
    df = pd.DataFrame.from_dict({
        'X': x_grid,
        'Model Init': initial_profile,
        'Model Final': final_profile,
        'Field Init': init_field_grid,
        'Field Final': final_field_grid,
        'Mask': smooth_mask[:, 0],
    })
    find_toe(df)

    return df

def make_profiles_df(summ_df, p, t0, tf, special=None):
    """
    Make a DataFrame with the gridded and masked model
    and field profile

    summ_df: DataFrame with summaries of the runs
    p: Int with the profile number
    t0: Int with the year the hindcast starts at
    tf: Int with the year the hindcast ends at
    special: String with the folder
    """

    # Set an empty DataFrame to loop into
    profiles_df = pd.DataFrame()

    for run in summ_df['Simulation']:
        if special is not None:
            USE_DIR = os.path.join('..', f'{special}')
        elif tf == 2017:
            USE_DIR = os.path.join('..', f'BGB{p} NN')
        pp = load_and_grid_profiles(DIR=os.path.join(USE_DIR, f'run{run}'), p=p, t0=t0, tf=tf)

        # The final model result is the only thing that differs between
        # simulations so eveything else can be pulled just from gen 1 run 1
        if run == 1:
            profiles_df['X'] = pp['X']
            profiles_df['Field Init'] = pp['Field Init']
            profiles_df['Field Final'] = pp['Field Final']
            profiles_df['Model Init'] = pp['Model Init']
            profiles_df['Mask'] = pp['Mask']
        profiles_df[f'Run {run}'] = pp['Model Final']

    return profiles_df

def make_summary_df(p, t0, tf, runs=24, dec=4, special=None):
    """
    Compile results from the GA simulations and store
    them in a DataFrame for analysis

    p: Int with the profile to analyze
    t0: Int with the initial year of analysis
    tf: Int with the final year of analysis
    runs: Int with the number of runs (Default = 24)
    dec: Int with the number of decimals to round to (Default = 4)
    """

    # Set empty lists to loop into. These will be used to
    # construct the DataFrame at the end
    nrun, m, cb, facas_lo, facas_hi, facsk_lo, facsk_hi, lsgrad, bedfric, wetslp, beta,\
    veg_density, rmses, drmses, brmses =\
        [], [], [], [], [], [], [], [], [], [], [], [], [], [], []

    # Set the path to the folder to work in
    if special is not None:
        USE_DIR = os.path.join('..', f'{special}')
    elif tf == 2017:
        USE_DIR = os.path.join('..', f'BGB{p} NN')

    # Loop through the folders
    for run in range(1, runs + 1):

        # Set a path to the run folder
        if special is None:
            filename = os.path.join(USE_DIR, f'run{run}', 'windsurf_setup.mat')
        else:
            filename = os.path.join(USE_DIR, f'BGB{p}' f'run{run}', 'windsurf_setup.mat')

        # Load the parameters sued
        file = io.loadmat(filename, squeeze_me=True, struct_as_record=False)
        m.append(np.around(file['veg'].CDM.m, decimals=dec))
        beta.append(np.around(file['veg'].CDM.beta, decimals=dec))
        cb.append(np.around(file['sed'].Aeolis.Cb, decimals=dec))
        facas_lo.append(np.around(file['sed'].XB.facAsLo, decimals=dec))
        facas_hi.append(np.around(file['sed'].XB.facAsHi, decimals=dec))
        facsk_lo.append(np.around(file['sed'].XB.facSkLo, decimals=dec))
        facsk_hi.append(np.around(file['sed'].XB.facSkHi, decimals=dec))
        lsgrad.append(np.around(file['sed'].XB.lsgrad, decimals=dec))
        bedfric.append(np.around(file['flow'].XB.bedfriccoef, decimals=dec))
        wetslp.append(np.around(file['sed'].XB.wetslope, decimals=dec))
        veg_density.append(np.around(file['veg'].CDM.startingDensity, decimals=dec))

        # Calculate the RMSE value
        if special is None:
            temp_dir = os.path.join(USE_DIR, f'run{run}')
        else:
            temp_dir = os.path.join(USE_DIR, f'BGB{p}', f'run{run}')

        df = load_and_grid_profiles(temp_dir, p, t0, tf)
        rmses.append(rmse(df.loc[df['Mask'] >= 1], dec=dec))
        drmses.append(rmse(df.loc[df['Mask'] == 1], dec=dec))
        brmses.append(rmse(df.loc[df['Mask'] == 2], dec=dec))

        # Update the lists
        nrun.append(run)

    return pd.DataFrame.from_dict({
        'Simulation': nrun,
        'm': m,
        'Cb': cb,
        'facAs': facas_lo,
        'facSk': facsk_lo,
        'lsgrad': lsgrad,
        'bedfric': bedfric,
        'wetslp': wetslp,
        'vegDensity': veg_density,
        'RMSE': rmses,
        'Dune RMSE': drmses,
        'Beach RMSE': brmses,
    })

def rmse(df, dec=3, mask=None, column='Model Final'):
    """
    Calculate the RMSE for the model run

    df: DataFrame with the gridded profiles in it
    dec: Number of decimals to round to (Default = 3)
    mask: Int with the values to use to mask out the profile
    """

    # Mask out values
    if mask is None:
        pass
    else:
        df = df.loc[df['Mask'] == mask]

    # Calculate the RMSE
    y = df[column]
    yhat = df['Field Final']
    err = y - yhat
    square_error = err**2
    mse = np.nanmean(square_error)
    root_mse = np.sqrt(mse)
    return np.around(root_mse, decimals=dec)

class WindsurfResult():

    def __init__(self, p, t0=2016, tf=2017, runs=24, dec=4, special=None):
        """
        Initialize the results object

        p: Int with the profile number being worked on
        t0: Int with the start year of the simulation
        tf: Int with the final year of the simulation (Default = 2016)
        runs: Number of runs performed (Default = 24_
        dec: Int with the number of decimal places to round to (Default = 4)
        special: String with the folder name of a combo calibration run (Default = None)
        """

        # Initialize input arguments
        self.p = p
        self.t0 = t0
        self.tf = tf
        self.dec = dec
        self.runs = runs
        self.adjusts = adjusts(self.p, self.tf)
        if special is None:
            self.special = None
        else:
            self.special = f'BGB15 and BGB22 NN'

        # Make a summary DataFrame with run parameters and error metrics
        self.summary = make_summary_df(self.p, self.t0, self.tf, self.runs, self.dec, self.special)

        # Make a profiles Dataframe with the gridded model and field profiles
        self.profiles = make_profiles_df(self.summary, self.p, self.t0, self.tf, self.special)

"""
Functions to make figures
"""

def nn_overlays(ws, save=False):
    """
    Plot all the neural network final profiles over the initial
    and final field profiles. Highlight the "best" neural network
    result

    ws: WindsurfResult object to work with
    save: Display the figure (False) or save it (True)
    """

    # Setup the figure
    fig, ax = plt.subplots(dpi=dpi, figsize=(inches, inches))
    best = ws.summary['Simulation'].loc[ws.summary['RMSE'] == np.nanmin(ws.summary['RMSE'])].values[0]

    # Add a grid and a line for MHW
    ax.grid(color='lightgrey', linewidth=0.5, zorder=0)
    ax.axhline(y=0.34, color='darkblue', linewidth=2, linestyle='--', zorder=2, label='MHW')

    # Add a line for the fence if necessary
    if ws.adjusts['Fence'] is None:
        pass
    else:
        ax.axvline(x=ws.adjusts['Fence'], color='sienna', linewidth=1, zorder=4, label='Fence')

    # Plot the initial and final field profiles
    ax.plot(ws.profiles['X'], ws.profiles['Field Init'], color='black', linewidth=2, linestyle='--', zorder=4, label='Field$_{0}$')
    ax.plot(ws.profiles['X'], ws.profiles['Field Final'], color='black', linewidth=2, linestyle='-', zorder=30, label='Field$_{f}$')

    # Plot the initial model profile and best model profile
    ax.plot(ws.profiles['X'], ws.profiles['Model Init'], color='red', linewidth=2, linestyle='--', zorder=4, label='Model$_{0}$')
    ax.plot(ws.profiles['X'], ws.profiles[f'Run {best}'], color='red', linewidth=2, linestyle='-', zorder=40, label='Model$_{f}$')

    # Plot all the other model results
    for ii in range(1, ws.runs+1):
        ax.plot(ws.profiles['X'], ws.profiles[f'Run {ii}'],
                color='darkgray', linewidth=1, linestyle='-', zorder=6 + ii)

    # Add a legend
    ax.legend(loc='upper right', fancybox=False, edgecolor='black')

    # Set the X-Axis
    ax.set_xlim(left=0, right=ws.adjusts['Right'])
    ax.set_xlabel('Cross-Shore Distance (m)', **font)

    # Set the Y-Axis
    ax.set_ylim(bottom=-1, top=ws.adjusts['Top'])
    ax.set_ylabel('Elevation (m NAVD88)', **font)

    # Save and close the figure
    title = f'BGB{ws.p} Neural Network Final Profiles'
    save_and_close(fig, title, save)

def save_and_close(fig, title, save):
    """
    Give the figure a tight and transparent background
    then close it

    fig: Figure object
    title: String with the title of the figure
    save: Save the figure (True) of display it (False)
    """

    # Set a tight layout and a transparent background
    plt.tight_layout()
    fig.patch.set_color('w')
    fig.patch.set_alpha(0.0)

    # Save and close the figure
    if save:
        title_w_extension = os.path.join(FIGURE_DIR, f'{title}.png')
        plt.savefig(title_w_extension, bbox_inches='tight', facecolor=fig.get_facecolor(), dpi='figure')
        print('Figure saved: %s' % title_w_extension)
        plt.close()
    else:
        plt.show()

"""
Run the analysis
"""

def main():
    """
    Run the analysis
    """

    # Load the results
    bgb22 = WindsurfResult(p=22)
    bgb15 = WindsurfResult(p=15)

    # Plot overlays of the results
    [nn_overlays(df, save=True) for df in [bgb15, bgb22]]

if __name__ == '__main__':
    main()