├── .gitignore ├── LICENSE ├── README.md ├── SleepLogger.py ├── Stimulus.py ├── accel.py ├── config.py ├── data └── processed │ └── README.md ├── data_parser.ipynb ├── drivers ├── MMA.py └── OLED.py ├── interface └── app │ ├── __init__.py │ ├── dataplotter.py │ ├── get_data.py │ ├── routes.py │ ├── static │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── custom.css │ │ └── dark-mode.css │ └── js │ │ ├── Chart.min.js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ ├── bootstrap.min.js.map │ │ ├── chartjs-plugin-annotation.min.js │ │ ├── dark-mode-switch.min.js │ │ └── jquery-3.5.1.min.js │ └── templates │ ├── activity_chart.html │ ├── base.html │ ├── doughnut_chart.html │ ├── index.html │ ├── sleepstage_chart.html │ └── tracker.html ├── requirements.txt ├── resources ├── 2019-12-05 13.42.17.jpg ├── Onton2016.jpg ├── audio_input.png ├── i2cdetect.png ├── img00.jpeg ├── img01.jpeg ├── img02.jpeg ├── img03.jpeg ├── img04.jpeg ├── img05.jpeg ├── img06.jpeg ├── img07.jpeg ├── img08.jpeg ├── img09.jpeg ├── img10.jpeg ├── img11.jpeg ├── img12.jpg ├── img13.jpeg ├── partlist.jpg ├── signal_pipeline.png ├── sleep.jpg └── sleep.png └── sound_generator.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *accel*: A python-based sleep tracker and acoustic brain stimulator built on a Raspberry Pi 💤🔊🧠〰️ 2 | 3 | *accel* is an unfinished project of building a low-cost sleep tracker and learn a lot on the way. It can track your 💤 sleep during night through an accelerometer and detect how much you are moving. Using this information it then determines whether the user is in the deep sleep (NREM stage II-III) and, if so, trigger the second stage: the acoustic brain stimulation protocol 🔊. This is a low-frequency audible stimulus 〰️ that is then delivered to the 🧠 brain using 🎧 headphones. 4 | 5 | Why trigger the stimulus in deep sleep? It is known that slow wave activity (~1Hz) in the sleeping human cortex is critical for memory consolidation, the process that writes memory from short-term memory to long-term memory [todo:cite]. It has been repeatedly shown [todo:cite] that electrical stimulation of the brain in this sleep stage can enhance memory consolidation in humans. There is also evidence that acoustic stimulation through the auditory cortex can improve memory consolidation as well. 6 | 7 | This project is an experiment to see how far I can approximate the vigorous methods of clinical research with a cheap custom setup that anyone could reproduce. However, it is hard to prove an effect with a subject count of N = 1 at this point. Whether the end goal of enhancing memory consolidation during sleep using an acoustic stimulus can be reached or not, it sure is a fun endeavour! 8 | 9 | ## Current build 10 | 11 | The hardware and the software is still in very early development. At this stage, the device consists of these parts: a Raspberry Pi Zero W (any other Pi should work), a [MMA8452Q accelerometer chip](https://www.aliexpress.com/wholesale?SearchText=MMA8452Q) (~0.8€) with an I2C interface and a [tiny SSD1306 OLED screen](https://www.aliexpress.com/wholesale?SearchText=ssd1306) (~1.2€), also with an I2C interface. 12 | 13 | The acoustic stimulation is not yet implemented. More literature research is necessary 14 | 15 | ![Parts: Raspberry Pi Zero W, MMA8452Q (Accelerometer), SSD1306 (OLED)](resources/partlist.jpg) 16 | 17 | You can find all of these parts fairly cheap on the internet. After soldering all the parts together, the "assembled" device is pretty compact and also a bit *shaggy*: 18 | 19 | ![Assembled device](resources/img12.jpg) 20 | 21 | ## Data processing 22 | 23 | ### Sampling data 24 | 25 | The data processing is done in multiple steps. The three-dimensional position data x,y,z is sampled from the accelerometer and is used to calculate the time derivative of the position we call dx/dt, giving us a velocity of the sensor. The data is sampled with an adaptive sampling rate: Whenever the velocity crosses a predefined threshold (activity is detected) the sampling rate doubles and we can record the movement with a high precision. The threshold was obtained by visually assessing the noise baseline and setting a threshold slightly above that. 26 | 27 | ### Simulating the activity model 28 | 29 | Whenever the measured movement crosses the threshold, a "spike" of activity is generated. This spike is fed into a very slow model that integrates those spikes in time (it "collects" them) which all ad up to a quantity called "activity". At the same time, the "activity" value always tries to decay back to zero, however slowly, within minutes to tens of minutes. Whenever the activity reaches a pre-defined value close to zero, the sleep stage is classified as "deep sleep". 30 | 31 | In deep sleep, the body's movement is reduced to a minimum, so we are trying to decide at every time step if the user hasn't been active for a long-enough time. This slow dynamical model lets us detect slow patterns in the movement activity over a long time scale and helps us decide whether the user hasn't moved for a long time and thus, could be in deep sleep. 32 | 33 | The parameters for this model are chosen manually and haven't been fine-tuned yet. I tried to chose values that consistently have less than 50% of deep sleep and result roughly in two to three "main" deep sleep phases, often occurring in the beginning of the night and at the very end. 34 | 35 | ![](resources/signal_pipeline.png) 36 | 37 | Whenever a very low pre-defined threshold is reached, the user is assumed to be in deep sleep. In this stage, the audio stimulus is triggered. 38 | 39 | ### Generating the audio stimulus 40 | 41 | The slow oscillations in slow-wave sleep or deep sleep are typically around a frequency of 0.75Hz (this is a very broad generalisation. There is inter- and intra-subject variability of the oscillation frequency). As a crude approximation, we will use this frequency for audio input to the user. The human ear cannot perceive much below 20Hz and most headphones stop to work around that frequency for that reason. What we can do, however, is to mix two audible frequencies (base frequency) of, say, 40Hz and 40.75Hz. The small difference between the signals will cause a slow beating sound at the frequency of 0.75Hz. Assuming that neuronal activity in the auditory cortex is resonant to these frequencies, we hope that oscillatory energy input to the brain can entrain or amplify ongoing slow-wave activity. 42 | 43 | In the plot below, a impractically low base frequency of 8Hz and a difference of 0.75 is chosen here for illustration purpose. You see a stimulus that lasts for around 7 seconds. 44 | 45 | ![](resources/audio_input.png) 46 | 47 | ## Web interface 48 | 49 | The current web interface is built on Flask and Bootstrap and plotting is handled via chart.js. The sleep stage detection relies on some hard-coded thresholds still and hasn't been validated well with what other sleep trackers output. There is certainly a lot of room for improvement still. 50 | 51 | Here is what a typical night looks like with the current integrator. The red spikes represent movements, the blue shaded area is the integrated level of activity. Periods when the activity drops close to zero are classified as periods of "deep sleep". 52 | 53 | ![](resources/sleep.jpg) 54 | 55 | ## Getting started 56 | 57 | First, I want to thank all the people who made all the modules and libraries that I could use to make this project possible. It is amazing what kind of amazing possibilities can lie just one pip install away. The drivers for the accelerometer and the OLED screen are snippets I found online and haven't yet documented where they are from (oops). Thanks also to their authors! 58 | 59 | This project is based on some debian and a python packages. Please make sure that you have installed them. The following commands should work for a fresh install of Raspbian Buster Lite. Make sure you set up ssh correctly and connect to a network ([Google search](https://www.google.com/search?q=raspberry+pi+zero+w+headless+setup)). 60 | 61 | ### Enable I2C interface 62 | This is the hardware interface that lets you communicate to connected chips. Enter 63 | 64 | ``` 65 | sudo raspi-config 66 | ``` 67 | 68 | and go to --> `Interfacing` --> `Enable I2C` 69 | ### Install python3 and other binaries 70 | ``` 71 | sudo apt update -y 72 | sudo apt install build-essential python3-dev python3-pip libatlas-base-dev libhdf5-dev i2c-tools git -y 73 | ``` 74 | Set python3 as your default python 75 | 76 | ``` 77 | sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 78 | sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 2 79 | sudo ln -s /usr/bin/pip3 /usr/bin/pip 80 | 81 | ``` 82 | ### Install python packages 83 | 84 | ``` 85 | pip install -r requirements.txt 86 | ``` 87 | This also installs the `h5py` package for logging data to the disk, `redis` for streaming the data to a redis server and `flask` for the web interface. These functions are optional but make sure to edit the `config.py` file accordingly. 88 | 89 | For the OLED display to work you need to install the Pillow library that is used to paint the image. (Note that compiling Pillow on the RPi zero is painfully slow...) 90 | 91 | ``` 92 | sudo apt install libjpeg-dev -y 93 | pip install Pillow 94 | ``` 95 | 96 | You should be all set up at this point 👍. 97 | 98 | ### Determine I2C addresses of connected devices 99 | 100 | In order to talk to the accelerometer and the OLED display, you need to find out what their addresses are. You can then put these into the `config.py` file in the root directory of this repository. The default values in there might work as well! 101 | 102 | ``` 103 | sudo i2cdetect -y 1 104 | ``` 105 | The output should look something like 106 | 107 | ![](resources/i2cdetect.png) 108 | 109 | Here, my accelerometer has the address `0x1d` and the OLED `0x3c`. Good to know! Put these values into `config.py`. (Note: I don't know how you can know which is which at this stage). 110 | 111 | ### Run the tracker 112 | 113 | To execute the script, run 114 | `python accel.py`. 115 | 116 | If you did everything I did, you should be able enable autostart of this script using the command below. 117 | 118 | ``` 119 | echo "sudo -u pi /usr/bin/python /home/pi/accel/accel.py &" | sudo tee -a /etc/rc.local 120 | ``` 121 | ## Project roadmap 122 | * [✓] Receive raw movement data from accelerometer 123 | * [✓] Build dynamical model for activity level 124 | * [✓] Save data to local hdf file 125 | * [✓] Save data to remote redis server 126 | * [✓] Plot live movement data to OLED display 127 | * [✗] Design acoustic stimulus 128 | * [✗] Build a hard case for the tracker 129 | * [✗] Assess deep-sleep detection accuracy using simultaneous sleep EEG 130 | * [✗] Trigger acoustic stimulus in deep sleep 131 | * [?] Use wireless EEG for sleep stage detection *somewhen in the far far future* 132 | 133 | ## Neuroscience background (wip) 134 | 135 | Integrated circuits with *I2C interfaces*, cheap linux computers like the *Raspberry Pi* and modular programming languages with a strong open source community like *python* enable everyone to build their own mobile sensory devices like never before. This is an educational project that helped me learn all sorts of things, based on a personal scientific research project. In this repository, I will describe the ideas and process behind building a sleep tracker that will trigger a stimulus in deep sleep. 136 | 137 | ### What is deep sleep? 138 | Human sleep is divided into sleep cycles, usually 3-4 cycles in a full night of sleep. Each cycles has a number of sleep stages, each with their own distinct brain activity signature as measured in electroencephalography (EEG). Below is a plot from [Onton et al, Front. Hum. Neurosci. (2016)](https://www.frontiersin.org/articles/10.3389/fnhum.2016.00605/full) showing EEG power spectra and a *Hypnogram* that segments sleep into different sleep stages. 139 | 140 | ![Onton et al, Front. Hum. Neurosci. (2016)](resources/Onton2016.jpg) 141 | 142 | Here, it is evident that the dominant frequency (most prominent frequency in the EEG power spectrum) in deep sleep is around and below 1 Hz. This stage is also referred to as **slow-wave sleep (SWS)** and the low-frequency activity is often called slow-wave activity (SWA) or slow-wave oscillations (SWO). Is is known that these slow oscillations that are most likely generated in the neocortex are key for successful memory consolidation during sleep. Simply said, memory consolidation is the process with which the brain transfers memories (encoded as *engrams*) from its short-term memory storage (believed to be in Hippocampus) to the long-term memory storage (in neocortex). 143 | 144 | We want to detect the deep sleep stage during night (referred to as HI DEEP and LO DEEP in the plot above) and trigger an action whenever it is detected. 145 | 146 | ## License 147 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /SleepLogger.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import threading 4 | import h5py 5 | import datetime 6 | import logging 7 | import config 8 | 9 | import drivers.MMA as MMA 10 | from drivers.OLED import OLED 11 | 12 | from Stimulus import AudioStimulus 13 | 14 | class SleepLogger: 15 | def __init__(self): 16 | """ 17 | Sleep logger with adaptive sampling and various logging methods. 18 | Provides current sleep state data to other objects. 19 | """ 20 | # Initiate sleep state variables 21 | self.diff = None # current movement magnitude 22 | self.diffs = None # past movement magnitudes 23 | self.activity = None # current activity level 24 | self.state = None # current sleep state 25 | 26 | # Initiate control variables 27 | self._run = False 28 | 29 | # Initiate accelerometer 30 | self.mma8452q = MMA.MMA8452Q() 31 | self.activity_threshold = config.ACCELEROMETER_ACTIVITY_THRESHOLD 32 | 33 | # Initiate logging 34 | self.dataset_name = datetime.datetime.now().strftime("%Y-%m-%d-%HH-%MM-%SS") 35 | logging.info(f"Recording name: {self.dataset_name}") 36 | 37 | config.LOG_TO_REDIS = config.LOG_TO_REDIS 38 | if config.LOG_TO_REDIS: 39 | import redis 40 | self.r = redis.Redis(host=config.REDIS_HOST, port=config.REDIS_PORT, db=0) 41 | logging.info(f"Logging to Redis server {config.REDIS_HOST}:{config.REDIS_PORT}") 42 | 43 | config.LOG_TO_HDF = config.LOG_TO_HDF 44 | if config.LOG_TO_HDF : 45 | self.H5_FILENAME = f"/home/pi/accel/{config.HDF_FILE}" 46 | self.H5_INIT = False 47 | logging.info("Logging to HDF file: {}.".format(self.H5_FILENAME)) 48 | 49 | # Initiate Display 50 | if config.OLED_DISPLAY: 51 | self.oled = OLED() 52 | logging.info(f"OLED initialized.") 53 | 54 | # Initiate Stimulus module 55 | if config.STIMULUS_ACTIVE: 56 | self.audiostim = AudioStimulus() 57 | logging.info("Stimulus module loaded") 58 | 59 | logging.info("Sleep tracker initialized.") 60 | 61 | def get_accel_data(self, mma8452q): 62 | acc = mma8452q.read_accl() 63 | accl = [acc['x'], acc['y'], acc['z']] 64 | millis = int(round(time.time() * 1000)) 65 | t = millis 66 | return t, accl 67 | 68 | def integrate_activity(self, activity, diff, dt, now, last_spike): 69 | """ 70 | Non-linear synaptic integrator. Sums up activity spikes over time. 71 | If no spike was detected for a time period larger than `decay_delay`. 72 | then the activity it will exponentially decay with time constant `decay` 73 | """ 74 | thresh = self.activity_threshold 75 | decay = config.ACTIVITY_DECAY_CONSTANT 76 | spike_strength = config.ACTIVITY_SPIKE_STRENGTH 77 | decay_delay = config.ACTIVITY_DECAY_DELAY 78 | 79 | # if spike is larger than noise threshold 80 | if diff > thresh: 81 | activity += (1.0 - activity) * spike_strength 82 | # note: the lie above should actually be multiplied with dt 83 | # in order to make the spike strength independent of dt but 84 | # since we have to mutiply with dt later for euler integration, 85 | # the division is skipped here to save time 86 | last_spike = now # set the time of this spike 87 | 88 | if now - last_spike > decay_delay and activity > config.ACTIVITY_LOWER_BOUND: 89 | # only when the last spike was longer ago than decay_delay 90 | # exponentially decay 91 | activity += - activity / decay * dt 92 | 93 | # limit activity, can undershoot because of large integration timesteps dt 94 | if activity < config.ACTIVITY_LOWER_BOUND: 95 | activity = 0.0 96 | 97 | # these are our state variables 98 | return activity, last_spike 99 | 100 | def detect_state(self, activity): 101 | # if activity crosses a threshold, classify as "deep sleep" => state = 1 102 | if activity < config.ACTIVITY_THRESHOLD_DEEP_SLEEP: 103 | return config.SLEEP_STATE_DEEP 104 | elif activity < config.ACTIVITY_THRESHOLD_WAKE: 105 | return config.SLEEP_STATE_LIGHT 106 | else: 107 | return config.SLEEP_STATE_WAKE 108 | 109 | def update_state_variables(self, diff=None, diffs=None, activity=None, state=None): 110 | if diff is not None: 111 | self.diff = diff # current movement magnitude 112 | if diffs is not None: 113 | self.diffs = difs # past movement magnitudes 114 | if activity is not None: 115 | self.activity = activity # current activity level 116 | if state is not None: 117 | self.state = state # current sleep state 118 | 119 | def trigger_stimulus(self): 120 | """Triggers the audio stimulus if in deep sleep and turns it off else. 121 | """ 122 | if self.state == config.SLEEP_STATE_DEEP: 123 | if not self.audiostim.isActive: 124 | print("STARTING STIMULUS") 125 | self.audiostim.start_stimulus() 126 | 127 | else: 128 | if self.audiostim.isActive: 129 | print("STOPPING STIMULUS") 130 | self.audiostim.stop_stimulus() 131 | 132 | 133 | def adaptive_logger(self, sample_size = 128, \ 134 | init_delay = 2, init_activity = 0.0, init_acc = [0.0, 0.0, 0.0], 135 | last_spike = -1e10, \ 136 | return_delay = False): 137 | # activity variable integrated in time 138 | activity = init_activity 139 | #last_spike = -1e10 140 | 141 | # activity detection threshold of diff value 142 | activity_threshold = self.activity_threshold 143 | 144 | # sampling speed 145 | current_delay = init_delay 146 | min_delay = config.LOGGER_MIN_DELAY 147 | max_delay = config.LOGGER_MAX_DELAY #maximum delay ms 148 | 149 | # prepare arrays for storing results 150 | raw_data = np.zeros((sample_size, 3)) # raw xyz data 151 | ts_data = np.zeros((raw_data.shape[0])) 152 | ts_realtime_data = np.zeros((raw_data.shape[0])) 153 | delays = np.zeros((raw_data.shape[0])) 154 | acts = np.zeros((raw_data.shape[0])) # activity data 155 | diffs = np.zeros((raw_data.shape[0])) 156 | states = np.zeros((raw_data.shape[0])) 157 | 158 | # start of integration 159 | start_milli = int(round(time.time() * 1000)) 160 | 161 | # get one sample 162 | last_t, acc = self.get_accel_data(self.mma8452q) 163 | last_acc = acc 164 | last_draw = last_t 165 | 166 | for i in range(sample_size): 167 | t, acc = self.get_accel_data(self.mma8452q) 168 | 169 | # calcuate diff value 170 | # fast versin of np.abs(np.mean(acc-last_acc, axis =1)) or so, much faster without numpy 171 | diff = abs(((acc[0] - last_acc[0]) + (acc[1] - last_acc[1]) + (acc[2] - last_acc[2])) / 3) 172 | 173 | # dt for this sample 174 | dt = t - last_t 175 | 176 | # one integratin step of the activity 177 | activity, last_spike = self.integrate_activity(activity, diff, dt, t, last_spike) 178 | 179 | # dynamic integration step size depending on activity level: 180 | # if there is a lot going on, sampling rate will go up 181 | # if there is no activity, samnpling rate will drop 182 | # increase sampling rate 183 | if diff > activity_threshold: 184 | current_delay /= config.DELAY_DIVIDE_BY 185 | if current_delay < min_delay: 186 | current_delay = min_delay 187 | else: # or decrease it 188 | current_delay *= config.DELAY_MULTIPLY_WITH 189 | if current_delay > max_delay: 190 | current_delay = max_delay 191 | 192 | # detect sleep state from activity level 193 | state = self.detect_state(activity) 194 | 195 | self.update_state_variables(diff=diff, activity=activity, state=state) 196 | 197 | if config.STIMULUS_ACTIVE: 198 | self.trigger_stimulus() 199 | 200 | if config.VERBOSE_OUTPUT: 201 | print("\rDelay: {}, activity: {:.2}, diff: {:.4}, state: {} "\ 202 | .format(int(current_delay), activity, diff, state), end='\r') 203 | 204 | # store data in time chunks 205 | raw_data[i, 0] = acc[0] 206 | raw_data[i, 1] = acc[1] 207 | raw_data[i, 2] = acc[2] 208 | ts_data[i] = t - start_milli 209 | ts_realtime_data[i] = t 210 | delays[i] = current_delay 211 | diffs[i] = diff 212 | states[i] = state 213 | acts[i] = activity 214 | 215 | last_acc = acc 216 | last_t = t 217 | 218 | # finally sleep according to current sampling rate 219 | time.sleep(current_delay / 1000.0) # seconds 220 | 221 | return ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states, last_spike 222 | 223 | def log_data(self, ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states): 224 | """ 225 | Data logger. Logs data into a database or on hdf5 file storage. 226 | """ 227 | if config.LOG_TO_REDIS: 228 | threading.Thread(target=self.log_to_redis, \ 229 | args=(self.r, ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states)).start() 230 | 231 | if config.LOG_TO_HDF: 232 | threading.Thread(target=self.log_to_hdf, \ 233 | args=(ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states)).start() 234 | 235 | def log_to_redis(self, r, ts, ts_realtime, raw_data, acts, diffs, delays, states): 236 | for i, t in enumerate(ts): 237 | # prepare storage format 238 | diff = "{0:.2f}".format(diffs[i]) 239 | activity = "{0:.4f}".format(acts[i]) 240 | delay = "{0:.2f}".format(delays[i]) 241 | state = int(states[i]) 242 | t = int(t) 243 | t_realtime = int(ts_realtime[i]) 244 | 245 | res = r.xadd('accel', {"t" : t , "t_realtime" : t_realtime, "x" : raw_data[i, 0], "y" : \ 246 | raw_data[i, 1], "z" : raw_data[i, 2], \ 247 | 'activity' : activity, 'diff' : diff, 'delay' : delay, 'state' : state}) 248 | return res 249 | 250 | def log_to_hdf(self, ts, ts_realtime, raw_data, acts, diffs, delays, states, verbose=False): 251 | variables = (ts, ts_realtime, raw_data, acts, diffs, delays, states) 252 | var_strings = ["ts", "ts_realtime", "raw_data", "acts", "diffs", "delays", "states"] 253 | with h5py.File(self.H5_FILENAME, 'a') as h5f: 254 | if self.H5_INIT == False: 255 | self.dataset_name = datetime.datetime.now().strftime("%Y-%m-%d-%HH-%MM-%SS") 256 | if config.VERBOSE_OUTPUT: 257 | logging.info("{}/{}: INIT".format(self.H5_FILENAME, self.dataset_name)) 258 | grp = h5f.create_group(self.dataset_name) 259 | 260 | for i, var in enumerate(variables): 261 | str_var = var_strings[i] 262 | if str_var == "raw_data": 263 | for k, str_var in enumerate(['x', 'y', 'z']): 264 | if config.VERBOSE_OUTPUT: 265 | logging.info("{}/{}: CREATE {}".format(self.H5_FILENAME, self.dataset_name, str_var)) 266 | grp.create_dataset(str_var, data=var[:, k], compression="gzip", chunks=True, maxshape=(None,)) 267 | else: 268 | if config.VERBOSE_OUTPUT: 269 | logging.info("{}/{}: CREATE {}".format(self.H5_FILENAME, self.dataset_name, str_var)) 270 | grp.create_dataset(str_var, data=var, compression="gzip", chunks=True, maxshape=(None,)) 271 | 272 | self.H5_INIT = True 273 | else: 274 | for i, var in enumerate(variables): 275 | str_var = var_strings[i] 276 | if str_var == "raw_data": 277 | for k, str_var in enumerate(['x', 'y', 'z']): 278 | h5f[self.dataset_name][str_var].resize((h5f[self.dataset_name][str_var].shape[0] \ 279 | + var[:, k].shape[0]), axis = 0) 280 | h5f[self.dataset_name][str_var][-var[:, k].shape[0]:] = var[:, k] 281 | else: 282 | h5f[self.dataset_name][str_var].resize((h5f[self.dataset_name][str_var].shape[0] \ 283 | + var.shape[0]), axis = 0) 284 | h5f[self.dataset_name][str_var][-var.shape[0]:] = var 285 | if config.VERBOSE_OUTPUT: 286 | logging.info("{}/{}: APPEND ... {}".format(self.H5_FILENAME, self.dataset_name, \ 287 | h5f[self.dataset_name]['diffs'].shape[0])) 288 | 289 | def chunkwise_logger(self, n_cycles = 10, t_size = 128): 290 | # inialize variables for integration 291 | current_delay = 2.0 292 | acts = [0.0] 293 | raw_data = [0.0, 0.0, 0.0] 294 | last_spike = -1e10 295 | for i in range(n_cycles): 296 | #print(i, "self._run: ", self._run) 297 | # one cycle of logging 298 | if self._run: 299 | # run the next chunk with the last values as initial conditions 300 | ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states, last_spike = self.adaptive_logger(t_size, \ 301 | init_activity = acts[-1], \ 302 | init_delay = current_delay, \ 303 | init_acc = raw_data[-1], \ 304 | last_spike = last_spike, \ 305 | return_delay=True) 306 | 307 | if config.OLED_DISPLAY: 308 | # stitch together the object to send to the OLED interfacer thread 309 | display_input = {} 310 | display_input['timeseries'] = diffs 311 | display_input['status'] = "{0:.2f}".format(acts[-1]) 312 | display_input['trigger'] = True if int(states[-1]) == config.SLEEP_STATE_DEEP else False 313 | threading.Thread(target=self.oled.draw_display, args=(display_input,)).start() 314 | #self.oled.draw_timeseries(diffs, text = "{0:.2f}".format(acts[-1])) 315 | 316 | elapsed_time = ts_realtime_data[-1] - ts_realtime_data[0] 317 | current_delay = delays[-1] 318 | 319 | if config.LOGGING: 320 | # log all data 321 | threading.Thread(target=self.log_data, args=(ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states)).start() 322 | else: 323 | if config.VERBOSE_OUTPUT: 324 | logging.info(f"Sleep tracking stopped. Elapsed time: {elapsed_time}") 325 | self.oled.print("Good Morning", draw_frame=1, font='large') 326 | break 327 | 328 | def start(self): 329 | self._run = True 330 | # kick off a thread that loops 331 | self.thread = threading.Thread(target=self.chunkwise_logger, 332 | args=(999999999999, 512)) 333 | self.thread.start() 334 | logging.info("Sleep tracking started.") 335 | return self.thread 336 | 337 | def stop(self): 338 | self._run = False 339 | #self.thread.join() 340 | logging.info("Sleep tracking stopped.") 341 | #threading.Thread(target=self.oled.draw_display, args=(dict(text="Stopping"),)).start() 342 | self.oled.print("Stopping ...", clear_display=False) -------------------------------------------------------------------------------- /Stimulus.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import pyaudio 4 | import threading 5 | import logging 6 | 7 | class AudioStimulus(): 8 | def __init__(self, base_fr=40.0, stim_fr=0.8, stim_len=5, bitrate=48000): 9 | self.PyAudio = pyaudio.PyAudio 10 | self.bitrate = bitrate 11 | self.isActive = False 12 | self.base_fr = base_fr 13 | self.stim_fr = stim_fr 14 | self.stim_len = stim_len # second 15 | 16 | self.SLEEP_AFTER_LOOP = 0.0 # in seconds 17 | 18 | logging.info("Initializing audio interface ...") 19 | self.p = self.PyAudio() 20 | self.stream = self.p.open(format=pyaudio.paFloat32, 21 | channels=1, 22 | rate=int(self.bitrate), 23 | output=True) 24 | 25 | logging.info("Generating waveform ...") 26 | self.waveform = self.generate_sin_waveform() 27 | logging.info("AudioStimulus initialized.") 28 | 29 | def terminate_audio_stream(self): 30 | self.stream.stop_stream() 31 | self.stream.close() 32 | self.p.terminate() 33 | logging.info("Audio stream terminated.") 34 | 35 | def generate_sin_waveform(self,): 36 | sound_len = self.stim_len # second 37 | waveform = np.ndarray( 38 | (int(sound_len * self.bitrate)), dtype=np.float32) 39 | 40 | f1 = self.base_fr 41 | f2 = self.base_fr + self.stim_fr 42 | 43 | # generate waveform 44 | for i in range(len(waveform)): 45 | frac = 1 + i / len(waveform) 46 | waveform[i] = np.sin(f1 * i / self.bitrate * 2 * np.pi) + \ 47 | np.sin(f2 * i / self.bitrate * 2 * np.pi + np.pi) 48 | waveform /= np.max(waveform) # normalize 49 | return waveform 50 | 51 | def play_audio(self, waveform): 52 | # start loop that generates audio 53 | while self.isActive: 54 | logging.info("Play stimulus.") 55 | self.stream.write(waveform, num_frames=len(waveform)) 56 | time.sleep(self.SLEEP_AFTER_LOOP) 57 | 58 | self.isActive = False 59 | logging.info("Stimulus stopped.") 60 | 61 | def start_stimulus(self): 62 | self.isActive = True 63 | # kick off a thread that loops 64 | x = threading.Thread(target=self.play_audio, 65 | args=(self.waveform,)) 66 | x.start() 67 | 68 | def play_waveform(self, waveform): 69 | self.isActive = True 70 | # kick off a thread that loops 71 | x = threading.Thread(target=self.play_audio, 72 | args=(waveform,)) 73 | x.start() 74 | 75 | def stop_stimulus(self): 76 | self.isActive = False 77 | logging.info("Stopping stimulus ...") -------------------------------------------------------------------------------- /accel.py: -------------------------------------------------------------------------------- 1 | from SleepLogger import SleepLogger 2 | import matplotlib.pyplot as plt 3 | import threading 4 | 5 | import logging 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | sl = SleepLogger() 10 | 11 | #ts_data, ts_realtime_data, raw_data, acts, diffs, delays, states = sl.chunkwise_logger(9999999999990999, 512) 12 | main_thread = sl.start() 13 | # # wait till end of main thread 14 | main_thread.join() 15 | # print("join over.") -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # SETTINGS 2 | # -------------------- 3 | VERBOSE_OUTPUT = True 4 | STIMULUS_ACTIVE = False 5 | 6 | # I2C addresses of connected devices. 7 | # Determine the addresses using sudo i2cdetect -y 1 8 | I2C_OLED_ADDR = 0x3C 9 | I2C_ACCEL_ADDR = 0x1D 10 | ACCELEROMETER_ACTIVITY_THRESHOLD = 12.5 11 | 12 | # oled display 13 | OLED_DISPLAY = True 14 | 15 | # Logging parameters 16 | LOGGING = True # log activity at all 17 | 18 | LOG_TO_REDIS = False 19 | REDIS_HOST = 'localhost' 20 | REDIS_PORT = 6379 21 | 22 | LOG_TO_HDF = True 23 | HDF_FILE = 'log.h5' 24 | 25 | # SLEEP DETECTION ALGO PARAMETERS 26 | # -------------------- 27 | LOGGER_MIN_DELAY = 2.0 28 | LOGGER_MAX_DELAY = 200.0 29 | 30 | # Adaptive delay parameters 31 | DELAY_DIVIDE_BY = 10 32 | DELAY_MULTIPLY_WITH = 1.2 33 | 34 | # Activity integrator parameters 35 | ACTIVITY_DECAY_CONSTANT = 2 * 60 * 1000.0 # for use: 2 * 60 * 1000.0, for testing: 10 * 1000.0 # 36 | ACTIVITY_SPIKE_STRENGTH = 0.05 # increase activity by this value for each movement spike 37 | ACTIVITY_DECAY_DELAY = 5 * 60 * 1000.0 # for use: 5 * 60 * 1000.0, for testing: 2 * 1000.0 38 | ACTIVITY_LOWER_BOUND = 1e-3 # stop decay below this value 39 | 40 | # Sleep stage detection parameters 41 | ACTIVITY_THRESHOLD_DEEP_SLEEP = 0.01 42 | ACTIVITY_THRESHOLD_WAKE = 0.7 43 | 44 | SLEEP_STATE_WAKE = 0 45 | SLEEP_STATE_LIGHT = 1 46 | SLEEP_STATE_DEEP = 2 -------------------------------------------------------------------------------- /data/processed/README.md: -------------------------------------------------------------------------------- 1 | processed data is stored here 2 | -------------------------------------------------------------------------------- /drivers/MMA.py: -------------------------------------------------------------------------------- 1 | import smbus 2 | import time 3 | 4 | import config 5 | 6 | # Get I2C bus 7 | bus = smbus.SMBus(1) 8 | # I2C address of the device 9 | MMA8452Q_DEFAULT_ADDRESS = config.I2C_ACCEL_ADDR 10 | # MMA8452Q Register Map 11 | MMA8452Q_REG_STATUS = 0x00 # Data status Register 12 | MMA8452Q_REG_OUT_X_MSB = 0x01 # Output Value X MSB 13 | MMA8452Q_REG_OUT_X_LSB = 0x02 # Output Value X LSB 14 | MMA8452Q_REG_OUT_Y_MSB = 0x03 # Output Value Y MSB 15 | MMA8452Q_REG_OUT_Y_LSB = 0x04 # Output Value Y LSB 16 | MMA8452Q_REG_OUT_Z_MSB = 0x05 # Output Value Z MSB 17 | MMA8452Q_REG_OUT_Z_LSB = 0x06 # Output Value Z LSB 18 | MMA8452Q_REG_SYSMOD = 0x0B # System mode Register 19 | MMA8452Q_REG_INT_SOURCE = 0x0C # System Interrupt Status Register 20 | MMA8452Q_REG_WHO_AM_I = 0x0D # Device ID Register 21 | MMA8452Q_REG_XYZ_DATA_CFG = 0x0E # Data Configuration Register 22 | MMA8452Q_REG_CTRL_REG1 = 0x2A # Control Register 1 23 | MMA8452Q_REG_CTRL_REG2 = 0x2B # Control Register 2 24 | MMA8452Q_REG_CTRL_REG3 = 0x2C # Control Register 3 25 | MMA8452Q_REG_CTRL_REG4 = 0x2D # Control Register 4 26 | MMA8452Q_REG_CTRL_REG5 = 0x2E # Control Register 5 27 | # MMA8452Q Data Configuration Register 28 | MMA8452Q_DATA_CFG_HPF_OUT = 0x10 # Output Data High-Pass Filtered 29 | MMA8452Q_DATA_CFG_FS_2 = 0x00 # Full-Scale Range = 2g 30 | MMA8452Q_DATA_CFG_FS_4 = 0x01 # Full-Scale Range = 4g 31 | MMA8452Q_DATA_CFG_FS_8 = 0x02 # Full-Scale Range = 8g 32 | # MMA8452Q Control Register 1 33 | MMA8452Q_ASLP_RATE_50 = 0x00 # Sleep mode rate = 50Hz 34 | MMA8452Q_ASLP_RATE_12_5 = 0x40 # Sleep mode rate = 12.5Hz 35 | MMA8452Q_ASLP_RATE_6_25 = 0x80 # Sleep mode rate = 6.25Hz 36 | MMA8452Q_ASLP_RATE_1_56 = 0xC0 # Sleep mode rate = 1.56Hz 37 | MMA8452Q_ODR_800 = 0x00 # Output Data Rate = 800Hz 38 | MMA8452Q_ODR_400 = 0x08 # Output Data Rate = 400Hz 39 | MMA8452Q_ODR_200 = 0x10 # Output Data Rate = 200Hz 40 | MMA8452Q_ODR_100 = 0x18 # Output Data Rate = 100Hz 41 | MMA8452Q_ODR_50 = 0x20 # Output Data Rate = 50Hz 42 | MMA8452Q_ODR_12_5 = 0x28 # Output Data Rate = 12.5Hz 43 | MMA8452Q_ODR_6_25 = 0x30 # Output Data Rate = 6.25Hz 44 | MMA8452Q_ODR_1_56 = 0x38 # Output Data Rate = 1_56Hz 45 | MMA8452Q_MODE_NORMAL = 0x00 # Normal Mode 46 | MMA8452Q_MODE_REDUCED_NOISE = 0x04 # Reduced Noise Mode 47 | MMA8452Q_MODE_FAST_READ = 0x02 # Fast Read Mode 48 | MMA8452Q_MODE_ACTIVE = 0x01 # Active Mode 49 | MMA8452Q_MODE_STANDBY = 0x00 # Standby Mode 50 | class MMA8452Q(): 51 | def __init__(self): 52 | self.mode_configuration() 53 | self.data_configuration() 54 | def write(self, REGISTER, SETTING): 55 | #print("Writing: {:08b}".format(SETTING)) 56 | bus.write_byte_data(MMA8452Q_DEFAULT_ADDRESS, REGISTER, SETTING) 57 | def mode_configuration(self, MODE=None): 58 | """Select the Control Register-1 configuration of the accelerometer from the given provided values""" 59 | if MODE == None: 60 | #MODE_CONFIG = (MMA8452Q_ODR_800 | MMA8452Q_MODE_REDUCED_NOISE | MMA8452Q_MODE_NORMAL | MMA8452Q_MODE_ACTIVE) 61 | MODE_CONFIG = (MMA8452Q_ODR_800 | MMA8452Q_MODE_NORMAL | MMA8452Q_MODE_ACTIVE) 62 | else: 63 | MODE_CONFIG = (MODE) 64 | #print("Writing: {:08b}".format(MODE_CONFIG)) 65 | bus.write_byte_data(MMA8452Q_DEFAULT_ADDRESS, MMA8452Q_REG_CTRL_REG1, MODE_CONFIG) 66 | def data_configuration(self): 67 | """Select the Data Configuration Register configuration of the accelerometer from the given provided values""" 68 | DATA_CONFIG = (MMA8452Q_DATA_CFG_FS_2) 69 | #print("Data: {:08b}".format(DATA_CONFIG)) 70 | bus.write_byte_data(MMA8452Q_DEFAULT_ADDRESS, MMA8452Q_REG_XYZ_DATA_CFG, DATA_CONFIG) 71 | def read_accl(self): 72 | """Read data back from MMA8452Q_REG_STATUS(0x00), 7 bytes 73 | Status register, X-Axis MSB, X-Axis LSB, Y-Axis MSB, Y-Axis LSB, Z-Axis MSB, Z-Axis LSB""" 74 | data = bus.read_i2c_block_data(MMA8452Q_DEFAULT_ADDRESS, MMA8452Q_REG_STATUS, 7) 75 | # Convert the data 76 | xAccl = (data[1] * 256 + data[2]) / 16 77 | if xAccl > 2047 : 78 | xAccl -= 4096 79 | yAccl = (data[3] * 256 + data[4]) / 16 80 | if yAccl > 2047 : 81 | yAccl -= 4096 82 | zAccl = (data[5] * 256 + data[6]) / 16 83 | if zAccl > 2047 : 84 | zAccl -= 4096 85 | return {'x' : xAccl, 'y' : yAccl, 'z' : zAccl} 86 | -------------------------------------------------------------------------------- /drivers/OLED.py: -------------------------------------------------------------------------------- 1 | import board 2 | import digitalio 3 | from PIL import Image, ImageDraw, ImageFont 4 | import adafruit_ssd1306 5 | 6 | import threading 7 | 8 | import config 9 | 10 | class OLED: 11 | def __init__(self): 12 | OLED_RESET = digitalio.DigitalInOut(board.D4) 13 | self.WIDTH = 128 14 | self.HEIGHT = 32 15 | 16 | self.i2c = board.I2C() 17 | self.oled = adafruit_ssd1306.SSD1306_I2C(self.WIDTH, self.HEIGHT, \ 18 | self.i2c, addr=config.I2C_OLED_ADDR, reset=OLED_RESET) 19 | self.oled.fill(0) 20 | self.oled.show() 21 | self.image = Image.new('1', (self.oled.width, self.oled.height)) 22 | self.draw = ImageDraw.Draw(self.image) 23 | 24 | self.font = ImageFont.load_default() 25 | self.small_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 8) 26 | self.medium_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 10) 27 | self.large_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 16) 28 | 29 | self.draw_text("Sleep Well", clear_display = True, draw_frame = 1, font='large') 30 | self.draw_image() 31 | def clear_display(self, redraw=False): 32 | self.draw.rectangle((0, 0, self.oled.width, self.oled.height * 2), outline=0, fill=0) 33 | if redraw: 34 | self.draw_image() 35 | def draw_image(self): 36 | self.oled.image(self.image) 37 | self.oled.show() 38 | 39 | def draw_text(self, text, clear_display = False, font=None, pos='center', \ 40 | draw_frame = None, redraw = False): 41 | # clear display 42 | if clear_display: 43 | self.clear_display() 44 | if font is None: 45 | font = self.font 46 | elif font == "large": 47 | font = self.large_font 48 | elif font == "medium": 49 | font = self.medium_font 50 | elif font == "small": 51 | font = self.small_font 52 | 53 | if draw_frame is not None: 54 | self.draw_frame(border = draw_frame) 55 | 56 | text_width, text_height = self.draw.textsize(text, font=font) 57 | if pos == 'center': 58 | self.draw.text((self.oled.width//2 - text_width//2, self.oled.height//2 - text_height//2), 59 | text, font=font, fill=255) 60 | elif pos == 'topright': 61 | self.draw.text((self.oled.width - text_width - 4, 1), 62 | text, font=font, fill=255) 63 | if redraw: 64 | self.draw_image() 65 | 66 | def draw_frame(self, border = 1): 67 | BORDER = border 68 | self.draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=255, fill=255) 69 | self.draw.rectangle((BORDER, BORDER, self.oled.width - BORDER - 1, \ 70 | self.oled.height - BORDER - 1), outline=0, fill=0) 71 | 72 | def draw_timeseries(self, data, text=None, clear_display = True, redraw = True): 73 | # subsample if data length is n times the display width 74 | n_data_windows = len(data)//self.WIDTH 75 | if n_data_windows > 1: 76 | data = data[::n_data_windows] 77 | 78 | # data must be maximum 128 steps long 79 | data = data[-self.WIDTH:] 80 | 81 | 82 | if max(data) > 20: # diffs larger than noise: 83 | data /= max(data) 84 | data *= self.HEIGHT # scale data to 32 pixels 85 | else: 86 | data /= max(data) 87 | data *= self.HEIGHT / 3 # if just noise, draw low amplitude 88 | 89 | if clear_display: 90 | self.clear_display() 91 | for x in range(len(data)-1): 92 | self.draw.line((x, self.HEIGHT - data[x], x+1, self.HEIGHT - data[x+1]), fill=1) 93 | #self.draw.point((x, self.HEIGHT - data[x]), fill=1) 94 | if text is not None: 95 | self.draw_text(text, font=self.small_font, clear_display = False, pos='topright') 96 | 97 | if redraw: 98 | self.draw_image() 99 | 100 | def draw_display(self, content): 101 | if "timeseries" in content and "status" in content: 102 | timeseries = content['timeseries'] 103 | status = content['status'] 104 | self.draw_timeseries(timeseries, text=status, clear_display = True, redraw = False) 105 | 106 | if "trigger" in content: 107 | trigger = content['trigger'] 108 | if trigger: 109 | self.draw_text("STIMULUS", font='medium') 110 | 111 | if "text" in content: 112 | self.draw_text(content["text"], font='medium', clear_display=True, draw_frame=True) 113 | self.draw_image() 114 | 115 | def print(self, text, **kwargs): 116 | args = dict(text=text) 117 | args['redraw'] = kwargs['redraw'] if 'redraw' in kwargs else True 118 | args['clear_display'] = kwargs['clear_display'] if 'clear_display' in kwargs else True 119 | args.update(kwargs) 120 | #threading.Thread(target=self.draw_text, args=(args,)).start() 121 | self.draw_text(**args) -------------------------------------------------------------------------------- /interface/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__, static_folder='static') 4 | 5 | from app import routes 6 | -------------------------------------------------------------------------------- /interface/app/dataplotter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import datetime 4 | import matplotlib.dates as mdates 5 | import h5py 6 | import matplotlib.pyplot as plt 7 | import matplotlib 8 | matplotlib.use('PS') 9 | 10 | STATIC_IMAGES_DIR = '../app/static/images/' 11 | 12 | 13 | def plot_last_runs(nRuns=3, filename="../../log.h5"): 14 | with h5py.File(filename, mode='r') as h5f: 15 | runs = list(h5f.keys()) 16 | runs = runs[-nRuns:][::-1] 17 | for r in runs: 18 | plot_recording(runName=r, filename=filename) 19 | return runs 20 | 21 | 22 | def plot_recording(rInd=-1, runName=None, filename="../../log.h5"): 23 | if runName is None: 24 | with h5py.File(filename, mode='r') as h5f: 25 | runs = list(h5f.keys()) 26 | runName = runs[rInd] 27 | 28 | image_dir = os.path.join(STATIC_IMAGES_DIR, "{}.png".format(runName)) 29 | if os.path.isfile(image_dir): 30 | print("File {} exists...".format(image_dir)) 31 | else: 32 | with h5py.File(filename, mode='r') as h5f: 33 | runs = list(h5f.keys()) 34 | if runName is None: 35 | runName = runs[rInd] 36 | print("Rendering {}".format(runName)) 37 | 38 | ts = h5f[runName]['ts_realtime'][()] 39 | diffs = h5f[runName]['diffs'][()] 40 | acts = h5f[runName]['acts'][()] 41 | 42 | times = [] 43 | for i, milli in enumerate(ts): 44 | times.append(datetime.datetime.fromtimestamp(milli/1000.0)) 45 | times = np.array(times) 46 | 47 | fig = plt.figure(figsize=(14, 4), dpi=100) 48 | plt.title(runName) 49 | ax = fig.add_subplot(1, 1, 1) 50 | 51 | ax.plot(times, acts, c='k', lw=3) 52 | ax.fill_between(times, 0, acts, color='C0', alpha=0.4, label='state') 53 | 54 | thr = 17 55 | ax.vlines(times[np.argwhere(diffs > thr)], 0, 56 | 1, color='red', zorder=1, alpha=0.3) 57 | 58 | ax.set_ylim(0, 1) 59 | 60 | hours = mdates.HourLocator(interval=1) 61 | h_fmt = mdates.DateFormatter('%H:%M:%S') 62 | ax.xaxis.set_tick_params(rotation=90) 63 | ax.xaxis.set_major_locator(hours) 64 | ax.xaxis.set_major_formatter(h_fmt) 65 | 66 | ax.spines['right'].set_visible(False) 67 | ax.spines['top'].set_visible(False) 68 | # ax.spines['bottom'].set_visible(False) 69 | ax.spines['left'].set_visible(False) 70 | #ax.tick_params(direction='out', length=4, width=1, colors='k', labelsize=6) 71 | 72 | # plt.grid() 73 | # plt.show() 74 | plt.savefig(image_dir, bbox_inches='tight') 75 | plt.close(fig) 76 | -------------------------------------------------------------------------------- /interface/app/get_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import datetime 4 | import matplotlib.dates as mdates 5 | from scipy import signal 6 | import pandas as pd 7 | import h5py 8 | import dill 9 | 10 | H5_FILE = '../../log.h5' 11 | DATA_DIR = '../../data/'#os.path.join(STATIC_IMAGES_DIR, "{}.png".format(runName)) 12 | PROCESSED_DATA_DIR = '../../data/processed/'##os.path.join(DATA_DIR, "/processed/") 13 | 14 | def get_run_names(nruns=1, filename=H5_FILE): 15 | with h5py.File(filename, mode='r') as h5f: 16 | runs = list(h5f.keys()) 17 | #runs = runs[-nruns:][::-1] 18 | 19 | # reverse order so we start with the newest recording 20 | runs = runs[::-1] 21 | return runs 22 | 23 | def get_runs(nruns=5, h5_filename=H5_FILE, run_name = None): 24 | 25 | if run_name is None: 26 | runs = get_run_names(h5_filename) 27 | else: 28 | runs = [run_name] 29 | 30 | n_valid_runs = 0 31 | nruns = int(nruns) 32 | data = {} 33 | 34 | for run_name in runs: 35 | print("Loading run {}".format(run_name)) 36 | # only return nruns runs 37 | if n_valid_runs >= nruns: 38 | break 39 | else: 40 | df = load_run_data(run_name, h5_filename) 41 | 42 | # check if data is long enough 43 | if not df.index[0] < df.index[-1] - datetime.timedelta(hours=1): 44 | print(f"{run_name} is too short: {df.index[0]} until {df.index[-1]}") 45 | continue 46 | else: 47 | n_valid_runs += 1 48 | 49 | data[run_name] = {} 50 | # get activity spikes 51 | diff_thrs = 15 52 | spike_list = df[df['diffs'].gt(diff_thrs)].index.strftime('%H:%M:%S').tolist() 53 | 54 | # get deep sleep duration 55 | sleep_duration, deep_duration, light_duration = get_sleep_stage_durations(df) 56 | 57 | # time in HH:MM:SS 58 | t = df.index.strftime('%H:%M:%S') 59 | 60 | # activity 61 | activity = df['data'] 62 | 63 | # states 64 | states = df['states'] 65 | 66 | # pack data 67 | data[run_name]['deep_duration'] = deep_duration 68 | data[run_name]['light_duration'] = light_duration 69 | data[run_name]['sleep_duration'] = sleep_duration 70 | data[run_name]['t'] = t 71 | data[run_name]['activity'] = activity 72 | data[run_name]['states'] = states 73 | data[run_name]['spikes'] = spike_list 74 | 75 | return data 76 | 77 | 78 | def data_to_pandas(t, data, diffs, states): 79 | df = pd.DataFrame({'t': t, 'data': data, 'diffs' : diffs, 'states' : states}) 80 | df = df.set_index('t') 81 | df['data'] = df['data'].fillna(0) 82 | df['diffs'] = df['diffs'].fillna(0) 83 | return df 84 | 85 | def process_data(t, data, diffs, states): 86 | df = data_to_pandas(t, data, diffs, states) 87 | # downsample, 1T == 1minute 88 | df = df.resample('1T').agg({'data':'mean', 'diffs':'max', 'states':'last'}).bfill() 89 | df.index = pd.to_datetime(df.index) 90 | return df 91 | 92 | def get_data_from_run(rInd=-1, runName=None, filename=H5_FILE): 93 | if runName is None: 94 | with h5py.File(filename, mode='r') as h5f: 95 | runs = list(h5f.keys()) 96 | runName = runs[rInd] 97 | 98 | with h5py.File(filename, mode='r') as h5f: 99 | runs = list(h5f.keys()) 100 | if runName is None: 101 | runName = runs[rInd] 102 | print("Getting data from {}".format(runName)) 103 | 104 | ts = h5f[runName]['ts_realtime'][()] 105 | diffs = h5f[runName]['diffs'][()] 106 | acts = h5f[runName]['acts'][()] 107 | states = h5f[runName]['states'][()] 108 | 109 | # convert loaded data into datetime 110 | times = [] 111 | for i, milli in enumerate(ts): 112 | times.append(datetime.datetime.fromtimestamp(milli/1000.0)) 113 | times = np.array(times) 114 | df = process_data(times, acts, diffs, states) 115 | return df 116 | 117 | def load_run_data(run_name, h5_filename): 118 | # check if preprocessed data is already available 119 | data_filename = os.path.join(PROCESSED_DATA_DIR, f"{run_name}.dill") 120 | if os.path.isfile(data_filename): 121 | print(f"File {data_filename} exists...") 122 | df = dill.load(open(data_filename, "br+")) 123 | else: 124 | df = get_data_from_run(runName=run_name, filename=h5_filename) 125 | #df = process_data(t, activity, diffs) 126 | print(f"Saving {data_filename}.") 127 | dill.dump(df, open(data_filename, "bw+")) 128 | return df 129 | 130 | 131 | 132 | def get_spike_times(t, diffs, thr=17): 133 | df = pd.DataFrame({'t': t, 'data': diffs}) 134 | df = df.set_index('t') 135 | return df 136 | df[df['data'].gt(thr)].index 137 | return df[df['data'].gt(thr)].index.strftime('%H:%M:%S') 138 | 139 | def get_sleep_stage_durations(df): 140 | activity_threshold = 0.05 141 | deep_duration = len(df[df['data'].le(activity_threshold)].index) 142 | sleep_duration = len(df) 143 | light_duration = sleep_duration - deep_duration 144 | return sleep_duration, deep_duration, light_duration 145 | 146 | def downsample_data(t, data, steps=100): 147 | t_resampled = signal.resample(t, steps) 148 | data_resampled = signal.resample(data, steps) 149 | return t_resampled, data_resampled 150 | -------------------------------------------------------------------------------- /interface/app/routes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import render_template, request 4 | from app import app 5 | 6 | #from app.dataplotter import plot_last_runs, plot_recording 7 | from app.get_data import get_runs, get_run_names 8 | 9 | # some_file.py 10 | import sys 11 | # insert at 1, 0 is the script path (or '' in REPL) 12 | sys.path.insert(1, '../../') 13 | try: 14 | from SleepLogger import SleepLogger 15 | except ImportError: 16 | logging.warn("SleepLogger could not be imported") 17 | class SleepLogger(): 18 | def start(self): 19 | return 1 20 | def stop(self): 21 | return 2 22 | 23 | sl = None 24 | 25 | @app.route('/') 26 | @app.route('/index') 27 | def index(): 28 | NRUNS_DEFAULT = 5 29 | n_runs = request.args.get('runs') 30 | run_name = request.args.get('run') 31 | 32 | n_runs = n_runs or NRUNS_DEFAULT 33 | 34 | user = {'username': 'caglorithm'} 35 | 36 | 37 | data = get_runs(n_runs, run_name = run_name) 38 | runs = [] 39 | for i, (key, value) in enumerate(data.items()): 40 | runs.append(value) 41 | runs[i].update({"id" : i}) 42 | runs[i].update({"name" : key}) 43 | 44 | return render_template('index.html', n_runs=n_runs, runs=runs, user=user) 45 | 46 | @app.route('/start') 47 | def start(): 48 | global sl 49 | if sl is None: 50 | sl = SleepLogger() 51 | sl.start() 52 | return render_template('tracker.html', status = "started") 53 | else: 54 | return render_template('tracker.html', status = "is already running") 55 | 56 | @app.route('/stop') 57 | def stop(): 58 | global sl 59 | if sl is not None: 60 | sl.stop() 61 | sl = None 62 | # process newest dataset 63 | _ = get_runs(nruns=1) 64 | return render_template('tracker.html', status = "stopped") 65 | else: 66 | return render_template('tracker.html', status = "is not running") -------------------------------------------------------------------------------- /interface/app/static/css/bootstrap-grid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Grid v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} 7 | /*# sourceMappingURL=bootstrap-grid.min.css.map */ -------------------------------------------------------------------------------- /interface/app/static/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | text-align: -webkit-match-parent; 190 | } 191 | 192 | label { 193 | display: inline-block; 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | button { 198 | border-radius: 0; 199 | } 200 | 201 | button:focus { 202 | outline: 1px dotted; 203 | outline: 5px auto -webkit-focus-ring-color; 204 | } 205 | 206 | input, 207 | button, 208 | select, 209 | optgroup, 210 | textarea { 211 | margin: 0; 212 | font-family: inherit; 213 | font-size: inherit; 214 | line-height: inherit; 215 | } 216 | 217 | button, 218 | input { 219 | overflow: visible; 220 | } 221 | 222 | button, 223 | select { 224 | text-transform: none; 225 | } 226 | 227 | [role="button"] { 228 | cursor: pointer; 229 | } 230 | 231 | select { 232 | word-wrap: normal; 233 | } 234 | 235 | button, 236 | [type="button"], 237 | [type="reset"], 238 | [type="submit"] { 239 | -webkit-appearance: button; 240 | } 241 | 242 | button:not(:disabled), 243 | [type="button"]:not(:disabled), 244 | [type="reset"]:not(:disabled), 245 | [type="submit"]:not(:disabled) { 246 | cursor: pointer; 247 | } 248 | 249 | button::-moz-focus-inner, 250 | [type="button"]::-moz-focus-inner, 251 | [type="reset"]::-moz-focus-inner, 252 | [type="submit"]::-moz-focus-inner { 253 | padding: 0; 254 | border-style: none; 255 | } 256 | 257 | input[type="radio"], 258 | input[type="checkbox"] { 259 | box-sizing: border-box; 260 | padding: 0; 261 | } 262 | 263 | textarea { 264 | overflow: auto; 265 | resize: vertical; 266 | } 267 | 268 | fieldset { 269 | min-width: 0; 270 | padding: 0; 271 | margin: 0; 272 | border: 0; 273 | } 274 | 275 | legend { 276 | display: block; 277 | width: 100%; 278 | max-width: 100%; 279 | padding: 0; 280 | margin-bottom: .5rem; 281 | font-size: 1.5rem; 282 | line-height: inherit; 283 | color: inherit; 284 | white-space: normal; 285 | } 286 | 287 | progress { 288 | vertical-align: baseline; 289 | } 290 | 291 | [type="number"]::-webkit-inner-spin-button, 292 | [type="number"]::-webkit-outer-spin-button { 293 | height: auto; 294 | } 295 | 296 | [type="search"] { 297 | outline-offset: -2px; 298 | -webkit-appearance: none; 299 | } 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | ::-webkit-file-upload-button { 306 | font: inherit; 307 | -webkit-appearance: button; 308 | } 309 | 310 | output { 311 | display: inline-block; 312 | } 313 | 314 | summary { 315 | display: list-item; 316 | cursor: pointer; 317 | } 318 | 319 | template { 320 | display: none; 321 | } 322 | 323 | [hidden] { 324 | display: none !important; 325 | } 326 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /interface/app/static/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /interface/app/static/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,2BACE,MAAA,QACA,gBAAA,KI/LA,iCJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAGA,mBAAA,UAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAOF,GAEE,WAAA,QACA,WAAA,qBAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBC7GF,ODgHA,MC9GA,SADA,OAEA,SDkHE,OAAA,EACA,YAAA,QE5PE,UAAA,QF8PF,YAAA,QAGF,OChHA,MDkHE,SAAA,QAGF,OChHA,ODkHE,eAAA,KGhHF,cHuHE,OAAA,QAMF,OACE,UAAA,OCnHF,cACA,aACA,cDwHA,OAIE,mBAAA,OCvHF,6BACA,4BACA,6BD0HE,sBAKI,OAAA,QC1HN,gCACA,+BACA,gCD8HA,yBAIE,QAAA,EACA,aAAA,KC7HF,qBDgIA,kBAEE,WAAA,WACA,QAAA,EAIF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MEnSI,UAAA,OFqSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SG1IF,yCFGA,yCD6IE,OAAA,KG3IF,cHmJE,eAAA,KACA,mBAAA,KG/IF,yCHuJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KG5JF,SHkKE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-` 30 | 31 |

41 | 42 | 43 | 44 | 45 |
46 |
47 | Start tracker 48 | Stop tracker 49 |
50 |
51 | {% block body %} 52 | 53 | {% endblock %} 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /interface/app/templates/doughnut_chart.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /interface/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
5 |

Hi, {{ user.username }}!

6 |

Displaying last {{n_runs}} recordings ...

7 |
8 | 9 |
10 | {% for run in runs %} 11 |
12 |
13 |
14 |
{{ run.name }}
15 |

16 | Total sleep duration: {{ run.sleep_duration }} min ({{ '%0.1f'| format(run.sleep_duration / 60|float) }} h)
17 | Deep: {{ run.deep_duration }} min ({{ '%0.1f'| format(run.deep_duration / 60|float) }} h) / 18 | Light: {{ run.light_duration }} min ({{ '%0.1f'| format(run.light_duration / 60|float) }} h) 19 |

20 | {% include 'activity_chart.html' %} 21 | {% include 'sleepstage_chart.html' %} 22 |
23 |
24 | {% include 'doughnut_chart.html' %} 25 |
26 |
27 |
28 | {% endfor %} 29 | {% endblock %} -------------------------------------------------------------------------------- /interface/app/templates/sleepstage_chart.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /interface/app/templates/tracker.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
5 |

Sleep tracker {{ status }}.

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scipy 2 | numpy 3 | pyaudio 4 | matplotlib 5 | h5py 6 | flask 7 | pandas 8 | smbus 9 | redis 10 | adafruit-circuitpython-ssd1306 11 | adafruit-blinka 12 | RPI.GPIO 13 | Pillow 14 | dill -------------------------------------------------------------------------------- /resources/2019-12-05 13.42.17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/2019-12-05 13.42.17.jpg -------------------------------------------------------------------------------- /resources/Onton2016.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/Onton2016.jpg -------------------------------------------------------------------------------- /resources/audio_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/audio_input.png -------------------------------------------------------------------------------- /resources/i2cdetect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/i2cdetect.png -------------------------------------------------------------------------------- /resources/img00.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img00.jpeg -------------------------------------------------------------------------------- /resources/img01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img01.jpeg -------------------------------------------------------------------------------- /resources/img02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img02.jpeg -------------------------------------------------------------------------------- /resources/img03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img03.jpeg -------------------------------------------------------------------------------- /resources/img04.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img04.jpeg -------------------------------------------------------------------------------- /resources/img05.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img05.jpeg -------------------------------------------------------------------------------- /resources/img06.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img06.jpeg -------------------------------------------------------------------------------- /resources/img07.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img07.jpeg -------------------------------------------------------------------------------- /resources/img08.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img08.jpeg -------------------------------------------------------------------------------- /resources/img09.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img09.jpeg -------------------------------------------------------------------------------- /resources/img10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img10.jpeg -------------------------------------------------------------------------------- /resources/img11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img11.jpeg -------------------------------------------------------------------------------- /resources/img12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img12.jpg -------------------------------------------------------------------------------- /resources/img13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/img13.jpeg -------------------------------------------------------------------------------- /resources/partlist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/partlist.jpg -------------------------------------------------------------------------------- /resources/signal_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/signal_pipeline.png -------------------------------------------------------------------------------- /resources/sleep.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/sleep.jpg -------------------------------------------------------------------------------- /resources/sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caglorithm/accel/7fe5c13ea9559565c599633bdb3318c8fbc57088/resources/sleep.png --------------------------------------------------------------------------------