[
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "### Expected Behavior\n\n\n### Actual Behavior\n\n\n### Steps to Reproduce the Problem\n\n  1.\n  1.\n  1.\n\n### Error message:\n<!-- If any, paste the *full* error message inside a code block\nas above (starting from line Traceback)\n-->\n\n```\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\n  ...\n```\n\n### Specifications\n\n<details><summary>Details</summary>\n\n  - Version:\n  - Python version:\n  - Platform:\n  - Anaconda environment (`conda list`):\n\n</details>\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nThank you for contributing a pull request!\nPlease name and describe your PR as you would write a\ncommit message.\n-->\n\n### Reference issue\n<!--Example: Closes gh-WXYZ.-->\n\n\n### What does this implement/fix?\n<!--Please explain your changes.-->\n\n\n### Additional information\n<!--Any additional information you think is important.-->\n- [ ] I updated the docs via typer-cli with `_SEML_COMPLETE=1 typer seml.__main__ utils docs --name seml --output docs.md` or did not change the CLI.\n"
  },
  {
    "path": ".github/workflows/actions.yaml",
    "content": "name: Test\n\non: [push, pull_request]\n\njobs:\n  # Run CLI test and pytest\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n        resolution: [\"highest\", \"lowest-direct\"]\n\n    steps:\n      # Checkout the code\n      - uses: actions/checkout@v4\n      # Install uv\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n        with:\n          enable-cache: true\n      # Test whether the CLI is working\n      - name: Test CLI\n        run: uv run -p ${{ matrix.python-version }} --resolution ${{ matrix.resolution }} -U seml --help\n      # Test with pytest\n      - name: Test with pytest\n        run: |\n          cd test\n          uv run -p ${{ matrix.python-version }} --resolution ${{ matrix.resolution }} -U pytest\n\n  # Test commands in a dummy environment\n  commands:\n    runs-on: ubuntu-22.04 # this is the lastest release supported by MongoDB\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n        resolution: [\"highest\", \"lowest-direct\"]\n    # MySQL for Slurm\n    services:\n      mysql:\n        image: mysql:8.0\n        env:\n          MYSQL_ROOT_PASSWORD: root\n        ports:\n          - \"8888:3306\"\n        options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n    steps:\n      - name: Start MongoDB\n        uses: supercharge/mongodb-github-action@1.11.0\n        with:\n          mongodb-username: admin\n          mongodb-password: admin\n          mongodb-port: 27017\n      - name: Install mongosh\n        run: |\n          sudo apt-get install gnupg\n          wget -qO- https://www.mongodb.org/static/pgp/server-7.0.asc | sudo tee /etc/apt/trusted.gpg.d/server-7.0.asc\n          echo \"deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse\" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list\n          sudo apt-get update\n          sudo apt-get install -y mongodb-mongosh\n          mongosh --version\n      - name: Create seml MongoDB user\n        run: |\n          mongosh --host localhost:27017 -u admin -p admin --authenticationDatabase admin <<EOF\n            db = db.getSiblingDB('seml');\n            db.createUser({ user: 'seml', pwd: 'seml', roles: [{ role: 'readWrite', db: 'seml' }] });\n          EOF\n      - name: Setup Slurm Cluster\n        uses: koesterlab/setup-slurm-action@v1\n      # Checkout the code\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n        with:\n          enable-cache: true\n      - name: Setup virtual env\n        run: uv sync -p ${{ matrix.python-version }} --resolution ${{ matrix.resolution }} -U\n      - name: Setup seml config\n        run: uv run seml configure --host=localhost --port=27017 --database=seml --username=seml --password=seml\n      - name: Add\n        run: uv run seml test_collection add ci/examples/example_config.yaml\n      - name: Start\n        run: uv run seml test_collection start\n      - name: Hold\n        run: uv run seml test_collection hold\n      - name: Release\n        run: uv run seml test_collection release\n      - name: Status\n        run: uv run seml test_collection status\n      - name: Queue\n        run: uv run seml queue\n      - name: Cancel\n        run: uv run seml test_collection cancel -y\n      - name: Delete\n        run: uv run seml test_collection delete -y\n      - name: Reload sources\n        run: uv run seml test_collection add ci/examples/example_config.yaml reload-sources -y\n      - name: Set description\n        run: uv run seml test_collection description set -y 'Hello World ${config.dataset}!'\n      - name: List description\n        run: uv run seml test_collection description list\n      - name: Delete description\n        run: uv run seml test_collection description delete -y\n      - name: List\n        run: uv run seml list\n"
  },
  {
    "path": ".github/workflows/precommit.yaml",
    "content": "name: precommit\non: [push, pull_request]\n\njobs:\n  precommit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Cache pre-commit # taken from https://github.com/pre-commit/action/blob/main/action.yml\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pre-commit\n          key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n        with:\n          enable-cache: true\n      - name: Setup virtual environment\n        run: uv sync\n      - name: Install pre-commit\n        run: uv tool install pre-commit --with pre-commit-uv\n      - name: Run pre-commit\n        run: uv tool run pre-commit run --show-diff-on-failure --color=always --all-files\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*.egg-info\nbuild/\ndist/\n\n# Latex aux files\n*.synctex.gz\n*.log\n*.aux\n*.out\n*.dvi\n*.xwm\n*.nav\n*.toc\n*.snm\ncomment.cut\n*.fdb_latexmk\n*.fls\n*.blg\n\n# Jupyter\n*.ipynb_checkpoints\n\n# Code editors\n.idea/\n.vscode/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-case-conflict\n      - id: check-toml\n      - id: check-xml\n      - id: check-yaml\n        exclude: |\n          (?x)^(\n              test/resources/config/config_with_duplicate_parameters_3.yaml\n          )$\n      - id: check-added-large-files\n      - id: trailing-whitespace\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n        name: pyupgrade\n        args: [--py38-plus]\n        exclude: .*\\/__main__.py # typer doesn't supper PEP604 yet, so let's stick to the old typing\n      - id: pyupgrade\n        name: pyupgrade (__main__.py)\n        args: [--py38-plus, --keep-runtime-typing]\n        files: .*\\/__main__.py # We run ruff after pyugprade to remove unused imports\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.14.6\n    hooks:\n      - id: ruff # Run the linter.\n        args: [--fix]\n      - id: ruff # Sort improts\n        name: sort imports with ruff\n        args: [--select, I, --fix]\n      - id: ruff-format # Run the formatter.\n  - repo: local # Run pyright type checker\n    hooks:\n      - id: pyright\n        name: pyright check\n        entry: uv run pyright src/\n        language: system\n        types: [python]\n        pass_filenames: false\n        always_run: true\n"
  },
  {
    "path": ".python-version",
    "content": "3.10"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Johannes Klicpera, Daniel Zügner, Nicholas Gao\nTechnical University of Munich\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude LICENSE\n\nrecursive-exclude test *\nrecursive-exclude examples *\n\nglobal-exclude */__pycache__/*\nglobal-exclude *.pyc\n\nrecursive-include src/seml/templates/ *\n"
  },
  {
    "path": "README.md",
    "content": "![Github Actions](https://github.com/TUM-DAML/seml/workflows/Test/badge.svg)\n\n# `SEML`: Slurm Experiment Management Library\n**`SEML`** is the missing link between the open-source workload scheduling system `Slurm`, the experiment management tool `sacred`, and a `MongoDB` experiment database. It is lightweight, hackable, written in pure Python, and scales to thousands of experiments.\n\nKeeping track of computational experiments can be annoying and failure to do so can lead to lost results, duplicate running of the same experiments, and lots of headaches.\nWhile workload scheduling systems such as [`Slurm`](https://slurm.schedmd.com/overview.html) make it easy to run many experiments in parallel on a cluster, it can be hard to keep track of which parameter configurations are running, failed, or completed.\n[`sacred`](https://github.com/IDSIA/sacred) is a great tool to collect and manage experiments and their results, especially when used with a [`MongoDB`](https://www.mongodb.com/). However, it is lacking integration with workload schedulers.\n\n**`SEML`** enables you to\n* very easily define hyperparameter search spaces using YAML files,\n* run these hyperparameter configurations on a compute cluster using `Slurm`,\n* and to track the experimental results using `sacred` and `MongoDB`.\n\n\nIn addition, **`SEML`** offers many more features to make your life easier, such as\n* automatically saving and loading your source code for reproducibility,\n* easy debugging on Slurm or locally,\n* automatically checking your experiment configurations,\n* extending Slurm with local workers,\n* and keeping track of resource usage (experiment runtime, RAM, etc.).\n\n## Get started\n### New projects\nThe fastest way to get started with `SEML` is via [`uv`](https://docs.astral.sh/uv/):\n1. Install `uv`:\n    ```bash\n    curl -LsSf https://astral.sh/uv/install.sh | sh\n    ```\n2. Setup a new project\n    ```bash\n    # uvx will execute `SEML` in a temporary virtual environment\n    # and run it to setup your new project.\n    uvx seml project init my_new_project\n    ```\n3. Setup a virtual environment\n    ```bash\n    cd my_new_project\n    uv sync\n    ```\n4. Activate your virtual environment\n    ```bash\n    source .venv/bin/activate\n    ```\n5. Configure `SEML`:\n    ```bash\n    seml configure\n    ```\n\nWhen executing `SEML` make sure to always use the `seml` command from your project's virtual environment and only use `uvx seml` for high-level commands that do not affect experiments (like setting up new projects).\n\n### Existing projects\nIf you want to include `SEML` into existing projects, you can install it via:\n```bash\npip install seml\n```\nThen configure your MongoDB via:\n```bash\nseml configure\n```\n\n\n### SSH Port Forwarding\nIf your MongoDB is only accessible via an SSH port forward, **`SEML`** allows you to directly configure this as well if you install the `ssh_forward` dependencies via:\n```bash\npip install seml[ssh_forward]\n```\nIt remains to configure the SSH settings:\n```bash\nseml configure --ssh_forward\n```\n\n### Development\nFor development, we recommend [`uv`](https://docs.astral.sh/uv/) which you can install via\n```bash\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n```\nSetup the right environment use and activate it:\n```bash\nuv sync --locked\nsource .venv/bin/activate\n```\nAlternatively, you can install the repository in any Python environment via:\n```bash\npip install -e .[dev]\n```\n\n#### Pre-commit hooks\nMake sure to install the pre-commit hooks via\n```bash\npre-commit install\n```\n\n## Documentation\nDocumentation is available in our [docs.md](docs.md) or via the CLI:\n```python\nseml --help\n```\n\n## Example\nSee our simple [example](examples) to get familiar with how **`SEML`** works.\n\n## CLI completion\nSEML supports command line completion. To install this feature run:\n```bash\nseml --install-completion {shell}\n```\n\nIf you are using the zsh shell, you might have to append `compinit -D` to the `~/.zshrc` file (see this [issue](https://github.com/tiangolo/typer/issues/180#issuecomment-812620805)).\n\n## Slurm version\n\nSEML should work with Slurm 18.08 and above out of the box. Version 17.11 and earlier do not have a SIGNALING job state, which you have to remove from the SLURM_STATES defined in SEML's settings (`seml/settings.py`). Earlier versions have not been tested and might have other issues.\n\n## Contact\nContact us at zuegnerd@in.tum.de, johannes.gasteiger@tum.de, or n.gao@tum.de for any questions.\n\n## Cite\nWhen you use SEML in your own work, please cite the software along the lines of the following bibtex:\n\n```\n@software{seml_2023,\n  author = {Z{\\\"u}gner, Daniel and Gasteiger, Johannes and Gao, Nicholas and Dominik Fuchsgruber},\n  title = {{SEML: Slurm Experiment Management Library}},\n  url = {https://github.com/TUM-DAML/seml},\n  version = {0.4.0},\n  year = {2023}\n}\n```\n\n\nCopyright (C) 2023\nDaniel Zügner, Johannes Gasteiger, Nicholas Gao, Dominik Fuchsgruber\nTechnical University of Munich\n"
  },
  {
    "path": "ci/examples/example_config.yaml",
    "content": "# Experiment configuration file for CI\n\nseml:\n  executable: example_experiment.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: .\n  description: An example configuration.\n\nslurm:\n  - experiments_per_job: 1\n    sbatch_options:\n      mem: 1G\n      cpus-per-task: 1\n      time: 0-08:00\n\n###### BEGIN PARAMETER CONFIGURATION ######\n\nfixed:\n  max_epochs: 500\n\ngrid:\n  learning_rate:\n    type: loguniform\n    min: 1e-5\n    max: 1e-1\n    num: 1\n\nrandom:\n  samples: 1\n  seed: 821\n\n  # SEML supports dot-notation for nested dictionaries.\n  regularization_params.dropout:\n    type: uniform\n    min: 0.0\n    max: 0.7\n    seed: 222\n\nsmall_datasets:\n  grid:\n    dataset:\n      type: choice\n      options:\n        - small_dataset_1\n        - small_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [16]\n        - [32, 16] # this will be parsed into a Python list.\n\n  random:\n    samples: 3\n    seed: 2223\n\n    max_epochs:\n      type: randint\n      min: 200\n      max: 1000\n\nlarge_datasets:\n  fixed:\n    max_epochs: 1000\n\n  grid:\n    learning_rate:\n      type: choice\n      options:\n        - 0.001\n\n    dataset:\n      type: choice\n      options:\n        - large_dataset_1\n        - large_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [64]\n        - [64, 32]\n"
  },
  {
    "path": "ci/examples/example_experiment.py",
    "content": "import logging\n\nimport numpy as np\n\nfrom seml import Experiment\n\nex = Experiment()\n\n\n@ex.automain\ndef run(\n    dataset: str,\n    hidden_sizes: list,\n    learning_rate: float,\n    max_epochs: int,\n    regularization_params: dict,\n):\n    # Note that regularization_params contains the corresponding sub-dictionary from the configuration.\n    logging.info('Received the following configuration:')\n    logging.info(\n        f'Dataset: {dataset}, hidden sizes: {hidden_sizes}, learning_rate: {learning_rate}, '\n        f'max_epochs: {max_epochs}, regularization: {regularization_params}'\n    )\n\n    #  do your processing here\n\n    results = {\n        'test_acc': 0.5 + 0.3 * np.random.randn(),\n        'test_loss': np.random.uniform(0, 10),\n        # ...\n    }\n    # the returned result will be written into the database\n    return results\n"
  },
  {
    "path": "ci/examples/example_reschedule.py",
    "content": "import logging\nfrom time import sleep\n\nfrom seml import Experiment\n\nex = Experiment()\n\n\n@ex.reschedule_hook\ndef reschedule(step: int):\n    logging.info(f'Reschedule triggered at step {step}.')\n    return {'checkpoint': step}\n\n\n@ex.automain\ndef run(\n    n_steps: int,\n    checkpoint: int | None = None,\n):\n    logging.info('Starting experiment with the following parameters:')\n    logging.info(f'n_steps: {n_steps}, checkpoint: {checkpoint}')\n\n    if checkpoint is not None:\n        logging.info(f'Resuming from checkpoint: {checkpoint}')\n        # Load your model/state from the checkpoint here\n\n    # Simulate some processing\n    for step in range(checkpoint or 0, n_steps):\n        reschedule(step)\n        logging.info(f'Processing step {step + 1}/{n_steps}')\n        sleep(1.0)\n\n    logging.info('Experiment completed successfully.')\n    return\n"
  },
  {
    "path": "ci/examples/example_reschedule_config.yaml",
    "content": "seml:\n  executable: example_reschedule.py\n  name: example_reschedule_experiment\n  output_dir: logs\n  project_root_dir: .\n  description: An example configuration with rescheduling.\n  reschedule_timeout: 120 # seconds\n\nslurm:\n  - experiments_per_job: 1\n    sbatch_options:\n      mem: 1G\n      cpus-per-task: 1\n      time: 00:03:00\n      partition: cpu_all\n\nfixed:\n  n_steps: 1000\n"
  },
  {
    "path": "docs.md",
    "content": "# `seml`\n\nSEML - Slurm Experiment Management Library.\n\n**Usage**:\n\n```console\n$ seml [OPTIONS] COLLECTION COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...\n```\n\n**Arguments**:\n\n* `COLLECTION`: The name of the database collection to use.  [required]\n\n**Options**:\n\n* `--migration-skip`: Skip the migration of the database collection.\n* `--migration-backup`: Backup the database collection before migration.\n* `-v, --verbose`: Whether to print debug messages.\n* `-V, --version`: Print the version number.\n* `--install-completion`: Install completion for the current shell.\n* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.\n* `--help`: Show this message and exit.\n\n**Commands**:\n\n* `add`: Add experiments to the database as defined...\n* `cancel`: Cancel the Slurm job/job step...\n* `claim-experiment`: Claim an experiment from the database.\n* `clean-db`: Remove orphaned artifacts in the DB from...\n* `clean-jobs`: Cancel empty pending jobs.\n* `configure`: Configure SEML (database, argument...\n* `delete`: Delete experiments by ID or state (cancels...\n* `description`: Manage descriptions of the experiments in...\n* `detect-duplicates`: Prints duplicate experiment configurations.\n* `detect-killed`: Detect experiments where the corresponding...\n* `download-sources`: Download source files from the database to...\n* `drop`: Drop collections from the database.\n* `hold`: Hold queued experiments via SLURM.\n* `launch-worker`: Launch a local worker that runs PENDING jobs.\n* `list`: Lists all collections in the database.\n* `prepare-experiment`: Fetch experiment from database, prepare it...\n* `print-command`: Print the commands that would be executed...\n* `print-experiment`: Print the experiment document.\n* `print-fail-trace`: Prints fail traces of all failed experiments.\n* `print-output`: Print the output of experiments.\n* `project`: Setting up new projects.\n* `queue`: Prints the collections of the given job IDs.\n* `release`: Release held experiments via SLURM.\n* `reload-sources`: Reload stashed source files.\n* `reset`: Reset the state of experiments by setting...\n* `start`: Fetch staged experiments from the database...\n* `start-jupyter`: Start a Jupyter slurm job.\n* `status`: Report status of experiments in the...\n* `update-working-dir`: Change the working directory of...\n\n## `seml add`\n\nAdd experiments to the database as defined in the configuration.\n\n**Usage**:\n\n```console\n$ seml add [OPTIONS] CONFIG_FILES...\n```\n\n**Arguments**:\n\n* `CONFIG_FILES...`: Path to the YAML configuration file for the experiment.  [required]\n\n**Options**:\n\n* `-nh, --no-hash`: By default, we use the hash of the config dictionary to filter out duplicates (by comparing all dictionary values individually). Only disable this if you have a good reason as it is faster.\n* `-ncs, --no-sanity-check`: Disable this if the check fails unexpectedly when using advanced Sacred features or to accelerate adding.\n* `-ncc, --no-code-checkpoint`: Disable this if you want your experiments to use the current codeinstead of the code at the time of adding.\n* `-f, --force`: Force adding the experiment even if it already exists in the database.\n* `-o, --overwrite-params DICT`: Dictionary (passed as a string, e.g. '{\"epochs\": 100}') to overwrite parameters in the config.\n* `-d, --description TEXT`: A description for the experiment.\n* `--no-resolve-descriptions`: Whether to prevent using omegaconf to resolve experiment descriptions\n* `--help`: Show this message and exit.\n\n## `seml cancel`\n\nCancel the Slurm job/job step corresponding to experiments, filtered by ID or state.\n\n**Usage**:\n\n```console\n$ seml cancel [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: PENDING, RUNNING]\n* `-w, --wait`: Wait until all jobs are properly cancelled.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml claim-experiment`\n\nClaim an experiment from the database.\n\n**Usage**:\n\n```console\n$ seml claim-experiment [OPTIONS] SACRED_IDS...\n```\n\n**Arguments**:\n\n* `SACRED_IDS...`: Sacred IDs (_id in the database collection) of the experiments to claim.  [required]\n\n**Options**:\n\n* `--help`: Show this message and exit.\n\n## `seml clean-db`\n\nRemove orphaned artifacts in the DB from runs which have been deleted..\n\n**Usage**:\n\n```console\n$ seml clean-db [OPTIONS]\n```\n\n**Options**:\n\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml clean-jobs`\n\nCancel empty pending jobs.\n\n**Usage**:\n\n```console\n$ seml clean-jobs [OPTIONS] SACRED_IDS...\n```\n\n**Arguments**:\n\n* `SACRED_IDS...`: Sacred IDs (_id in the database collection) of the experiments to claim.  [required]\n\n**Options**:\n\n* `--help`: Show this message and exit.\n\n## `seml configure`\n\nConfigure SEML (database, argument completion, ...).\n\n**Usage**:\n\n```console\n$ seml configure [OPTIONS]\n```\n\n**Options**:\n\n* `--host TEXT`: The host of the MongoDB server.\n* `--port INTEGER`: The port of the MongoDB server.\n* `--database TEXT`: The name of the MongoDB database to use.\n* `--username TEXT`: The username for the MongoDB server.\n* `--password TEXT`: The password for the MongoDB server.\n* `-sf, --ssh-forward`: Configure SSH forwarding settings for MongoDB.\n* `--help`: Show this message and exit.\n\n## `seml delete`\n\nDelete experiments by ID or state (cancels Slurm jobs first if not --no-cancel).\n\n**Usage**:\n\n```console\n$ seml delete [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: STAGED, QUEUED, FAILED, KILLED, INTERRUPTED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-nc, --no-cancel`: Do not cancel the experiments before deleting them.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml description`\n\nManage descriptions of the experiments in a collection.\n\n**Usage**:\n\n```console\n$ seml description [OPTIONS] COMMAND [ARGS]...\n```\n\n**Options**:\n\n* `--help`: Show this message and exit.\n\n**Commands**:\n\n* `delete`: Deletes the description of experiment(s).\n* `list`: Lists the descriptions of all experiments.\n* `set`: Sets the description of experiment(s).\n\n### `seml description delete`\n\nDeletes the description of experiment(s).\n\n**Usage**:\n\n```console\n$ seml description delete [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n### `seml description list`\n\nLists the descriptions of all experiments.\n\n**Usage**:\n\n```console\n$ seml description list [OPTIONS]\n```\n\n**Options**:\n\n* `-u, --update-status`: Whether to update the status of experiments in the database. This can take a while for large collections. Use only if necessary.\n* `--help`: Show this message and exit.\n\n### `seml description set`\n\nSets the description of experiment(s).\n\n**Usage**:\n\n```console\n$ seml description set [OPTIONS] DESCRIPTION\n```\n\n**Arguments**:\n\n* `DESCRIPTION`: The description to set.  [required]\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--no-resolve-descriptions`: Whether to prevent using omegaconf to resolve experiment descriptions\n* `--help`: Show this message and exit.\n\n## `seml detect-duplicates`\n\nPrints duplicate experiment configurations.\n\n**Usage**:\n\n```console\n$ seml detect-duplicates [OPTIONS]\n```\n\n**Options**:\n\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: STAGED, QUEUED, FAILED, KILLED, INTERRUPTED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n\n## `seml detect-killed`\n\nDetect experiments where the corresponding Slurm jobs were killed externally.\n\n**Usage**:\n\n```console\n$ seml detect-killed [OPTIONS]\n```\n\n**Options**:\n\n* `--help`: Show this message and exit.\n\n## `seml download-sources`\n\nDownload source files from the database to the provided path.\n\n**Usage**:\n\n```console\n$ seml download-sources [OPTIONS] TARGET_DIRECTORY\n```\n\n**Arguments**:\n\n* `TARGET_DIRECTORY`: The directory where the source files should be restored.  [required]\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n\n## `seml drop`\n\nDrop collections from the database.\n\nNote: This is a dangerous operation and should only be used if you know what you are doing.\n\n**Usage**:\n\n```console\n$ seml drop [OPTIONS] [PATTERN]\n```\n\n**Arguments**:\n\n* `[PATTERN]`: A regex that must match the collections to print.  [default: .*]\n\n**Options**:\n\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml hold`\n\nHold queued experiments via SLURM.\n\n**Usage**:\n\n```console\n$ seml hold [OPTIONS]\n```\n\n**Options**:\n\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n\n## `seml launch-worker`\n\nLaunch a local worker that runs PENDING jobs.\n\n**Usage**:\n\n```console\n$ seml launch-worker [OPTIONS]\n```\n\n**Options**:\n\n* `-n, --num-experiments INTEGER`: Number of experiments to start. 0: all (staged) experiments   [default: 0]\n* `-nf, --no-file-output`: Do not write the experiment's output to a file.\n* `-ss, --steal-slurm`: Local jobs 'steal' from the Slurm queue, i.e. also execute experiments waiting for execution via Slurm.\n* `-pm, --post-mortem`: Activate post-mortem debugging with pdb.\n* `-d, --debug`: Run a single interactive experiment without Sacred observers and with post-mortem debugging. Implies `--verbose --num-exps 1 --post-mortem --output-to-console`.\n* `-ds, --debug-server`: Run the experiment with a debug server, to which you can remotely connect with e.g. VS Code. Implies `--debug`.\n* `-o, --output-to-console`: Write the experiment's output to the console.\n* `-wg, --worker-gpus TEXT`: The IDs of the GPUs used by the local worker. Will be directly passed to CUDA_VISIBLE_DEVICES.\n* `-wc, --worker-cpus INTEGER`: The number of CPUs used by the local worker. Will be directly passed to OMP_NUM_THREADS.\n* `-we, --worker-env DICT`: Further environment variables to be set for the local worker.\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n\n## `seml list`\n\nLists all collections in the database.\n\n**Usage**:\n\n```console\n$ seml list [OPTIONS] [PATTERN]\n```\n\n**Arguments**:\n\n* `[PATTERN]`: A regex that must match the collections to print.  [default: .*]\n\n**Options**:\n\n* `-p, --progress`: Whether to print a progress bar for iterating over collections.\n* `-u, --update-status`: Whether to update the status of experiments in the database. This can take a while for large collections. Use only if necessary.\n* `-fd, --full-descriptions`: Whether to print full descriptions (possibly with line breaks).\n* `--help`: Show this message and exit.\n\n## `seml prepare-experiment`\n\nFetch experiment from database, prepare it and print the command to execute it.\n\n**Usage**:\n\n```console\n$ seml prepare-experiment [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.  [required]\n* `-v, --verbose`: Whether to print debug messages.\n* `-u, --unobserved`: Run the experiments without Sacred observers.\n* `-pm, --post-mortem`: Activate post-mortem debugging with pdb.\n* `-ssd, --stored-sources-dir TEXT`: Load source files into this directory before starting.\n* `-ds, --debug-server`: Run the experiment with a debug server, to which you can remotely connect with e.g. VS Code. Implies `--debug`.\n* `--help`: Show this message and exit.\n\n## `seml print-command`\n\nPrint the commands that would be executed by `start`.\n\n**Usage**:\n\n```console\n$ seml print-command [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: STAGED, QUEUED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-n, --num-experiments INTEGER`: Number of experiments to start. 0: all (staged) experiments   [default: 0]\n* `-wg, --worker-gpus TEXT`: The IDs of the GPUs used by the local worker. Will be directly passed to CUDA_VISIBLE_DEVICES.\n* `-wc, --worker-cpus INTEGER`: The number of CPUs used by the local worker. Will be directly passed to OMP_NUM_THREADS.\n* `-we, --worker-env DICT`: Further environment variables to be set for the local worker.\n* `--unresolved`: Whether to print the unresolved command.\n* `--no-interpolation`: Whether disable variable interpolation. Only compatible with --unresolved.\n* `--help`: Show this message and exit.\n\n## `seml print-experiment`\n\nPrint the experiment document.\n\n**Usage**:\n\n```console\n$ seml print-experiment [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: PENDING, STAGED, QUEUED, RUNNING, FAILED, KILLED, INTERRUPTED, COMPLETED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-p, --projection KEY`: List of configuration keys, e.g., `config.model`, to additionally print.\n* `-F, --format TEXT`: The format in which to print the experiment document.  [default: yaml]\n* `--help`: Show this message and exit.\n\n## `seml print-fail-trace`\n\nPrints fail traces of all failed experiments.\n\n**Usage**:\n\n```console\n$ seml print-fail-trace [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: FAILED, KILLED, INTERRUPTED]\n* `-p, --projection KEY`: List of configuration keys, e.g., `config.model`, to additionally print.\n* `--help`: Show this message and exit.\n\n## `seml print-output`\n\nPrint the output of experiments.\n\n**Usage**:\n\n```console\n$ seml print-output [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: RUNNING, FAILED, KILLED, INTERRUPTED, COMPLETED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-sl, --slurm`: Whether to print the Slurm output instead of the experiment output.\n* `-h, --head INTEGER`: Print the first n lines of the output.\n* `-t, --tail INTEGER`: Print the last n lines of the output.\n* `--help`: Show this message and exit.\n\n## `seml project`\n\nSetting up new projects.\n\n**Usage**:\n\n```console\n$ seml project [OPTIONS] COMMAND [ARGS]...\n```\n\n**Options**:\n\n* `--help`: Show this message and exit.\n\n**Commands**:\n\n* `init`: Initialize a new project in the given...\n* `list-templates`: List available project templates.\n\n### `seml project init`\n\nInitialize a new project in the given directory.\n\n**Usage**:\n\n```console\n$ seml project init [OPTIONS] [DIRECTORY]\n```\n\n**Arguments**:\n\n* `[DIRECTORY]`: The directory in which to initialize the project.  [default: .]\n\n**Options**:\n\n* `-t, --template TEXT`: The template to use for the project. To view available templates use `seml project list-templates`.  [default: default]\n* `-n, --name TEXT`: The name of the project. (By default inferred from the directory name.)\n* `-u, --username TEXT`: The author name to use for the project. (By default inferred from $USER)\n* `-m, --usermail TEXT`: The author email to use for the project. (By default empty.)\n* `-r, --git-remote TEXT`: The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)\n* `-c, --git-commit TEXT`: The exact git commit to use. May also be a tag or branch (By default latest)\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n### `seml project list-templates`\n\nList available project templates.\n\n**Usage**:\n\n```console\n$ seml project list-templates [OPTIONS]\n```\n\n**Options**:\n\n* `-r, --git-remote TEXT`: The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)\n* `-c, --git-commit TEXT`: The exact git commit to use. May also be a tag or branch (By default latest)\n* `--help`: Show this message and exit.\n\n## `seml queue`\n\nPrints the collections of the given job IDs. If none is specified, all jobs are considered.\n\n**Usage**:\n\n```console\n$ seml queue [OPTIONS] [JOB_IDS]...\n```\n\n**Arguments**:\n\n* `[JOB_IDS]...`: The job IDs of the experiments to get the collection for.\n\n**Options**:\n\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: PENDING, RUNNING]\n* `-a, --all`: Whether to attempt finding the collection of the jobs of all users.\n* `-w, --watch`: Whether to watch the queue.\n* `--help`: Show this message and exit.\n\n## `seml release`\n\nRelease held experiments via SLURM.\n\n**Usage**:\n\n```console\n$ seml release [OPTIONS]\n```\n\n**Options**:\n\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n\n## `seml reload-sources`\n\nReload stashed source files.\n\n**Usage**:\n\n```console\n$ seml reload-sources [OPTIONS]\n```\n\n**Options**:\n\n* `-k, -keep-old`: Keep the old source files in the database.\n* `-b, --batch-ids INTEGER`: Batch IDs (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml reset`\n\nReset the state of experiments by setting their state to STAGED and cleaning their database entry.\nDoes not cancel Slurm jobs.\n\n**Usage**:\n\n```console\n$ seml reset [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-s, --filter-states [STAGED|QUEUED|PENDING|RUNNING|FAILED|KILLED|INTERRUPTED|COMPLETED]`: List of states to filter the experiments by. If empty (\"\"), all states are considered.  [default: FAILED, KILLED, INTERRUPTED]\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-y, --yes`: Automatically confirm all dialogues with yes.\n* `--help`: Show this message and exit.\n\n## `seml start`\n\nFetch staged experiments from the database and run them (by default via Slurm).\n\n**Usage**:\n\n```console\n$ seml start [OPTIONS]\n```\n\n**Options**:\n\n* `-id, --sacred-id INTEGER`: Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n* `-fd, --filter-dict DICT`: Dictionary (passed as a string, e.g. '{\"config.dataset\": \"cora_ml\"}') to filter the experiments by.\n* `-b, --batch-id INTEGER`: Batch ID (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `-d, --debug`: Run a single interactive experiment without Sacred observers and with post-mortem debugging. Implies `--verbose --num-exps 1 --post-mortem --output-to-console`.\n* `-ds, --debug-server`: Run the experiment with a debug server, to which you can remotely connect with e.g. VS Code. Implies `--debug`.\n* `-l, --local`: Run the experiment locally instead of on a Slurm cluster.\n* `-nw, --no-worker`: Do not launch a local worker after setting experiments' state to PENDING.\n* `-n, --num-experiments INTEGER`: Number of experiments to start. 0: all (staged) experiments   [default: 0]\n* `-nf, --no-file-output`: Do not write the experiment's output to a file.\n* `-ss, --steal-slurm`: Local jobs 'steal' from the Slurm queue, i.e. also execute experiments waiting for execution via Slurm.\n* `-pm, --post-mortem`: Activate post-mortem debugging with pdb.\n* `-o, --output-to-console`: Write the experiment's output to the console.\n* `-wg, --worker-gpus TEXT`: The IDs of the GPUs used by the local worker. Will be directly passed to CUDA_VISIBLE_DEVICES.\n* `-wc, --worker-cpus INTEGER`: The number of CPUs used by the local worker. Will be directly passed to OMP_NUM_THREADS.\n* `-we, --worker-env DICT`: Further environment variables to be set for the local worker.\n* `--help`: Show this message and exit.\n\n## `seml start-jupyter`\n\nStart a Jupyter slurm job. Uses SBATCH options defined in settings.py under\nSBATCH_OPTIONS_TEMPLATES.JUPYTER\n\n**Usage**:\n\n```console\n$ seml start-jupyter [OPTIONS]\n```\n\n**Options**:\n\n* `-l, --lab`: Start a jupyter-lab instance instead of jupyter notebook.\n* `-c, --conda-env TEXT`: Start the Jupyter instance in a Conda environment.\n* `-sb, --sbatch-options DICT`: Dictionary (passed as a string, e.g. '{\"gres\": \"gpu:2\"}') to request two GPUs.\n* `--help`: Show this message and exit.\n\n## `seml status`\n\nReport status of experiments in the database collection.\n\n**Usage**:\n\n```console\n$ seml status [OPTIONS]\n```\n\n**Options**:\n\n* `-u, --update-status`: Whether to update the status of experiments in the database. This can take a while for large collections. Use only if necessary.  [default: True]\n* `-p, --projection KEY`: List of configuration keys, e.g., `config.model`, to additionally print.\n* `--help`: Show this message and exit.\n\n## `seml update-working-dir`\n\nChange the working directory of experiments in case you moved the source code to a different location.\n\n**Usage**:\n\n```console\n$ seml update-working-dir [OPTIONS] WORKING_DIR\n```\n\n**Arguments**:\n\n* `WORKING_DIR`: The new working directory for the experiments.  [required]\n\n**Options**:\n\n* `-b, --batch-ids INTEGER`: Batch IDs (batch_id in the database collection) of the experiments. Experiments that were staged together have the same batch_id.\n* `--help`: Show this message and exit.\n"
  },
  {
    "path": "examples/.ruff.toml",
    "content": "[lint]\nignore = [\n    \"F841\", # ignore errors due to unused variables\n]\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Start a Jupyter job\nTo start a Jupyter instance, you can use the convenience function `seml start-jupyter`. This requires having Jupyter Notebook or Jupyter Lab installed in the current (or specified) environment.\n\nTo modify the default Slurm `SBATCH`\noptions, see `seml/settings.py`. The easiest way of changing these is via a file in `$HOME/.config/seml/settings.py`.\nThis file must contain a `SETTINGS` dictionary, structured in the same way as the one in `seml/settings.py`.\n\nAfter the Jupyter instance has successfully started, `seml` will provide useful information such as the hostname and\nport of the instance, e.g.:\n```\nStarted Jupyter job in Slurm job with ID 12345.\nThe logfile of the job is /nfs/homedirs/zuegnerd/libraries/seml/slurm-6322311.out.\nTrying to fetch the machine and port of the Jupyter instance once the job is running... (ctrl-C to cancel).\nJupyter instance is starting up...\nStartup completed. The Jupyter instance is running at 'gpuxx.kdd.in.tum.de:8889'.\nTo stop the job, run 'scancel 12345'.\n```\n# Experiment tracking example\nThis example will show you how to track your experiments using Sacred, how to perform hyperparameter search and how to perform the experiments in a distributed manner on our Slurm cluster.\n\n\n## MongoDB configuration\nBefore starting, please make sure you have your MongoDB credentials stored in `$HOME/.config/seml/mongodb.config`. The easiest way to do so is to run `seml configure`, which will store your credentials in the correct format in the right place.\n\n\n## Experiment configuration\n\nIn `example_config.yaml` we define the parameter configurations that will be run.\nFor a more advanced example with modular structure using\n[Sacred prefixes](https://sacred.readthedocs.io/en/stable/configuration.html#prefix),\nsee the [advanced example configuration](advanced_example_config.yaml) and the corresponding\n[experiment](advanced_example_experiment.py).\n<details><summary><b>Example config file</b></summary>\n\n```yaml\nseml:\n  executable: examples/example_experiment.py\n  name: example_experiment\n  output_dir: examples/logs\n  project_root_dir: ..\n\nslurm:\n  experiments_per_job: 1\n  sbatch_options:\n    gres: gpu:1       # num GPUs\n    mem: 16G          # memory\n    cpus-per-task: 2  # num cores\n    time: 0-08:00     # max time, D-HH:MM\n\n###### BEGIN PARAMETER CONFIGURATION ######\n\nfixed:\n  max_epochs: 500\n\ngrid:\n\n  learning_rate:\n    type: loguniform\n    min: 1e-5\n    max: 1e-1\n    num: 5\n\nrandom:\n  samples: 3\n  seed: 821\n\n  # SEML supports dot-notation for nested dictionaries.\n  regularization_params.dropout:\n    type: uniform\n    min: 0.0\n    max: 0.7\n    seed: 222\n\nsmall_datasets:\n\n  grid:\n    dataset:\n      type: choice\n      options:\n        - small_dataset_1\n        - small_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [16]\n        - [32, 16]  # this will be parsed into a Python list.\n\n  random:\n    samples: 3\n    seed: 2223\n\n    max_epochs:\n       type: randint\n       min: 200\n       max: 1000\n\nlarge_datasets:\n\n  fixed:\n    max_epochs: 1000\n\n  grid:\n    learning_rate:\n      type: choice\n      options:\n        - 0.001\n\n    dataset:\n      type: choice\n      options:\n        - large_dataset_1\n        - large_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [64]\n        - [64, 32]\n```\n</details>\nThere are two special blocks for meta-configuration: `seml` and `slurm`.\n\n### `seml` block\nThe `seml` block is required for every experiment. It has to contain the following values:\n   - `executable`: Name of the Python script containing the experiment. The path should be relative to the `project_root_dir`.\n                   For backward compatibility SEML also supports paths relative to the location of the config file.\n                   In case there are files present both relative to the project root and the config file, the former takes precedence.\nOptionally, it can contain\n   - `name`: Prefix for output file and Slurm job name. Default: Collection name\n   - `output_dir`: Directory to store log files in. Default: Current directory\n   - `conda_environment`: Specifies which Anaconda virtual environment will be activated before the experiment is executed.\n                          Default: The environment used when queuing.\n   - `project_root_dir`: (Relative or absolute) path to the root of the project. seml will then upload all the source\n                         files imported by the experiment to the MongoDB. Moreover, the uploaded source files will be\n                         downloaded before starting an experiment, so any changes to the source files in the project\n                         between staging and starting the experiment will have no effect.\n### `slurm` block\nThe special 'slurm' block contains the slurm parameters. This block and all values are optional. Possible values are:\n   - `experiments_per_job`: Number of parallel experiments to run in each Slurm job. Note that only experiments from the same batch share a job. Default: 1\n   - `max_simultaneous_jobs`: Maximum number of simultaneously running Slurm jobs per job array. Default: No restriction\n   - `sbatch_options_template`: Name of a custom template of `SBATCH` options. Define your own templates in `settings.py`\n     under `SBATCH_OPTIONS_TEMPLATES`, e.g. for long-running jobs, CPU-only jobs, etc.\n   - `sbatch_options`: dictionary that contains custom values that will be passed to `sbatch`, specifying e.g. the\n                       memory and the number of GPUs to be allocated. See [here](https://slurm.schedmd.com/sbatch.html)\n                       for possible parameters of `sbatch` (prepended dashes are not required). Values provided here\n                       overwrite any values defined in a `SBATCH` options template.\n\n### Sub-configurations\nIn the `small_datasets` and `large_datasets` (names are of course only examples; you can name sub-configs as you like) we have specified different sets of parameters to try.\nThey will be combined with the parameters in `grid` in the root of the document.\n\nIf a specific configuration (e.g. `large_datasets`) defines the same parameters as a higher-level configuration (e.g., the \"root\" configuration),\n they will override the ones defined before, e.g. the learning rate in the example above.\nThis means that for all configurations in the `large_datasets` the learning rate will be `0.001` and not `0.01` or\n`0.05` as defined in the root of the document.\nThis can be nested arbitrarily deeply (be aware of combinatorial explosion of the parameter space, though).\n\nIf a parameter is defined in (at least) two **different blocks** in `[grid, random, fixed]` on the same level, `seml` will throw an error to avoid ambiguity.\nIf a parameter is re-defined in a sub-configuration, the redefinition overrides any previous definitions of that parameter.\n\n### Grid parameters\nIn an experiment config, under `grid` you can define parameters that should be sampled from a regular grid. Currently supported\nare:\n   - `choice`: List the different values you want to evaluate under `options`.\n   - `range`: Specify the `min`, `max`, and `step`. Parameter values will be generated using `np.arange(min, max, step)`.\n   - `uniform`: Specify the `min`, `max`, and `num`. Parameter values will be generated using\n                `np.linspace(min, max, num, endpoint=True)`\n   - `loguniform`: Specify `min`, `max`, and `num`. Parameter values will be uniformly generated in log space (base 10).\n\nAdditionally, `grid` parameters might be coupled by setting the `zip_id` property. All parameters with the same `zip_id` are treated as a single dimension when constructing the cartesian product of parameters. This ensures that zipped parameters only change jointly.\n\n### Random parameters\nUnder 'random' you can specify parameters for which you want to try several random values. Specify the number\nof samples per parameter with the `samples` value and optionally the random seed with `seed` as in the examples below. Supported parameter types are:\n  - `choice`: Randomly samples `<samples>` entries (with replacement) from the list in `options`\n  - `uniform`: Uniformly samples between `min` and `max` as specified in the parameter dict.\n  - `loguniform`:  Uniformly samples in log space between `min` and `max` as specified in the parameter dict.\n  - `randint`: Randomly samples integers between `min` (included) and `max` (excluded).\n\n### Named Configurations\n`sacred`, the on which experiments are based on, allows to define subgroups of configurations via its [named configurations](https://sacred.readthedocs.io/en/stable/configuration.html#named-configurations) feature. These can either be defined in external files (yaml, json, ...) or in functions decorated with `experiment.named_config`. SEML also supports this functionality by defining parameter groups that have the prefix `'+'`. Two config values can be defined for such parameter groups:\n- `name`: The name of the named config, i.e. the name of the python function or the path to the file to load\n- `priority`: Defines in which order the named configs will be loaded. Configs with lower priority will be listed first and thus resolved first. Therefore, the highest priority item will have the highest precedence. If no priority is given, this will be treated as `infinity`. Ties are broken based on the name of the named config.\n\n\n\n### Variable Interpolation\nConfig values can be interpolated relative to other values using [OmegaConf](https://omegaconf.readthedocs.io/en/2.3_branch/usage.html#variable-interpolation). For example:\n\n```yaml\nmodel.name: resnet\ndataset: mnist\nsomething.name: ${model.name}_${dataset} # will have value: resnet_mnist\n```\n\n## Add experiments to database\n\nAll SEML commands follow the pattern\n```\nseml [database_collection_name] [command] [command_options]\n```\n\nTo insert the experiments to the database, open a terminal on a machine with access to the `Slurm` system. Move to this directory and run\n\n```\nseml seml_example add example_config.yaml\n```\n\nIf you open your MongoDB (e.g. with the software `robo3t`), you should now find a collection `seml_example` with the staged experiments.\nNote that the collection name is specified _before_ the operation (`add`).\n\nTo see what the option `--force` does, run the above command again. The output should now read something like:\n\n```\n72 of 72 experiments were already found in the database. They were not added again.\n```\n\nThat is, the script checks whether experiments with the same configuration are already in the database collection.\nIn this case, they are not added to the database to avoid redundant computations. In order to force add duplicates to the database, use the `--force` argument.\n\nAll experiments are now already in the database collection you specified and in the STAGED state.\n\n## Run experiments using Slurm\nTo run the staged experiments on the Slurm cluster, run:\n```bash\nseml seml_example start\n```\nThis will start all experiments in the MongoDB collection `seml_example` that currently are in the STAGED state.\n\n## Run experiments locally\nYou can also run your experiments locally without Slurm. For this, add the `--local` option:\n```bash\nseml seml_example start --local\n```\nYou can even have multiple local workers running jobs in parallel. To add a local worker, run\n```bash\nseml seml_example launch-worker --worker-gpus=\"1\" --worker-cpus=8\n```\nIn this example, the worker will use the GPU with ID 1 (i.e., set `CUDA_VISIBLE_DEVICES=\"1\"`) and can use 8 CPU cores.\n\nThe `--steal-slurm` option allows local workers to pop experiments from the Slurm queue. Since SEML checks the\ndatabase state of each experiment before actually executing it via Slurm, there is no risk of running duplicate\nexperiments.\n\n## Debugging experiments\n\nTo run an interactive debug session on Slurm (or locally) you can start an experiment with the `--debug` option.\n\nFor even more convenience you can also use VS Code for a remote debug session. First make sure that your experiments were added to the database with the `--no-code-checkpoint` option:\n\n```\nseml seml_example add example_config.yaml -ncc\n```\n\nThis will prevent the caching of your code in the MongoDB and allow you to directly run the code that is in your working directory, set breakpoints and interactively step through your code in VS Code.\n\nTo start a remote debug server run:\n\n```\nseml seml_example start --debug-server\n```\n\nThis will add your experiment to the queue, wait for the necessary resources to be assigned, spawn a debug process on the server and print the debug server's IP address and port number. The experiment will only start running once the VS Code client is attached.\n\nTo attach to the debug server you need to add the printed IP address and port number to the `.vscode/launch.json` config:\n```\n{\n    \"configurations\": [\n        {\n            \"name\": \"Python: Attach\",\n            \"type\": \"python\",\n            \"request\": \"attach\",\n            \"connect\": {\n                \"host\": \"YOUR_DEBUG_SERVER_IP\",\n                \"port\": YOUR_DEBUG_SERVER_PORT\n            }\n        }\n    ]\n}\n```\nThe IP address and port number of the debug server might change at every start, so make sure to update the `host` and `port` launch config.\nNote: The \"restart\" operation of the VS Code Debugger is not supported.\n\n## Running multiple experiments per Slurm job\nOften a single experiment does not fully utilize the GPU and requires much less GPU RAM than available. Thus, we can often\nrun multiple experiments per Slurm job (which commonly uses a single GPU) to increase the throughput of our experiments.\nThis can be done by setting the `experiments_per_job` argument in the `slurm` block of the config file.\n\nNote that this will only run your own experiments in parallel on a GPU. It will never run\nyour experiments on a GPU that is reserved by another user's job.\nFurthermore, only experiments from the same batch share jobs.\n\n## Check the status of your Slurm jobs\n\nYou can check the status of your Slurm jobs by running `squeue` or `seml seml_example status`\nin the terminal. To check the console output of a experiment, open the corresponding logfile, e.g. `cat slurm-564.out`.\n\nTo check whether some experiments may have failed due to errors, you can run:\n```bash\nseml seml_example status\n```\n\nYou can cancel (interrupt) all pending and running experiments with\n```bash\nseml seml_example cancel\n```\n\nYou can reset all failed, killed, or interrupted experiments to STAGED with\n```bash\nseml seml_example reset\n```\n\nYou can delete all staged, failed, killed, or interrupted experiments with\n```bash\nseml seml_example delete\n```\n\nThese three commands also support passing a specific Sacred ID and a custom list of states.\n\nMoreover, you can specifically cancel/reset/delete experiments that match a custom dictionary, e.g.\n```bash\nseml seml_example cancel --filter-dict '{\"config.dataset\":\"cora_ml\", \"config.hidden_sizes\": [16]}'\n```\n\nFinally, you can manually detect experiments whose corresponding Slurm jobs were killed unexpectedly with\n```bash\nseml seml_example detect-killed\n```\n(Detection is run automatically when executing the `status`, `delete`, `reset`, and `cancel` commands and therefore rarely necessary to do manually.)\n\n### Batches\n`seml` assigns each experiment a batch ID, where all experiments that were staged together get the same batch ID.\nYou can use this to cancel all the experiments from the last configuration that you've started, e.g. if you find a bug.\nUse\n```bash\nseml seml_example cancel --batch-id i\n```\nor equivalently\n ```bash\nseml seml_example cancel --filter-dict '{\"batch_id\": i}'\n```\nto cancel all jobs from batch `i`.\n\n## Retrieve and evaluate results\nSee the [example notebook](notebooks/experiment_results.ipynb) for an example of how to retrieve and evaluate our toy experiment's results.\n\n\n## Command chaining\n`seml` also supports command chaining to execute multiple `seml` commands sequentially, i.e.,\n```bash\nseml seml_example add advanced_example_config.yaml start\n```\nto add a config file and start it immediately after or\n```\nseml seml_example cancel -y reset -y reload-sources start\n```\nto cancel experiments, reset them, reload their source files and restarting them.\n"
  },
  {
    "path": "examples/advanced_example_config.yaml",
    "content": "# Experiment configuration file.\n#\n# There are two special blocks. The 'seml' block is required for every experiment.\n# It has to contain the following values:\n# executable:        Name of the Python script containing the experiment. The path should be relative to the `project_root_dir`.\n#                    For backward compatibility SEML also supports paths relative to the location of the config file.\n#                    In case there are files present both relative to the project root and the config file,\n#                    the former takes precedence.\n# It can optionally also contain the following values:\n# name:              Prefix for output file and Slurm job name. Default: Collection name\n# output_dir:        Directory to store log files in. Default: Current directory\n# conda_environment: Specifies which Anaconda virtual environment will be activated before the experiment is executed.\n#                    Default: The environment used when queuing.\n# project_root_dir:  (Relative or absolute) path to the root of the project. seml will then upload all the source\n#                    files imported by the experiment to the MongoDB. Moreover, the uploaded source files will be\n#                    downloaded before starting an experiment, so any changes to the source files in the project\n#                    between queueing and starting the experiment will have no effect.\n#\n# The special 'slurm' block contains the slurm parameters. This block and all values are optional. Possible values are:\n# experiments_per_job:   Number of parallel experiments to run in each Slurm job.\n#                        Note that only experiments from the same batch share a job. Default: 1\n# max_simultaneous_jobs: Maximum number of simultaneously running Slurm jobs per job array. Default: No restriction\n# sbatch_options:        dictionary that contains custom values that will be passed to `sbatch`, specifying e.g.\n#                        the memory and number of GPUs to be allocated (prepended dashes are not required). See\n#                        https://slurm.schedmd.com/sbatch.html for all possible options.\n#\n# Parameters under 'fixed' will be used for all the experiments.\n#\n# Under 'grid' you can define parameters that should be sampled from a regular grid. Options are:\n#   - choice:     List the different values you want to evaluate under 'choices' as in the example below.\n#   - range:      Specify the min, max, and step. Parameter values will be generated using np.arange(min, max, step).\n#   - uniform:    Specify the min, max, and num. Parameter values will be generated using\n#                 np.linspace(min, max, num, endpoint=True)\n#   - loguniform: Specify min, max, and num. Parameter values will be uniformly generated in log space (base 10).\n# Additionally, one may supply the 'zip_id' argument to zip multiple parameters to a single dimension.\n# This causes these parameters to only change jointly. All parameters within a group must have the same number of options.\n#\n#\n# Under 'random' you can specify parameters for which you want to try several random values. Specify the number\n# of samples per parameter with the 'samples' value as in the examples below.\n# Specify the the seed under the 'random' dict or directly for the desired parameter(s).\n# Supported parameter types are:\n#   - choice:      Randomly samples <samples> entries (with replacement) from the list in parameter['options']\n#   - uniform:     Uniformly samples between 'min' and 'max' as specified in the parameter dict.\n#   - loguniform:  Uniformly samples in log space between 'min' and 'max' as specified in the parameter dict.\n#   - randint:     Randomly samples integers between 'min' (included) and 'max' (excluded).\n#\n# The configuration file can be nested (as the example below) so that we can run different parameter sets\n# e.g. for different datasets or models.\n# We take the cartesian product of all `grid` parameters on a path and sample all random parameters on the path.\n# The number of random parameters sampled will be max{n_samples} of all n_samples on the path. This is done because\n# we need the same number of samples from all random parameters in a configuration.\n#\n# More specific settings (i.e., further down the hierarchy) always overwrite more general ones.\n\nseml:\n  executable: advanced_example_experiment.py\n  name: advanced_example_experiment\n  output_dir: logs\n  project_root_dir: .\n  description: \"An advanced example configuration. We can also use variable interpolation here: ${config.model.model_type}\"\n  reschedule_timeout: 300 # The time (in seconds) that are left on the job before SEML will try to reschedule unfinished experiments.\n  # Note that you have to implement a `reschedule_hook` to use this feature.\n\nslurm:\n  - experiments_per_job: 1\n    max_simultaneous_jobs: 4 # Restrict number of simultaneously running jobs per job array\n    sbatch_options:\n      gres: gpu:1 # num GPUs\n      mem: 16G # memory\n      cpus-per-task: 2 # num cores\n      time: 0-08:00 # max time, D-HH:MM\n      partition: gpu_gtx1080 # use the 1080ti partition\n  # We can also increase the number of jobs if we run on A100s\n  - experiments_per_job: 4\n    max_simultaneous_jobs: 4 # Restrict number of simultaneously running jobs per job array\n    sbatch_options:\n      gres: gpu:1 # num GPUs\n      mem: 16G # memory\n      cpus-per-task: 8 # num cores\n      time: 0-08:00 # max time, D-HH:MM\n      partition: gpu_a100 # Use A100\n\n###### BEGIN PARAMETER CONFIGURATION ######\n\nfixed:\n  training.patience: 20\n  training.num_epochs: 100\n  optimization:\n    optimizer_type: Adam\n  +batchnorm.priority: 2\n\ngrid:\n  # SEML supports dot-notation for nested parameter dictionaries. E.g., `model.model_type` resolves to\n  # {'model': {'model_type': xxx}}\n  model.model_type:\n    type: choice\n    options:\n      - variant_1\n      - variant_2\n    zip_id: model_dataset # The model_type jointly changes with all other parameters of the mode_dataset group\n\n  # The `+` prefix indicates that a named config should be used. The `name` attribute selects which named config is run\n  # Here, setting `batchnorm.name` to `batchnorm`, will run the named config `batchnorm` (see `advanced_example_experiment.py`)\n  # Setting `batchnorm.priority` to `2`, will affect the order in which the named configs are loaded\n  +batchnorm.name:\n    type: choice\n    options:\n      - batchnorm\n      - no_batchnorm\n\n  # Instead of setting the `name` attribute, you can equivalently set `preprocessing` to a the corresponding named config.\n  # seml will infer to set the `name` attribute for you\n  +preprocessing:\n    type: choice\n    options:\n      - preprocessing_none\n      - preprocessing_normalize\n\n  # You can also specify named configs from file paths, e.g. yaml files. These paths are expected to be relative to `project_root_dir`.\n  +augmentation:\n    type: choice\n    options:\n      - config/flip_augmentation.yaml\n\n  optimization.regularization.weight_decay:\n    type: loguniform\n    min: 1e-6\n    max: 1e-3\n    num: 4\n\nrandom:\n  samples: 5\n  seed: 7059\n  model.model_params.dropout:\n    type: uniform\n    min: 0.0\n    max: 0.5\n\nlarge_datasets:\n  grid:\n    data.dataset:\n      type: choice\n      options:\n        - large_dataset_1\n        - large_dataset_2\n      zip_id: model_dataset # Use variant_1 for large_dataset_1 and variant_2 for large_dataset_2\n\n    model.model_params.hidden_sizes:\n      type: choice\n      options:\n        - [64]\n        - [64, 32]\n\nsmall_datasets:\n  grid:\n    data.dataset:\n      type: choice\n      options:\n        - small_dataset_1\n        - small_dataset_2\n      zip_id: model_dataset # Use variant_1 for small_dataset_1 and variant_2 for small_dataset_2\n\n    model.model_params.hidden_sizes:\n      type: choice\n      options:\n        - [32]\n        - [32, 16]\n"
  },
  {
    "path": "examples/advanced_example_experiment.py",
    "content": "\"\"\"\nThis is an advanced experiment example, which makes use of sacred's captured functions with prefixes.\nWe wrap all the experiment-specific functionality inside the \"ExperimentWrapper\" class, and define methods with sacred's\n@ex.capture decorator. This allows a modular design of the configuration, where certain sub-dictionaries (e.g., \"data\")\nare parsed by a specific method. This avoids having one large \"main\" function which takes all parameters as input.\n\"\"\"\n\nimport numpy as np\nfrom seml import Experiment\n\nex = Experiment()\n\n\n# Named configs can be used to define subconfigurations in a modular way. They can be composed in the experiment's configuration yaml file.\n\n\n@ex.named_config\ndef preprocessing_none():\n    \"\"\"A named configuration that can be enabled in the configuration yaml file\"\"\"\n    preprocessing = {\n        \"mean\": 0.0,\n        \"std\": 1.0,\n    }\n\n\n@ex.named_config\ndef preprocessing_normalize():\n    \"\"\"A named configuration that can be enabled in the configuration yaml file\"\"\"\n    preprocessing = {\n        \"mean\": 0.377,\n        \"std\": 0.23,\n    }\n\n\n@ex.named_config\ndef batchnorm():\n    \"\"\"A named configuration that can be enabled in the configuration yaml file\"\"\"\n    model = {\"batchnorm\": True}\n\n\n@ex.named_config\ndef no_batchnorm():\n    \"\"\"A named configuration that can be enabled in the configuration yaml file\"\"\"\n    model = {\"batchnorm\": False, \"residual\": False}\n\n\n@ex.config\ndef config():\n    name = \"${config.model.model_type}_${config.data.dataset}\"\n\n\nclass ModelVariant1:\n    \"\"\"\n    A dummy model variant 1, which could, e.g., be a certain model or baseline in practice.\n    \"\"\"\n\n    def __init__(self, hidden_sizes, dropout, batchnorm, residual):\n        self.hidden_sizes = hidden_sizes\n        self.dropout = dropout\n        self.batchnorm = batchnorm\n        self.residual = residual\n\n\nclass ModelVariant2:\n    \"\"\"\n    A dummy model variant 2, which could, e.g., be a certain model or baseline in practice.\n    \"\"\"\n\n    def __init__(self, hidden_sizes, dropout, batchnorm, residual):\n        self.hidden_sizes = hidden_sizes\n        self.dropout = dropout\n        self.batchnorm = batchnorm\n        self.residual = residual\n\n\nclass ExperimentWrapper:\n    \"\"\"\n    A simple wrapper around a sacred experiment, making use of sacred's captured functions with prefixes.\n    This allows a modular design of the configuration, where certain sub-dictionaries (e.g., \"data\") are parsed by\n    specific method. This avoids having one large \"main\" function which takes all parameters as input.\n    \"\"\"\n\n    def __init__(self, init_all=True):\n        if init_all:\n            self.init_all()\n\n    # With the prefix option we can \"filter\" the configuration for the sub-dictionary under \"data\".\n    @ex.capture(prefix=\"data\")\n    def init_dataset(self, dataset):\n        \"\"\"\n        Perform dataset loading, preprocessing etc.\n        Since we set prefix=\"data\", this method only gets passed the respective sub-dictionary, enabling a modular\n        experiment design.\n        \"\"\"\n        if dataset == \"large_dataset_1\":\n            self.data = \"load_dataset_here\"\n        elif dataset == \"large_dataset_2\":\n            self.data = \"and so on\"\n        # ...\n        else:\n            self.data = \"...\"\n\n    @ex.capture(prefix=\"model\")\n    def init_model(\n        self,\n        model_type: str,\n        model_params: dict,\n        batchnorm: bool,\n        residual: bool = True,\n    ):\n        if model_type == \"variant_1\":\n            # Here we can pass the \"model_params\" dict to the constructor directly, which can be very useful in\n            # practice, since we don't have to do any model-specific processing of the config dictionary.\n            self.model = ModelVariant1(\n                **model_params, batchnorm=batchnorm, residual=residual\n            )\n        elif model_type == \"variant_2\":\n            self.model = ModelVariant2(\n                **model_params, batchnorm=batchnorm, residual=residual\n            )\n\n    @ex.capture(prefix=\"optimization\")\n    def init_optimizer(self, regularization: dict, optimizer_type: str):\n        weight_decay = regularization[\"weight_decay\"]\n        self.optimizer = optimizer_type  # initialize optimizer\n\n    @ex.capture(prefix=\"preprocessing\")\n    def init_preprocessing(self, mean: float, std: float):\n        self.preprocessing_parameters = (mean, std)\n\n    @ex.capture(prefix=\"augmentation\")\n    def init_augmentation(self, flip: bool):\n        self.augmentation_parameters = (flip,)\n\n    def init_all(self):\n        \"\"\"\n        Sequentially run the sub-initializers of the experiment.\n        \"\"\"\n        self.init_dataset()\n        self.init_model()\n        self.init_optimizer()\n        self.init_preprocessing()\n        self.init_augmentation()\n\n    @ex.capture(prefix=\"training\")\n    def train(self, patience, num_epochs):\n        # everything is set up\n        for e in range(num_epochs):\n            # simulate training\n\n            # calling reschedule hook\n            reschedule_hook(model_weights={}, step=e)\n            continue\n        results = {\n            \"test_acc\": 0.5 + 0.3 * np.random.randn(),\n            \"test_loss\": np.random.uniform(0, 10),\n            # ...\n        }\n        return results\n\n\n# We can call this command, e.g., from a Jupyter notebook with init_all=False to get an \"empty\" experiment wrapper,\n# where we can then for instance load a pretrained model to inspect the performance.\n@ex.command(unobserved=True)\ndef get_experiment(init_all=False):\n    print(\"get_experiment\")\n    experiment = ExperimentWrapper(init_all=init_all)\n    return experiment\n\n\n# This function will be called when the reschedule is triggered.\n# It should save the current state of the experiment and return a\n# dictionary that may be used to update the configuration upon rescheduling.\n# You are responsible for implementing the actual saving/loading of the experiment state\n# due to the updated config.\n@ex.reschedule_hook\ndef reschedule_hook(model_weights, step, **kwargs):\n    # Here you would save the current state of the experiment\n    # and return any necessary configuration updates.\n\n    # !!! You will need to call this function regularly from within your training loop\n    # to check if rescheduling is needed.\n    # Pass everything you need to store your state to this function.\n    return {\"checkpoint_path\": \"path/to/saved/checkpoint\"}\n\n\n# This function will be called by default. Note that we could in principle manually pass an experiment instance,\n# e.g., obtained by loading a model from the database or by calling this from a Jupyter notebook.\n@ex.automain\ndef train(experiment=None):\n    if experiment is None:\n        experiment = ExperimentWrapper()\n    return experiment.train()\n"
  },
  {
    "path": "examples/config/flip_augmentation.yaml",
    "content": "augmentation:\n  flip: True"
  },
  {
    "path": "examples/example_config.yaml",
    "content": "# Experiment configuration file.\n#\n# There are two special blocks. The 'seml' block is required for every experiment.\n# It has to contain the following values:\n# executable:        Name of the Python script containing the experiment. The path should be relative to the `project_root_dir`.\n#                    For backward compatibility SEML also supports paths relative to the location of the config file.\n#                    In case there are files present both relative to the project root and the config file,\n#                    the former takes precedence.\n# It can optionally also contain the following values:\n# name:              Prefix for output file and Slurm job name. Default: Collection name\n# output_dir:        Directory to store log files in. Default: Current directory\n# conda_environment: Specifies which Anaconda virtual environment will be activated before the experiment is executed.\n#                    Default: The environment used when queuing.\n# project_root_dir:  (Relative or absolute) path to the root of the project. seml will then upload all the source\n#                    files imported by the experiment to the MongoDB. Moreover, the uploaded source files will be\n#                    downloaded before starting an experiment, so any changes to the source files in the project\n#                    between queueing and starting the experiment will have no effect.\n#\n# The special 'slurm' block contains the slurm parameters. This block and all values are optional. Possible values are:\n# experiments_per_job:     Number of parallel experiments to run in each Slurm job.\n#                          Note that only experiments from the same batch share a job. Default: 1\n# max_simultaneous_jobs:   Maximum number of simultaneously running Slurm jobs per job array. Default: No restriction\n# sbatch_options_template: Name of a custom template of `SBATCH` options. Define your own templates in `settings.py`\n#                          under `SBATCH_OPTIONS_TEMPLATES`, e.g. for long-running jobs, CPU-only jobs, etc.\n# sbatch_options:          dictionary that contains custom values that will be passed to `sbatch`, specifying e.g.\n#                          the memory and number of GPUs to be allocated (prepended dashes are not required). See\n#                          https://slurm.schedmd.com/sbatch.html for all possible options.\n#\n# Parameters under 'fixed' will be used for all the experiments.\n#\n# Under 'grid' you can define parameters that should be sampled from a regular grid. Options are:\n#   - choice:     List the different values you want to evaluate under 'choices' as in the example below.\n#   - range:      Specify the min, max, and step. Parameter values will be generated using np.arange(min, max, step).\n#   - uniform:    Specify the min, max, and num. Parameter values will be generated using\n#                 np.linspace(min, max, num, endpoint=True)\n#   - loguniform: Specify min, max, and num. Parameter values will be uniformly generated in log space (base 10).\n#\n# Under 'random' you can specify parameters for which you want to try several random values. Specify the number\n# of samples per parameter with the 'samples' value as in the examples below.\n# Specify the the seed under the 'random' dict or directly for the desired parameter(s).\n# Supported parameter types are:\n#   - choice:      Randomly samples <samples> entries (with replacement) from the list in parameter['options']\n#   - uniform:     Uniformly samples between 'min' and 'max' as specified in the parameter dict.\n#   - loguniform:  Uniformly samples in log space between 'min' and 'max' as specified in the parameter dict.\n#   - randint:     Randomly samples integers between 'min' (included) and 'max' (excluded).\n#\n# The configuration file can be nested (as the example below) so that we can run different parameter sets\n# e.g. for different datasets or models.\n# We take the cartesian product of all `grid` parameters on a path and sample all random parameters on the path.\n# The number of random parameters sampled will be max{n_samples} of all n_samples on the path. This is done because\n# we need the same number of samples from all random parameters in a configuration.\n#\n# More specific settings (i.e., further down the hierarchy) always overwrite more general ones.\n\nseml:\n  executable: examples/example_experiment.py\n  name: example_experiment\n  output_dir: examples/logs\n  project_root_dir: ..\n  description: An example configuration.\n\nslurm:\n  - experiments_per_job: 1\n    sbatch_options:\n      gres: gpu:0\n      mem: 1G\n      cpus-per-task: 1\n      time: 0-08:00\n      partition: cpu_all,cpu_large\n  - experiments_per_job: 4\n    sbatch_options:\n      gres: gpu:1 # num GPUs\n      mem: 1G # memory\n      cpus-per-task: 1 # num cores\n      time: 0-08:00 # max time, D-HH:MM\n      partition: gpu_gtx1080\n  - experiments_per_job: 16\n    sbatch_options:\n      gres: gpu:1 # num GPUs\n      mem: 1G # memory\n      cpus-per-task: 1 # num cores\n      time: 0-08:00 # max time, D-HH:MM\n      partition: gpu_a100\n\n###### BEGIN PARAMETER CONFIGURATION ######\n\nfixed:\n  max_epochs: 500\n\ngrid:\n  learning_rate:\n    type: loguniform\n    min: 1e-5\n    max: 1e-1\n    num: 1\n\nrandom:\n  samples: 1\n  seed: 821\n\n  # SEML supports dot-notation for nested dictionaries.\n  regularization_params.dropout:\n    type: uniform\n    min: 0.0\n    max: 0.7\n    seed: 222\n\nsmall_datasets:\n  grid:\n    dataset:\n      type: choice\n      options:\n        - small_dataset_1\n        - small_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [16]\n        - [32, 16] # this will be parsed into a Python list.\n\n  random:\n    samples: 3\n    seed: 2223\n\n    max_epochs:\n      type: randint\n      min: 200\n      max: 1000\n\nlarge_datasets:\n  fixed:\n    max_epochs: 1000\n\n  grid:\n    learning_rate:\n      type: choice\n      options:\n        - 0.001\n\n    dataset:\n      type: choice\n      options:\n        - large_dataset_1\n        - large_dataset_2\n\n    hidden_sizes:\n      type: choice\n      options:\n        - [64]\n        - [64, 32]\n"
  },
  {
    "path": "examples/example_experiment.py",
    "content": "import logging\n\nimport numpy as np\nfrom seml import Experiment\n\nex = Experiment()\n\n\n@ex.automain\ndef run(\n    dataset: str,\n    hidden_sizes: list,\n    learning_rate: float,\n    max_epochs: int,\n    regularization_params: dict,\n):\n    # Note that regularization_params contains the corresponding sub-dictionary from the configuration.\n    logging.info(\"Received the following configuration:\")\n    logging.info(\n        f\"Dataset: {dataset}, hidden sizes: {hidden_sizes}, learning_rate: {learning_rate}, \"\n        f\"max_epochs: {max_epochs}, regularization: {regularization_params}\"\n    )\n\n    #  do your processing here\n\n    results = {\n        \"test_acc\": 0.5 + 0.3 * np.random.randn(),\n        \"test_loss\": np.random.uniform(0, 10),\n        # ...\n    }\n    # the returned result will be written into the database\n    return results\n"
  },
  {
    "path": "examples/logs/.gitignore",
    "content": "# Ignore everything in this directory\n*\n# Except this file\n!.gitignore"
  },
  {
    "path": "examples/notebooks/experiment_results.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"/nfs/homedirs/zuegnerd/libraries/seml/seml/database.py:7: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\\n\",\n      \"  from tqdm.autonotebook import tqdm\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import seml\\n\",\n    \"from matplotlib import pyplot as plt\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {\n    \"tags\": []\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"application/vnd.jupyter.widget-view+json\": {\n       \"model_id\": \"373af497df2e440bb7ba65bb735be9a2\",\n       \"version_major\": 2,\n       \"version_minor\": 0\n      },\n      \"text/plain\": [\n       \"HBox(children=(FloatProgress(value=0.0, max=72.0), HTML(value='')))\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"application/vnd.jupyter.widget-view+json\": {\n       \"model_id\": \"2c9a3e5d482f4243a743dcebddd2ede1\",\n       \"version_major\": 2,\n       \"version_minor\": 0\n      },\n      \"text/plain\": [\n       \"HBox(children=(FloatProgress(value=0.0, max=72.0), HTML(value='')))\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"/nfs/homedirs/zuegnerd/libraries/seml/seml/evaluation.py:80: FutureWarning: pandas.io.json.json_normalize is deprecated, use pandas.json_normalize instead\\n\",\n      \"  parsed = pd.io.json.json_normalize(parsed, sep='.')\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"results = seml.get_results(\\\"seml_example\\\", to_data_frame=True)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>_id</th>\\n\",\n       \"      <th>config.dataset</th>\\n\",\n       \"      <th>config.db_collection</th>\\n\",\n       \"      <th>config.hidden_sizes</th>\\n\",\n       \"      <th>config.learning_rate</th>\\n\",\n       \"      <th>config.max_epochs</th>\\n\",\n       \"      <th>config.overwrite</th>\\n\",\n       \"      <th>config.regularization_params.dropout</th>\\n\",\n       \"      <th>config.seed</th>\\n\",\n       \"      <th>result.test_acc</th>\\n\",\n       \"      <th>result.test_loss</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>small_dataset_1</td>\\n\",\n       \"      <td>seml_example</td>\\n\",\n       \"      <td>[16]</td>\\n\",\n       \"      <td>0.00001</td>\\n\",\n       \"      <td>519</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0.440696</td>\\n\",\n       \"      <td>49592375</td>\\n\",\n       \"      <td>0.738594</td>\\n\",\n       \"      <td>0.333637</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>small_dataset_1</td>\\n\",\n       \"      <td>seml_example</td>\\n\",\n       \"      <td>[16]</td>\\n\",\n       \"      <td>0.00001</td>\\n\",\n       \"      <td>626</td>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>0.446743</td>\\n\",\n       \"      <td>912760071</td>\\n\",\n       \"      <td>0.714019</td>\\n\",\n       \"      <td>6.445487</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>2</th>\\n\",\n       \"      <td>3</td>\\n\",\n       \"      <td>small_dataset_1</td>\\n\",\n       \"      <td>seml_example</td>\\n\",\n       \"      <td>[16]</td>\\n\",\n       \"      <td>0.00001</td>\\n\",\n       \"      <td>768</td>\\n\",\n       \"      <td>3</td>\\n\",\n       \"      <td>0.011091</td>\\n\",\n       \"      <td>946656409</td>\\n\",\n       \"      <td>0.268248</td>\\n\",\n       \"      <td>8.519550</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>3</th>\\n\",\n       \"      <td>4</td>\\n\",\n       \"      <td>small_dataset_1</td>\\n\",\n       \"      <td>seml_example</td>\\n\",\n       \"      <td>[32, 16]</td>\\n\",\n       \"      <td>0.00001</td>\\n\",\n       \"      <td>519</td>\\n\",\n       \"      <td>4</td>\\n\",\n       \"      <td>0.440696</td>\\n\",\n       \"      <td>488101063</td>\\n\",\n       \"      <td>0.334359</td>\\n\",\n       \"      <td>9.716397</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>4</th>\\n\",\n       \"      <td>5</td>\\n\",\n       \"      <td>small_dataset_1</td>\\n\",\n       \"      <td>seml_example</td>\\n\",\n       \"      <td>[32, 16]</td>\\n\",\n       \"      <td>0.00001</td>\\n\",\n       \"      <td>626</td>\\n\",\n       \"      <td>5</td>\\n\",\n       \"      <td>0.446743</td>\\n\",\n       \"      <td>682478206</td>\\n\",\n       \"      <td>0.248766</td>\\n\",\n       \"      <td>8.092757</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\"\n      ],\n      \"text/plain\": [\n       \"   _id   config.dataset config.db_collection config.hidden_sizes  \\\\\\n\",\n       \"0    1  small_dataset_1         seml_example                [16]   \\n\",\n       \"1    2  small_dataset_1         seml_example                [16]   \\n\",\n       \"2    3  small_dataset_1         seml_example                [16]   \\n\",\n       \"3    4  small_dataset_1         seml_example            [32, 16]   \\n\",\n       \"4    5  small_dataset_1         seml_example            [32, 16]   \\n\",\n       \"\\n\",\n       \"   config.learning_rate  config.max_epochs  config.overwrite  \\\\\\n\",\n       \"0               0.00001                519                 1   \\n\",\n       \"1               0.00001                626                 2   \\n\",\n       \"2               0.00001                768                 3   \\n\",\n       \"3               0.00001                519                 4   \\n\",\n       \"4               0.00001                626                 5   \\n\",\n       \"\\n\",\n       \"   config.regularization_params.dropout  config.seed  result.test_acc  \\\\\\n\",\n       \"0                              0.440696     49592375         0.738594   \\n\",\n       \"1                              0.446743    912760071         0.714019   \\n\",\n       \"2                              0.011091    946656409         0.268248   \\n\",\n       \"3                              0.440696    488101063         0.334359   \\n\",\n       \"4                              0.446743    682478206         0.248766   \\n\",\n       \"\\n\",\n       \"   result.test_loss  \\n\",\n       \"0          0.333637  \\n\",\n       \"1          6.445487  \\n\",\n       \"2          8.519550  \\n\",\n       \"3          9.716397  \\n\",\n       \"4          8.092757  \"\n      ]\n     },\n     \"execution_count\": 3,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"results.head()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Pandas doesn't like lists as groupby keys.\\n\",\n    \"results[\\\"config.hidden_sizes\\\"] = results[\\\"config.hidden_sizes\\\"].astype(str)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"config.dataset   config.hidden_sizes\\n\",\n       \"large_dataset_1  [64, 32]               6.082730\\n\",\n       \"                 [64]                   6.708074\\n\",\n       \"large_dataset_2  [64, 32]               4.429962\\n\",\n       \"                 [64]                   5.904191\\n\",\n       \"small_dataset_1  [16]                   5.063250\\n\",\n       \"                 [32, 16]               5.105564\\n\",\n       \"small_dataset_2  [16]                   6.231065\\n\",\n       \"                 [32, 16]               4.949711\\n\",\n       \"Name: result.test_loss, dtype: float64\"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"results.groupby([\\\"config.dataset\\\", \\\"config.hidden_sizes\\\"])[\\\"result.test_loss\\\"].agg(\\n\",\n    \"    \\\"mean\\\"\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {\n      \"needs_background\": \"light\"\n     },\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"plt.scatter(results[\\\"result.test_acc\\\"], results[\\\"result.test_loss\\\"])\\n\",\n    \"plt.show()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python (torch)\",\n   \"language\": \"python\",\n   \"name\": \"torch\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.8.1\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/tutorial/example_config.yaml",
    "content": "seml:\n  executable: examples/tutorial/example_experiment.py\n  name: example_experiment\n  output_dir: examples/logs\n  project_root_dir: ../..\n\nslurm:\n  experiments_per_job: 1\n  sbatch_options:\n    gres: gpu:1       # num GPUs\n    mem: 16G          # memory\n    cpus-per-task: 2  # num cores\n    time: 0-08:00     # max time, D-HH:MM\n\nfixed:\n  max_epochs: 500\n\ngrid:\n  learning_rate:\n    type: choice\n    options:\n      - 1\n      - 2\n      - 3\n\nsmall_datasets:\n  fixed:\n    hidden_sizes: [32, 16] # this will be parsed into a Python list.\n\nlarge_datasets:\n\n  fixed:\n    hidden_sizes: [128, 64, 32]\n"
  },
  {
    "path": "examples/tutorial/example_experiment.py",
    "content": "import logging\nimport time\n\nimport numpy as np\nimport seml\nfrom sacred import Experiment\n\nex = Experiment()\nseml.setup_logger(ex)\n\n\n@ex.config\ndef config():\n    overwrite = None\n    db_collection = None\n    if db_collection is not None:\n        ex.observers.append(\n            seml.create_mongodb_observer(db_collection, overwrite=overwrite)\n        )\n\n\n@ex.automain\ndef run(hidden_sizes: list, learning_rate: float, max_epochs: int):\n    # Note that regularization_params contains the corresponding sub-dictionary from the configuration.\n    logging.info(\"Received the following configuration:\")\n    logging.info(\n        f\"Hidden sizes: {hidden_sizes}, learning_rate: {learning_rate}, \"\n        f\"max_epochs: {max_epochs}\"\n    )\n    # res = hidden_sizes / 2\n    #  do your processing here\n    time.sleep(60)\n    results = {\n        \"test_acc\": learning_rate * np.sqrt(np.arange(1, 1001, 1))\n        + np.random.uniform(0, 5),\n        # ...\n    }\n    # the returned result will be written into the database\n    return results\n"
  },
  {
    "path": "examples/tutorial/intro_slides.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"source\": [\n    \"# Introduction to   \\n\",\n    \"# `SEML`: Slurm Experiment Management Library\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"source\": [\n    \"## Why `SEML`?\\n\",\n    \"\\n\",\n    \"In a nutshell, **`SEML`** enables you to leverage the massive parallelization of a compute cluster without boilerplate code or having to worry about keeping track of experiments.\\n\",\n    \"That is, it enables you to:\\n\",\n    \"* very easily define hyperparameter search spaces using YAML files,\\n\",\n    \"* run these hyperparameter configurations on a compute cluster using `Slurm`,\\n\",\n    \"* and to track the experimental results using `sacred` and `MongoDB`.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"source\": [\n    \"In addition, **`SEML`** offers many more features to make your life easier, such as\\n\",\n    \"* automatically saving and loading your source code for reproducibility,\\n\",\n    \"* collecting experiment results into a `Pandas` dataframe,\\n\",\n    \"* easy debugging on Slurm or locally,\\n\",\n    \"* automatically checking your experiment configurations,\\n\",\n    \"* extending Slurm with local workers,\\n\",\n    \"* and keeping track of resource usage (experiment runtime, RAM, etc.).\\n\",\n    \"\\n\",\n    \"You can even get notified on Mattermost whenever an experiment starts, completes, or fails!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"source\": [\n    \"## How does it work?\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"-\"\n    }\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAAqsAAAH7CAYAAADrfdabAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3xUVdrA8d+50yeT3klIgCRAKKGJ9A6CXbH3tq7v6rqLrq6uZe3r6qprb2sXWLuuoijSRQGBSO+B9EB6nz7n/eNOQkLvCXC+n89lkrlz733unYQ8c+5zzhFSShRFURRFURSlPdLaOgBFURRFURRF2ReVrCqKoiiKoijtlkpWFUVRFEVRlHZLJauKoiiKoihKu6WSVUVRFEVRFKXdUsmqoiiKoiiK0m6pZFVRFEVRFEVpt1SyqiiKoiiKorRbxrYOQFGUo0/TtO4Oh+PfdXV1jraORTmxaZoWsFgsPzudzmeklJVtHY+iKKcelawqyklICBE6fvz4ke+88469rWNRTmz5+fn+s846q7qoqMjc1rEoinJqUsmqopykjEYjERERbR2GcoKrrq5GCNHWYSiKcgpTNauKoiiKoihKu6WSVUVRFEVRFKXdUsmqoiiKoiiK0m6pZFVRFEVRFEVpt1SyqiiKoiiKorRbKllVFEVRFEVR2i2VrCqKoiiKoijtlkpWFUVRFEVRlHZLJauKoiiKoihKu6WSVUVRFEVRFKXdUsmqoiiKoiiK0m6pZFVRFEVRFEVpt1SyqiiKoiiKorRbxrYOQFGUE1d+fj6zZ8/G7/cTHh7O8OHDSUxMxO/3s3XrVgwGA126dMFgMBzRcTweD7/88gu9evUiJibmKEW/b1JKduzYQUlJCT169CAnJwdN08jMzGz1Oq/Xy6ZNmwgJCSEuLo7ly5fTr18/wsLCqK6uxuFwYDQe+X+zHo+HX3/9lfz8fIYPH07Hjh0RQhzxfhVFUU4EqmVVUZRD5vV6mTlzJu+//z4ulwuAgoICXnnlFXJzc/F6vfz0008sXboUn893xMdzuVy8++675OfnH/G+DoaUki1btvDNN9/Q0NCwz9e53W7mzJlDdnY2fr+/+flt27bx6aefNl+bI42lqKiIzz77jOrq6iPen6IoyolGtawqinJImhK5efPmcf755zN48GAMBgONjY28++67TJ06lT/96U+tXh8IBJpbAoUQSCmb1zU913Jd0/Mt17XcX9Nj07rdX99ym5br9ndO+zoeQI8ePfZ6/Jax2mw2Ro4cCcC6devYuHEjgUBgv8ff27ntfk2aklWbzcall15KdHS0alVVFOWUolpWFeUUFggEqKuro7i4mNLSUrxe7wG38fv9rFu3jpSUFPr06YOm6f+N2O12brnlFu655x7MZjOBQIB169YxatQoevXqxZ133klJSQlSSurq6nj11Vfp1q0b3bp14/nnn8fpdOL3+1m1ahVjx46lW7dujBo1ilmzZrVqtaypqeGZZ55h8uTJbNmyhSVLlnDuuefSvXt3zjrrLDZt2tQqQfR4PEydOpX33nsPj8cD6MnkU089RV5eHgsXLqRbt250796dLl268Pe//53GxsZW5/z666/z9ttvI6UkLy+PG264gczMTC644AK2bt0KQGlpKf/3f//HvHnzePbZZ3n33Xf5/e9/z5tvvsmjjz5KVVUVUkqWLFnCE088QWVlZatjeDweFi9eTFZWFt26dePKK69k48aNrFq1iquuuoo33niDe+65h507dx7em60oinKCUsmqopzCysrKuO++++jevTsjR47khx9+OOA2Pp+PxsZGoqKiMJlMrVr5jEYjZrMZ0G/dl5eX8+WXX7Js2TJSU1P5/vvvcblcTJs2DbfbzZo1a1i9ejU1NTVMnTqVgoICXnzxRaZMmcKGDRt47bXXKCoqar79XVVVxbRp0/D7/bzzzjvExsby3Xff8de//pXNmzfzwAMPsHr16lbJrclkonPnzuTk5LBjxw78fj+bNm3CarViMBjIzs5mwYIFbN68mW+//ZYNGzY0J6Atz9nn81FTU8PHH3/MxIkTWbt2LU8//TQFBQV4PB6klHg8HlJSUrjrrru48cYbefPNNxkyZAj19fWUlJTg9/vZsGEDcXFxhIWFNe9fSsnSpUt56623+Oqrr1i7di1nnnkm06dPJyUlhQ8//JA//OEPPPPMMyQkJBzJW64oinLCUWUAinIK27ZtGx9++CF1dXVs2rSJ559/nnPOOeeA2wkhMBgM+70dbTKZOO2004iKikLTNBISEqisrKS+vp7ly5djMpn4xz/+AUBJSQl5eXlkZGSQnp7O4MGDMRqN9OrVi169elFbW4vf7+eFF15ACMETTzxBZGQkNTU1CCF49913yc7O5pJLLmHYsGGt4tI0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wsLCuO6668jMzGx13NzcXN59913Ky8uxWCxs376dSZMmcfnll/Pqq69itVqx2+3k5OSgaRp33nknaWlpLFmyhOnTp2M0GgkPD2fr1q1ccMEFrc4rEAiwceNGpk2bRnV1NUIIIiIiuOaaa3A4HLzyyis0NjY2T/96ww030KNHD6ZPn055eTlhYWHk5eXh9/u55ZZbKCsrY+3atezYsYNBgwaRn59PWVkZd9xxB3PnzuWrr75qjjcvL49rrrmG4cOH8/7777Nq1SpsNht+v5/bb7+dHj16qFZZRVFOWSpZVRTlkHi9XpYsWYLJZOKxxx4jLCyMTZs2YbVaWbp0Kdu3b+eZZ54hLCyMuXPn8vnnn5OVlcUvv/xCeno6l156KX6/n2eeeQa3243f7+ebb74hJSWFO++8EyEEX3/9Ne+99x4PPfRQq3rWkJAQLr30Urp3747JZOLHH3/kf//7X3Mrrclk4s4778TlcvHUU0+xefNmIiMj+emnn7jooosYO3YsJSUl/PWvf93jvFwuF7NnzyY1NZUHH3wQKSWbNm0iNDSUuXPn0tjYyKOPPordbuerr77iww8/5KGHHsLtdmOz2bj99tsRQvDee++xadMmLrnkElatWkWXLl0YP348U6dOBaCkpITs7GxuvPFGTj/9dDZt2sQTTzwBQFFREbNnz+aVV16hQ4cOrF+/noiIiGP/piqKorRjajQARVEOidfrpby8nJSUFEJDQzEajfTs2ZPU1FS2bdvGiBEjiIiIwGAwNE9XWlRURElJCenp6dhsNux2OwMGDCAkJASXy8WaNWt48MEH6devH3379uWee+5h+/bte9SzxsXFkZ6ezvvvv8/gwYO5/fbbcblczetPO+00TCYTZrMZu92Oz+ejqqqKiooKunXrhtFoJD4+niFDhuxxXo2NjVRWVpKZmYnZbMZms9G3b19iYmLYsmULo0ePJiwsDKPRyKhRo5r3bbfbyczMxGq1YjQam48bCAT2OEZT2YDH46Fz584YDAaSk5Pp378/oLc0d+jQgQkTJnDttddisVhISEg4yu+goijKiUUlq4qiHDIp5R63pffWWamxsZGamhqklPvszFRfX095eTnvvfce2dnZZGdns27dOqZPn05oaGir/f/www+MHDmSbdu2MWPGDN5++21CQkKaX7O32s6m47aMd1+31A+2w1VdXR11dXUAzYP9H6q9xeBwOHj11VdZvHgxXbt25eyzz+avf/3rce+EdqoQVmEQ4aKDCBfpIlykCpswHdL2QpiFEClCiHghxBENKyGEsAX3lbz7voQQkUKINCFEtBDihPu7HbxOXYLX6YSLX2l76odGUZRDYjKZiI2NpaSkBJfLRSAQoKCggNzcXFJTU1m0aBE1NTX4/X42bNiA1WolOTmZxMREcnJycLlcNDY2kp2dTUNDA+Hh4fTu3Ztly5bh9/sxm80UFhaybt26Vq2TUkry8vK48cYbm2/HL168uDlp3JeoqCiio6PZtGkTPp+P0tJSFi9evMfr7HY7UVFRbN++Hb/fj8/nY8uWLezYsYP09HQWLFhAXV0dPp+PlStXEhkZeci36IUQxMXFYTabm49TVFTEb7/9BkBNTQ0//fQTVquVhx9+mGnTpuF0Olu1HitHURIOBnItF/Myl/ENA0g/xD10Bh4FLgYOe/w1oX9y6QJMAR4BonZ7yUjgceAqIISjIJg42vYVjxCioxBi9zgOVwLwd+CP7HluhywYX+y+4t/PdpoQIlEIoUogTzDqDVMU5ZCYTCYGDRrEF198wcMPP9zcEWn06NEMGjSIlStX8uijj+L3+/F4PFx44YXExsYycuRIPv/8c+6//37cbjc+n4/k5GSsVivXXXcdr7zyClOmTMHhcABwxhln0KtXr+bjCiHo2bMn06dP56677sJutyOEQAhBTU3NPuONjIxkzJgxfPbZZ3zzzTeEhobi9Xr3eJ3VamX8+PF88cUX3HvvvWiaRnR0NGeddRajR49m3bp1PPTQQ/h8PrxeL9dddx022yH9rQSgQ4cO9O3bl3fffZePPvoIq9XaHI/X62XmzJlMmzaNsLAwAM4///zma6IcXTJH1gD/FNeIH7Hx8mHswgW8L6Wcd0Rx6E3664A7hRAXAoHd1v9PCLEQ6LW37Q9TH2A9ULiXdQKwAUelSV9KmS+E+D160n00GIFMYCvgPITtzEBfYBGw/0+5SrsijvU4g4qiHH8Gg2HgRRddNP+TTz45JqPtN9VeNrUOxsXF0blz5+ahq9avX4/T6SQmJoaMjAxMJlPzsFB5eXlIKenYsSNCCBITEzEYDBQVFbFt2zZAH7Kpa9eue9zW93g8bNmyhYqKCmw2G126dKGiooKEhATKy8ubWzubBuS32+1ER0fj8/nIycmhtLSUyMhIwsPDsVqtxMbGtrodHwgEKCoqIi8vDyEESUlJJCcnYzAYqK6uZvPmzbjdbhITE0lLS0NKyY4dO7BYLERF6Q1GpaWlCCGIjY2lsLAQm81GbGwspaWluN3u5qGsNm/eTG1tbXMLcZ8+fRgxYgQ7d+5sHsIrMjKSrl27Yjab22w0gNzcXP+IESO+LSwsvEVKuaNNgggS40QEdp5E0CP4VDX1/FnOlbmit+hLZ/6G3opXg4830KhB414gdLddrWEH98pfZT2AuEYMwMbLrOdGuUhuOOh4hEgFurRMVoOtpIOBPwPxwFfAcuBXKaVXCPE74BqgGHgGyJYt/hAHk9WFUsqK3Y4ViZ6srpRS1gWfMwLnAzegt7jOA16UUlYH1ycA9wD9gZ+CcSwEzkRv5RSAG1gJPC2lLAq2pt4LJAP/Ch5PBvdnA24HzgLygtusE0LEAn8CSoEhwffgVinlxhbxm9GT1ZVSyvKDvL5ZwEvBb38CfgYWADcCv0P/sOAMntO/pZTVQogOwD/QW73XBWPMFUJEoLdcXwTUBLebAbwN9Ahep78Er8l9QA7wnJTSI4RIAp4OXpMC4HP28h4px45qWVUU5ZAJIUhISNhr55+QkBAGDhy4x/OappGUlNQ8XNXukpOTSU5O3u9xzWYzPXv2bPVcdHQ0QHNLZFN8KSkpzd+bTCa6d+9O9+7d97t/TdPo2LEjHTt23GNdZGQkgwYN2uP53c+n5TVpGUNcXBxSSnJycpg6dSqTJk2iZ8+eLF++nC1btjB58uT9XlcFSOJOaillHs8wlG505AHKsYhuYiDDeRSND/iCZUTTmdFMQfIOgtnAhODXVxLgFTRuwcMIYOYxiLIn0Bv4K1APDAcGACuEEJcBFcAFQBhwGVAJbD/MY3VBL+f7K+ABMoCzhRCfAhbgJuAd9FKFLsD1wBz0ZKsR2IyeNHuD3wNUBV/fP7hPoDlRvQZYCvwHiANGCiHqgKLgfoejJ3oCmCCEyJFS7nkb4yAIIUxAd+D/AD8wIXhMN/ABsA3I3Uv8fuB59JbTBOAcIcR/0BPUl9BbkxcH17vRE95s9A8VdmAj8BBwqR6GMABpwX1WAV3RW3V/OZzzUg6PSlYVRVGOEyEEnTp1Yvjw4Tz99NNUVFQQGxvLc889R4cOHdo6vPavlNU4mMJl3AwY8XMvaylnFAMRdKeCPzMMvcB3B27ARxxOYDt+KjBRipcizPg5Bn//gi2d8eitqPnBp78OrosDTMCMYAJXJYT4Jvj6Q05Wgx2VUtFbGe1AU+vsHMCKnriulFKuCT6/Irg0bd8A1Egpq1ruN9iKWh9c37IcIRk9OVwmpWwMxh/e4tjFwOxgK6YteK4W9ETykAVbocuB14JPrQJelVL6hRBO9A8C1U2tyMFzEkA0eoLbPRjDEvS7yDJ4TnXB7VqWAfiEEI3B4waCCbgv+L1fCFEGPIz+Xu1ET9ZbXTfl2FLJqqIoynFkNBoZP34848ePb+tQTigiTVhwYEDjGgrpTwojieA+BrONAqpIIZ8dPMYS5mDHQRdGA/lEEXcc/9JJ9OQsSQixLphwxaC3auagt+JZAG8w2YwHDuqW+D6OVYzeWrhAStnQcqUQohYYLIQIkVI2BG/Dj0BPpOvQ//5bgwleZ/R6zi1Syr1P77arxtMQ3L8JvfSggl2J8u7xHTYhhAMok1KODn4/GBgghNgSfIkJMAfjTwt+n4feAvswelLZH721t2UsVsAU/GDRFWgA8tFbg0OC++tJsCNY8Lr5geullM5gScBo9KS45EjOUTl4KllVFEVR2r9M7HTgDlzUUUwlJkxIvmM+66jCj8aX9ORuQvg9VmwkIoDvgUuQhKIndpkYuR5BODFcJMYJG4IJpJIKZJDMc2K8WIOX5+UCWXyoIQZb4TYC5wJvCCHs6EnqN0At+i3ox4J1lRp6/eX7QggL+ogC56G3YF4nhNgGvAGUodda9gbCgWohxPzguu3oieYLQoimutyfgXfRE7cC4J/BVt2K4PVo6jRVBjwR/H4N+m1wKYQYgF5WEImepFUFb6MvQK/z/HfwWA3Aj8Ay9MTuj4Aj2CrZD33kgkohxOeHWQoQDtwlhLAGv98GTA+2fEr05PIh9OTzJ/SSDi+wAXgxuE0ReulAIXrpgwc973k5+Nr5wPfBVtdtwG3o5RmrgHT092MO+nvTN3jcKuBLVMvqcaU6WCnKSehYd7BqEggEWo0BajQam3voHyopZfNQVZqmqelFD1HTWLZSyqN6/dpLBysRIjQMJKK3TDapkrX6bWxhFVbMxLGrEcaJnlA1DZVUj97D3Yue6GjodY5hBFsLgzxIdsq6AydYwQ5WXYG5LVskg8lcVHC/jegthP4WZQJN51AhpawJtuZFAhEtdu9FTyi9wW2sLdbVB/cpg7fcY1ucdz1QHkzqzOjJmjm4nx1NiWOwZbQpaa4FqoLb2NFrPZvI4P7qgkl1fPBY/mD89cG6zoTgdS1HLw0IB6qDC8HnhqF3KDtga3Jwny3PuyEYhz+43ho8N0Mw9urg8xYgqcU2AnC1WB8SvF4E42/ZWa3pvalGb6n1oNe6RqL/nIjgdSyTUqrx5I4j1bKqKMohCwQCVFZWMnv2bJ588kkCgQDx8fHceuutTJo0CZvNdsjJksfj4fPPP6ehoYGrr776sIaFOpX5/X6+/vpriouLuf7660+64a5kgwygt5Ttfb1LutBv5+6u9gC7PpIWMhN6feQYIcS/muo/gwnQHkMjSSl97OUcgnWilcFlb/bZyiuldLL380ZK6WHvQ1MRTFrz9vJ8I3or5t62ce/tWMEEsuV5uWhxLkKIDOA99BbQZXs/k73uc3/nvdf3OxjjXuMPrm9AT2J3f36v701QRXBR2ohKVhVFOWRlZWW89tprhIWFsWTJEmw2G3l5eTz77LP4/X4mT558WLM6BQKBg55FStnT/mYKU44+KeVW9KGQlP2QUm5Bb1VVlMOiklVFUQ6J3+9n1apVmEwmrr76aqxW/S5damoqt912G6WlpUgpqampYdq0aWzcuLF5/VVXXUVCQgI+n48ffviBH374AYDJkyczZMgQQE+E//GPf1BXV0f//v2ZPHkyDocDv9/PF198wU8//URERASXX345mZmZ+P1+li5dyowZM3A6nQwdOpTzzz8fi8XSKu7KykrefvttCgoKiIyMbN5eSsnWrVuZNm0alZWVDBkyhIsvvhgpJUuXLiUkJIQ+ffogpSQ7Oxu/389pp53GggULcLlcZGdnM2zYMEaMGMH8+fOZMWMGAOecc05zJ6rVq1fzxRdfUFtby7BhwzjvvPP2iA9g48aNvPXWW3g8Hnr06MGll15KZGQklZWVTJ8+nS1bttCpUyduuukmwsPD8Xg8zJo1i3nz5mG1WomMjGzer9/vZ+XKlXzxxRfU1dVxxhlncPbZZ6vyCkVRTjhqulVFOYU5nU7++9//MmnSJH73u9+Rn7/Xu4mt+Hw+SkpKSEhIIDQ0tFXy061bN0aMGIHf72fWrFl4PB4uueQSLrjgAvLy8pgxYwYul4tZs2bx2WefceaZZzJhwgTefPNNNm7ciMfjYc2aNc1J6vbt2/n2229pbGzkjTfeoLCwkCuvvJLTTjuNf//732zfvp0tW7bw888/M2rUKC6++GLWrFnDsmWt7zQ2Njby1Vdf4XA4uPTSS+nSpQszZsygqqqKDRs28Ic//IG+ffty4YUX8vPPPzN9+nS8Xi+rV69m69at+P1+/H4/GzduZN26dfh8PhYvXsz//vc/Ro4cSWZmJvPnz+fDDz9k4sSJTJw4kQ8++KA5lm+++Ybhw4dzySWXkJOTw6efforH0zyEJVJKNm3axDPPPEO/fv2YPHkyxcXFfPfdd5SWlvLEE09gMpm48sor8Xg8PPnkk9TV1bF48WJ++eUXzj77bAYNGsS8efMoLS0FYOnSpUydOpVx48Zx8cUXs2jRIr755hvV8qooyglHtawqyils48aNPPjgg2zbtg2DwYDP5+O9997b7zZSSoxGI+Hh4XvMMNXEbDZz/vnnI6XE7XZTV1dHz549cblcOJ1O5s+fz6WXXsrEiRMBGDt2LEIIli9fTs+ePRk7diwhISFUVlaSk5NDQUEBK1euZMqUKcTExNCpUyc2btzIjBkz6NOnD6WlpXTo0IGUlBQGDBiwR1wul4ucnBy6du1Keno6ffv2xWg0Yjab+e9//8u5557Lueeei6ZpRERE8MILL3DGGWfs9zoYDAZ69OjB0KFDkVIyf/58LrnkEs4880wAxowZg5SSr776io4dO9KrV6/mazx79myKi4vp1KkTsKsVtE+fPkyePBmr1drc0rxy5Urq6uq4/PLLiYiIICMjgzvvvJNVq1bx66+/MmLECEaNGoXH46GgoID6+nqklCxatIghQ4bQs2dPhBD4fD4+/vhjBg8eTFxc3IF+NBRFUdoNlawqyimsvr6eoqIipJT4fD42bdp0wG2EEPj9fioqKvD5fJhMpuZ1LpeL3NxcOnfuzM6dO1m3bh3ffvstGzZsoKGhgUsvvRS3201xcTHJyclomn5zJyQkBLfbjcViIT4+HrPZDOijAkgpKS8v56effmLz5s3NiagQgptvvpmePXsyb948/vznP2O32/nDH/5A79696dixY/P+w8PDOeOMM3jttdd47bXXGDFiBJdeeilpaWns2LGDAQMGNNfYpqSkYLFYqK3df78cg8HQHGttbS2FhYWtjmm326mvr2flypX8+OOPTJ06tXnbAQMGtCoD8Pv9lJaWEh0d3TyiQtP6kpISOnTogN2uD+wQERFBt27dyMvLw+fzER0djRACk8lEQkICxcXFOJ1O1q1bx8cff8ybb77ZfC2HDRvW6v1SlBNBcFSDTPTB/PfoFLbba21AVvDbDVLKA3WwU04AKllVlFNYx44dOeecc/j222+JioriuuuuO+A2JpOJTp06sWTJEmpra7Farc2lANnZ2Xz00Ufce++9fPbZZ2zcuJGrr76a008/nenTp1NXV4fBYMDhcFBfX3/QcXo8Hrp168Zrr71GYmLiHusffvhhXC4Xn3/+OTNmzGDlypXcfvvthIeHA3piOWrUKEaNGkVOTg6vvvoqr776Ktdffz12u526ul0dtysqKqivr9+jpjQQCNDY2LjX2+gGg4HQ0NA9zklKicPh4N577+Xiiy/eZ0u0pmnY7XZcLtce+3c4HNTV1eH36yMjuVwuCgoKyMrKQgiBy+VqFZ/X6yUQCGA0GnnkkUc455xzDnR5FaW9MwNDAacQ4gMpZWA/r7UBQ4AY9DFkV+zntc2CQ1d1BrYHRwZQ2hFVs6oop7Dk5GSefPJJPvvsM6ZNm8bVV199wG00TaN37954vV7efffd5mRp+/btTJ06lbFjx2IwGKirq+OCCy5g2LBhbNmyhf/97394PB5sNhtZWVnMnz8fl8uF1+tl+vTpe9SZtpSWlkZiYiLff/89Pp8Pl8vF66+/zqJFi1i1ahXvv/8+jY2NXH755c2381uORlBbW8v777/P4sWL6dSpE3fddRcZGRnYbDb69evH+++/T1FRES6XixkzZtCpUydiY2OxWq1s3LgRt9tNZWUlK1aswOvdc/hNm81G3759mTNnDi6XC5/PxyeffMJvv/1Gjx49+O233ygrKyMQCLBs2TLeeustampqmrc3Go1kZGSwevVqqqqqCAQCrFixgm+++Ybk5GSKi4vJzs4G4KeffqKmpoasrCw6duzI8uXLaWxspKysjO+//57KykpsNhtDhw7lo48+orxcH9Jy0aJFvPHGG83vl6KcKKSU9egD8R9wxigpZSX6pACfoI+TerBM6K236tZDO6SSVUU5hRmNRtLT0znrrLMYNWrUQY/NGRERwR//+EfCwsLo378/3bp148Ybb+Sss85i0qRJxMTEMHbsWJ5//nl69OjBE088wbhx42hoaMDpdHLllVficrnIysqiZ8+elJWV0bNnTwwGQ6skU9M0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L64zRiwrkrLw8HAGDBhA3759g1NTU5kzZ4787rvv/NLS0iRqwYLWyAkhwlBbbXZF9b8MRNUIvgxs8vkCPk3TtNOou6/K54Cfnx/bt29n48aN9OrVqyJZdTgczJo1i6lTp7Jv3z4mTpyIxWLho48+Yvfu3QAMGjSIW2+99ajzOZ1Ofv31VzZs2MC4ceOIiIjgxx9/ZPr06QDcc8899OvXD4/Hw9y5c/nuO1WzPmzYMK688koCAwOPi3Hx4sW8//77FbcbM2YMFouFGTNmMG3atIrH8eCDD9KmTRvmzJmD2Wxm2rRpXHbZZVxyySUsXbqUzz77DIvFwqWXXsrIkSOP20VLSsnu3bt59913yczMZODAgdx2223k5OTw0UcfsXDhQnJycnjiiSeOagU2c+ZMgoODGTx4MABz585FCEHXrl1ZvHgxJSUlzJ07l+DgYG666SZ69+6NEIL9+/fz/vvvk56ezoUXXlhRclH5uSkrKyMiIoJbbrmF8PBw3njjDQ4dOkRSUhIjRoxg+vTpXH311ezevZu8vDz27t3LihUraNWqFXfffTcJCQm4XC5++OEHfv75Z5o1a0afPn3wer2MGjUKi8WClJKCggKmTJnCrFmzcLvdXH/99bhcLn799VdeeeUViouLmThxIm3atAEgOzubd955h507d9KyZUvGjRt30rZi58L+/fvl999/L2fNmpW2bt26b3Nzc+dIKdc25tXsGgghOgEXoLbTzENtvzkLtZtR9ikO1RoRY4OHoahFdPuFEDFAQXVGZIUQ1krnyKylULVGrNaTVa/XW5GIwJ973ZdfV/6mXghx1HXlNYTlx5bXDpbXFHq93opa0vJzVK4vLD93+W0q19CVX9e6dWt69+7NkiWVFzSq3q/nnXceW7duZcSIEYSFhfH555/Tpk0bxowZQ15eHm+++SbNmzena9eugEqy5s2bx9y5cxk7dixhYWFMmTKF1NRUHn74YZxOJx9//HHF41yzZg333HMPLpeL33//nU2bNtG7d++KxyClZPny5bz55pv87W9/w26389VXX5GQkEBERARr166t2K1rxowZvPvuu/zjH/9g9erV5Obmct999xEbG8usWbPYtGkTDz30EE6nk+nTp2M2m7nkkkuOGkHctm0bd911FxMnTiQ6OppJkybx8ccfc+eddzJs2DC8Xi8333zzUf1cy4+L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hg0bRmxsbMWCpMjISPbv33/cucuFh4ezYsUKRo0aRUFBAT/88AO33HILADt37uTHH3+kY8eOpKamkp6ezujRo0lISGDWrFmsXr2avn37MmPGDDZv3ozH4yE3N5e2bdtyxRVXUFxczFdffVWRsAcHB+NyuXA4qlZKFRAQwAUXXMDUqVPp0KEDBQUFfPjhhwwfPvy42wYGBuL1enE4HBVviI5lsVjo3r07kyZNYtiwYfTr14+0tDSmTp3KFVdcQYsWLWqsZrWoqIj58+fzwQcfyK1bt36/f//+L6SU66SUh+pjz0RhsmILahE7sHvSI4N6ms9ouN7r9UT5h7Sr6dDqLCGEP2oF/3DUyNQuYCPwI2pkKkM37G80PMA2o3dqBHAhMEAIUV6DHIdKBlegEsfJwDLgKSllobFoqrqyUNuffo6RVEopT9RfMAZV35p3zOU21Busy4BbUMVEQagdsGahRku3GI+p/JhNxtchqJHeb42/dzlCiD9Qvw8C1Ut6n05UG49aS1allGRmZvLqq69WjEImJydX1JpedNFFPPXUU/Ts2ZOOHTty8cUX88EHH/D000/z3HPP8eKLL/L000/ToUMHhBDYbDaeeOIJwsPDefLJJ5k8eTIFBQUMGDAAk8nEc889x7XXXsuIESOQUnLXXXdx5MgRvF4vXbp04corr6RVq1a8+uqrPProo/ztb39j8ODBDBw4kJdffhkpJc8++2zFdHRoaChxcXH89a9/5c033+SKK67g7rvvJjs7m+uvv56//OUv7N27l969exMYGIjZbCYhIYEbb7yR//3vf/To0YO7776bl156ia5duyKE4IYbbuCRRx5BSsnvv/9Oz549KSsrY/DgwTz33HMEBARUPH8mk4lLL72UwsLCiuOvueYaHn74YdxuN08++SQdO3akWbNm3HfffcydO5eMjAwCAgIqpsP9/Py45ZZbeP/99+nVqxcej4fBgwfzz3/+87ja0wEDBvD8889z0003cfDgQUaMGMEzzzyD2WzGYrHg7+9/wkTs2muvZf369fTs2ZNLLrmEUaNGVTyOTp06UVZWRrdu3YiOjuapp56iW7duCCG45557mDhxIrfffjvXX389/fv3x9/fn8svv5wXX3yRbt26ERERwdixY0lJSSErK4vk5OSK7gzjxo3DbrdjMpmw2WxHPR6r1Yq/vz8mk4lLLrmE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\\n\",\n      \"text/plain\": [\n       \"<Figure size 864x864 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {\n      \"needs_background\": \"light\"\n     },\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"import matplotlib.image as mpimg\\n\",\n    \"import matplotlib.pyplot as plt\\n\",\n    \"\\n\",\n    \"plt.figure(figsize=(12, 12))\\n\",\n    \"img = mpimg.imread(\\\"seml.png\\\")\\n\",\n    \"plt.axis(\\\"off\\\")\\n\",\n    \"plt.imshow(img);\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"source\": [\n    \"## How does it work?\\n\",\n    \"* **`SEML`** takes a `YAML` file containing hyperparameters and metadata about a set of experiments.\\n\",\n    \"* **`SEML`** stores each individual experiment's data as an entry in a `MongoDB` database collection.\\n\",\n    \"* In general, each type of experiments gets their own database collection.\\n\",\n    \"* Each individual experiment is an entry in the respective collection.\\n\",\n    \"* A database entry is essentially a `JSON` dictionary containing (among others):\\n\",\n    \"  * the state of the experiment\\n\",\n    \"  * the experiment configuration (i.e., hyperparameters),\\n\",\n    \"  * the generated results, and\\n\",\n    \"  * the cached source code (by default).\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import Markdown, display\\n\",\n    \"\\n\",\n    \"with open(\\\"example_experiment.py\\\", \\\"r\\\") as f:\\n\",\n    \"    code = f.read()\\n\",\n    \"display(\\n\",\n    \"    Markdown(f\\\"\\\"\\\"```python\\n\",\n    \"{code}\\n\",\n    \"```\\\"\\\"\\\")\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"with open(\\\"example_config.yaml\\\", \\\"r\\\") as f:\\n\",\n    \"    config = f.read()\\n\",\n    \"display(\\n\",\n    \"    Markdown(f\\\"\\\"\\\"```yaml\\n\",\n    \"{config}\\n\",\n    \"```\\n\",\n    \"\\\"\\\"\\\")\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial add example_config.yaml\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial status\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial start\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!squeue -u $(whoami)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial status\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial reset -y reload-sources -y start\\n\",\n    \"# Identical to:\\n\",\n    \"# seml seml_tutorial reset\\n\",\n    \"# seml seml_tutorial reload-sources\\n\",\n    \"# seml seml_tutorial start\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial status\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml seml_tutorial delete -s COMPLETED\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import seml\\n\",\n    \"from matplotlib import pyplot as plt\\n\",\n    \"\\n\",\n    \"results = seml.get_results(\\\"seml_tutorial\\\", to_data_frame=True)\\n\",\n    \"results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"for i, exp in results.iterrows():\\n\",\n    \"    plt.plot(\\n\",\n    \"        exp[\\\"result.test_acc\\\"],\\n\",\n    \"        label=f\\\"{exp['config.hidden_sizes']} {exp['config.learning_rate']}\\\",\\n\",\n    \"    )\\n\",\n    \"plt.legend()\\n\",\n    \"plt.show()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"slide\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! seml jupyter\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"! scancel 6537602\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"slideshow\": {\n     \"slide_type\": \"fragment\"\n    }\n   },\n   \"source\": [\n    \"## More examples (including this one)!\\n\",\n    \"https://github.com/TUM-DAML/seml/tree/master/examples\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"celltoolbar\": \"Slideshow\",\n  \"kernelspec\": {\n   \"display_name\": \"jax11\",\n   \"language\": \"python\",\n   \"name\": \"jax11\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.6\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/tutorial/intro_slides.slides.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1\" />\n\n<meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\n\n\n<title>intro_slides slides</title>\n\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js\"></script>\n\n<!-- General and theme style sheets -->\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/css/reveal.css\">\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/css/theme/simple.css\" id=\"theme\">\n\n<!-- If the query includes 'print-pdf', include the PDF print sheet -->\n<script>\nif( window.location.search.match( /print-pdf/gi ) ) {\n        var link = document.createElement( 'link' );\n        link.rel = 'stylesheet';\n        link.type = 'text/css';\n        link.href = 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/css/print/pdf.css';\n        document.getElementsByTagName( 'head' )[0].appendChild( link );\n}\n\n</script>\n\n<!--[if lt IE 9]>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/lib/js/html5shiv.js\"></script>\n<![endif]-->\n\n<!-- Loading the mathjax macro -->\n<!-- Load mathjax -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-AMS_HTML\"></script>\n    <!-- MathJax configuration -->\n    <script type=\"text/x-mathjax-config\">\n    MathJax.Hub.Config({\n        tex2jax: {\n            inlineMath: [ ['$','$'], [\"\\\\(\",\"\\\\)\"] ],\n            displayMath: [ ['$$','$$'], [\"\\\\[\",\"\\\\]\"] ],\n            processEscapes: true,\n            processEnvironments: true\n        },\n        // Center justify equations in code and markdown cells. Elsewhere\n        // we use CSS to left justify single line equations in code cells.\n        displayAlign: 'center',\n        \"HTML-CSS\": {\n            styles: {'.MathJax_Display': {\"margin\": 0}},\n            linebreaks: { automatic: true }\n        }\n    });\n    </script>\n    <!-- End of mathjax configuration -->\n\n<!-- Get Font-awesome from cdn -->\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css\">\n\n<style type=\"text/css\">\n    /*!\n*\n* Twitter Bootstrap\n*\n*/\n/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n  font-family: sans-serif;\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n}\nbody {\n  margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n  display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n  vertical-align: baseline;\n}\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n[hidden],\ntemplate {\n  display: none;\n}\na {\n  background-color: transparent;\n}\na:active,\na:hover {\n  outline: 0;\n}\nabbr[title] {\n  border-bottom: 1px dotted;\n}\nb,\nstrong {\n  font-weight: bold;\n}\ndfn {\n  font-style: italic;\n}\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\nmark {\n  background: #ff0;\n  color: #000;\n}\nsmall {\n  font-size: 80%;\n}\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\nsup {\n  top: -0.5em;\n}\nsub {\n  bottom: -0.25em;\n}\nimg {\n  border: 0;\n}\nsvg:not(:root) {\n  overflow: hidden;\n}\nfigure {\n  margin: 1em 40px;\n}\nhr {\n  box-sizing: content-box;\n  height: 0;\n}\npre {\n  overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit;\n  font: inherit;\n  margin: 0;\n}\nbutton {\n  overflow: visible;\n}\nbutton,\nselect {\n  text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\ninput {\n  line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box;\n  padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: textfield;\n  box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n  border: 0;\n  padding: 0;\n}\ntextarea {\n  overflow: auto;\n}\noptgroup {\n  font-weight: bold;\n}\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\ntd,\nth {\n  padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n  *,\n  *:before,\n  *:after {\n    background: transparent !important;\n    box-shadow: none !important;\n    text-shadow: none !important;\n  }\n  a,\n  a:visited {\n    text-decoration: underline;\n  }\n  a[href]:after {\n    content: \" (\" attr(href) \")\";\n  }\n  abbr[title]:after {\n    content: \" (\" attr(title) \")\";\n  }\n  a[href^=\"#\"]:after,\n  a[href^=\"javascript:\"]:after {\n    content: \"\";\n  }\n  pre,\n  blockquote {\n    border: 1px solid #999;\n    page-break-inside: avoid;\n  }\n  thead {\n    display: table-header-group;\n  }\n  tr,\n  img {\n    page-break-inside: avoid;\n  }\n  img {\n    max-width: 100% !important;\n  }\n  p,\n  h2,\n  h3 {\n    orphans: 3;\n    widows: 3;\n  }\n  h2,\n  h3 {\n    page-break-after: avoid;\n  }\n  .navbar {\n    display: none;\n  }\n  .btn > .caret,\n  .dropup > .btn > .caret {\n    border-top-color: #000 !important;\n  }\n  .label {\n    border: 1px solid #000;\n  }\n  .table {\n    border-collapse: collapse !important;\n  }\n  .table td,\n  .table th {\n    background-color: #fff !important;\n  }\n  .table-bordered th,\n  .table-bordered td {\n    border: 1px solid #ddd !important;\n  }\n}\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: url('../components/bootstrap/fonts/glyphicons-halflings-regular.eot');\n  src: url('../components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../components/bootstrap/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n  content: \"\\002a\";\n}\n.glyphicon-plus:before {\n  content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n  content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n  content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n  content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n  content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n  content: \"\\270f\";\n}\n.glyphicon-glass:before {\n  content: \"\\e001\";\n}\n.glyphicon-music:before {\n  content: \"\\e002\";\n}\n.glyphicon-search:before {\n  content: \"\\e003\";\n}\n.glyphicon-heart:before {\n  content: \"\\e005\";\n}\n.glyphicon-star:before {\n  content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n  content: \"\\e007\";\n}\n.glyphicon-user:before {\n  content: \"\\e008\";\n}\n.glyphicon-film:before {\n  content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n  content: \"\\e010\";\n}\n.glyphicon-th:before {\n  content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n  content: \"\\e012\";\n}\n.glyphicon-ok:before {\n  content: \"\\e013\";\n}\n.glyphicon-remove:before {\n  content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n  content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n  content: \"\\e016\";\n}\n.glyphicon-off:before {\n  content: \"\\e017\";\n}\n.glyphicon-signal:before {\n  content: \"\\e018\";\n}\n.glyphicon-cog:before {\n  content: \"\\e019\";\n}\n.glyphicon-trash:before {\n  content: \"\\e020\";\n}\n.glyphicon-home:before {\n  content: \"\\e021\";\n}\n.glyphicon-file:before {\n  content: \"\\e022\";\n}\n.glyphicon-time:before {\n  content: \"\\e023\";\n}\n.glyphicon-road:before {\n  content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n  content: \"\\e025\";\n}\n.glyphicon-download:before {\n  content: \"\\e026\";\n}\n.glyphicon-upload:before {\n  content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n  content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n  content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n  content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n  content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n  content: \"\\e032\";\n}\n.glyphicon-lock:before {\n  content: \"\\e033\";\n}\n.glyphicon-flag:before {\n  content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n  content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n  content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n  content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n  content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n  content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n  content: \"\\e040\";\n}\n.glyphicon-tag:before {\n  content: \"\\e041\";\n}\n.glyphicon-tags:before {\n  content: \"\\e042\";\n}\n.glyphicon-book:before {\n  content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n  content: \"\\e044\";\n}\n.glyphicon-print:before {\n  content: \"\\e045\";\n}\n.glyphicon-camera:before {\n  content: \"\\e046\";\n}\n.glyphicon-font:before {\n  content: \"\\e047\";\n}\n.glyphicon-bold:before {\n  content: \"\\e048\";\n}\n.glyphicon-italic:before {\n  content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n  content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n  content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n  content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n  content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n  content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n  content: \"\\e055\";\n}\n.glyphicon-list:before {\n  content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n  content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n  content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n  content: \"\\e059\";\n}\n.glyphicon-picture:before {\n  content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n  content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n  content: \"\\e063\";\n}\n.glyphicon-tint:before {\n  content: \"\\e064\";\n}\n.glyphicon-edit:before {\n  content: \"\\e065\";\n}\n.glyphicon-share:before {\n  content: \"\\e066\";\n}\n.glyphicon-check:before {\n  content: \"\\e067\";\n}\n.glyphicon-move:before {\n  content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n  content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n  content: \"\\e070\";\n}\n.glyphicon-backward:before {\n  content: \"\\e071\";\n}\n.glyphicon-play:before {\n  content: \"\\e072\";\n}\n.glyphicon-pause:before {\n  content: \"\\e073\";\n}\n.glyphicon-stop:before {\n  content: \"\\e074\";\n}\n.glyphicon-forward:before {\n  content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n  content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n  content: \"\\e077\";\n}\n.glyphicon-eject:before {\n  content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n  content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n  content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n  content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n  content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n  content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n  content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n  content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n  content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n  content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n  content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n  content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n  content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n  content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n  content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n  content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n  content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n  content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n  content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n  content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n  content: \"\\e101\";\n}\n.glyphicon-gift:before {\n  content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n  content: \"\\e103\";\n}\n.glyphicon-fire:before {\n  content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n  content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n  content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n  content: \"\\e107\";\n}\n.glyphicon-plane:before {\n  content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n  content: \"\\e109\";\n}\n.glyphicon-random:before {\n  content: \"\\e110\";\n}\n.glyphicon-comment:before {\n  content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n  content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n  content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n  content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n  content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n  content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n  content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n  content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n  content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n  content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n  content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n  content: \"\\e122\";\n}\n.glyphicon-bell:before {\n  content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n  content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n  content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n  content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n  content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n  content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n  content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n  content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n  content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n  content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n  content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n  content: \"\\e134\";\n}\n.glyphicon-globe:before {\n  content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n  content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n  content: \"\\e137\";\n}\n.glyphicon-filter:before {\n  content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n  content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n  content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n  content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n  content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n  content: \"\\e143\";\n}\n.glyphicon-link:before {\n  content: \"\\e144\";\n}\n.glyphicon-phone:before {\n  content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n  content: \"\\e146\";\n}\n.glyphicon-usd:before {\n  content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n  content: \"\\e149\";\n}\n.glyphicon-sort:before {\n  content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n  content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n  content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n  content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n  content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n  content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n  content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n  content: \"\\e157\";\n}\n.glyphicon-expand:before {\n  content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n  content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n  content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n  content: \"\\e161\";\n}\n.glyphicon-flash:before {\n  content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n  content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n  content: \"\\e164\";\n}\n.glyphicon-record:before {\n  content: \"\\e165\";\n}\n.glyphicon-save:before {\n  content: \"\\e166\";\n}\n.glyphicon-open:before {\n  content: \"\\e167\";\n}\n.glyphicon-saved:before {\n  content: \"\\e168\";\n}\n.glyphicon-import:before {\n  content: \"\\e169\";\n}\n.glyphicon-export:before {\n  content: \"\\e170\";\n}\n.glyphicon-send:before {\n  content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n  content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n  content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n  content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n  content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n  content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n  content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n  content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n  content: \"\\e179\";\n}\n.glyphicon-header:before {\n  content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n  content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n  content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n  content: \"\\e183\";\n}\n.glyphicon-tower:before {\n  content: \"\\e184\";\n}\n.glyphicon-stats:before {\n  content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n  content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n  content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n  content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n  content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n  content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n  content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n  content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n  content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n  content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n  content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n  content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n  content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n  content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n  content: \"\\e200\";\n}\n.glyphicon-cd:before {\n  content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n  content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n  content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n  content: \"\\e204\";\n}\n.glyphicon-copy:before {\n  content: \"\\e205\";\n}\n.glyphicon-paste:before {\n  content: \"\\e206\";\n}\n.glyphicon-alert:before {\n  content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n  content: \"\\e210\";\n}\n.glyphicon-king:before {\n  content: \"\\e211\";\n}\n.glyphicon-queen:before {\n  content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n  content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n  content: \"\\e214\";\n}\n.glyphicon-knight:before {\n  content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n  content: \"\\e216\";\n}\n.glyphicon-tent:before {\n  content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n  content: \"\\e218\";\n}\n.glyphicon-bed:before {\n  content: \"\\e219\";\n}\n.glyphicon-apple:before {\n  content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n  content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n  content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n  content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n  content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n  content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n  content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n  content: \"\\e227\";\n}\n.glyphicon-btc:before {\n  content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n  content: \"\\e227\";\n}\n.glyphicon-yen:before {\n  content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n  content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n  content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n  content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n  content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n  content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n  content: \"\\e232\";\n}\n.glyphicon-education:before {\n  content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n  content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n  content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n  content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n  content: \"\\e237\";\n}\n.glyphicon-oil:before {\n  content: \"\\e238\";\n}\n.glyphicon-grain:before {\n  content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n  content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n  content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n  content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n  content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n  content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n  content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n  content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n  content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n  content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n  content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n  content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n  content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n  content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n  content: \"\\e253\";\n}\n.glyphicon-console:before {\n  content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n  content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n  content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n  content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n  content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n  content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n  content: \"\\e260\";\n}\n* {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n*:before,\n*:after {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\nhtml {\n  font-size: 10px;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #000;\n  background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\na {\n  color: #337ab7;\n  text-decoration: none;\n}\na:hover,\na:focus {\n  color: #23527c;\n  text-decoration: underline;\n}\na:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\nfigure {\n  margin: 0;\n}\nimg {\n  vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  display: block;\n  max-width: 100%;\n  height: auto;\n}\n.img-rounded {\n  border-radius: 3px;\n}\n.img-thumbnail {\n  padding: 4px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 2px;\n  -webkit-transition: all 0.2s ease-in-out;\n  -o-transition: all 0.2s ease-in-out;\n  transition: all 0.2s ease-in-out;\n  display: inline-block;\n  max-width: 100%;\n  height: auto;\n}\n.img-circle {\n  border-radius: 50%;\n}\nhr {\n  margin-top: 18px;\n  margin-bottom: 18px;\n  border: 0;\n  border-top: 1px solid #eeeeee;\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n[role=\"button\"] {\n  cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n  font-family: inherit;\n  font-weight: 500;\n  line-height: 1.1;\n  color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n  font-weight: normal;\n  line-height: 1;\n  color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n  margin-top: 18px;\n  margin-bottom: 9px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n  font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n  margin-top: 9px;\n  margin-bottom: 9px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n  font-size: 75%;\n}\nh1,\n.h1 {\n  font-size: 33px;\n}\nh2,\n.h2 {\n  font-size: 27px;\n}\nh3,\n.h3 {\n  font-size: 23px;\n}\nh4,\n.h4 {\n  font-size: 17px;\n}\nh5,\n.h5 {\n  font-size: 13px;\n}\nh6,\n.h6 {\n  font-size: 12px;\n}\np {\n  margin: 0 0 9px;\n}\n.lead {\n  margin-bottom: 18px;\n  font-size: 14px;\n  font-weight: 300;\n  line-height: 1.4;\n}\n@media (min-width: 768px) {\n  .lead {\n    font-size: 19.5px;\n  }\n}\nsmall,\n.small {\n  font-size: 92%;\n}\nmark,\n.mark {\n  background-color: #fcf8e3;\n  padding: .2em;\n}\n.text-left {\n  text-align: left;\n}\n.text-right {\n  text-align: right;\n}\n.text-center {\n  text-align: center;\n}\n.text-justify {\n  text-align: justify;\n}\n.text-nowrap {\n  white-space: nowrap;\n}\n.text-lowercase {\n  text-transform: lowercase;\n}\n.text-uppercase {\n  text-transform: uppercase;\n}\n.text-capitalize {\n  text-transform: capitalize;\n}\n.text-muted {\n  color: #777777;\n}\n.text-primary {\n  color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n  color: #286090;\n}\n.text-success {\n  color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n  color: #2b542c;\n}\n.text-info {\n  color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n  color: #245269;\n}\n.text-warning {\n  color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n  color: #66512c;\n}\n.text-danger {\n  color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n  color: #843534;\n}\n.bg-primary {\n  color: #fff;\n  background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n  background-color: #286090;\n}\n.bg-success {\n  background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n  background-color: #c1e2b3;\n}\n.bg-info {\n  background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n  background-color: #afd9ee;\n}\n.bg-warning {\n  background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n  background-color: #f7ecb5;\n}\n.bg-danger {\n  background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n  background-color: #e4b9b9;\n}\n.page-header {\n  padding-bottom: 8px;\n  margin: 36px 0 18px;\n  border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n  margin-top: 0;\n  margin-bottom: 9px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n  margin-bottom: 0;\n}\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n.list-inline {\n  padding-left: 0;\n  list-style: none;\n  margin-left: -5px;\n}\n.list-inline > li {\n  display: inline-block;\n  padding-left: 5px;\n  padding-right: 5px;\n}\ndl {\n  margin-top: 0;\n  margin-bottom: 18px;\n}\ndt,\ndd {\n  line-height: 1.42857143;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0;\n}\n@media (min-width: 541px) {\n  .dl-horizontal dt {\n    float: left;\n    width: 160px;\n    clear: left;\n    text-align: right;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .dl-horizontal dd {\n    margin-left: 180px;\n  }\n}\nabbr[title],\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted #777777;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\nblockquote {\n  padding: 9px 18px;\n  margin: 0 0 18px;\n  font-size: inherit;\n  border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n  margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n  display: block;\n  font-size: 80%;\n  line-height: 1.42857143;\n  color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n  content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid #eeeeee;\n  border-left: 0;\n  text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n  content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n  content: '\\00A0 \\2014';\n}\naddress {\n  margin-bottom: 18px;\n  font-style: normal;\n  line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace;\n}\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #c7254e;\n  background-color: #f9f2f4;\n  border-radius: 2px;\n}\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #888;\n  background-color: transparent;\n  border-radius: 1px;\n  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n  padding: 0;\n  font-size: 100%;\n  font-weight: bold;\n  box-shadow: none;\n}\npre {\n  display: block;\n  padding: 8.5px;\n  margin: 0 0 9px;\n  font-size: 12px;\n  line-height: 1.42857143;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: #333333;\n  background-color: #f5f5f5;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n}\npre code {\n  padding: 0;\n  font-size: inherit;\n  color: inherit;\n  white-space: pre-wrap;\n  background-color: transparent;\n  border-radius: 0;\n}\n.pre-scrollable {\n  max-height: 340px;\n  overflow-y: scroll;\n}\n.container {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 0px;\n  padding-right: 0px;\n}\n@media (min-width: 768px) {\n  .container {\n    width: 768px;\n  }\n}\n@media (min-width: 992px) {\n  .container {\n    width: 940px;\n  }\n}\n@media (min-width: 1200px) {\n  .container {\n    width: 1140px;\n  }\n}\n.container-fluid {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 0px;\n  padding-right: 0px;\n}\n.row {\n  margin-left: 0px;\n  margin-right: 0px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n  position: relative;\n  min-height: 1px;\n  padding-left: 0px;\n  padding-right: 0px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n  float: left;\n}\n.col-xs-12 {\n  width: 100%;\n}\n.col-xs-11 {\n  width: 91.66666667%;\n}\n.col-xs-10 {\n  width: 83.33333333%;\n}\n.col-xs-9 {\n  width: 75%;\n}\n.col-xs-8 {\n  width: 66.66666667%;\n}\n.col-xs-7 {\n  width: 58.33333333%;\n}\n.col-xs-6 {\n  width: 50%;\n}\n.col-xs-5 {\n  width: 41.66666667%;\n}\n.col-xs-4 {\n  width: 33.33333333%;\n}\n.col-xs-3 {\n  width: 25%;\n}\n.col-xs-2 {\n  width: 16.66666667%;\n}\n.col-xs-1 {\n  width: 8.33333333%;\n}\n.col-xs-pull-12 {\n  right: 100%;\n}\n.col-xs-pull-11 {\n  right: 91.66666667%;\n}\n.col-xs-pull-10 {\n  right: 83.33333333%;\n}\n.col-xs-pull-9 {\n  right: 75%;\n}\n.col-xs-pull-8 {\n  right: 66.66666667%;\n}\n.col-xs-pull-7 {\n  right: 58.33333333%;\n}\n.col-xs-pull-6 {\n  right: 50%;\n}\n.col-xs-pull-5 {\n  right: 41.66666667%;\n}\n.col-xs-pull-4 {\n  right: 33.33333333%;\n}\n.col-xs-pull-3 {\n  right: 25%;\n}\n.col-xs-pull-2 {\n  right: 16.66666667%;\n}\n.col-xs-pull-1 {\n  right: 8.33333333%;\n}\n.col-xs-pull-0 {\n  right: auto;\n}\n.col-xs-push-12 {\n  left: 100%;\n}\n.col-xs-push-11 {\n  left: 91.66666667%;\n}\n.col-xs-push-10 {\n  left: 83.33333333%;\n}\n.col-xs-push-9 {\n  left: 75%;\n}\n.col-xs-push-8 {\n  left: 66.66666667%;\n}\n.col-xs-push-7 {\n  left: 58.33333333%;\n}\n.col-xs-push-6 {\n  left: 50%;\n}\n.col-xs-push-5 {\n  left: 41.66666667%;\n}\n.col-xs-push-4 {\n  left: 33.33333333%;\n}\n.col-xs-push-3 {\n  left: 25%;\n}\n.col-xs-push-2 {\n  left: 16.66666667%;\n}\n.col-xs-push-1 {\n  left: 8.33333333%;\n}\n.col-xs-push-0 {\n  left: auto;\n}\n.col-xs-offset-12 {\n  margin-left: 100%;\n}\n.col-xs-offset-11 {\n  margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n  margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n  margin-left: 75%;\n}\n.col-xs-offset-8 {\n  margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n  margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n  margin-left: 50%;\n}\n.col-xs-offset-5 {\n  margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n  margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n  margin-left: 25%;\n}\n.col-xs-offset-2 {\n  margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n  margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n  margin-left: 0%;\n}\n@media (min-width: 768px) {\n  .col-sm-1, .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-10, .col-sm-11, .col-sm-12 {\n    float: left;\n  }\n  .col-sm-12 {\n    width: 100%;\n  }\n  .col-sm-11 {\n    width: 91.66666667%;\n  }\n  .col-sm-10 {\n    width: 83.33333333%;\n  }\n  .col-sm-9 {\n    width: 75%;\n  }\n  .col-sm-8 {\n    width: 66.66666667%;\n  }\n  .col-sm-7 {\n    width: 58.33333333%;\n  }\n  .col-sm-6 {\n    width: 50%;\n  }\n  .col-sm-5 {\n    width: 41.66666667%;\n  }\n  .col-sm-4 {\n    width: 33.33333333%;\n  }\n  .col-sm-3 {\n    width: 25%;\n  }\n  .col-sm-2 {\n    width: 16.66666667%;\n  }\n  .col-sm-1 {\n    width: 8.33333333%;\n  }\n  .col-sm-pull-12 {\n    right: 100%;\n  }\n  .col-sm-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-sm-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-sm-pull-9 {\n    right: 75%;\n  }\n  .col-sm-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-sm-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-sm-pull-6 {\n    right: 50%;\n  }\n  .col-sm-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-sm-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-sm-pull-3 {\n    right: 25%;\n  }\n  .col-sm-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-sm-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-sm-pull-0 {\n    right: auto;\n  }\n  .col-sm-push-12 {\n    left: 100%;\n  }\n  .col-sm-push-11 {\n    left: 91.66666667%;\n  }\n  .col-sm-push-10 {\n    left: 83.33333333%;\n  }\n  .col-sm-push-9 {\n    left: 75%;\n  }\n  .col-sm-push-8 {\n    left: 66.66666667%;\n  }\n  .col-sm-push-7 {\n    left: 58.33333333%;\n  }\n  .col-sm-push-6 {\n    left: 50%;\n  }\n  .col-sm-push-5 {\n    left: 41.66666667%;\n  }\n  .col-sm-push-4 {\n    left: 33.33333333%;\n  }\n  .col-sm-push-3 {\n    left: 25%;\n  }\n  .col-sm-push-2 {\n    left: 16.66666667%;\n  }\n  .col-sm-push-1 {\n    left: 8.33333333%;\n  }\n  .col-sm-push-0 {\n    left: auto;\n  }\n  .col-sm-offset-12 {\n    margin-left: 100%;\n  }\n  .col-sm-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-sm-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-sm-offset-9 {\n    margin-left: 75%;\n  }\n  .col-sm-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-sm-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-sm-offset-6 {\n    margin-left: 50%;\n  }\n  .col-sm-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-sm-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-sm-offset-3 {\n    margin-left: 25%;\n  }\n  .col-sm-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-sm-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-sm-offset-0 {\n    margin-left: 0%;\n  }\n}\n@media (min-width: 992px) {\n  .col-md-1, .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-10, .col-md-11, .col-md-12 {\n    float: left;\n  }\n  .col-md-12 {\n    width: 100%;\n  }\n  .col-md-11 {\n    width: 91.66666667%;\n  }\n  .col-md-10 {\n    width: 83.33333333%;\n  }\n  .col-md-9 {\n    width: 75%;\n  }\n  .col-md-8 {\n    width: 66.66666667%;\n  }\n  .col-md-7 {\n    width: 58.33333333%;\n  }\n  .col-md-6 {\n    width: 50%;\n  }\n  .col-md-5 {\n    width: 41.66666667%;\n  }\n  .col-md-4 {\n    width: 33.33333333%;\n  }\n  .col-md-3 {\n    width: 25%;\n  }\n  .col-md-2 {\n    width: 16.66666667%;\n  }\n  .col-md-1 {\n    width: 8.33333333%;\n  }\n  .col-md-pull-12 {\n    right: 100%;\n  }\n  .col-md-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-md-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-md-pull-9 {\n    right: 75%;\n  }\n  .col-md-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-md-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-md-pull-6 {\n    right: 50%;\n  }\n  .col-md-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-md-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-md-pull-3 {\n    right: 25%;\n  }\n  .col-md-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-md-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-md-pull-0 {\n    right: auto;\n  }\n  .col-md-push-12 {\n    left: 100%;\n  }\n  .col-md-push-11 {\n    left: 91.66666667%;\n  }\n  .col-md-push-10 {\n    left: 83.33333333%;\n  }\n  .col-md-push-9 {\n    left: 75%;\n  }\n  .col-md-push-8 {\n    left: 66.66666667%;\n  }\n  .col-md-push-7 {\n    left: 58.33333333%;\n  }\n  .col-md-push-6 {\n    left: 50%;\n  }\n  .col-md-push-5 {\n    left: 41.66666667%;\n  }\n  .col-md-push-4 {\n    left: 33.33333333%;\n  }\n  .col-md-push-3 {\n    left: 25%;\n  }\n  .col-md-push-2 {\n    left: 16.66666667%;\n  }\n  .col-md-push-1 {\n    left: 8.33333333%;\n  }\n  .col-md-push-0 {\n    left: auto;\n  }\n  .col-md-offset-12 {\n    margin-left: 100%;\n  }\n  .col-md-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-md-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-md-offset-9 {\n    margin-left: 75%;\n  }\n  .col-md-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-md-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-md-offset-6 {\n    margin-left: 50%;\n  }\n  .col-md-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-md-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-md-offset-3 {\n    margin-left: 25%;\n  }\n  .col-md-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-md-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-md-offset-0 {\n    margin-left: 0%;\n  }\n}\n@media (min-width: 1200px) {\n  .col-lg-1, .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-10, .col-lg-11, .col-lg-12 {\n    float: left;\n  }\n  .col-lg-12 {\n    width: 100%;\n  }\n  .col-lg-11 {\n    width: 91.66666667%;\n  }\n  .col-lg-10 {\n    width: 83.33333333%;\n  }\n  .col-lg-9 {\n    width: 75%;\n  }\n  .col-lg-8 {\n    width: 66.66666667%;\n  }\n  .col-lg-7 {\n    width: 58.33333333%;\n  }\n  .col-lg-6 {\n    width: 50%;\n  }\n  .col-lg-5 {\n    width: 41.66666667%;\n  }\n  .col-lg-4 {\n    width: 33.33333333%;\n  }\n  .col-lg-3 {\n    width: 25%;\n  }\n  .col-lg-2 {\n    width: 16.66666667%;\n  }\n  .col-lg-1 {\n    width: 8.33333333%;\n  }\n  .col-lg-pull-12 {\n    right: 100%;\n  }\n  .col-lg-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-lg-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-lg-pull-9 {\n    right: 75%;\n  }\n  .col-lg-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-lg-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-lg-pull-6 {\n    right: 50%;\n  }\n  .col-lg-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-lg-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-lg-pull-3 {\n    right: 25%;\n  }\n  .col-lg-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-lg-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-lg-pull-0 {\n    right: auto;\n  }\n  .col-lg-push-12 {\n    left: 100%;\n  }\n  .col-lg-push-11 {\n    left: 91.66666667%;\n  }\n  .col-lg-push-10 {\n    left: 83.33333333%;\n  }\n  .col-lg-push-9 {\n    left: 75%;\n  }\n  .col-lg-push-8 {\n    left: 66.66666667%;\n  }\n  .col-lg-push-7 {\n    left: 58.33333333%;\n  }\n  .col-lg-push-6 {\n    left: 50%;\n  }\n  .col-lg-push-5 {\n    left: 41.66666667%;\n  }\n  .col-lg-push-4 {\n    left: 33.33333333%;\n  }\n  .col-lg-push-3 {\n    left: 25%;\n  }\n  .col-lg-push-2 {\n    left: 16.66666667%;\n  }\n  .col-lg-push-1 {\n    left: 8.33333333%;\n  }\n  .col-lg-push-0 {\n    left: auto;\n  }\n  .col-lg-offset-12 {\n    margin-left: 100%;\n  }\n  .col-lg-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-lg-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-lg-offset-9 {\n    margin-left: 75%;\n  }\n  .col-lg-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-lg-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-lg-offset-6 {\n    margin-left: 50%;\n  }\n  .col-lg-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-lg-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-lg-offset-3 {\n    margin-left: 25%;\n  }\n  .col-lg-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-lg-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-lg-offset-0 {\n    margin-left: 0%;\n  }\n}\ntable {\n  background-color: transparent;\n}\ncaption {\n  padding-top: 8px;\n  padding-bottom: 8px;\n  color: #777777;\n  text-align: left;\n}\nth {\n  text-align: left;\n}\n.table {\n  width: 100%;\n  max-width: 100%;\n  margin-bottom: 18px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n  padding: 8px;\n  line-height: 1.42857143;\n  vertical-align: top;\n  border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n  vertical-align: bottom;\n  border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n  border-top: 0;\n}\n.table > tbody + tbody {\n  border-top: 2px solid #ddd;\n}\n.table .table {\n  background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n  padding: 5px;\n}\n.table-bordered {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n  border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n  background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n  background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n  position: static;\n  float: none;\n  display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n  position: static;\n  float: none;\n  display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n  background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n  background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n  background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n  background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n  background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n  background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n  background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n  background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n  background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n  background-color: #ebcccc;\n}\n.table-responsive {\n  overflow-x: auto;\n  min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: 13.5px;\n    overflow-y: hidden;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid #ddd;\n  }\n  .table-responsive > .table {\n    margin-bottom: 0;\n  }\n  .table-responsive > .table > thead > tr > th,\n  .table-responsive > .table > tbody > tr > th,\n  .table-responsive > .table > tfoot > tr > th,\n  .table-responsive > .table > thead > tr > td,\n  .table-responsive > .table > tbody > tr > td,\n  .table-responsive > .table > tfoot > tr > td {\n    white-space: nowrap;\n  }\n  .table-responsive > .table-bordered {\n    border: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:first-child,\n  .table-responsive > .table-bordered > tbody > tr > th:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n  .table-responsive > .table-bordered > thead > tr > td:first-child,\n  .table-responsive > .table-bordered > tbody > tr > td:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n    border-left: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:last-child,\n  .table-responsive > .table-bordered > tbody > tr > th:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n  .table-responsive > .table-bordered > thead > tr > td:last-child,\n  .table-responsive > .table-bordered > tbody > tr > td:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n    border-right: 0;\n  }\n  .table-responsive > .table-bordered > tbody > tr:last-child > th,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n  .table-responsive > .table-bordered > tbody > tr:last-child > td,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n    border-bottom: 0;\n  }\n}\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  min-width: 0;\n}\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 18px;\n  font-size: 19.5px;\n  line-height: inherit;\n  color: #333333;\n  border: 0;\n  border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n  display: inline-block;\n  max-width: 100%;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9;\n  line-height: normal;\n}\ninput[type=\"file\"] {\n  display: block;\n}\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\nselect[multiple],\nselect[size] {\n  height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\noutput {\n  display: block;\n  padding-top: 7px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #555555;\n}\n.form-control {\n  display: block;\n  width: 100%;\n  height: 32px;\n  padding: 6px 12px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #555555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n  color: #999;\n}\n.form-control::-webkit-input-placeholder {\n  color: #999;\n}\n.form-control::-ms-expand {\n  border: 0;\n  background-color: transparent;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n  background-color: #eeeeee;\n  opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n  cursor: not-allowed;\n}\ntextarea.form-control {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n  input[type=\"date\"].form-control,\n  input[type=\"time\"].form-control,\n  input[type=\"datetime-local\"].form-control,\n  input[type=\"month\"].form-control {\n    line-height: 32px;\n  }\n  input[type=\"date\"].input-sm,\n  input[type=\"time\"].input-sm,\n  input[type=\"datetime-local\"].input-sm,\n  input[type=\"month\"].input-sm,\n  .input-group-sm input[type=\"date\"],\n  .input-group-sm input[type=\"time\"],\n  .input-group-sm input[type=\"datetime-local\"],\n  .input-group-sm input[type=\"month\"] {\n    line-height: 30px;\n  }\n  input[type=\"date\"].input-lg,\n  input[type=\"time\"].input-lg,\n  input[type=\"datetime-local\"].input-lg,\n  input[type=\"month\"].input-lg,\n  .input-group-lg input[type=\"date\"],\n  .input-group-lg input[type=\"time\"],\n  .input-group-lg input[type=\"datetime-local\"],\n  .input-group-lg input[type=\"month\"] {\n    line-height: 45px;\n  }\n}\n.form-group {\n  margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n  position: relative;\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n  min-height: 18px;\n  padding-left: 20px;\n  margin-bottom: 0;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  position: absolute;\n  margin-left: -20px;\n  margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n  position: relative;\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n  cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n  cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n  cursor: not-allowed;\n}\n.form-control-static {\n  padding-top: 7px;\n  padding-bottom: 7px;\n  margin-bottom: 0;\n  min-height: 31px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n  padding-left: 0;\n  padding-right: 0;\n}\n.input-sm {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n}\nselect.input-sm {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n  height: auto;\n}\n.form-group-sm .form-control {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n}\n.form-group-sm select.form-control {\n  height: 30px;\n  line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n  height: auto;\n}\n.form-group-sm .form-control-static {\n  height: 30px;\n  min-height: 30px;\n  padding: 6px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n}\n.input-lg {\n  height: 45px;\n  padding: 10px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n  border-radius: 3px;\n}\nselect.input-lg {\n  height: 45px;\n  line-height: 45px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n  height: auto;\n}\n.form-group-lg .form-control {\n  height: 45px;\n  padding: 10px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n  border-radius: 3px;\n}\n.form-group-lg select.form-control {\n  height: 45px;\n  line-height: 45px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n  height: auto;\n}\n.form-group-lg .form-control-static {\n  height: 45px;\n  min-height: 35px;\n  padding: 11px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n}\n.has-feedback {\n  position: relative;\n}\n.has-feedback .form-control {\n  padding-right: 40px;\n}\n.form-control-feedback {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 2;\n  display: block;\n  width: 32px;\n  height: 32px;\n  line-height: 32px;\n  text-align: center;\n  pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n  width: 45px;\n  height: 45px;\n  line-height: 45px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n  width: 30px;\n  height: 30px;\n  line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n  color: #3c763d;\n}\n.has-success .form-control {\n  border-color: #3c763d;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n  border-color: #2b542c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n  color: #3c763d;\n  border-color: #3c763d;\n  background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n  color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n  color: #8a6d3b;\n}\n.has-warning .form-control {\n  border-color: #8a6d3b;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n  border-color: #66512c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n  color: #8a6d3b;\n  border-color: #8a6d3b;\n  background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n  color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n  color: #a94442;\n}\n.has-error .form-control {\n  border-color: #a94442;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n  border-color: #843534;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n  color: #a94442;\n  border-color: #a94442;\n  background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n  color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n  top: 23px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n  top: 0;\n}\n.help-block {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: #404040;\n}\n@media (min-width: 768px) {\n  .form-inline .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .form-inline .form-control-static {\n    display: inline-block;\n  }\n  .form-inline .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .form-inline .input-group .input-group-addon,\n  .form-inline .input-group .input-group-btn,\n  .form-inline .input-group .form-control {\n    width: auto;\n  }\n  .form-inline .input-group > .form-control {\n    width: 100%;\n  }\n  .form-inline .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio,\n  .form-inline .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio label,\n  .form-inline .checkbox label {\n    padding-left: 0;\n  }\n  .form-inline .radio input[type=\"radio\"],\n  .form-inline .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .form-inline .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n  margin-top: 0;\n  margin-bottom: 0;\n  padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n  min-height: 25px;\n}\n.form-horizontal .form-group {\n  margin-left: 0px;\n  margin-right: 0px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .control-label {\n    text-align: right;\n    margin-bottom: 0;\n    padding-top: 7px;\n  }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n  right: 0px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-lg .control-label {\n    padding-top: 11px;\n    font-size: 17px;\n  }\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-sm .control-label {\n    padding-top: 6px;\n    font-size: 12px;\n  }\n}\n.btn {\n  display: inline-block;\n  margin-bottom: 0;\n  font-weight: normal;\n  text-align: center;\n  vertical-align: middle;\n  touch-action: manipulation;\n  cursor: pointer;\n  background-image: none;\n  border: 1px solid transparent;\n  white-space: nowrap;\n  padding: 6px 12px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  border-radius: 2px;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n  color: #333;\n  text-decoration: none;\n}\n.btn:active,\n.btn.active {\n  outline: 0;\n  background-image: none;\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n  cursor: not-allowed;\n  opacity: 0.65;\n  filter: alpha(opacity=65);\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n  pointer-events: none;\n}\n.btn-default {\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #8c8c8c;\n}\n.btn-default:hover {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n  color: #333;\n  background-color: #d4d4d4;\n  border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default .badge {\n  color: #fff;\n  background-color: #333;\n}\n.btn-primary {\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n  color: #fff;\n  background-color: #286090;\n  border-color: #122b40;\n}\n.btn-primary:hover {\n  color: #fff;\n  background-color: #286090;\n  border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  color: #fff;\n  background-color: #286090;\n  border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n  color: #fff;\n  background-color: #204d74;\n  border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n  background-color: #337ab7;\n  border-color: #2e6da4;\n}\n.btn-primary .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.btn-success {\n  color: #fff;\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #255625;\n}\n.btn-success:hover {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n  color: #fff;\n  background-color: #398439;\n  border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success .badge {\n  color: #5cb85c;\n  background-color: #fff;\n}\n.btn-info {\n  color: #fff;\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #1b6d85;\n}\n.btn-info:hover {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n  color: #fff;\n  background-color: #269abc;\n  border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info .badge {\n  color: #5bc0de;\n  background-color: #fff;\n}\n.btn-warning {\n  color: #fff;\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #985f0d;\n}\n.btn-warning:hover {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n  color: #fff;\n  background-color: #d58512;\n  border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning .badge {\n  color: #f0ad4e;\n  background-color: #fff;\n}\n.btn-danger {\n  color: #fff;\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #761c19;\n}\n.btn-danger:hover {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n  color: #fff;\n  background-color: #ac2925;\n  border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger .badge {\n  color: #d9534f;\n  background-color: #fff;\n}\n.btn-link {\n  color: #337ab7;\n  font-weight: normal;\n  border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n  background-color: transparent;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n  border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n  color: #23527c;\n  text-decoration: underline;\n  background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n  color: #777777;\n  text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n  padding: 10px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n  border-radius: 3px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n  padding: 1px 5px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n}\n.btn-block {\n  display: block;\n  width: 100%;\n}\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n  width: 100%;\n}\n.fade {\n  opacity: 0;\n  -webkit-transition: opacity 0.15s linear;\n  -o-transition: opacity 0.15s linear;\n  transition: opacity 0.15s linear;\n}\n.fade.in {\n  opacity: 1;\n}\n.collapse {\n  display: none;\n}\n.collapse.in {\n  display: block;\n}\ntr.collapse.in {\n  display: table-row;\n}\ntbody.collapse.in {\n  display: table-row-group;\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  -webkit-transition-property: height, visibility;\n  transition-property: height, visibility;\n  -webkit-transition-duration: 0.35s;\n  transition-duration: 0.35s;\n  -webkit-transition-timing-function: ease;\n  transition-timing-function: ease;\n}\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top: 4px dashed;\n  border-top: 4px solid \\9;\n  border-right: 4px solid transparent;\n  border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n  position: relative;\n}\n.dropdown-toggle:focus {\n  outline: 0;\n}\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  display: none;\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0;\n  list-style: none;\n  font-size: 13px;\n  text-align: left;\n  background-color: #fff;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, 0.15);\n  border-radius: 2px;\n  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu .divider {\n  height: 1px;\n  margin: 8px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n  display: block;\n  padding: 3px 20px;\n  clear: both;\n  font-weight: normal;\n  line-height: 1.42857143;\n  color: #333333;\n  white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  text-decoration: none;\n  color: #262626;\n  background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  color: #fff;\n  text-decoration: none;\n  outline: 0;\n  background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  text-decoration: none;\n  background-color: transparent;\n  background-image: none;\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  cursor: not-allowed;\n}\n.open > .dropdown-menu {\n  display: block;\n}\n.open > a {\n  outline: 0;\n}\n.dropdown-menu-right {\n  left: auto;\n  right: 0;\n}\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: 12px;\n  line-height: 1.42857143;\n  color: #777777;\n  white-space: nowrap;\n}\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: 990;\n}\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n  border-top: 0;\n  border-bottom: 4px dashed;\n  border-bottom: 4px solid \\9;\n  content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n  top: auto;\n  bottom: 100%;\n  margin-bottom: 2px;\n}\n@media (min-width: 541px) {\n  .navbar-right .dropdown-menu {\n    left: auto;\n    right: 0;\n  }\n  .navbar-right .dropdown-menu-left {\n    left: 0;\n    right: auto;\n  }\n}\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n  position: relative;\n  float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n  z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n  margin-left: -1px;\n}\n.btn-toolbar {\n  margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n  float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n  margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n.btn-group > .btn:first-child {\n  margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.btn .caret {\n  margin-left: 0;\n}\n.btn-lg .caret {\n  border-width: 5px 5px 0;\n  border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n  border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n  display: block;\n  float: none;\n  width: 100%;\n  max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n  float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n  margin-top: -1px;\n  margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n  border-top-right-radius: 2px;\n  border-top-left-radius: 2px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 2px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n  float: none;\n  display: table-cell;\n  width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n  width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n  left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n  position: absolute;\n  clip: rect(0, 0, 0, 0);\n  pointer-events: none;\n}\n.input-group {\n  position: relative;\n  display: table;\n  border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n  float: none;\n  padding-left: 0;\n  padding-right: 0;\n}\n.input-group .form-control {\n  position: relative;\n  z-index: 2;\n  float: left;\n  width: 100%;\n  margin-bottom: 0;\n}\n.input-group .form-control:focus {\n  z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n  height: 45px;\n  padding: 10px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n  border-radius: 3px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n  height: 45px;\n  line-height: 45px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle;\n}\n.input-group-addon {\n  padding: 6px 12px;\n  font-size: 13px;\n  font-weight: normal;\n  line-height: 1;\n  color: #555555;\n  text-align: center;\n  background-color: #eeeeee;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n}\n.input-group-addon.input-sm {\n  padding: 5px 10px;\n  font-size: 12px;\n  border-radius: 1px;\n}\n.input-group-addon.input-lg {\n  padding: 10px 16px;\n  font-size: 17px;\n  border-radius: 3px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n  margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n.input-group-btn {\n  position: relative;\n  font-size: 0;\n  white-space: nowrap;\n}\n.input-group-btn > .btn {\n  position: relative;\n}\n.input-group-btn > .btn + .btn {\n  margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n  z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n  margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n  z-index: 2;\n  margin-left: -1px;\n}\n.nav {\n  margin-bottom: 0;\n  padding-left: 0;\n  list-style: none;\n}\n.nav > li {\n  position: relative;\n  display: block;\n}\n.nav > li > a {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n  text-decoration: none;\n  background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n  color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n  color: #777777;\n  text-decoration: none;\n  background-color: transparent;\n  cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n  background-color: #eeeeee;\n  border-color: #337ab7;\n}\n.nav .nav-divider {\n  height: 1px;\n  margin: 8px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.nav > li > a > img {\n  max-width: none;\n}\n.nav-tabs {\n  border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n  float: left;\n  margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n  margin-right: 2px;\n  line-height: 1.42857143;\n  border: 1px solid transparent;\n  border-radius: 2px 2px 0 0;\n}\n.nav-tabs > li > a:hover {\n  border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n  color: #555555;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-bottom-color: transparent;\n  cursor: default;\n}\n.nav-tabs.nav-justified {\n  width: 100%;\n  border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n  float: none;\n}\n.nav-tabs.nav-justified > li > a {\n  text-align: center;\n  margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-tabs.nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs.nav-justified > li > a {\n  margin-right: 0;\n  border-radius: 2px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 2px 2px 0 0;\n  }\n  .nav-tabs.nav-justified > .active > a,\n  .nav-tabs.nav-justified > .active > a:hover,\n  .nav-tabs.nav-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.nav-pills > li {\n  float: left;\n}\n.nav-pills > li > a {\n  border-radius: 2px;\n}\n.nav-pills > li + li {\n  margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n  color: #fff;\n  background-color: #337ab7;\n}\n.nav-stacked > li {\n  float: none;\n}\n.nav-stacked > li + li {\n  margin-top: 2px;\n  margin-left: 0;\n}\n.nav-justified {\n  width: 100%;\n}\n.nav-justified > li {\n  float: none;\n}\n.nav-justified > li > a {\n  text-align: center;\n  margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs-justified {\n  border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n  margin-right: 0;\n  border-radius: 2px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 2px 2px 0 0;\n  }\n  .nav-tabs-justified > .active > a,\n  .nav-tabs-justified > .active > a:hover,\n  .nav-tabs-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.tab-content > .tab-pane {\n  display: none;\n}\n.tab-content > .active {\n  display: block;\n}\n.nav-tabs .dropdown-menu {\n  margin-top: -1px;\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.navbar {\n  position: relative;\n  min-height: 30px;\n  margin-bottom: 18px;\n  border: 1px solid transparent;\n}\n@media (min-width: 541px) {\n  .navbar {\n    border-radius: 2px;\n  }\n}\n@media (min-width: 541px) {\n  .navbar-header {\n    float: left;\n  }\n}\n.navbar-collapse {\n  overflow-x: visible;\n  padding-right: 0px;\n  padding-left: 0px;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n  -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n  overflow-y: auto;\n}\n@media (min-width: 541px) {\n  .navbar-collapse {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n  }\n  .navbar-collapse.collapse {\n    display: block !important;\n    height: auto !important;\n    padding-bottom: 0;\n    overflow: visible !important;\n  }\n  .navbar-collapse.in {\n    overflow-y: visible;\n  }\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-static-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    padding-left: 0;\n    padding-right: 0;\n  }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n  max-height: 340px;\n}\n@media (max-device-width: 540px) and (orientation: landscape) {\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    max-height: 200px;\n  }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n  margin-right: 0px;\n  margin-left: 0px;\n}\n@media (min-width: 541px) {\n  .container > .navbar-header,\n  .container-fluid > .navbar-header,\n  .container > .navbar-collapse,\n  .container-fluid > .navbar-collapse {\n    margin-right: 0;\n    margin-left: 0;\n  }\n}\n.navbar-static-top {\n  z-index: 1000;\n  border-width: 0 0 1px;\n}\n@media (min-width: 541px) {\n  .navbar-static-top {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: 1030;\n}\n@media (min-width: 541px) {\n  .navbar-fixed-top,\n  .navbar-fixed-bottom {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0;\n  border-width: 1px 0 0;\n}\n.navbar-brand {\n  float: left;\n  padding: 6px 0px;\n  font-size: 17px;\n  line-height: 18px;\n  height: 30px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n  text-decoration: none;\n}\n.navbar-brand > img {\n  display: block;\n}\n@media (min-width: 541px) {\n  .navbar > .container .navbar-brand,\n  .navbar > .container-fluid .navbar-brand {\n    margin-left: 0px;\n  }\n}\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: 0px;\n  padding: 9px 10px;\n  margin-top: -2px;\n  margin-bottom: -2px;\n  background-color: transparent;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 2px;\n}\n.navbar-toggle:focus {\n  outline: 0;\n}\n.navbar-toggle .icon-bar {\n  display: block;\n  width: 22px;\n  height: 2px;\n  border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n  margin-top: 4px;\n}\n@media (min-width: 541px) {\n  .navbar-toggle {\n    display: none;\n  }\n}\n.navbar-nav {\n  margin: 3px 0px;\n}\n.navbar-nav > li > a {\n  padding-top: 10px;\n  padding-bottom: 10px;\n  line-height: 18px;\n}\n@media (max-width: 540px) {\n  .navbar-nav .open .dropdown-menu {\n    position: static;\n    float: none;\n    width: auto;\n    margin-top: 0;\n    background-color: transparent;\n    border: 0;\n    box-shadow: none;\n  }\n  .navbar-nav .open .dropdown-menu > li > a,\n  .navbar-nav .open .dropdown-menu .dropdown-header {\n    padding: 5px 15px 5px 25px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a {\n    line-height: 18px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-nav .open .dropdown-menu > li > a:focus {\n    background-image: none;\n  }\n}\n@media (min-width: 541px) {\n  .navbar-nav {\n    float: left;\n    margin: 0;\n  }\n  .navbar-nav > li {\n    float: left;\n  }\n  .navbar-nav > li > a {\n    padding-top: 6px;\n    padding-bottom: 6px;\n  }\n}\n.navbar-form {\n  margin-left: 0px;\n  margin-right: 0px;\n  padding: 10px 0px;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n  margin-top: -1px;\n  margin-bottom: -1px;\n}\n@media (min-width: 768px) {\n  .navbar-form .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control-static {\n    display: inline-block;\n  }\n  .navbar-form .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .navbar-form .input-group .input-group-addon,\n  .navbar-form .input-group .input-group-btn,\n  .navbar-form .input-group .form-control {\n    width: auto;\n  }\n  .navbar-form .input-group > .form-control {\n    width: 100%;\n  }\n  .navbar-form .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio,\n  .navbar-form .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio label,\n  .navbar-form .checkbox label {\n    padding-left: 0;\n  }\n  .navbar-form .radio input[type=\"radio\"],\n  .navbar-form .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .navbar-form .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n@media (max-width: 540px) {\n  .navbar-form .form-group {\n    margin-bottom: 5px;\n  }\n  .navbar-form .form-group:last-child {\n    margin-bottom: 0;\n  }\n}\n@media (min-width: 541px) {\n  .navbar-form {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    -webkit-box-shadow: none;\n    box-shadow: none;\n  }\n}\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  margin-bottom: 0;\n  border-top-right-radius: 2px;\n  border-top-left-radius: 2px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.navbar-btn {\n  margin-top: -1px;\n  margin-bottom: -1px;\n}\n.navbar-btn.btn-sm {\n  margin-top: 0px;\n  margin-bottom: 0px;\n}\n.navbar-btn.btn-xs {\n  margin-top: 4px;\n  margin-bottom: 4px;\n}\n.navbar-text {\n  margin-top: 6px;\n  margin-bottom: 6px;\n}\n@media (min-width: 541px) {\n  .navbar-text {\n    float: left;\n    margin-left: 0px;\n    margin-right: 0px;\n  }\n}\n@media (min-width: 541px) {\n  .navbar-left {\n    float: left !important;\n    float: left;\n  }\n  .navbar-right {\n    float: right !important;\n    float: right;\n    margin-right: 0px;\n  }\n  .navbar-right ~ .navbar-right {\n    margin-right: 0;\n  }\n}\n.navbar-default {\n  background-color: #f8f8f8;\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n  color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n  color: #5e5e5e;\n  background-color: transparent;\n}\n.navbar-default .navbar-text {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n  color: #333;\n  background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n  color: #555;\n  background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n  color: #ccc;\n  background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n  border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n  background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n  background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n  background-color: #e7e7e7;\n  color: #555;\n}\n@media (max-width: 540px) {\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n    color: #777;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #333;\n    background-color: transparent;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #555;\n    background-color: #e7e7e7;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #ccc;\n    background-color: transparent;\n  }\n}\n.navbar-default .navbar-link {\n  color: #777;\n}\n.navbar-default .navbar-link:hover {\n  color: #333;\n}\n.navbar-default .btn-link {\n  color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n  color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n  color: #ccc;\n}\n.navbar-inverse {\n  background-color: #222;\n  border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n  color: #fff;\n  background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n  color: #444;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n  border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n  background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n  background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n  border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n  background-color: #080808;\n  color: #fff;\n}\n@media (max-width: 540px) {\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n    border-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n    color: #9d9d9d;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #fff;\n    background-color: transparent;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #fff;\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #444;\n    background-color: transparent;\n  }\n}\n.navbar-inverse .navbar-link {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n  color: #fff;\n}\n.navbar-inverse .btn-link {\n  color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n  color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n  color: #444;\n}\n.breadcrumb {\n  padding: 8px 15px;\n  margin-bottom: 18px;\n  list-style: none;\n  background-color: #f5f5f5;\n  border-radius: 2px;\n}\n.breadcrumb > li {\n  display: inline-block;\n}\n.breadcrumb > li + li:before {\n  content: \"/\\00a0\";\n  padding: 0 5px;\n  color: #5e5e5e;\n}\n.breadcrumb > .active {\n  color: #777777;\n}\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: 18px 0;\n  border-radius: 2px;\n}\n.pagination > li {\n  display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n  position: relative;\n  float: left;\n  padding: 6px 12px;\n  line-height: 1.42857143;\n  text-decoration: none;\n  color: #337ab7;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n  margin-left: 0;\n  border-bottom-left-radius: 2px;\n  border-top-left-radius: 2px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n  border-bottom-right-radius: 2px;\n  border-top-right-radius: 2px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n  z-index: 2;\n  color: #23527c;\n  background-color: #eeeeee;\n  border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n  z-index: 3;\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #337ab7;\n  cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n  color: #777777;\n  background-color: #fff;\n  border-color: #ddd;\n  cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n  padding: 10px 16px;\n  font-size: 17px;\n  line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n  border-bottom-left-radius: 3px;\n  border-top-left-radius: 3px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n  border-bottom-right-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n  border-bottom-left-radius: 1px;\n  border-top-left-radius: 1px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n  border-bottom-right-radius: 1px;\n  border-top-right-radius: 1px;\n}\n.pager {\n  padding-left: 0;\n  margin: 18px 0;\n  list-style: none;\n  text-align: center;\n}\n.pager li {\n  display: inline;\n}\n.pager li > a,\n.pager li > span {\n  display: inline-block;\n  padding: 5px 14px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n  text-decoration: none;\n  background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n  float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n  float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n  color: #777777;\n  background-color: #fff;\n  cursor: not-allowed;\n}\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: #fff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.label:empty {\n  display: none;\n}\n.btn .label {\n  position: relative;\n  top: -1px;\n}\n.label-default {\n  background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n  background-color: #5e5e5e;\n}\n.label-primary {\n  background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n  background-color: #286090;\n}\n.label-success {\n  background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n  background-color: #449d44;\n}\n.label-info {\n  background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n  background-color: #31b0d5;\n}\n.label-warning {\n  background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n  background-color: #ec971f;\n}\n.label-danger {\n  background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n  background-color: #c9302c;\n}\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: 12px;\n  font-weight: bold;\n  color: #fff;\n  line-height: 1;\n  vertical-align: middle;\n  white-space: nowrap;\n  text-align: center;\n  background-color: #777777;\n  border-radius: 10px;\n}\n.badge:empty {\n  display: none;\n}\n.btn .badge {\n  position: relative;\n  top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n  top: 0;\n  padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.list-group-item > .badge {\n  float: right;\n}\n.list-group-item > .badge + .badge {\n  margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n.jumbotron {\n  padding-top: 30px;\n  padding-bottom: 30px;\n  margin-bottom: 30px;\n  color: inherit;\n  background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n  color: inherit;\n}\n.jumbotron p {\n  margin-bottom: 15px;\n  font-size: 20px;\n  font-weight: 200;\n}\n.jumbotron > hr {\n  border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n  border-radius: 3px;\n  padding-left: 0px;\n  padding-right: 0px;\n}\n.jumbotron .container {\n  max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n  .jumbotron {\n    padding-top: 48px;\n    padding-bottom: 48px;\n  }\n  .container .jumbotron,\n  .container-fluid .jumbotron {\n    padding-left: 60px;\n    padding-right: 60px;\n  }\n  .jumbotron h1,\n  .jumbotron .h1 {\n    font-size: 59px;\n  }\n}\n.thumbnail {\n  display: block;\n  padding: 4px;\n  margin-bottom: 18px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 2px;\n  -webkit-transition: border 0.2s ease-in-out;\n  -o-transition: border 0.2s ease-in-out;\n  transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n  margin-left: auto;\n  margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n  border-color: #337ab7;\n}\n.thumbnail .caption {\n  padding: 9px;\n  color: #000;\n}\n.alert {\n  padding: 15px;\n  margin-bottom: 18px;\n  border: 1px solid transparent;\n  border-radius: 2px;\n}\n.alert h4 {\n  margin-top: 0;\n  color: inherit;\n}\n.alert .alert-link {\n  font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n  margin-bottom: 0;\n}\n.alert > p + p {\n  margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n  padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n  position: relative;\n  top: -2px;\n  right: -21px;\n  color: inherit;\n}\n.alert-success {\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n  color: #3c763d;\n}\n.alert-success hr {\n  border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n  color: #2b542c;\n}\n.alert-info {\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n  color: #31708f;\n}\n.alert-info hr {\n  border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n  color: #245269;\n}\n.alert-warning {\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n  color: #8a6d3b;\n}\n.alert-warning hr {\n  border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n  color: #66512c;\n}\n.alert-danger {\n  background-color: #f2dede;\n  border-color: #ebccd1;\n  color: #a94442;\n}\n.alert-danger hr {\n  border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n  color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n@keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n.progress {\n  overflow: hidden;\n  height: 18px;\n  margin-bottom: 18px;\n  background-color: #f5f5f5;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: 12px;\n  line-height: 18px;\n  color: #fff;\n  text-align: center;\n  background-color: #337ab7;\n  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n  -webkit-transition: width 0.6s ease;\n  -o-transition: width 0.6s ease;\n  transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n  -webkit-animation: progress-bar-stripes 2s linear infinite;\n  -o-animation: progress-bar-stripes 2s linear infinite;\n  animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n  background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n  background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n  background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n  background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n.media,\n.media-body {\n  zoom: 1;\n  overflow: hidden;\n}\n.media-body {\n  width: 10000px;\n}\n.media-object {\n  display: block;\n}\n.media-object.img-thumbnail {\n  max-width: none;\n}\n.media-right,\n.media > .pull-right {\n  padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n  padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n  display: table-cell;\n  vertical-align: top;\n}\n.media-middle {\n  vertical-align: middle;\n}\n.media-bottom {\n  vertical-align: bottom;\n}\n.media-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n.list-group {\n  margin-bottom: 20px;\n  padding-left: 0;\n}\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n  margin-bottom: -1px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n  border-top-right-radius: 2px;\n  border-top-left-radius: 2px;\n}\n.list-group-item:last-child {\n  margin-bottom: 0;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 2px;\n}\na.list-group-item,\nbutton.list-group-item {\n  color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n  color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n  text-decoration: none;\n  color: #555;\n  background-color: #f5f5f5;\n}\nbutton.list-group-item {\n  width: 100%;\n  text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n  background-color: #eeeeee;\n  color: #777777;\n  cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n  color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n  color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  z-index: 2;\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n  color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n  color: #c7ddef;\n}\n.list-group-item-success {\n  color: #3c763d;\n  background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n  color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n  color: #3c763d;\n  background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n  color: #fff;\n  background-color: #3c763d;\n  border-color: #3c763d;\n}\n.list-group-item-info {\n  color: #31708f;\n  background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n  color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n  color: #31708f;\n  background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n  color: #fff;\n  background-color: #31708f;\n  border-color: #31708f;\n}\n.list-group-item-warning {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n  color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n  color: #8a6d3b;\n  background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n  color: #fff;\n  background-color: #8a6d3b;\n  border-color: #8a6d3b;\n}\n.list-group-item-danger {\n  color: #a94442;\n  background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n  color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n  color: #a94442;\n  background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n  color: #fff;\n  background-color: #a94442;\n  border-color: #a94442;\n}\n.list-group-item-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.list-group-item-text {\n  margin-bottom: 0;\n  line-height: 1.3;\n}\n.panel {\n  margin-bottom: 18px;\n  background-color: #fff;\n  border: 1px solid transparent;\n  border-radius: 2px;\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n  padding: 15px;\n}\n.panel-heading {\n  padding: 10px 15px;\n  border-bottom: 1px solid transparent;\n  border-top-right-radius: 1px;\n  border-top-left-radius: 1px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n  color: inherit;\n}\n.panel-title {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: 15px;\n  color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n  color: inherit;\n}\n.panel-footer {\n  padding: 10px 15px;\n  background-color: #f5f5f5;\n  border-top: 1px solid #ddd;\n  border-bottom-right-radius: 1px;\n  border-bottom-left-radius: 1px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n  margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n  border-width: 1px 0;\n  border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n  border-top: 0;\n  border-top-right-radius: 1px;\n  border-top-left-radius: 1px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n  border-bottom: 0;\n  border-bottom-right-radius: 1px;\n  border-bottom-left-radius: 1px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n  border-top-width: 0;\n}\n.list-group + .panel-footer {\n  border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n  margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n  padding-left: 15px;\n  padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n  border-top-right-radius: 1px;\n  border-top-left-radius: 1px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n  border-top-left-radius: 1px;\n  border-top-right-radius: 1px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n  border-top-left-radius: 1px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n  border-top-right-radius: 1px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n  border-bottom-right-radius: 1px;\n  border-bottom-left-radius: 1px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n  border-bottom-left-radius: 1px;\n  border-bottom-right-radius: 1px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n  border-bottom-left-radius: 1px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n  border-bottom-right-radius: 1px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n  border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n  border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n  border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n  border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n  border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n  border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n  border-bottom: 0;\n}\n.panel > .table-responsive {\n  border: 0;\n  margin-bottom: 0;\n}\n.panel-group {\n  margin-bottom: 18px;\n}\n.panel-group .panel {\n  margin-bottom: 0;\n  border-radius: 2px;\n}\n.panel-group .panel + .panel {\n  margin-top: 5px;\n}\n.panel-group .panel-heading {\n  border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n  border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n  border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n  border-bottom: 1px solid #ddd;\n}\n.panel-default {\n  border-color: #ddd;\n}\n.panel-default > .panel-heading {\n  color: #333333;\n  background-color: #f5f5f5;\n  border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n  color: #f5f5f5;\n  background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #ddd;\n}\n.panel-primary {\n  border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #337ab7;\n}\n.panel-success {\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n  color: #dff0d8;\n  background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #d6e9c6;\n}\n.panel-info {\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n  color: #31708f;\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n  color: #d9edf7;\n  background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #bce8f1;\n}\n.panel-warning {\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n  color: #fcf8e3;\n  background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #faebcc;\n}\n.panel-danger {\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n  color: #f2dede;\n  background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n  position: relative;\n  display: block;\n  height: 0;\n  padding: 0;\n  overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  height: 100%;\n  width: 100%;\n  border: 0;\n}\n.embed-responsive-16by9 {\n  padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n  padding-bottom: 75%;\n}\n.well {\n  min-height: 20px;\n  padding: 19px;\n  margin-bottom: 20px;\n  background-color: #f5f5f5;\n  border: 1px solid #e3e3e3;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n  border-color: #ddd;\n  border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n  padding: 24px;\n  border-radius: 3px;\n}\n.well-sm {\n  padding: 9px;\n  border-radius: 1px;\n}\n.close {\n  float: right;\n  font-size: 19.5px;\n  font-weight: bold;\n  line-height: 1;\n  color: #000;\n  text-shadow: 0 1px 0 #fff;\n  opacity: 0.2;\n  filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n  color: #000;\n  text-decoration: none;\n  cursor: pointer;\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n}\nbutton.close {\n  padding: 0;\n  cursor: pointer;\n  background: transparent;\n  border: 0;\n  -webkit-appearance: none;\n}\n.modal-open {\n  overflow: hidden;\n}\n.modal {\n  display: none;\n  overflow: hidden;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1050;\n  -webkit-overflow-scrolling: touch;\n  outline: 0;\n}\n.modal.fade .modal-dialog {\n  -webkit-transform: translate(0, -25%);\n  -ms-transform: translate(0, -25%);\n  -o-transform: translate(0, -25%);\n  transform: translate(0, -25%);\n  -webkit-transition: -webkit-transform 0.3s ease-out;\n  -moz-transition: -moz-transform 0.3s ease-out;\n  -o-transition: -o-transform 0.3s ease-out;\n  transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.modal-open .modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n.modal-content {\n  position: relative;\n  background-color: #fff;\n  border: 1px solid #999;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  border-radius: 3px;\n  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  background-clip: padding-box;\n  outline: 0;\n}\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1040;\n  background-color: #000;\n}\n.modal-backdrop.fade {\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n}\n.modal-header {\n  padding: 15px;\n  border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n  margin-top: -2px;\n}\n.modal-title {\n  margin: 0;\n  line-height: 1.42857143;\n}\n.modal-body {\n  position: relative;\n  padding: 15px;\n}\n.modal-footer {\n  padding: 15px;\n  text-align: right;\n  border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n  margin-left: 5px;\n  margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n  margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n  margin-left: 0;\n}\n.modal-scrollbar-measure {\n  position: absolute;\n  top: -9999px;\n  width: 50px;\n  height: 50px;\n  overflow: scroll;\n}\n@media (min-width: 768px) {\n  .modal-dialog {\n    width: 600px;\n    margin: 30px auto;\n  }\n  .modal-content {\n    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n  }\n  .modal-sm {\n    width: 300px;\n  }\n}\n@media (min-width: 992px) {\n  .modal-lg {\n    width: 900px;\n  }\n}\n.tooltip {\n  position: absolute;\n  z-index: 1070;\n  display: block;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-style: normal;\n  font-weight: normal;\n  letter-spacing: normal;\n  line-break: auto;\n  line-height: 1.42857143;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  white-space: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  font-size: 12px;\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.tooltip.in {\n  opacity: 0.9;\n  filter: alpha(opacity=90);\n}\n.tooltip.top {\n  margin-top: -3px;\n  padding: 5px 0;\n}\n.tooltip.right {\n  margin-left: 3px;\n  padding: 0 5px;\n}\n.tooltip.bottom {\n  margin-top: 3px;\n  padding: 5px 0;\n}\n.tooltip.left {\n  margin-left: -3px;\n  padding: 0 5px;\n}\n.tooltip-inner {\n  max-width: 200px;\n  padding: 3px 8px;\n  color: #fff;\n  text-align: center;\n  background-color: #000;\n  border-radius: 2px;\n}\n.tooltip-arrow {\n  position: absolute;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n  bottom: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n  bottom: 0;\n  right: 5px;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n  bottom: 0;\n  left: 5px;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n  top: 50%;\n  left: 0;\n  margin-top: -5px;\n  border-width: 5px 5px 5px 0;\n  border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n  top: 50%;\n  right: 0;\n  margin-top: -5px;\n  border-width: 5px 0 5px 5px;\n  border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n  top: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n  top: 0;\n  right: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n  top: 0;\n  left: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1060;\n  display: none;\n  max-width: 276px;\n  padding: 1px;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-style: normal;\n  font-weight: normal;\n  letter-spacing: normal;\n  line-break: auto;\n  line-height: 1.42857143;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  white-space: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  font-size: 13px;\n  background-color: #fff;\n  background-clip: padding-box;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  border-radius: 3px;\n  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n  margin-top: -10px;\n}\n.popover.right {\n  margin-left: 10px;\n}\n.popover.bottom {\n  margin-top: 10px;\n}\n.popover.left {\n  margin-left: -10px;\n}\n.popover-title {\n  margin: 0;\n  padding: 8px 14px;\n  font-size: 13px;\n  background-color: #f7f7f7;\n  border-bottom: 1px solid #ebebeb;\n  border-radius: 2px 2px 0 0;\n}\n.popover-content {\n  padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n  position: absolute;\n  display: block;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.popover > .arrow {\n  border-width: 11px;\n}\n.popover > .arrow:after {\n  border-width: 10px;\n  content: \"\";\n}\n.popover.top > .arrow {\n  left: 50%;\n  margin-left: -11px;\n  border-bottom-width: 0;\n  border-top-color: #999999;\n  border-top-color: rgba(0, 0, 0, 0.25);\n  bottom: -11px;\n}\n.popover.top > .arrow:after {\n  content: \" \";\n  bottom: 1px;\n  margin-left: -10px;\n  border-bottom-width: 0;\n  border-top-color: #fff;\n}\n.popover.right > .arrow {\n  top: 50%;\n  left: -11px;\n  margin-top: -11px;\n  border-left-width: 0;\n  border-right-color: #999999;\n  border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n  content: \" \";\n  left: 1px;\n  bottom: -10px;\n  border-left-width: 0;\n  border-right-color: #fff;\n}\n.popover.bottom > .arrow {\n  left: 50%;\n  margin-left: -11px;\n  border-top-width: 0;\n  border-bottom-color: #999999;\n  border-bottom-color: rgba(0, 0, 0, 0.25);\n  top: -11px;\n}\n.popover.bottom > .arrow:after {\n  content: \" \";\n  top: 1px;\n  margin-left: -10px;\n  border-top-width: 0;\n  border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n  top: 50%;\n  right: -11px;\n  margin-top: -11px;\n  border-right-width: 0;\n  border-left-color: #999999;\n  border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n  content: \" \";\n  right: 1px;\n  border-right-width: 0;\n  border-left-color: #fff;\n  bottom: -10px;\n}\n.carousel {\n  position: relative;\n}\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n}\n.carousel-inner > .item {\n  display: none;\n  position: relative;\n  -webkit-transition: 0.6s ease-in-out left;\n  -o-transition: 0.6s ease-in-out left;\n  transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n  .carousel-inner > .item {\n    -webkit-transition: -webkit-transform 0.6s ease-in-out;\n    -moz-transition: -moz-transform 0.6s ease-in-out;\n    -o-transition: -o-transform 0.6s ease-in-out;\n    transition: transform 0.6s ease-in-out;\n    -webkit-backface-visibility: hidden;\n    -moz-backface-visibility: hidden;\n    backface-visibility: hidden;\n    -webkit-perspective: 1000px;\n    -moz-perspective: 1000px;\n    perspective: 1000px;\n  }\n  .carousel-inner > .item.next,\n  .carousel-inner > .item.active.right {\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n    left: 0;\n  }\n  .carousel-inner > .item.prev,\n  .carousel-inner > .item.active.left {\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n    left: 0;\n  }\n  .carousel-inner > .item.next.left,\n  .carousel-inner > .item.prev.right,\n  .carousel-inner > .item.active {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n    left: 0;\n  }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  display: block;\n}\n.carousel-inner > .active {\n  left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n.carousel-inner > .next {\n  left: 100%;\n}\n.carousel-inner > .prev {\n  left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n  left: 0;\n}\n.carousel-inner > .active.left {\n  left: -100%;\n}\n.carousel-inner > .active.right {\n  left: 100%;\n}\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: 15%;\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n  font-size: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n  background-color: rgba(0, 0, 0, 0);\n}\n.carousel-control.left {\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n  left: auto;\n  right: 0;\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n  outline: 0;\n  color: #fff;\n  text-decoration: none;\n  opacity: 0.9;\n  filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n  position: absolute;\n  top: 50%;\n  margin-top: -10px;\n  z-index: 5;\n  display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n  left: 50%;\n  margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n  right: 50%;\n  margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n  width: 20px;\n  height: 20px;\n  line-height: 1;\n  font-family: serif;\n}\n.carousel-control .icon-prev:before {\n  content: '\\2039';\n}\n.carousel-control .icon-next:before {\n  content: '\\203a';\n}\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n}\n.carousel-indicators li {\n  display: inline-block;\n  width: 10px;\n  height: 10px;\n  margin: 1px;\n  text-indent: -999px;\n  border: 1px solid #fff;\n  border-radius: 10px;\n  cursor: pointer;\n  background-color: #000 \\9;\n  background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n  margin: 0;\n  width: 12px;\n  height: 12px;\n  background-color: #fff;\n}\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n  text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-prev,\n  .carousel-control .icon-next {\n    width: 30px;\n    height: 30px;\n    margin-top: -10px;\n    font-size: 30px;\n  }\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .icon-prev {\n    margin-left: -10px;\n  }\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-next {\n    margin-right: -10px;\n  }\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after,\n.item_buttons:before,\n.item_buttons:after {\n  content: \" \";\n  display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after,\n.item_buttons:after {\n  clear: both;\n}\n.center-block {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  font: 0/0 a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n.hidden {\n  display: none !important;\n}\n.affix {\n  position: fixed;\n}\n@-ms-viewport {\n  width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n  display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n  display: none !important;\n}\n@media (max-width: 767px) {\n  .visible-xs {\n    display: block !important;\n  }\n  table.visible-xs {\n    display: table !important;\n  }\n  tr.visible-xs {\n    display: table-row !important;\n  }\n  th.visible-xs,\n  td.visible-xs {\n    display: table-cell !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-block {\n    display: block !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline {\n    display: inline !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm {\n    display: block !important;\n  }\n  table.visible-sm {\n    display: table !important;\n  }\n  tr.visible-sm {\n    display: table-row !important;\n  }\n  th.visible-sm,\n  td.visible-sm {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-block {\n    display: block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md {\n    display: block !important;\n  }\n  table.visible-md {\n    display: table !important;\n  }\n  tr.visible-md {\n    display: table-row !important;\n  }\n  th.visible-md,\n  td.visible-md {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-block {\n    display: block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg {\n    display: block !important;\n  }\n  table.visible-lg {\n    display: table !important;\n  }\n  tr.visible-lg {\n    display: table-row !important;\n  }\n  th.visible-lg,\n  td.visible-lg {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-block {\n    display: block !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (max-width: 767px) {\n  .hidden-xs {\n    display: none !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .hidden-sm {\n    display: none !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .hidden-md {\n    display: none !important;\n  }\n}\n@media (min-width: 1200px) {\n  .hidden-lg {\n    display: none !important;\n  }\n}\n.visible-print {\n  display: none !important;\n}\n@media print {\n  .visible-print {\n    display: block !important;\n  }\n  table.visible-print {\n    display: table !important;\n  }\n  tr.visible-print {\n    display: table-row !important;\n  }\n  th.visible-print,\n  td.visible-print {\n    display: table-cell !important;\n  }\n}\n.visible-print-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-block {\n    display: block !important;\n  }\n}\n.visible-print-inline {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline {\n    display: inline !important;\n  }\n}\n.visible-print-inline-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline-block {\n    display: inline-block !important;\n  }\n}\n@media print {\n  .hidden-print {\n    display: none !important;\n  }\n}\n/*!\n*\n* Font Awesome\n*\n*/\n/*!\n *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome\n *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)\n */\n/* FONT PATH\n * -------------------------- */\n@font-face {\n  font-family: 'FontAwesome';\n  src: url('../components/font-awesome/fonts/fontawesome-webfont.eot?v=4.7.0');\n  src: url('../components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../components/font-awesome/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../components/font-awesome/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../components/font-awesome/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');\n  font-weight: normal;\n  font-style: normal;\n}\n.fa {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n/* makes the font 33% larger relative to the icon container */\n.fa-lg {\n  font-size: 1.33333333em;\n  line-height: 0.75em;\n  vertical-align: -15%;\n}\n.fa-2x {\n  font-size: 2em;\n}\n.fa-3x {\n  font-size: 3em;\n}\n.fa-4x {\n  font-size: 4em;\n}\n.fa-5x {\n  font-size: 5em;\n}\n.fa-fw {\n  width: 1.28571429em;\n  text-align: center;\n}\n.fa-ul {\n  padding-left: 0;\n  margin-left: 2.14285714em;\n  list-style-type: none;\n}\n.fa-ul > li {\n  position: relative;\n}\n.fa-li {\n  position: absolute;\n  left: -2.14285714em;\n  width: 2.14285714em;\n  top: 0.14285714em;\n  text-align: center;\n}\n.fa-li.fa-lg {\n  left: -1.85714286em;\n}\n.fa-border {\n  padding: .2em .25em .15em;\n  border: solid 0.08em #eee;\n  border-radius: .1em;\n}\n.fa-pull-left {\n  float: left;\n}\n.fa-pull-right {\n  float: right;\n}\n.fa.fa-pull-left {\n  margin-right: .3em;\n}\n.fa.fa-pull-right {\n  margin-left: .3em;\n}\n/* Deprecated as of 4.4.0 */\n.pull-right {\n  float: right;\n}\n.pull-left {\n  float: left;\n}\n.fa.pull-left {\n  margin-right: .3em;\n}\n.fa.pull-right {\n  margin-left: .3em;\n}\n.fa-spin {\n  -webkit-animation: fa-spin 2s infinite linear;\n  animation: fa-spin 2s infinite linear;\n}\n.fa-pulse {\n  -webkit-animation: fa-spin 1s infinite steps(8);\n  animation: fa-spin 1s infinite steps(8);\n}\n@-webkit-keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n@keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n.fa-rotate-90 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)\";\n  -webkit-transform: rotate(90deg);\n  -ms-transform: rotate(90deg);\n  transform: rotate(90deg);\n}\n.fa-rotate-180 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)\";\n  -webkit-transform: rotate(180deg);\n  -ms-transform: rotate(180deg);\n  transform: rotate(180deg);\n}\n.fa-rotate-270 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)\";\n  -webkit-transform: rotate(270deg);\n  -ms-transform: rotate(270deg);\n  transform: rotate(270deg);\n}\n.fa-flip-horizontal {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)\";\n  -webkit-transform: scale(-1, 1);\n  -ms-transform: scale(-1, 1);\n  transform: scale(-1, 1);\n}\n.fa-flip-vertical {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\";\n  -webkit-transform: scale(1, -1);\n  -ms-transform: scale(1, -1);\n  transform: scale(1, -1);\n}\n:root .fa-rotate-90,\n:root .fa-rotate-180,\n:root .fa-rotate-270,\n:root .fa-flip-horizontal,\n:root .fa-flip-vertical {\n  filter: none;\n}\n.fa-stack {\n  position: relative;\n  display: inline-block;\n  width: 2em;\n  height: 2em;\n  line-height: 2em;\n  vertical-align: middle;\n}\n.fa-stack-1x,\n.fa-stack-2x {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  text-align: center;\n}\n.fa-stack-1x {\n  line-height: inherit;\n}\n.fa-stack-2x {\n  font-size: 2em;\n}\n.fa-inverse {\n  color: #fff;\n}\n/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen\n   readers do not read off random characters that represent icons */\n.fa-glass:before {\n  content: \"\\f000\";\n}\n.fa-music:before {\n  content: \"\\f001\";\n}\n.fa-search:before {\n  content: \"\\f002\";\n}\n.fa-envelope-o:before {\n  content: \"\\f003\";\n}\n.fa-heart:before {\n  content: \"\\f004\";\n}\n.fa-star:before {\n  content: \"\\f005\";\n}\n.fa-star-o:before {\n  content: \"\\f006\";\n}\n.fa-user:before {\n  content: \"\\f007\";\n}\n.fa-film:before {\n  content: \"\\f008\";\n}\n.fa-th-large:before {\n  content: \"\\f009\";\n}\n.fa-th:before {\n  content: \"\\f00a\";\n}\n.fa-th-list:before {\n  content: \"\\f00b\";\n}\n.fa-check:before {\n  content: \"\\f00c\";\n}\n.fa-remove:before,\n.fa-close:before,\n.fa-times:before {\n  content: \"\\f00d\";\n}\n.fa-search-plus:before {\n  content: \"\\f00e\";\n}\n.fa-search-minus:before {\n  content: \"\\f010\";\n}\n.fa-power-off:before {\n  content: \"\\f011\";\n}\n.fa-signal:before {\n  content: \"\\f012\";\n}\n.fa-gear:before,\n.fa-cog:before {\n  content: \"\\f013\";\n}\n.fa-trash-o:before {\n  content: \"\\f014\";\n}\n.fa-home:before {\n  content: \"\\f015\";\n}\n.fa-file-o:before {\n  content: \"\\f016\";\n}\n.fa-clock-o:before {\n  content: \"\\f017\";\n}\n.fa-road:before {\n  content: \"\\f018\";\n}\n.fa-download:before {\n  content: \"\\f019\";\n}\n.fa-arrow-circle-o-down:before {\n  content: \"\\f01a\";\n}\n.fa-arrow-circle-o-up:before {\n  content: \"\\f01b\";\n}\n.fa-inbox:before {\n  content: \"\\f01c\";\n}\n.fa-play-circle-o:before {\n  content: \"\\f01d\";\n}\n.fa-rotate-right:before,\n.fa-repeat:before {\n  content: \"\\f01e\";\n}\n.fa-refresh:before {\n  content: \"\\f021\";\n}\n.fa-list-alt:before {\n  content: \"\\f022\";\n}\n.fa-lock:before {\n  content: \"\\f023\";\n}\n.fa-flag:before {\n  content: \"\\f024\";\n}\n.fa-headphones:before {\n  content: \"\\f025\";\n}\n.fa-volume-off:before {\n  content: \"\\f026\";\n}\n.fa-volume-down:before {\n  content: \"\\f027\";\n}\n.fa-volume-up:before {\n  content: \"\\f028\";\n}\n.fa-qrcode:before {\n  content: \"\\f029\";\n}\n.fa-barcode:before {\n  content: \"\\f02a\";\n}\n.fa-tag:before {\n  content: \"\\f02b\";\n}\n.fa-tags:before {\n  content: \"\\f02c\";\n}\n.fa-book:before {\n  content: \"\\f02d\";\n}\n.fa-bookmark:before {\n  content: \"\\f02e\";\n}\n.fa-print:before {\n  content: \"\\f02f\";\n}\n.fa-camera:before {\n  content: \"\\f030\";\n}\n.fa-font:before {\n  content: \"\\f031\";\n}\n.fa-bold:before {\n  content: \"\\f032\";\n}\n.fa-italic:before {\n  content: \"\\f033\";\n}\n.fa-text-height:before {\n  content: \"\\f034\";\n}\n.fa-text-width:before {\n  content: \"\\f035\";\n}\n.fa-align-left:before {\n  content: \"\\f036\";\n}\n.fa-align-center:before {\n  content: \"\\f037\";\n}\n.fa-align-right:before {\n  content: \"\\f038\";\n}\n.fa-align-justify:before {\n  content: \"\\f039\";\n}\n.fa-list:before {\n  content: \"\\f03a\";\n}\n.fa-dedent:before,\n.fa-outdent:before {\n  content: \"\\f03b\";\n}\n.fa-indent:before {\n  content: \"\\f03c\";\n}\n.fa-video-camera:before {\n  content: \"\\f03d\";\n}\n.fa-photo:before,\n.fa-image:before,\n.fa-picture-o:before {\n  content: \"\\f03e\";\n}\n.fa-pencil:before {\n  content: \"\\f040\";\n}\n.fa-map-marker:before {\n  content: \"\\f041\";\n}\n.fa-adjust:before {\n  content: \"\\f042\";\n}\n.fa-tint:before {\n  content: \"\\f043\";\n}\n.fa-edit:before,\n.fa-pencil-square-o:before {\n  content: \"\\f044\";\n}\n.fa-share-square-o:before {\n  content: \"\\f045\";\n}\n.fa-check-square-o:before {\n  content: \"\\f046\";\n}\n.fa-arrows:before {\n  content: \"\\f047\";\n}\n.fa-step-backward:before {\n  content: \"\\f048\";\n}\n.fa-fast-backward:before {\n  content: \"\\f049\";\n}\n.fa-backward:before {\n  content: \"\\f04a\";\n}\n.fa-play:before {\n  content: \"\\f04b\";\n}\n.fa-pause:before {\n  content: \"\\f04c\";\n}\n.fa-stop:before {\n  content: \"\\f04d\";\n}\n.fa-forward:before {\n  content: \"\\f04e\";\n}\n.fa-fast-forward:before {\n  content: \"\\f050\";\n}\n.fa-step-forward:before {\n  content: \"\\f051\";\n}\n.fa-eject:before {\n  content: \"\\f052\";\n}\n.fa-chevron-left:before {\n  content: \"\\f053\";\n}\n.fa-chevron-right:before {\n  content: \"\\f054\";\n}\n.fa-plus-circle:before {\n  content: \"\\f055\";\n}\n.fa-minus-circle:before {\n  content: \"\\f056\";\n}\n.fa-times-circle:before {\n  content: \"\\f057\";\n}\n.fa-check-circle:before {\n  content: \"\\f058\";\n}\n.fa-question-circle:before {\n  content: \"\\f059\";\n}\n.fa-info-circle:before {\n  content: \"\\f05a\";\n}\n.fa-crosshairs:before {\n  content: \"\\f05b\";\n}\n.fa-times-circle-o:before {\n  content: \"\\f05c\";\n}\n.fa-check-circle-o:before {\n  content: \"\\f05d\";\n}\n.fa-ban:before {\n  content: \"\\f05e\";\n}\n.fa-arrow-left:before {\n  content: \"\\f060\";\n}\n.fa-arrow-right:before {\n  content: \"\\f061\";\n}\n.fa-arrow-up:before {\n  content: \"\\f062\";\n}\n.fa-arrow-down:before {\n  content: \"\\f063\";\n}\n.fa-mail-forward:before,\n.fa-share:before {\n  content: \"\\f064\";\n}\n.fa-expand:before {\n  content: \"\\f065\";\n}\n.fa-compress:before {\n  content: \"\\f066\";\n}\n.fa-plus:before {\n  content: \"\\f067\";\n}\n.fa-minus:before {\n  content: \"\\f068\";\n}\n.fa-asterisk:before {\n  content: \"\\f069\";\n}\n.fa-exclamation-circle:before {\n  content: \"\\f06a\";\n}\n.fa-gift:before {\n  content: \"\\f06b\";\n}\n.fa-leaf:before {\n  content: \"\\f06c\";\n}\n.fa-fire:before {\n  content: \"\\f06d\";\n}\n.fa-eye:before {\n  content: \"\\f06e\";\n}\n.fa-eye-slash:before {\n  content: \"\\f070\";\n}\n.fa-warning:before,\n.fa-exclamation-triangle:before {\n  content: \"\\f071\";\n}\n.fa-plane:before {\n  content: \"\\f072\";\n}\n.fa-calendar:before {\n  content: \"\\f073\";\n}\n.fa-random:before {\n  content: \"\\f074\";\n}\n.fa-comment:before {\n  content: \"\\f075\";\n}\n.fa-magnet:before {\n  content: \"\\f076\";\n}\n.fa-chevron-up:before {\n  content: \"\\f077\";\n}\n.fa-chevron-down:before {\n  content: \"\\f078\";\n}\n.fa-retweet:before {\n  content: \"\\f079\";\n}\n.fa-shopping-cart:before {\n  content: \"\\f07a\";\n}\n.fa-folder:before {\n  content: \"\\f07b\";\n}\n.fa-folder-open:before {\n  content: \"\\f07c\";\n}\n.fa-arrows-v:before {\n  content: \"\\f07d\";\n}\n.fa-arrows-h:before {\n  content: \"\\f07e\";\n}\n.fa-bar-chart-o:before,\n.fa-bar-chart:before {\n  content: \"\\f080\";\n}\n.fa-twitter-square:before {\n  content: \"\\f081\";\n}\n.fa-facebook-square:before {\n  content: \"\\f082\";\n}\n.fa-camera-retro:before {\n  content: \"\\f083\";\n}\n.fa-key:before {\n  content: \"\\f084\";\n}\n.fa-gears:before,\n.fa-cogs:before {\n  content: \"\\f085\";\n}\n.fa-comments:before {\n  content: \"\\f086\";\n}\n.fa-thumbs-o-up:before {\n  content: \"\\f087\";\n}\n.fa-thumbs-o-down:before {\n  content: \"\\f088\";\n}\n.fa-star-half:before {\n  content: \"\\f089\";\n}\n.fa-heart-o:before {\n  content: \"\\f08a\";\n}\n.fa-sign-out:before {\n  content: \"\\f08b\";\n}\n.fa-linkedin-square:before {\n  content: \"\\f08c\";\n}\n.fa-thumb-tack:before {\n  content: \"\\f08d\";\n}\n.fa-external-link:before {\n  content: \"\\f08e\";\n}\n.fa-sign-in:before {\n  content: \"\\f090\";\n}\n.fa-trophy:before {\n  content: \"\\f091\";\n}\n.fa-github-square:before {\n  content: \"\\f092\";\n}\n.fa-upload:before {\n  content: \"\\f093\";\n}\n.fa-lemon-o:before {\n  content: \"\\f094\";\n}\n.fa-phone:before {\n  content: \"\\f095\";\n}\n.fa-square-o:before {\n  content: \"\\f096\";\n}\n.fa-bookmark-o:before {\n  content: \"\\f097\";\n}\n.fa-phone-square:before {\n  content: \"\\f098\";\n}\n.fa-twitter:before {\n  content: \"\\f099\";\n}\n.fa-facebook-f:before,\n.fa-facebook:before {\n  content: \"\\f09a\";\n}\n.fa-github:before {\n  content: \"\\f09b\";\n}\n.fa-unlock:before {\n  content: \"\\f09c\";\n}\n.fa-credit-card:before {\n  content: \"\\f09d\";\n}\n.fa-feed:before,\n.fa-rss:before {\n  content: \"\\f09e\";\n}\n.fa-hdd-o:before {\n  content: \"\\f0a0\";\n}\n.fa-bullhorn:before {\n  content: \"\\f0a1\";\n}\n.fa-bell:before {\n  content: \"\\f0f3\";\n}\n.fa-certificate:before {\n  content: \"\\f0a3\";\n}\n.fa-hand-o-right:before {\n  content: \"\\f0a4\";\n}\n.fa-hand-o-left:before {\n  content: \"\\f0a5\";\n}\n.fa-hand-o-up:before {\n  content: \"\\f0a6\";\n}\n.fa-hand-o-down:before {\n  content: \"\\f0a7\";\n}\n.fa-arrow-circle-left:before {\n  content: \"\\f0a8\";\n}\n.fa-arrow-circle-right:before {\n  content: \"\\f0a9\";\n}\n.fa-arrow-circle-up:before {\n  content: \"\\f0aa\";\n}\n.fa-arrow-circle-down:before {\n  content: \"\\f0ab\";\n}\n.fa-globe:before {\n  content: \"\\f0ac\";\n}\n.fa-wrench:before {\n  content: \"\\f0ad\";\n}\n.fa-tasks:before {\n  content: \"\\f0ae\";\n}\n.fa-filter:before {\n  content: \"\\f0b0\";\n}\n.fa-briefcase:before {\n  content: \"\\f0b1\";\n}\n.fa-arrows-alt:before {\n  content: \"\\f0b2\";\n}\n.fa-group:before,\n.fa-users:before {\n  content: \"\\f0c0\";\n}\n.fa-chain:before,\n.fa-link:before {\n  content: \"\\f0c1\";\n}\n.fa-cloud:before {\n  content: \"\\f0c2\";\n}\n.fa-flask:before {\n  content: \"\\f0c3\";\n}\n.fa-cut:before,\n.fa-scissors:before {\n  content: \"\\f0c4\";\n}\n.fa-copy:before,\n.fa-files-o:before {\n  content: \"\\f0c5\";\n}\n.fa-paperclip:before {\n  content: \"\\f0c6\";\n}\n.fa-save:before,\n.fa-floppy-o:before {\n  content: \"\\f0c7\";\n}\n.fa-square:before {\n  content: \"\\f0c8\";\n}\n.fa-navicon:before,\n.fa-reorder:before,\n.fa-bars:before {\n  content: \"\\f0c9\";\n}\n.fa-list-ul:before {\n  content: \"\\f0ca\";\n}\n.fa-list-ol:before {\n  content: \"\\f0cb\";\n}\n.fa-strikethrough:before {\n  content: \"\\f0cc\";\n}\n.fa-underline:before {\n  content: \"\\f0cd\";\n}\n.fa-table:before {\n  content: \"\\f0ce\";\n}\n.fa-magic:before {\n  content: \"\\f0d0\";\n}\n.fa-truck:before {\n  content: \"\\f0d1\";\n}\n.fa-pinterest:before {\n  content: \"\\f0d2\";\n}\n.fa-pinterest-square:before {\n  content: \"\\f0d3\";\n}\n.fa-google-plus-square:before {\n  content: \"\\f0d4\";\n}\n.fa-google-plus:before {\n  content: \"\\f0d5\";\n}\n.fa-money:before {\n  content: \"\\f0d6\";\n}\n.fa-caret-down:before {\n  content: \"\\f0d7\";\n}\n.fa-caret-up:before {\n  content: \"\\f0d8\";\n}\n.fa-caret-left:before {\n  content: \"\\f0d9\";\n}\n.fa-caret-right:before {\n  content: \"\\f0da\";\n}\n.fa-columns:before {\n  content: \"\\f0db\";\n}\n.fa-unsorted:before,\n.fa-sort:before {\n  content: \"\\f0dc\";\n}\n.fa-sort-down:before,\n.fa-sort-desc:before {\n  content: \"\\f0dd\";\n}\n.fa-sort-up:before,\n.fa-sort-asc:before {\n  content: \"\\f0de\";\n}\n.fa-envelope:before {\n  content: \"\\f0e0\";\n}\n.fa-linkedin:before {\n  content: \"\\f0e1\";\n}\n.fa-rotate-left:before,\n.fa-undo:before {\n  content: \"\\f0e2\";\n}\n.fa-legal:before,\n.fa-gavel:before {\n  content: \"\\f0e3\";\n}\n.fa-dashboard:before,\n.fa-tachometer:before {\n  content: \"\\f0e4\";\n}\n.fa-comment-o:before {\n  content: \"\\f0e5\";\n}\n.fa-comments-o:before {\n  content: \"\\f0e6\";\n}\n.fa-flash:before,\n.fa-bolt:before {\n  content: \"\\f0e7\";\n}\n.fa-sitemap:before {\n  content: \"\\f0e8\";\n}\n.fa-umbrella:before {\n  content: \"\\f0e9\";\n}\n.fa-paste:before,\n.fa-clipboard:before {\n  content: \"\\f0ea\";\n}\n.fa-lightbulb-o:before {\n  content: \"\\f0eb\";\n}\n.fa-exchange:before {\n  content: \"\\f0ec\";\n}\n.fa-cloud-download:before {\n  content: \"\\f0ed\";\n}\n.fa-cloud-upload:before {\n  content: \"\\f0ee\";\n}\n.fa-user-md:before {\n  content: \"\\f0f0\";\n}\n.fa-stethoscope:before {\n  content: \"\\f0f1\";\n}\n.fa-suitcase:before {\n  content: \"\\f0f2\";\n}\n.fa-bell-o:before {\n  content: \"\\f0a2\";\n}\n.fa-coffee:before {\n  content: \"\\f0f4\";\n}\n.fa-cutlery:before {\n  content: \"\\f0f5\";\n}\n.fa-file-text-o:before {\n  content: \"\\f0f6\";\n}\n.fa-building-o:before {\n  content: \"\\f0f7\";\n}\n.fa-hospital-o:before {\n  content: \"\\f0f8\";\n}\n.fa-ambulance:before {\n  content: \"\\f0f9\";\n}\n.fa-medkit:before {\n  content: \"\\f0fa\";\n}\n.fa-fighter-jet:before {\n  content: \"\\f0fb\";\n}\n.fa-beer:before {\n  content: \"\\f0fc\";\n}\n.fa-h-square:before {\n  content: \"\\f0fd\";\n}\n.fa-plus-square:before {\n  content: \"\\f0fe\";\n}\n.fa-angle-double-left:before {\n  content: \"\\f100\";\n}\n.fa-angle-double-right:before {\n  content: \"\\f101\";\n}\n.fa-angle-double-up:before {\n  content: \"\\f102\";\n}\n.fa-angle-double-down:before {\n  content: \"\\f103\";\n}\n.fa-angle-left:before {\n  content: \"\\f104\";\n}\n.fa-angle-right:before {\n  content: \"\\f105\";\n}\n.fa-angle-up:before {\n  content: \"\\f106\";\n}\n.fa-angle-down:before {\n  content: \"\\f107\";\n}\n.fa-desktop:before {\n  content: \"\\f108\";\n}\n.fa-laptop:before {\n  content: \"\\f109\";\n}\n.fa-tablet:before {\n  content: \"\\f10a\";\n}\n.fa-mobile-phone:before,\n.fa-mobile:before {\n  content: \"\\f10b\";\n}\n.fa-circle-o:before {\n  content: \"\\f10c\";\n}\n.fa-quote-left:before {\n  content: \"\\f10d\";\n}\n.fa-quote-right:before {\n  content: \"\\f10e\";\n}\n.fa-spinner:before {\n  content: \"\\f110\";\n}\n.fa-circle:before {\n  content: \"\\f111\";\n}\n.fa-mail-reply:before,\n.fa-reply:before {\n  content: \"\\f112\";\n}\n.fa-github-alt:before {\n  content: \"\\f113\";\n}\n.fa-folder-o:before {\n  content: \"\\f114\";\n}\n.fa-folder-open-o:before {\n  content: \"\\f115\";\n}\n.fa-smile-o:before {\n  content: \"\\f118\";\n}\n.fa-frown-o:before {\n  content: \"\\f119\";\n}\n.fa-meh-o:before {\n  content: \"\\f11a\";\n}\n.fa-gamepad:before {\n  content: \"\\f11b\";\n}\n.fa-keyboard-o:before {\n  content: \"\\f11c\";\n}\n.fa-flag-o:before {\n  content: \"\\f11d\";\n}\n.fa-flag-checkered:before {\n  content: \"\\f11e\";\n}\n.fa-terminal:before {\n  content: \"\\f120\";\n}\n.fa-code:before {\n  content: \"\\f121\";\n}\n.fa-mail-reply-all:before,\n.fa-reply-all:before {\n  content: \"\\f122\";\n}\n.fa-star-half-empty:before,\n.fa-star-half-full:before,\n.fa-star-half-o:before {\n  content: \"\\f123\";\n}\n.fa-location-arrow:before {\n  content: \"\\f124\";\n}\n.fa-crop:before {\n  content: \"\\f125\";\n}\n.fa-code-fork:before {\n  content: \"\\f126\";\n}\n.fa-unlink:before,\n.fa-chain-broken:before {\n  content: \"\\f127\";\n}\n.fa-question:before {\n  content: \"\\f128\";\n}\n.fa-info:before {\n  content: \"\\f129\";\n}\n.fa-exclamation:before {\n  content: \"\\f12a\";\n}\n.fa-superscript:before {\n  content: \"\\f12b\";\n}\n.fa-subscript:before {\n  content: \"\\f12c\";\n}\n.fa-eraser:before {\n  content: \"\\f12d\";\n}\n.fa-puzzle-piece:before {\n  content: \"\\f12e\";\n}\n.fa-microphone:before {\n  content: \"\\f130\";\n}\n.fa-microphone-slash:before {\n  content: \"\\f131\";\n}\n.fa-shield:before {\n  content: \"\\f132\";\n}\n.fa-calendar-o:before {\n  content: \"\\f133\";\n}\n.fa-fire-extinguisher:before {\n  content: \"\\f134\";\n}\n.fa-rocket:before {\n  content: \"\\f135\";\n}\n.fa-maxcdn:before {\n  content: \"\\f136\";\n}\n.fa-chevron-circle-left:before {\n  content: \"\\f137\";\n}\n.fa-chevron-circle-right:before {\n  content: \"\\f138\";\n}\n.fa-chevron-circle-up:before {\n  content: \"\\f139\";\n}\n.fa-chevron-circle-down:before {\n  content: \"\\f13a\";\n}\n.fa-html5:before {\n  content: \"\\f13b\";\n}\n.fa-css3:before {\n  content: \"\\f13c\";\n}\n.fa-anchor:before {\n  content: \"\\f13d\";\n}\n.fa-unlock-alt:before {\n  content: \"\\f13e\";\n}\n.fa-bullseye:before {\n  content: \"\\f140\";\n}\n.fa-ellipsis-h:before {\n  content: \"\\f141\";\n}\n.fa-ellipsis-v:before {\n  content: \"\\f142\";\n}\n.fa-rss-square:before {\n  content: \"\\f143\";\n}\n.fa-play-circle:before {\n  content: \"\\f144\";\n}\n.fa-ticket:before {\n  content: \"\\f145\";\n}\n.fa-minus-square:before {\n  content: \"\\f146\";\n}\n.fa-minus-square-o:before {\n  content: \"\\f147\";\n}\n.fa-level-up:before {\n  content: \"\\f148\";\n}\n.fa-level-down:before {\n  content: \"\\f149\";\n}\n.fa-check-square:before {\n  content: \"\\f14a\";\n}\n.fa-pencil-square:before {\n  content: \"\\f14b\";\n}\n.fa-external-link-square:before {\n  content: \"\\f14c\";\n}\n.fa-share-square:before {\n  content: \"\\f14d\";\n}\n.fa-compass:before {\n  content: \"\\f14e\";\n}\n.fa-toggle-down:before,\n.fa-caret-square-o-down:before {\n  content: \"\\f150\";\n}\n.fa-toggle-up:before,\n.fa-caret-square-o-up:before {\n  content: \"\\f151\";\n}\n.fa-toggle-right:before,\n.fa-caret-square-o-right:before {\n  content: \"\\f152\";\n}\n.fa-euro:before,\n.fa-eur:before {\n  content: \"\\f153\";\n}\n.fa-gbp:before {\n  content: \"\\f154\";\n}\n.fa-dollar:before,\n.fa-usd:before {\n  content: \"\\f155\";\n}\n.fa-rupee:before,\n.fa-inr:before {\n  content: \"\\f156\";\n}\n.fa-cny:before,\n.fa-rmb:before,\n.fa-yen:before,\n.fa-jpy:before {\n  content: \"\\f157\";\n}\n.fa-ruble:before,\n.fa-rouble:before,\n.fa-rub:before {\n  content: \"\\f158\";\n}\n.fa-won:before,\n.fa-krw:before {\n  content: \"\\f159\";\n}\n.fa-bitcoin:before,\n.fa-btc:before {\n  content: \"\\f15a\";\n}\n.fa-file:before {\n  content: \"\\f15b\";\n}\n.fa-file-text:before {\n  content: \"\\f15c\";\n}\n.fa-sort-alpha-asc:before {\n  content: \"\\f15d\";\n}\n.fa-sort-alpha-desc:before {\n  content: \"\\f15e\";\n}\n.fa-sort-amount-asc:before {\n  content: \"\\f160\";\n}\n.fa-sort-amount-desc:before {\n  content: \"\\f161\";\n}\n.fa-sort-numeric-asc:before {\n  content: \"\\f162\";\n}\n.fa-sort-numeric-desc:before {\n  content: \"\\f163\";\n}\n.fa-thumbs-up:before {\n  content: \"\\f164\";\n}\n.fa-thumbs-down:before {\n  content: \"\\f165\";\n}\n.fa-youtube-square:before {\n  content: \"\\f166\";\n}\n.fa-youtube:before {\n  content: \"\\f167\";\n}\n.fa-xing:before {\n  content: \"\\f168\";\n}\n.fa-xing-square:before {\n  content: \"\\f169\";\n}\n.fa-youtube-play:before {\n  content: \"\\f16a\";\n}\n.fa-dropbox:before {\n  content: \"\\f16b\";\n}\n.fa-stack-overflow:before {\n  content: \"\\f16c\";\n}\n.fa-instagram:before {\n  content: \"\\f16d\";\n}\n.fa-flickr:before {\n  content: \"\\f16e\";\n}\n.fa-adn:before {\n  content: \"\\f170\";\n}\n.fa-bitbucket:before {\n  content: \"\\f171\";\n}\n.fa-bitbucket-square:before {\n  content: \"\\f172\";\n}\n.fa-tumblr:before {\n  content: \"\\f173\";\n}\n.fa-tumblr-square:before {\n  content: \"\\f174\";\n}\n.fa-long-arrow-down:before {\n  content: \"\\f175\";\n}\n.fa-long-arrow-up:before {\n  content: \"\\f176\";\n}\n.fa-long-arrow-left:before {\n  content: \"\\f177\";\n}\n.fa-long-arrow-right:before {\n  content: \"\\f178\";\n}\n.fa-apple:before {\n  content: \"\\f179\";\n}\n.fa-windows:before {\n  content: \"\\f17a\";\n}\n.fa-android:before {\n  content: \"\\f17b\";\n}\n.fa-linux:before {\n  content: \"\\f17c\";\n}\n.fa-dribbble:before {\n  content: \"\\f17d\";\n}\n.fa-skype:before {\n  content: \"\\f17e\";\n}\n.fa-foursquare:before {\n  content: \"\\f180\";\n}\n.fa-trello:before {\n  content: \"\\f181\";\n}\n.fa-female:before {\n  content: \"\\f182\";\n}\n.fa-male:before {\n  content: \"\\f183\";\n}\n.fa-gittip:before,\n.fa-gratipay:before {\n  content: \"\\f184\";\n}\n.fa-sun-o:before {\n  content: \"\\f185\";\n}\n.fa-moon-o:before {\n  content: \"\\f186\";\n}\n.fa-archive:before {\n  content: \"\\f187\";\n}\n.fa-bug:before {\n  content: \"\\f188\";\n}\n.fa-vk:before {\n  content: \"\\f189\";\n}\n.fa-weibo:before {\n  content: \"\\f18a\";\n}\n.fa-renren:before {\n  content: \"\\f18b\";\n}\n.fa-pagelines:before {\n  content: \"\\f18c\";\n}\n.fa-stack-exchange:before {\n  content: \"\\f18d\";\n}\n.fa-arrow-circle-o-right:before {\n  content: \"\\f18e\";\n}\n.fa-arrow-circle-o-left:before {\n  content: \"\\f190\";\n}\n.fa-toggle-left:before,\n.fa-caret-square-o-left:before {\n  content: \"\\f191\";\n}\n.fa-dot-circle-o:before {\n  content: \"\\f192\";\n}\n.fa-wheelchair:before {\n  content: \"\\f193\";\n}\n.fa-vimeo-square:before {\n  content: \"\\f194\";\n}\n.fa-turkish-lira:before,\n.fa-try:before {\n  content: \"\\f195\";\n}\n.fa-plus-square-o:before {\n  content: \"\\f196\";\n}\n.fa-space-shuttle:before {\n  content: \"\\f197\";\n}\n.fa-slack:before {\n  content: \"\\f198\";\n}\n.fa-envelope-square:before {\n  content: \"\\f199\";\n}\n.fa-wordpress:before {\n  content: \"\\f19a\";\n}\n.fa-openid:before {\n  content: \"\\f19b\";\n}\n.fa-institution:before,\n.fa-bank:before,\n.fa-university:before {\n  content: \"\\f19c\";\n}\n.fa-mortar-board:before,\n.fa-graduation-cap:before {\n  content: \"\\f19d\";\n}\n.fa-yahoo:before {\n  content: \"\\f19e\";\n}\n.fa-google:before {\n  content: \"\\f1a0\";\n}\n.fa-reddit:before {\n  content: \"\\f1a1\";\n}\n.fa-reddit-square:before {\n  content: \"\\f1a2\";\n}\n.fa-stumbleupon-circle:before {\n  content: \"\\f1a3\";\n}\n.fa-stumbleupon:before {\n  content: \"\\f1a4\";\n}\n.fa-delicious:before {\n  content: \"\\f1a5\";\n}\n.fa-digg:before {\n  content: \"\\f1a6\";\n}\n.fa-pied-piper-pp:before {\n  content: \"\\f1a7\";\n}\n.fa-pied-piper-alt:before {\n  content: \"\\f1a8\";\n}\n.fa-drupal:before {\n  content: \"\\f1a9\";\n}\n.fa-joomla:before {\n  content: \"\\f1aa\";\n}\n.fa-language:before {\n  content: \"\\f1ab\";\n}\n.fa-fax:before {\n  content: \"\\f1ac\";\n}\n.fa-building:before {\n  content: \"\\f1ad\";\n}\n.fa-child:before {\n  content: \"\\f1ae\";\n}\n.fa-paw:before {\n  content: \"\\f1b0\";\n}\n.fa-spoon:before {\n  content: \"\\f1b1\";\n}\n.fa-cube:before {\n  content: \"\\f1b2\";\n}\n.fa-cubes:before {\n  content: \"\\f1b3\";\n}\n.fa-behance:before {\n  content: \"\\f1b4\";\n}\n.fa-behance-square:before {\n  content: \"\\f1b5\";\n}\n.fa-steam:before {\n  content: \"\\f1b6\";\n}\n.fa-steam-square:before {\n  content: \"\\f1b7\";\n}\n.fa-recycle:before {\n  content: \"\\f1b8\";\n}\n.fa-automobile:before,\n.fa-car:before {\n  content: \"\\f1b9\";\n}\n.fa-cab:before,\n.fa-taxi:before {\n  content: \"\\f1ba\";\n}\n.fa-tree:before {\n  content: \"\\f1bb\";\n}\n.fa-spotify:before {\n  content: \"\\f1bc\";\n}\n.fa-deviantart:before {\n  content: \"\\f1bd\";\n}\n.fa-soundcloud:before {\n  content: \"\\f1be\";\n}\n.fa-database:before {\n  content: \"\\f1c0\";\n}\n.fa-file-pdf-o:before {\n  content: \"\\f1c1\";\n}\n.fa-file-word-o:before {\n  content: \"\\f1c2\";\n}\n.fa-file-excel-o:before {\n  content: \"\\f1c3\";\n}\n.fa-file-powerpoint-o:before {\n  content: \"\\f1c4\";\n}\n.fa-file-photo-o:before,\n.fa-file-picture-o:before,\n.fa-file-image-o:before {\n  content: \"\\f1c5\";\n}\n.fa-file-zip-o:before,\n.fa-file-archive-o:before {\n  content: \"\\f1c6\";\n}\n.fa-file-sound-o:before,\n.fa-file-audio-o:before {\n  content: \"\\f1c7\";\n}\n.fa-file-movie-o:before,\n.fa-file-video-o:before {\n  content: \"\\f1c8\";\n}\n.fa-file-code-o:before {\n  content: \"\\f1c9\";\n}\n.fa-vine:before {\n  content: \"\\f1ca\";\n}\n.fa-codepen:before {\n  content: \"\\f1cb\";\n}\n.fa-jsfiddle:before {\n  content: \"\\f1cc\";\n}\n.fa-life-bouy:before,\n.fa-life-buoy:before,\n.fa-life-saver:before,\n.fa-support:before,\n.fa-life-ring:before {\n  content: \"\\f1cd\";\n}\n.fa-circle-o-notch:before {\n  content: \"\\f1ce\";\n}\n.fa-ra:before,\n.fa-resistance:before,\n.fa-rebel:before {\n  content: \"\\f1d0\";\n}\n.fa-ge:before,\n.fa-empire:before {\n  content: \"\\f1d1\";\n}\n.fa-git-square:before {\n  content: \"\\f1d2\";\n}\n.fa-git:before {\n  content: \"\\f1d3\";\n}\n.fa-y-combinator-square:before,\n.fa-yc-square:before,\n.fa-hacker-news:before {\n  content: \"\\f1d4\";\n}\n.fa-tencent-weibo:before {\n  content: \"\\f1d5\";\n}\n.fa-qq:before {\n  content: \"\\f1d6\";\n}\n.fa-wechat:before,\n.fa-weixin:before {\n  content: \"\\f1d7\";\n}\n.fa-send:before,\n.fa-paper-plane:before {\n  content: \"\\f1d8\";\n}\n.fa-send-o:before,\n.fa-paper-plane-o:before {\n  content: \"\\f1d9\";\n}\n.fa-history:before {\n  content: \"\\f1da\";\n}\n.fa-circle-thin:before {\n  content: \"\\f1db\";\n}\n.fa-header:before {\n  content: \"\\f1dc\";\n}\n.fa-paragraph:before {\n  content: \"\\f1dd\";\n}\n.fa-sliders:before {\n  content: \"\\f1de\";\n}\n.fa-share-alt:before {\n  content: \"\\f1e0\";\n}\n.fa-share-alt-square:before {\n  content: \"\\f1e1\";\n}\n.fa-bomb:before {\n  content: \"\\f1e2\";\n}\n.fa-soccer-ball-o:before,\n.fa-futbol-o:before {\n  content: \"\\f1e3\";\n}\n.fa-tty:before {\n  content: \"\\f1e4\";\n}\n.fa-binoculars:before {\n  content: \"\\f1e5\";\n}\n.fa-plug:before {\n  content: \"\\f1e6\";\n}\n.fa-slideshare:before {\n  content: \"\\f1e7\";\n}\n.fa-twitch:before {\n  content: \"\\f1e8\";\n}\n.fa-yelp:before {\n  content: \"\\f1e9\";\n}\n.fa-newspaper-o:before {\n  content: \"\\f1ea\";\n}\n.fa-wifi:before {\n  content: \"\\f1eb\";\n}\n.fa-calculator:before {\n  content: \"\\f1ec\";\n}\n.fa-paypal:before {\n  content: \"\\f1ed\";\n}\n.fa-google-wallet:before {\n  content: \"\\f1ee\";\n}\n.fa-cc-visa:before {\n  content: \"\\f1f0\";\n}\n.fa-cc-mastercard:before {\n  content: \"\\f1f1\";\n}\n.fa-cc-discover:before {\n  content: \"\\f1f2\";\n}\n.fa-cc-amex:before {\n  content: \"\\f1f3\";\n}\n.fa-cc-paypal:before {\n  content: \"\\f1f4\";\n}\n.fa-cc-stripe:before {\n  content: \"\\f1f5\";\n}\n.fa-bell-slash:before {\n  content: \"\\f1f6\";\n}\n.fa-bell-slash-o:before {\n  content: \"\\f1f7\";\n}\n.fa-trash:before {\n  content: \"\\f1f8\";\n}\n.fa-copyright:before {\n  content: \"\\f1f9\";\n}\n.fa-at:before {\n  content: \"\\f1fa\";\n}\n.fa-eyedropper:before {\n  content: \"\\f1fb\";\n}\n.fa-paint-brush:before {\n  content: \"\\f1fc\";\n}\n.fa-birthday-cake:before {\n  content: \"\\f1fd\";\n}\n.fa-area-chart:before {\n  content: \"\\f1fe\";\n}\n.fa-pie-chart:before {\n  content: \"\\f200\";\n}\n.fa-line-chart:before {\n  content: \"\\f201\";\n}\n.fa-lastfm:before {\n  content: \"\\f202\";\n}\n.fa-lastfm-square:before {\n  content: \"\\f203\";\n}\n.fa-toggle-off:before {\n  content: \"\\f204\";\n}\n.fa-toggle-on:before {\n  content: \"\\f205\";\n}\n.fa-bicycle:before {\n  content: \"\\f206\";\n}\n.fa-bus:before {\n  content: \"\\f207\";\n}\n.fa-ioxhost:before {\n  content: \"\\f208\";\n}\n.fa-angellist:before {\n  content: \"\\f209\";\n}\n.fa-cc:before {\n  content: \"\\f20a\";\n}\n.fa-shekel:before,\n.fa-sheqel:before,\n.fa-ils:before {\n  content: \"\\f20b\";\n}\n.fa-meanpath:before {\n  content: \"\\f20c\";\n}\n.fa-buysellads:before {\n  content: \"\\f20d\";\n}\n.fa-connectdevelop:before {\n  content: \"\\f20e\";\n}\n.fa-dashcube:before {\n  content: \"\\f210\";\n}\n.fa-forumbee:before {\n  content: \"\\f211\";\n}\n.fa-leanpub:before {\n  content: \"\\f212\";\n}\n.fa-sellsy:before {\n  content: \"\\f213\";\n}\n.fa-shirtsinbulk:before {\n  content: \"\\f214\";\n}\n.fa-simplybuilt:before {\n  content: \"\\f215\";\n}\n.fa-skyatlas:before {\n  content: \"\\f216\";\n}\n.fa-cart-plus:before {\n  content: \"\\f217\";\n}\n.fa-cart-arrow-down:before {\n  content: \"\\f218\";\n}\n.fa-diamond:before {\n  content: \"\\f219\";\n}\n.fa-ship:before {\n  content: \"\\f21a\";\n}\n.fa-user-secret:before {\n  content: \"\\f21b\";\n}\n.fa-motorcycle:before {\n  content: \"\\f21c\";\n}\n.fa-street-view:before {\n  content: \"\\f21d\";\n}\n.fa-heartbeat:before {\n  content: \"\\f21e\";\n}\n.fa-venus:before {\n  content: \"\\f221\";\n}\n.fa-mars:before {\n  content: \"\\f222\";\n}\n.fa-mercury:before {\n  content: \"\\f223\";\n}\n.fa-intersex:before,\n.fa-transgender:before {\n  content: \"\\f224\";\n}\n.fa-transgender-alt:before {\n  content: \"\\f225\";\n}\n.fa-venus-double:before {\n  content: \"\\f226\";\n}\n.fa-mars-double:before {\n  content: \"\\f227\";\n}\n.fa-venus-mars:before {\n  content: \"\\f228\";\n}\n.fa-mars-stroke:before {\n  content: \"\\f229\";\n}\n.fa-mars-stroke-v:before {\n  content: \"\\f22a\";\n}\n.fa-mars-stroke-h:before {\n  content: \"\\f22b\";\n}\n.fa-neuter:before {\n  content: \"\\f22c\";\n}\n.fa-genderless:before {\n  content: \"\\f22d\";\n}\n.fa-facebook-official:before {\n  content: \"\\f230\";\n}\n.fa-pinterest-p:before {\n  content: \"\\f231\";\n}\n.fa-whatsapp:before {\n  content: \"\\f232\";\n}\n.fa-server:before {\n  content: \"\\f233\";\n}\n.fa-user-plus:before {\n  content: \"\\f234\";\n}\n.fa-user-times:before {\n  content: \"\\f235\";\n}\n.fa-hotel:before,\n.fa-bed:before {\n  content: \"\\f236\";\n}\n.fa-viacoin:before {\n  content: \"\\f237\";\n}\n.fa-train:before {\n  content: \"\\f238\";\n}\n.fa-subway:before {\n  content: \"\\f239\";\n}\n.fa-medium:before {\n  content: \"\\f23a\";\n}\n.fa-yc:before,\n.fa-y-combinator:before {\n  content: \"\\f23b\";\n}\n.fa-optin-monster:before {\n  content: \"\\f23c\";\n}\n.fa-opencart:before {\n  content: \"\\f23d\";\n}\n.fa-expeditedssl:before {\n  content: \"\\f23e\";\n}\n.fa-battery-4:before,\n.fa-battery:before,\n.fa-battery-full:before {\n  content: \"\\f240\";\n}\n.fa-battery-3:before,\n.fa-battery-three-quarters:before {\n  content: \"\\f241\";\n}\n.fa-battery-2:before,\n.fa-battery-half:before {\n  content: \"\\f242\";\n}\n.fa-battery-1:before,\n.fa-battery-quarter:before {\n  content: \"\\f243\";\n}\n.fa-battery-0:before,\n.fa-battery-empty:before {\n  content: \"\\f244\";\n}\n.fa-mouse-pointer:before {\n  content: \"\\f245\";\n}\n.fa-i-cursor:before {\n  content: \"\\f246\";\n}\n.fa-object-group:before {\n  content: \"\\f247\";\n}\n.fa-object-ungroup:before {\n  content: \"\\f248\";\n}\n.fa-sticky-note:before {\n  content: \"\\f249\";\n}\n.fa-sticky-note-o:before {\n  content: \"\\f24a\";\n}\n.fa-cc-jcb:before {\n  content: \"\\f24b\";\n}\n.fa-cc-diners-club:before {\n  content: \"\\f24c\";\n}\n.fa-clone:before {\n  content: \"\\f24d\";\n}\n.fa-balance-scale:before {\n  content: \"\\f24e\";\n}\n.fa-hourglass-o:before {\n  content: \"\\f250\";\n}\n.fa-hourglass-1:before,\n.fa-hourglass-start:before {\n  content: \"\\f251\";\n}\n.fa-hourglass-2:before,\n.fa-hourglass-half:before {\n  content: \"\\f252\";\n}\n.fa-hourglass-3:before,\n.fa-hourglass-end:before {\n  content: \"\\f253\";\n}\n.fa-hourglass:before {\n  content: \"\\f254\";\n}\n.fa-hand-grab-o:before,\n.fa-hand-rock-o:before {\n  content: \"\\f255\";\n}\n.fa-hand-stop-o:before,\n.fa-hand-paper-o:before {\n  content: \"\\f256\";\n}\n.fa-hand-scissors-o:before {\n  content: \"\\f257\";\n}\n.fa-hand-lizard-o:before {\n  content: \"\\f258\";\n}\n.fa-hand-spock-o:before {\n  content: \"\\f259\";\n}\n.fa-hand-pointer-o:before {\n  content: \"\\f25a\";\n}\n.fa-hand-peace-o:before {\n  content: \"\\f25b\";\n}\n.fa-trademark:before {\n  content: \"\\f25c\";\n}\n.fa-registered:before {\n  content: \"\\f25d\";\n}\n.fa-creative-commons:before {\n  content: \"\\f25e\";\n}\n.fa-gg:before {\n  content: \"\\f260\";\n}\n.fa-gg-circle:before {\n  content: \"\\f261\";\n}\n.fa-tripadvisor:before {\n  content: \"\\f262\";\n}\n.fa-odnoklassniki:before {\n  content: \"\\f263\";\n}\n.fa-odnoklassniki-square:before {\n  content: \"\\f264\";\n}\n.fa-get-pocket:before {\n  content: \"\\f265\";\n}\n.fa-wikipedia-w:before {\n  content: \"\\f266\";\n}\n.fa-safari:before {\n  content: \"\\f267\";\n}\n.fa-chrome:before {\n  content: \"\\f268\";\n}\n.fa-firefox:before {\n  content: \"\\f269\";\n}\n.fa-opera:before {\n  content: \"\\f26a\";\n}\n.fa-internet-explorer:before {\n  content: \"\\f26b\";\n}\n.fa-tv:before,\n.fa-television:before {\n  content: \"\\f26c\";\n}\n.fa-contao:before {\n  content: \"\\f26d\";\n}\n.fa-500px:before {\n  content: \"\\f26e\";\n}\n.fa-amazon:before {\n  content: \"\\f270\";\n}\n.fa-calendar-plus-o:before {\n  content: \"\\f271\";\n}\n.fa-calendar-minus-o:before {\n  content: \"\\f272\";\n}\n.fa-calendar-times-o:before {\n  content: \"\\f273\";\n}\n.fa-calendar-check-o:before {\n  content: \"\\f274\";\n}\n.fa-industry:before {\n  content: \"\\f275\";\n}\n.fa-map-pin:before {\n  content: \"\\f276\";\n}\n.fa-map-signs:before {\n  content: \"\\f277\";\n}\n.fa-map-o:before {\n  content: \"\\f278\";\n}\n.fa-map:before {\n  content: \"\\f279\";\n}\n.fa-commenting:before {\n  content: \"\\f27a\";\n}\n.fa-commenting-o:before {\n  content: \"\\f27b\";\n}\n.fa-houzz:before {\n  content: \"\\f27c\";\n}\n.fa-vimeo:before {\n  content: \"\\f27d\";\n}\n.fa-black-tie:before {\n  content: \"\\f27e\";\n}\n.fa-fonticons:before {\n  content: \"\\f280\";\n}\n.fa-reddit-alien:before {\n  content: \"\\f281\";\n}\n.fa-edge:before {\n  content: \"\\f282\";\n}\n.fa-credit-card-alt:before {\n  content: \"\\f283\";\n}\n.fa-codiepie:before {\n  content: \"\\f284\";\n}\n.fa-modx:before {\n  content: \"\\f285\";\n}\n.fa-fort-awesome:before {\n  content: \"\\f286\";\n}\n.fa-usb:before {\n  content: \"\\f287\";\n}\n.fa-product-hunt:before {\n  content: \"\\f288\";\n}\n.fa-mixcloud:before {\n  content: \"\\f289\";\n}\n.fa-scribd:before {\n  content: \"\\f28a\";\n}\n.fa-pause-circle:before {\n  content: \"\\f28b\";\n}\n.fa-pause-circle-o:before {\n  content: \"\\f28c\";\n}\n.fa-stop-circle:before {\n  content: \"\\f28d\";\n}\n.fa-stop-circle-o:before {\n  content: \"\\f28e\";\n}\n.fa-shopping-bag:before {\n  content: \"\\f290\";\n}\n.fa-shopping-basket:before {\n  content: \"\\f291\";\n}\n.fa-hashtag:before {\n  content: \"\\f292\";\n}\n.fa-bluetooth:before {\n  content: \"\\f293\";\n}\n.fa-bluetooth-b:before {\n  content: \"\\f294\";\n}\n.fa-percent:before {\n  content: \"\\f295\";\n}\n.fa-gitlab:before {\n  content: \"\\f296\";\n}\n.fa-wpbeginner:before {\n  content: \"\\f297\";\n}\n.fa-wpforms:before {\n  content: \"\\f298\";\n}\n.fa-envira:before {\n  content: \"\\f299\";\n}\n.fa-universal-access:before {\n  content: \"\\f29a\";\n}\n.fa-wheelchair-alt:before {\n  content: \"\\f29b\";\n}\n.fa-question-circle-o:before {\n  content: \"\\f29c\";\n}\n.fa-blind:before {\n  content: \"\\f29d\";\n}\n.fa-audio-description:before {\n  content: \"\\f29e\";\n}\n.fa-volume-control-phone:before {\n  content: \"\\f2a0\";\n}\n.fa-braille:before {\n  content: \"\\f2a1\";\n}\n.fa-assistive-listening-systems:before {\n  content: \"\\f2a2\";\n}\n.fa-asl-interpreting:before,\n.fa-american-sign-language-interpreting:before {\n  content: \"\\f2a3\";\n}\n.fa-deafness:before,\n.fa-hard-of-hearing:before,\n.fa-deaf:before {\n  content: \"\\f2a4\";\n}\n.fa-glide:before {\n  content: \"\\f2a5\";\n}\n.fa-glide-g:before {\n  content: \"\\f2a6\";\n}\n.fa-signing:before,\n.fa-sign-language:before {\n  content: \"\\f2a7\";\n}\n.fa-low-vision:before {\n  content: \"\\f2a8\";\n}\n.fa-viadeo:before {\n  content: \"\\f2a9\";\n}\n.fa-viadeo-square:before {\n  content: \"\\f2aa\";\n}\n.fa-snapchat:before {\n  content: \"\\f2ab\";\n}\n.fa-snapchat-ghost:before {\n  content: \"\\f2ac\";\n}\n.fa-snapchat-square:before {\n  content: \"\\f2ad\";\n}\n.fa-pied-piper:before {\n  content: \"\\f2ae\";\n}\n.fa-first-order:before {\n  content: \"\\f2b0\";\n}\n.fa-yoast:before {\n  content: \"\\f2b1\";\n}\n.fa-themeisle:before {\n  content: \"\\f2b2\";\n}\n.fa-google-plus-circle:before,\n.fa-google-plus-official:before {\n  content: \"\\f2b3\";\n}\n.fa-fa:before,\n.fa-font-awesome:before {\n  content: \"\\f2b4\";\n}\n.fa-handshake-o:before {\n  content: \"\\f2b5\";\n}\n.fa-envelope-open:before {\n  content: \"\\f2b6\";\n}\n.fa-envelope-open-o:before {\n  content: \"\\f2b7\";\n}\n.fa-linode:before {\n  content: \"\\f2b8\";\n}\n.fa-address-book:before {\n  content: \"\\f2b9\";\n}\n.fa-address-book-o:before {\n  content: \"\\f2ba\";\n}\n.fa-vcard:before,\n.fa-address-card:before {\n  content: \"\\f2bb\";\n}\n.fa-vcard-o:before,\n.fa-address-card-o:before {\n  content: \"\\f2bc\";\n}\n.fa-user-circle:before {\n  content: \"\\f2bd\";\n}\n.fa-user-circle-o:before {\n  content: \"\\f2be\";\n}\n.fa-user-o:before {\n  content: \"\\f2c0\";\n}\n.fa-id-badge:before {\n  content: \"\\f2c1\";\n}\n.fa-drivers-license:before,\n.fa-id-card:before {\n  content: \"\\f2c2\";\n}\n.fa-drivers-license-o:before,\n.fa-id-card-o:before {\n  content: \"\\f2c3\";\n}\n.fa-quora:before {\n  content: \"\\f2c4\";\n}\n.fa-free-code-camp:before {\n  content: \"\\f2c5\";\n}\n.fa-telegram:before {\n  content: \"\\f2c6\";\n}\n.fa-thermometer-4:before,\n.fa-thermometer:before,\n.fa-thermometer-full:before {\n  content: \"\\f2c7\";\n}\n.fa-thermometer-3:before,\n.fa-thermometer-three-quarters:before {\n  content: \"\\f2c8\";\n}\n.fa-thermometer-2:before,\n.fa-thermometer-half:before {\n  content: \"\\f2c9\";\n}\n.fa-thermometer-1:before,\n.fa-thermometer-quarter:before {\n  content: \"\\f2ca\";\n}\n.fa-thermometer-0:before,\n.fa-thermometer-empty:before {\n  content: \"\\f2cb\";\n}\n.fa-shower:before {\n  content: \"\\f2cc\";\n}\n.fa-bathtub:before,\n.fa-s15:before,\n.fa-bath:before {\n  content: \"\\f2cd\";\n}\n.fa-podcast:before {\n  content: \"\\f2ce\";\n}\n.fa-window-maximize:before {\n  content: \"\\f2d0\";\n}\n.fa-window-minimize:before {\n  content: \"\\f2d1\";\n}\n.fa-window-restore:before {\n  content: \"\\f2d2\";\n}\n.fa-times-rectangle:before,\n.fa-window-close:before {\n  content: \"\\f2d3\";\n}\n.fa-times-rectangle-o:before,\n.fa-window-close-o:before {\n  content: \"\\f2d4\";\n}\n.fa-bandcamp:before {\n  content: \"\\f2d5\";\n}\n.fa-grav:before {\n  content: \"\\f2d6\";\n}\n.fa-etsy:before {\n  content: \"\\f2d7\";\n}\n.fa-imdb:before {\n  content: \"\\f2d8\";\n}\n.fa-ravelry:before {\n  content: \"\\f2d9\";\n}\n.fa-eercast:before {\n  content: \"\\f2da\";\n}\n.fa-microchip:before {\n  content: \"\\f2db\";\n}\n.fa-snowflake-o:before {\n  content: \"\\f2dc\";\n}\n.fa-superpowers:before {\n  content: \"\\f2dd\";\n}\n.fa-wpexplorer:before {\n  content: \"\\f2de\";\n}\n.fa-meetup:before {\n  content: \"\\f2e0\";\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n/*!\n*\n* IPython base\n*\n*/\n.modal.fade .modal-dialog {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\ncode {\n  color: #000;\n}\npre {\n  font-size: inherit;\n  line-height: inherit;\n}\nlabel {\n  font-weight: normal;\n}\n/* Make the page background atleast 100% the height of the view port */\n/* Make the page itself atleast 70% the height of the view port */\n.border-box-sizing {\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n}\n.corner-all {\n  border-radius: 2px;\n}\n.no-padding {\n  padding: 0px;\n}\n/* Flexible box model classes */\n/* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */\n/* This file is a compatability layer.  It allows the usage of flexible box\nmodel layouts accross multiple browsers, including older browsers.  The newest,\nuniversal implementation of the flexible box model is used when available (see\n`Modern browsers` comments below).  Browsers that are known to implement this\nnew spec completely include:\n\n    Firefox 28.0+\n    Chrome 29.0+\n    Internet Explorer 11+\n    Opera 17.0+\n\nBrowsers not listed, including Safari, are supported via the styling under the\n`Old browsers` comments below.\n*/\n.hbox {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n}\n.hbox > * {\n  /* Old browsers */\n  -webkit-box-flex: 0;\n  -moz-box-flex: 0;\n  box-flex: 0;\n  /* Modern browsers */\n  flex: none;\n}\n.vbox {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n}\n.vbox > * {\n  /* Old browsers */\n  -webkit-box-flex: 0;\n  -moz-box-flex: 0;\n  box-flex: 0;\n  /* Modern browsers */\n  flex: none;\n}\n.hbox.reverse,\n.vbox.reverse,\n.reverse {\n  /* Old browsers */\n  -webkit-box-direction: reverse;\n  -moz-box-direction: reverse;\n  box-direction: reverse;\n  /* Modern browsers */\n  flex-direction: row-reverse;\n}\n.hbox.box-flex0,\n.vbox.box-flex0,\n.box-flex0 {\n  /* Old browsers */\n  -webkit-box-flex: 0;\n  -moz-box-flex: 0;\n  box-flex: 0;\n  /* Modern browsers */\n  flex: none;\n  width: auto;\n}\n.hbox.box-flex1,\n.vbox.box-flex1,\n.box-flex1 {\n  /* Old browsers */\n  -webkit-box-flex: 1;\n  -moz-box-flex: 1;\n  box-flex: 1;\n  /* Modern browsers */\n  flex: 1;\n}\n.hbox.box-flex,\n.vbox.box-flex,\n.box-flex {\n  /* Old browsers */\n  /* Old browsers */\n  -webkit-box-flex: 1;\n  -moz-box-flex: 1;\n  box-flex: 1;\n  /* Modern browsers */\n  flex: 1;\n}\n.hbox.box-flex2,\n.vbox.box-flex2,\n.box-flex2 {\n  /* Old browsers */\n  -webkit-box-flex: 2;\n  -moz-box-flex: 2;\n  box-flex: 2;\n  /* Modern browsers */\n  flex: 2;\n}\n.box-group1 {\n  /*  Deprecated */\n  -webkit-box-flex-group: 1;\n  -moz-box-flex-group: 1;\n  box-flex-group: 1;\n}\n.box-group2 {\n  /* Deprecated */\n  -webkit-box-flex-group: 2;\n  -moz-box-flex-group: 2;\n  box-flex-group: 2;\n}\n.hbox.start,\n.vbox.start,\n.start {\n  /* Old browsers */\n  -webkit-box-pack: start;\n  -moz-box-pack: start;\n  box-pack: start;\n  /* Modern browsers */\n  justify-content: flex-start;\n}\n.hbox.end,\n.vbox.end,\n.end {\n  /* Old browsers */\n  -webkit-box-pack: end;\n  -moz-box-pack: end;\n  box-pack: end;\n  /* Modern browsers */\n  justify-content: flex-end;\n}\n.hbox.center,\n.vbox.center,\n.center {\n  /* Old browsers */\n  -webkit-box-pack: center;\n  -moz-box-pack: center;\n  box-pack: center;\n  /* Modern browsers */\n  justify-content: center;\n}\n.hbox.baseline,\n.vbox.baseline,\n.baseline {\n  /* Old browsers */\n  -webkit-box-pack: baseline;\n  -moz-box-pack: baseline;\n  box-pack: baseline;\n  /* Modern browsers */\n  justify-content: baseline;\n}\n.hbox.stretch,\n.vbox.stretch,\n.stretch {\n  /* Old browsers */\n  -webkit-box-pack: stretch;\n  -moz-box-pack: stretch;\n  box-pack: stretch;\n  /* Modern browsers */\n  justify-content: stretch;\n}\n.hbox.align-start,\n.vbox.align-start,\n.align-start {\n  /* Old browsers */\n  -webkit-box-align: start;\n  -moz-box-align: start;\n  box-align: start;\n  /* Modern browsers */\n  align-items: flex-start;\n}\n.hbox.align-end,\n.vbox.align-end,\n.align-end {\n  /* Old browsers */\n  -webkit-box-align: end;\n  -moz-box-align: end;\n  box-align: end;\n  /* Modern browsers */\n  align-items: flex-end;\n}\n.hbox.align-center,\n.vbox.align-center,\n.align-center {\n  /* Old browsers */\n  -webkit-box-align: center;\n  -moz-box-align: center;\n  box-align: center;\n  /* Modern browsers */\n  align-items: center;\n}\n.hbox.align-baseline,\n.vbox.align-baseline,\n.align-baseline {\n  /* Old browsers */\n  -webkit-box-align: baseline;\n  -moz-box-align: baseline;\n  box-align: baseline;\n  /* Modern browsers */\n  align-items: baseline;\n}\n.hbox.align-stretch,\n.vbox.align-stretch,\n.align-stretch {\n  /* Old browsers */\n  -webkit-box-align: stretch;\n  -moz-box-align: stretch;\n  box-align: stretch;\n  /* Modern browsers */\n  align-items: stretch;\n}\ndiv.error {\n  margin: 2em;\n  text-align: center;\n}\ndiv.error > h1 {\n  font-size: 500%;\n  line-height: normal;\n}\ndiv.error > p {\n  font-size: 200%;\n  line-height: normal;\n}\ndiv.traceback-wrapper {\n  text-align: left;\n  max-width: 800px;\n  margin: auto;\n}\ndiv.traceback-wrapper pre.traceback {\n  max-height: 600px;\n  overflow: auto;\n}\n/**\n * Primary styles\n *\n * Author: Jupyter Development Team\n */\nbody {\n  background-color: #fff;\n  /* This makes sure that the body covers the entire window and needs to\n       be in a different element than the display: box in wrapper below */\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  overflow: visible;\n}\nbody > #header {\n  /* Initially hidden to prevent FLOUC */\n  display: none;\n  background-color: #fff;\n  /* Display over codemirror */\n  position: relative;\n  z-index: 100;\n}\nbody > #header #header-container {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  padding: 5px;\n  padding-bottom: 5px;\n  padding-top: 5px;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n}\nbody > #header .header-bar {\n  width: 100%;\n  height: 1px;\n  background: #e7e7e7;\n  margin-bottom: -1px;\n}\n@media print {\n  body > #header {\n    display: none !important;\n  }\n}\n#header-spacer {\n  width: 100%;\n  visibility: hidden;\n}\n@media print {\n  #header-spacer {\n    display: none;\n  }\n}\n#ipython_notebook {\n  padding-left: 0px;\n  padding-top: 1px;\n  padding-bottom: 1px;\n}\n[dir=\"rtl\"] #ipython_notebook {\n  margin-right: 10px;\n  margin-left: 0;\n}\n[dir=\"rtl\"] #ipython_notebook.pull-left {\n  float: right !important;\n  float: right;\n}\n.flex-spacer {\n  flex: 1;\n}\n#noscript {\n  width: auto;\n  padding-top: 16px;\n  padding-bottom: 16px;\n  text-align: center;\n  font-size: 22px;\n  color: red;\n  font-weight: bold;\n}\n#ipython_notebook img {\n  height: 28px;\n}\n#site {\n  width: 100%;\n  display: none;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  overflow: auto;\n}\n@media print {\n  #site {\n    height: auto !important;\n  }\n}\n/* Smaller buttons */\n.ui-button .ui-button-text {\n  padding: 0.2em 0.8em;\n  font-size: 77%;\n}\ninput.ui-button {\n  padding: 0.3em 0.9em;\n}\nspan#kernel_logo_widget {\n  margin: 0 10px;\n}\nspan#login_widget {\n  float: right;\n}\n[dir=\"rtl\"] span#login_widget {\n  float: left;\n}\nspan#login_widget > .button,\n#logout {\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n}\nspan#login_widget > .button:focus,\n#logout:focus,\nspan#login_widget > .button.focus,\n#logout.focus {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #8c8c8c;\n}\nspan#login_widget > .button:hover,\n#logout:hover {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\nspan#login_widget > .button:active,\n#logout:active,\nspan#login_widget > .button.active,\n#logout.active,\n.open > .dropdown-togglespan#login_widget > .button,\n.open > .dropdown-toggle#logout {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\nspan#login_widget > .button:active:hover,\n#logout:active:hover,\nspan#login_widget > .button.active:hover,\n#logout.active:hover,\n.open > .dropdown-togglespan#login_widget > .button:hover,\n.open > .dropdown-toggle#logout:hover,\nspan#login_widget > .button:active:focus,\n#logout:active:focus,\nspan#login_widget > .button.active:focus,\n#logout.active:focus,\n.open > .dropdown-togglespan#login_widget > .button:focus,\n.open > .dropdown-toggle#logout:focus,\nspan#login_widget > .button:active.focus,\n#logout:active.focus,\nspan#login_widget > .button.active.focus,\n#logout.active.focus,\n.open > .dropdown-togglespan#login_widget > .button.focus,\n.open > .dropdown-toggle#logout.focus {\n  color: #333;\n  background-color: #d4d4d4;\n  border-color: #8c8c8c;\n}\nspan#login_widget > .button:active,\n#logout:active,\nspan#login_widget > .button.active,\n#logout.active,\n.open > .dropdown-togglespan#login_widget > .button,\n.open > .dropdown-toggle#logout {\n  background-image: none;\n}\nspan#login_widget > .button.disabled:hover,\n#logout.disabled:hover,\nspan#login_widget > .button[disabled]:hover,\n#logout[disabled]:hover,\nfieldset[disabled] span#login_widget > .button:hover,\nfieldset[disabled] #logout:hover,\nspan#login_widget > .button.disabled:focus,\n#logout.disabled:focus,\nspan#login_widget > .button[disabled]:focus,\n#logout[disabled]:focus,\nfieldset[disabled] span#login_widget > .button:focus,\nfieldset[disabled] #logout:focus,\nspan#login_widget > .button.disabled.focus,\n#logout.disabled.focus,\nspan#login_widget > .button[disabled].focus,\n#logout[disabled].focus,\nfieldset[disabled] span#login_widget > .button.focus,\nfieldset[disabled] #logout.focus {\n  background-color: #fff;\n  border-color: #ccc;\n}\nspan#login_widget > .button .badge,\n#logout .badge {\n  color: #fff;\n  background-color: #333;\n}\n.nav-header {\n  text-transform: none;\n}\n#header > span {\n  margin-top: 10px;\n}\n.modal_stretch .modal-dialog {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  min-height: 80vh;\n}\n.modal_stretch .modal-dialog .modal-body {\n  max-height: calc(100vh - 200px);\n  overflow: auto;\n  flex: 1;\n}\n.modal-header {\n  cursor: move;\n}\n@media (min-width: 768px) {\n  .modal .modal-dialog {\n    width: 700px;\n  }\n}\n@media (min-width: 768px) {\n  select.form-control {\n    margin-left: 12px;\n    margin-right: 12px;\n  }\n}\n/*!\n*\n* IPython auth\n*\n*/\n.center-nav {\n  display: inline-block;\n  margin-bottom: -4px;\n}\n[dir=\"rtl\"] .center-nav form.pull-left {\n  float: right !important;\n  float: right;\n}\n[dir=\"rtl\"] .center-nav .navbar-text {\n  float: right;\n}\n[dir=\"rtl\"] .navbar-inner {\n  text-align: right;\n}\n[dir=\"rtl\"] div.text-left {\n  text-align: right;\n}\n/*!\n*\n* IPython tree view\n*\n*/\n/* We need an invisible input field on top of the sentense*/\n/* \"Drag file onto the list ...\" */\n.alternate_upload {\n  background-color: none;\n  display: inline;\n}\n.alternate_upload.form {\n  padding: 0;\n  margin: 0;\n}\n.alternate_upload input.fileinput {\n  position: absolute;\n  display: block;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  cursor: pointer;\n  opacity: 0;\n  z-index: 2;\n}\n.alternate_upload .btn-xs > input.fileinput {\n  margin: -1px -5px;\n}\n.alternate_upload .btn-upload {\n  position: relative;\n  height: 22px;\n}\n::-webkit-file-upload-button {\n  cursor: pointer;\n}\n/**\n * Primary styles\n *\n * Author: Jupyter Development Team\n */\nul#tabs {\n  margin-bottom: 4px;\n}\nul#tabs a {\n  padding-top: 6px;\n  padding-bottom: 4px;\n}\n[dir=\"rtl\"] ul#tabs.nav-tabs > li {\n  float: right;\n}\n[dir=\"rtl\"] ul#tabs.nav.nav-tabs {\n  padding-right: 0;\n}\nul.breadcrumb a:focus,\nul.breadcrumb a:hover {\n  text-decoration: none;\n}\nul.breadcrumb i.icon-home {\n  font-size: 16px;\n  margin-right: 4px;\n}\nul.breadcrumb span {\n  color: #5e5e5e;\n}\n.list_toolbar {\n  padding: 4px 0 4px 0;\n  vertical-align: middle;\n}\n.list_toolbar .tree-buttons {\n  padding-top: 1px;\n}\n[dir=\"rtl\"] .list_toolbar .tree-buttons .pull-right {\n  float: left !important;\n  float: left;\n}\n[dir=\"rtl\"] .list_toolbar .col-sm-4,\n[dir=\"rtl\"] .list_toolbar .col-sm-8 {\n  float: right;\n}\n.dynamic-buttons {\n  padding-top: 3px;\n  display: inline-block;\n}\n.list_toolbar [class*=\"span\"] {\n  min-height: 24px;\n}\n.list_header {\n  font-weight: bold;\n  background-color: #EEE;\n}\n.list_placeholder {\n  font-weight: bold;\n  padding-top: 4px;\n  padding-bottom: 4px;\n  padding-left: 7px;\n  padding-right: 7px;\n}\n.list_container {\n  margin-top: 4px;\n  margin-bottom: 20px;\n  border: 1px solid #ddd;\n  border-radius: 2px;\n}\n.list_container > div {\n  border-bottom: 1px solid #ddd;\n}\n.list_container > div:hover .list-item {\n  background-color: red;\n}\n.list_container > div:last-child {\n  border: none;\n}\n.list_item:hover .list_item {\n  background-color: #ddd;\n}\n.list_item a {\n  text-decoration: none;\n}\n.list_item:hover {\n  background-color: #fafafa;\n}\n.list_header > div,\n.list_item > div {\n  padding-top: 4px;\n  padding-bottom: 4px;\n  padding-left: 7px;\n  padding-right: 7px;\n  line-height: 22px;\n}\n.list_header > div input,\n.list_item > div input {\n  margin-right: 7px;\n  margin-left: 14px;\n  vertical-align: text-bottom;\n  line-height: 22px;\n  position: relative;\n  top: -1px;\n}\n.list_header > div .item_link,\n.list_item > div .item_link {\n  margin-left: -1px;\n  vertical-align: baseline;\n  line-height: 22px;\n}\n[dir=\"rtl\"] .list_item > div input {\n  margin-right: 0;\n}\n.new-file input[type=checkbox] {\n  visibility: hidden;\n}\n.item_name {\n  line-height: 22px;\n  height: 24px;\n}\n.item_icon {\n  font-size: 14px;\n  color: #5e5e5e;\n  margin-right: 7px;\n  margin-left: 7px;\n  line-height: 22px;\n  vertical-align: baseline;\n}\n.item_modified {\n  margin-right: 7px;\n  margin-left: 7px;\n}\n[dir=\"rtl\"] .item_modified.pull-right {\n  float: left !important;\n  float: left;\n}\n.item_buttons {\n  line-height: 1em;\n  margin-left: -5px;\n}\n.item_buttons .btn,\n.item_buttons .btn-group,\n.item_buttons .input-group {\n  float: left;\n}\n.item_buttons > .btn,\n.item_buttons > .btn-group,\n.item_buttons > .input-group {\n  margin-left: 5px;\n}\n.item_buttons .btn {\n  min-width: 13ex;\n}\n.item_buttons .running-indicator {\n  padding-top: 4px;\n  color: #5cb85c;\n}\n.item_buttons .kernel-name {\n  padding-top: 4px;\n  color: #5bc0de;\n  margin-right: 7px;\n  float: left;\n}\n[dir=\"rtl\"] .item_buttons.pull-right {\n  float: left !important;\n  float: left;\n}\n[dir=\"rtl\"] .item_buttons .kernel-name {\n  margin-left: 7px;\n  float: right;\n}\n.toolbar_info {\n  height: 24px;\n  line-height: 24px;\n}\n.list_item input:not([type=checkbox]) {\n  padding-top: 3px;\n  padding-bottom: 3px;\n  height: 22px;\n  line-height: 14px;\n  margin: 0px;\n}\n.highlight_text {\n  color: blue;\n}\n#project_name {\n  display: inline-block;\n  padding-left: 7px;\n  margin-left: -2px;\n}\n#project_name > .breadcrumb {\n  padding: 0px;\n  margin-bottom: 0px;\n  background-color: transparent;\n  font-weight: bold;\n}\n.sort_button {\n  display: inline-block;\n  padding-left: 7px;\n}\n[dir=\"rtl\"] .sort_button.pull-right {\n  float: left !important;\n  float: left;\n}\n#tree-selector {\n  padding-right: 0px;\n}\n#button-select-all {\n  min-width: 50px;\n}\n[dir=\"rtl\"] #button-select-all.btn {\n  float: right ;\n}\n#select-all {\n  margin-left: 7px;\n  margin-right: 2px;\n  margin-top: 2px;\n  height: 16px;\n}\n[dir=\"rtl\"] #select-all.pull-left {\n  float: right !important;\n  float: right;\n}\n.menu_icon {\n  margin-right: 2px;\n}\n.tab-content .row {\n  margin-left: 0px;\n  margin-right: 0px;\n}\n.folder_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f114\";\n}\n.folder_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.folder_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.folder_icon:before.pull-left {\n  margin-right: .3em;\n}\n.folder_icon:before.pull-right {\n  margin-left: .3em;\n}\n.notebook_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f02d\";\n  position: relative;\n  top: -1px;\n}\n.notebook_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.notebook_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.notebook_icon:before.pull-left {\n  margin-right: .3em;\n}\n.notebook_icon:before.pull-right {\n  margin-left: .3em;\n}\n.running_notebook_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f02d\";\n  position: relative;\n  top: -1px;\n  color: #5cb85c;\n}\n.running_notebook_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.running_notebook_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.running_notebook_icon:before.pull-left {\n  margin-right: .3em;\n}\n.running_notebook_icon:before.pull-right {\n  margin-left: .3em;\n}\n.file_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f016\";\n  position: relative;\n  top: -2px;\n}\n.file_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.file_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.file_icon:before.pull-left {\n  margin-right: .3em;\n}\n.file_icon:before.pull-right {\n  margin-left: .3em;\n}\n#notebook_toolbar .pull-right {\n  padding-top: 0px;\n  margin-right: -1px;\n}\nul#new-menu {\n  left: auto;\n  right: 0;\n}\n#new-menu .dropdown-header {\n  font-size: 10px;\n  border-bottom: 1px solid #e5e5e5;\n  padding: 0 0 3px;\n  margin: -3px 20px 0;\n}\n.kernel-menu-icon {\n  padding-right: 12px;\n  width: 24px;\n  content: \"\\f096\";\n}\n.kernel-menu-icon:before {\n  content: \"\\f096\";\n}\n.kernel-menu-icon-current:before {\n  content: \"\\f00c\";\n}\n#tab_content {\n  padding-top: 20px;\n}\n#running .panel-group .panel {\n  margin-top: 3px;\n  margin-bottom: 1em;\n}\n#running .panel-group .panel .panel-heading {\n  background-color: #EEE;\n  padding-top: 4px;\n  padding-bottom: 4px;\n  padding-left: 7px;\n  padding-right: 7px;\n  line-height: 22px;\n}\n#running .panel-group .panel .panel-heading a:focus,\n#running .panel-group .panel .panel-heading a:hover {\n  text-decoration: none;\n}\n#running .panel-group .panel .panel-body {\n  padding: 0px;\n}\n#running .panel-group .panel .panel-body .list_container {\n  margin-top: 0px;\n  margin-bottom: 0px;\n  border: 0px;\n  border-radius: 0px;\n}\n#running .panel-group .panel .panel-body .list_container .list_item {\n  border-bottom: 1px solid #ddd;\n}\n#running .panel-group .panel .panel-body .list_container .list_item:last-child {\n  border-bottom: 0px;\n}\n.delete-button {\n  display: none;\n}\n.duplicate-button {\n  display: none;\n}\n.rename-button {\n  display: none;\n}\n.move-button {\n  display: none;\n}\n.download-button {\n  display: none;\n}\n.shutdown-button {\n  display: none;\n}\n.dynamic-instructions {\n  display: inline-block;\n  padding-top: 4px;\n}\n/*!\n*\n* IPython text editor webapp\n*\n*/\n.selected-keymap i.fa {\n  padding: 0px 5px;\n}\n.selected-keymap i.fa:before {\n  content: \"\\f00c\";\n}\n#mode-menu {\n  overflow: auto;\n  max-height: 20em;\n}\n.edit_app #header {\n  -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n}\n.edit_app #menubar .navbar {\n  /* Use a negative 1 bottom margin, so the border overlaps the border of the\n    header */\n  margin-bottom: -1px;\n}\n.dirty-indicator {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  width: 20px;\n}\n.dirty-indicator.fa-pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator.fa-pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator.pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator.pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-dirty {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  width: 20px;\n}\n.dirty-indicator-dirty.fa-pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-dirty.fa-pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-dirty.pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-dirty.pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-clean {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  width: 20px;\n}\n.dirty-indicator-clean.fa-pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-clean.fa-pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-clean.pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-clean.pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-clean:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f00c\";\n}\n.dirty-indicator-clean:before.fa-pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-clean:before.fa-pull-right {\n  margin-left: .3em;\n}\n.dirty-indicator-clean:before.pull-left {\n  margin-right: .3em;\n}\n.dirty-indicator-clean:before.pull-right {\n  margin-left: .3em;\n}\n#filename {\n  font-size: 16pt;\n  display: table;\n  padding: 0px 5px;\n}\n#current-mode {\n  padding-left: 5px;\n  padding-right: 5px;\n}\n#texteditor-backdrop {\n  padding-top: 20px;\n  padding-bottom: 20px;\n}\n@media not print {\n  #texteditor-backdrop {\n    background-color: #EEE;\n  }\n}\n@media print {\n  #texteditor-backdrop #texteditor-container .CodeMirror-gutter,\n  #texteditor-backdrop #texteditor-container .CodeMirror-gutters {\n    background-color: #fff;\n  }\n}\n@media not print {\n  #texteditor-backdrop #texteditor-container .CodeMirror-gutter,\n  #texteditor-backdrop #texteditor-container .CodeMirror-gutters {\n    background-color: #fff;\n  }\n}\n@media not print {\n  #texteditor-backdrop #texteditor-container {\n    padding: 0px;\n    background-color: #fff;\n    -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n    box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  }\n}\n.CodeMirror-dialog {\n  background-color: #fff;\n}\n/*!\n*\n* IPython notebook\n*\n*/\n/* CSS font colors for translated ANSI escape sequences */\n/* The color values are a mix of\n   http://www.xcolors.net/dl/baskerville-ivorylight and\n   http://www.xcolors.net/dl/euphrasia */\n.ansi-black-fg {\n  color: #3E424D;\n}\n.ansi-black-bg {\n  background-color: #3E424D;\n}\n.ansi-black-intense-fg {\n  color: #282C36;\n}\n.ansi-black-intense-bg {\n  background-color: #282C36;\n}\n.ansi-red-fg {\n  color: #E75C58;\n}\n.ansi-red-bg {\n  background-color: #E75C58;\n}\n.ansi-red-intense-fg {\n  color: #B22B31;\n}\n.ansi-red-intense-bg {\n  background-color: #B22B31;\n}\n.ansi-green-fg {\n  color: #00A250;\n}\n.ansi-green-bg {\n  background-color: #00A250;\n}\n.ansi-green-intense-fg {\n  color: #007427;\n}\n.ansi-green-intense-bg {\n  background-color: #007427;\n}\n.ansi-yellow-fg {\n  color: #DDB62B;\n}\n.ansi-yellow-bg {\n  background-color: #DDB62B;\n}\n.ansi-yellow-intense-fg {\n  color: #B27D12;\n}\n.ansi-yellow-intense-bg {\n  background-color: #B27D12;\n}\n.ansi-blue-fg {\n  color: #208FFB;\n}\n.ansi-blue-bg {\n  background-color: #208FFB;\n}\n.ansi-blue-intense-fg {\n  color: #0065CA;\n}\n.ansi-blue-intense-bg {\n  background-color: #0065CA;\n}\n.ansi-magenta-fg {\n  color: #D160C4;\n}\n.ansi-magenta-bg {\n  background-color: #D160C4;\n}\n.ansi-magenta-intense-fg {\n  color: #A03196;\n}\n.ansi-magenta-intense-bg {\n  background-color: #A03196;\n}\n.ansi-cyan-fg {\n  color: #60C6C8;\n}\n.ansi-cyan-bg {\n  background-color: #60C6C8;\n}\n.ansi-cyan-intense-fg {\n  color: #258F8F;\n}\n.ansi-cyan-intense-bg {\n  background-color: #258F8F;\n}\n.ansi-white-fg {\n  color: #C5C1B4;\n}\n.ansi-white-bg {\n  background-color: #C5C1B4;\n}\n.ansi-white-intense-fg {\n  color: #A1A6B2;\n}\n.ansi-white-intense-bg {\n  background-color: #A1A6B2;\n}\n.ansi-default-inverse-fg {\n  color: #FFFFFF;\n}\n.ansi-default-inverse-bg {\n  background-color: #000000;\n}\n.ansi-bold {\n  font-weight: bold;\n}\n.ansi-underline {\n  text-decoration: underline;\n}\n/* The following styles are deprecated an will be removed in a future version */\n.ansibold {\n  font-weight: bold;\n}\n.ansi-inverse {\n  outline: 0.5px dotted;\n}\n/* use dark versions for foreground, to improve visibility */\n.ansiblack {\n  color: black;\n}\n.ansired {\n  color: darkred;\n}\n.ansigreen {\n  color: darkgreen;\n}\n.ansiyellow {\n  color: #c4a000;\n}\n.ansiblue {\n  color: darkblue;\n}\n.ansipurple {\n  color: darkviolet;\n}\n.ansicyan {\n  color: steelblue;\n}\n.ansigray {\n  color: gray;\n}\n/* and light for background, for the same reason */\n.ansibgblack {\n  background-color: black;\n}\n.ansibgred {\n  background-color: red;\n}\n.ansibggreen {\n  background-color: green;\n}\n.ansibgyellow {\n  background-color: yellow;\n}\n.ansibgblue {\n  background-color: blue;\n}\n.ansibgpurple {\n  background-color: magenta;\n}\n.ansibgcyan {\n  background-color: cyan;\n}\n.ansibggray {\n  background-color: gray;\n}\ndiv.cell {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  border-radius: 2px;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  border-width: 1px;\n  border-style: solid;\n  border-color: transparent;\n  width: 100%;\n  padding: 5px;\n  /* This acts as a spacer between cells, that is outside the border */\n  margin: 0px;\n  outline: none;\n  position: relative;\n  overflow: visible;\n}\ndiv.cell:before {\n  position: absolute;\n  display: block;\n  top: -1px;\n  left: -1px;\n  width: 5px;\n  height: calc(100% +  2px);\n  content: '';\n  background: transparent;\n}\ndiv.cell.jupyter-soft-selected {\n  border-left-color: #E3F2FD;\n  border-left-width: 1px;\n  padding-left: 5px;\n  border-right-color: #E3F2FD;\n  border-right-width: 1px;\n  background: #E3F2FD;\n}\n@media print {\n  div.cell.jupyter-soft-selected {\n    border-color: transparent;\n  }\n}\ndiv.cell.selected,\ndiv.cell.selected.jupyter-soft-selected {\n  border-color: #ababab;\n}\ndiv.cell.selected:before,\ndiv.cell.selected.jupyter-soft-selected:before {\n  position: absolute;\n  display: block;\n  top: -1px;\n  left: -1px;\n  width: 5px;\n  height: calc(100% +  2px);\n  content: '';\n  background: #42A5F5;\n}\n@media print {\n  div.cell.selected,\n  div.cell.selected.jupyter-soft-selected {\n    border-color: transparent;\n  }\n}\n.edit_mode div.cell.selected {\n  border-color: #66BB6A;\n}\n.edit_mode div.cell.selected:before {\n  position: absolute;\n  display: block;\n  top: -1px;\n  left: -1px;\n  width: 5px;\n  height: calc(100% +  2px);\n  content: '';\n  background: #66BB6A;\n}\n@media print {\n  .edit_mode div.cell.selected {\n    border-color: transparent;\n  }\n}\n.prompt {\n  /* This needs to be wide enough for 3 digit prompt numbers: In[100]: */\n  min-width: 14ex;\n  /* This padding is tuned to match the padding on the CodeMirror editor. */\n  padding: 0.4em;\n  margin: 0px;\n  font-family: monospace;\n  text-align: right;\n  /* This has to match that of the the CodeMirror class line-height below */\n  line-height: 1.21429em;\n  /* Don't highlight prompt number selection */\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  /* Use default cursor */\n  cursor: default;\n}\n@media (max-width: 540px) {\n  .prompt {\n    text-align: left;\n  }\n}\ndiv.inner_cell {\n  min-width: 0;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  /* Old browsers */\n  -webkit-box-flex: 1;\n  -moz-box-flex: 1;\n  box-flex: 1;\n  /* Modern browsers */\n  flex: 1;\n}\n/* input_area and input_prompt must match in top border and margin for alignment */\ndiv.input_area {\n  border: 1px solid #cfcfcf;\n  border-radius: 2px;\n  background: #f7f7f7;\n  line-height: 1.21429em;\n}\n/* This is needed so that empty prompt areas can collapse to zero height when there\n   is no content in the output_subarea and the prompt. The main purpose of this is\n   to make sure that empty JavaScript output_subareas have no height. */\ndiv.prompt:empty {\n  padding-top: 0;\n  padding-bottom: 0;\n}\ndiv.unrecognized_cell {\n  padding: 5px 5px 5px 0px;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n}\ndiv.unrecognized_cell .inner_cell {\n  border-radius: 2px;\n  padding: 5px;\n  font-weight: bold;\n  color: red;\n  border: 1px solid #cfcfcf;\n  background: #eaeaea;\n}\ndiv.unrecognized_cell .inner_cell a {\n  color: inherit;\n  text-decoration: none;\n}\ndiv.unrecognized_cell .inner_cell a:hover {\n  color: inherit;\n  text-decoration: none;\n}\n@media (max-width: 540px) {\n  div.unrecognized_cell > div.prompt {\n    display: none;\n  }\n}\ndiv.code_cell {\n  /* avoid page breaking on code cells when printing */\n}\n@media print {\n  div.code_cell {\n    page-break-inside: avoid;\n  }\n}\n/* any special styling for code cells that are currently running goes here */\ndiv.input {\n  page-break-inside: avoid;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n}\n@media (max-width: 540px) {\n  div.input {\n    /* Old browsers */\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-box-align: stretch;\n    display: -moz-box;\n    -moz-box-orient: vertical;\n    -moz-box-align: stretch;\n    display: box;\n    box-orient: vertical;\n    box-align: stretch;\n    /* Modern browsers */\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n  }\n}\n/* input_area and input_prompt must match in top border and margin for alignment */\ndiv.input_prompt {\n  color: #303F9F;\n  border-top: 1px solid transparent;\n}\ndiv.input_area > div.highlight {\n  margin: 0.4em;\n  border: none;\n  padding: 0px;\n  background-color: transparent;\n}\ndiv.input_area > div.highlight > pre {\n  margin: 0px;\n  border: none;\n  padding: 0px;\n  background-color: transparent;\n}\n/* The following gets added to the <head> if it is detected that the user has a\n * monospace font with inconsistent normal/bold/italic height.  See\n * notebookmain.js.  Such fonts will have keywords vertically offset with\n * respect to the rest of the text.  The user should select a better font.\n * See: https://github.com/ipython/ipython/issues/1503\n *\n * .CodeMirror span {\n *      vertical-align: bottom;\n * }\n */\n.CodeMirror {\n  line-height: 1.21429em;\n  /* Changed from 1em to our global default */\n  font-size: 14px;\n  height: auto;\n  /* Changed to auto to autogrow */\n  background: none;\n  /* Changed from white to allow our bg to show through */\n}\n.CodeMirror-scroll {\n  /*  The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/\n  /*  We have found that if it is visible, vertical scrollbars appear with font size changes.*/\n  overflow-y: hidden;\n  overflow-x: auto;\n}\n.CodeMirror-lines {\n  /* In CM2, this used to be 0.4em, but in CM3 it went to 4px. We need the em value because */\n  /* we have set a different line-height and want this to scale with that. */\n  /* Note that this should set vertical padding only, since CodeMirror assumes\n       that horizontal padding will be set on CodeMirror pre */\n  padding: 0.4em 0;\n}\n.CodeMirror-linenumber {\n  padding: 0 8px 0 4px;\n}\n.CodeMirror-gutters {\n  border-bottom-left-radius: 2px;\n  border-top-left-radius: 2px;\n}\n.CodeMirror pre {\n  /* In CM3 this went to 4px from 0 in CM2. This sets horizontal padding only,\n    use .CodeMirror-lines for vertical */\n  padding: 0 0.4em;\n  border: 0;\n  border-radius: 0;\n}\n.CodeMirror-cursor {\n  border-left: 1.4px solid black;\n}\n@media screen and (min-width: 2138px) and (max-width: 4319px) {\n  .CodeMirror-cursor {\n    border-left: 2px solid black;\n  }\n}\n@media screen and (min-width: 4320px) {\n  .CodeMirror-cursor {\n    border-left: 4px solid black;\n  }\n}\n/*\n\nOriginal style from softwaremaniacs.org (c) Ivan Sagalaev <Maniac@SoftwareManiacs.Org>\nAdapted from GitHub theme\n\n*/\n.highlight-base {\n  color: #000;\n}\n.highlight-variable {\n  color: #000;\n}\n.highlight-variable-2 {\n  color: #1a1a1a;\n}\n.highlight-variable-3 {\n  color: #333333;\n}\n.highlight-string {\n  color: #BA2121;\n}\n.highlight-comment {\n  color: #408080;\n  font-style: italic;\n}\n.highlight-number {\n  color: #080;\n}\n.highlight-atom {\n  color: #88F;\n}\n.highlight-keyword {\n  color: #008000;\n  font-weight: bold;\n}\n.highlight-builtin {\n  color: #008000;\n}\n.highlight-error {\n  color: #f00;\n}\n.highlight-operator {\n  color: #AA22FF;\n  font-weight: bold;\n}\n.highlight-meta {\n  color: #AA22FF;\n}\n/* previously not defined, copying from default codemirror */\n.highlight-def {\n  color: #00f;\n}\n.highlight-string-2 {\n  color: #f50;\n}\n.highlight-qualifier {\n  color: #555;\n}\n.highlight-bracket {\n  color: #997;\n}\n.highlight-tag {\n  color: #170;\n}\n.highlight-attribute {\n  color: #00c;\n}\n.highlight-header {\n  color: blue;\n}\n.highlight-quote {\n  color: #090;\n}\n.highlight-link {\n  color: #00c;\n}\n/* apply the same style to codemirror */\n.cm-s-ipython span.cm-keyword {\n  color: #008000;\n  font-weight: bold;\n}\n.cm-s-ipython span.cm-atom {\n  color: #88F;\n}\n.cm-s-ipython span.cm-number {\n  color: #080;\n}\n.cm-s-ipython span.cm-def {\n  color: #00f;\n}\n.cm-s-ipython span.cm-variable {\n  color: #000;\n}\n.cm-s-ipython span.cm-operator {\n  color: #AA22FF;\n  font-weight: bold;\n}\n.cm-s-ipython span.cm-variable-2 {\n  color: #1a1a1a;\n}\n.cm-s-ipython span.cm-variable-3 {\n  color: #333333;\n}\n.cm-s-ipython span.cm-comment {\n  color: #408080;\n  font-style: italic;\n}\n.cm-s-ipython span.cm-string {\n  color: #BA2121;\n}\n.cm-s-ipython span.cm-string-2 {\n  color: #f50;\n}\n.cm-s-ipython span.cm-meta {\n  color: #AA22FF;\n}\n.cm-s-ipython span.cm-qualifier {\n  color: #555;\n}\n.cm-s-ipython span.cm-builtin {\n  color: #008000;\n}\n.cm-s-ipython span.cm-bracket {\n  color: #997;\n}\n.cm-s-ipython span.cm-tag {\n  color: #170;\n}\n.cm-s-ipython span.cm-attribute {\n  color: #00c;\n}\n.cm-s-ipython span.cm-header {\n  color: blue;\n}\n.cm-s-ipython span.cm-quote {\n  color: #090;\n}\n.cm-s-ipython span.cm-link {\n  color: #00c;\n}\n.cm-s-ipython span.cm-error {\n  color: #f00;\n}\n.cm-s-ipython span.cm-tab {\n  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);\n  background-position: right;\n  background-repeat: no-repeat;\n}\ndiv.output_wrapper {\n  /* this position must be relative to enable descendents to be absolute within it */\n  position: relative;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  z-index: 1;\n}\n/* class for the output area when it should be height-limited */\ndiv.output_scroll {\n  /* ideally, this would be max-height, but FF barfs all over that */\n  height: 24em;\n  /* FF needs this *and the wrapper* to specify full width, or it will shrinkwrap */\n  width: 100%;\n  overflow: auto;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.8);\n  box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.8);\n  display: block;\n}\n/* output div while it is collapsed */\ndiv.output_collapsed {\n  margin: 0px;\n  padding: 0px;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n}\ndiv.out_prompt_overlay {\n  height: 100%;\n  padding: 0px 0.4em;\n  position: absolute;\n  border-radius: 2px;\n}\ndiv.out_prompt_overlay:hover {\n  /* use inner shadow to get border that is computed the same on WebKit/FF */\n  -webkit-box-shadow: inset 0 0 1px #000;\n  box-shadow: inset 0 0 1px #000;\n  background: rgba(240, 240, 240, 0.5);\n}\ndiv.output_prompt {\n  color: #D84315;\n}\n/* This class is the outer container of all output sections. */\ndiv.output_area {\n  padding: 0px;\n  page-break-inside: avoid;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n}\ndiv.output_area .MathJax_Display {\n  text-align: left !important;\n}\ndiv.output_area .rendered_html table {\n  margin-left: 0;\n  margin-right: 0;\n}\ndiv.output_area .rendered_html img {\n  margin-left: 0;\n  margin-right: 0;\n}\ndiv.output_area img,\ndiv.output_area svg {\n  max-width: 100%;\n  height: auto;\n}\ndiv.output_area img.unconfined,\ndiv.output_area svg.unconfined {\n  max-width: none;\n}\ndiv.output_area .mglyph > img {\n  max-width: none;\n}\n/* This is needed to protect the pre formating from global settings such\n   as that of bootstrap */\n.output {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: vertical;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: vertical;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n}\n@media (max-width: 540px) {\n  div.output_area {\n    /* Old browsers */\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-box-align: stretch;\n    display: -moz-box;\n    -moz-box-orient: vertical;\n    -moz-box-align: stretch;\n    display: box;\n    box-orient: vertical;\n    box-align: stretch;\n    /* Modern browsers */\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n  }\n}\ndiv.output_area pre {\n  margin: 0;\n  padding: 1px 0 1px 0;\n  border: 0;\n  vertical-align: baseline;\n  color: black;\n  background-color: transparent;\n  border-radius: 0;\n}\n/* This class is for the output subarea inside the output_area and after\n   the prompt div. */\ndiv.output_subarea {\n  overflow-x: auto;\n  padding: 0.4em;\n  /* Old browsers */\n  -webkit-box-flex: 1;\n  -moz-box-flex: 1;\n  box-flex: 1;\n  /* Modern browsers */\n  flex: 1;\n  max-width: calc(100% - 14ex);\n}\ndiv.output_scroll div.output_subarea {\n  overflow-x: visible;\n}\n/* The rest of the output_* classes are for special styling of the different\n   output types */\n/* all text output has this class: */\ndiv.output_text {\n  text-align: left;\n  color: #000;\n  /* This has to match that of the the CodeMirror class line-height below */\n  line-height: 1.21429em;\n}\n/* stdout/stderr are 'text' as well as 'stream', but execute_result/error are *not* streams */\ndiv.output_stderr {\n  background: #fdd;\n  /* very light red background for stderr */\n}\ndiv.output_latex {\n  text-align: left;\n}\n/* Empty output_javascript divs should have no height */\ndiv.output_javascript:empty {\n  padding: 0;\n}\n.js-error {\n  color: darkred;\n}\n/* raw_input styles */\ndiv.raw_input_container {\n  line-height: 1.21429em;\n  padding-top: 5px;\n}\npre.raw_input_prompt {\n  /* nothing needed here. */\n}\ninput.raw_input {\n  font-family: monospace;\n  font-size: inherit;\n  color: inherit;\n  width: auto;\n  /* make sure input baseline aligns with prompt */\n  vertical-align: baseline;\n  /* padding + margin = 0.5em between prompt and cursor */\n  padding: 0em 0.25em;\n  margin: 0em 0.25em;\n}\ninput.raw_input:focus {\n  box-shadow: none;\n}\np.p-space {\n  margin-bottom: 10px;\n}\ndiv.output_unrecognized {\n  padding: 5px;\n  font-weight: bold;\n  color: red;\n}\ndiv.output_unrecognized a {\n  color: inherit;\n  text-decoration: none;\n}\ndiv.output_unrecognized a:hover {\n  color: inherit;\n  text-decoration: none;\n}\n.rendered_html {\n  color: #000;\n  /* any extras will just be numbers: */\n}\n.rendered_html em {\n  font-style: italic;\n}\n.rendered_html strong {\n  font-weight: bold;\n}\n.rendered_html u {\n  text-decoration: underline;\n}\n.rendered_html :link {\n  text-decoration: underline;\n}\n.rendered_html :visited {\n  text-decoration: underline;\n}\n.rendered_html h1 {\n  font-size: 185.7%;\n  margin: 1.08em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n}\n.rendered_html h2 {\n  font-size: 157.1%;\n  margin: 1.27em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n}\n.rendered_html h3 {\n  font-size: 128.6%;\n  margin: 1.55em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n}\n.rendered_html h4 {\n  font-size: 100%;\n  margin: 2em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n}\n.rendered_html h5 {\n  font-size: 100%;\n  margin: 2em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n  font-style: italic;\n}\n.rendered_html h6 {\n  font-size: 100%;\n  margin: 2em 0 0 0;\n  font-weight: bold;\n  line-height: 1.0;\n  font-style: italic;\n}\n.rendered_html h1:first-child {\n  margin-top: 0.538em;\n}\n.rendered_html h2:first-child {\n  margin-top: 0.636em;\n}\n.rendered_html h3:first-child {\n  margin-top: 0.777em;\n}\n.rendered_html h4:first-child {\n  margin-top: 1em;\n}\n.rendered_html h5:first-child {\n  margin-top: 1em;\n}\n.rendered_html h6:first-child {\n  margin-top: 1em;\n}\n.rendered_html ul:not(.list-inline),\n.rendered_html ol:not(.list-inline) {\n  padding-left: 2em;\n}\n.rendered_html ul {\n  list-style: disc;\n}\n.rendered_html ul ul {\n  list-style: square;\n  margin-top: 0;\n}\n.rendered_html ul ul ul {\n  list-style: circle;\n}\n.rendered_html ol {\n  list-style: decimal;\n}\n.rendered_html ol ol {\n  list-style: upper-alpha;\n  margin-top: 0;\n}\n.rendered_html ol ol ol {\n  list-style: lower-alpha;\n}\n.rendered_html ol ol ol ol {\n  list-style: lower-roman;\n}\n.rendered_html ol ol ol ol ol {\n  list-style: decimal;\n}\n.rendered_html * + ul {\n  margin-top: 1em;\n}\n.rendered_html * + ol {\n  margin-top: 1em;\n}\n.rendered_html hr {\n  color: black;\n  background-color: black;\n}\n.rendered_html pre {\n  margin: 1em 2em;\n  padding: 0px;\n  background-color: #fff;\n}\n.rendered_html code {\n  background-color: #eff0f1;\n}\n.rendered_html p code {\n  padding: 1px 5px;\n}\n.rendered_html pre code {\n  background-color: #fff;\n}\n.rendered_html pre,\n.rendered_html code {\n  border: 0;\n  color: #000;\n  font-size: 100%;\n}\n.rendered_html blockquote {\n  margin: 1em 2em;\n}\n.rendered_html table {\n  margin-left: auto;\n  margin-right: auto;\n  border: none;\n  border-collapse: collapse;\n  border-spacing: 0;\n  color: black;\n  font-size: 12px;\n  table-layout: fixed;\n}\n.rendered_html thead {\n  border-bottom: 1px solid black;\n  vertical-align: bottom;\n}\n.rendered_html tr,\n.rendered_html th,\n.rendered_html td {\n  text-align: right;\n  vertical-align: middle;\n  padding: 0.5em 0.5em;\n  line-height: normal;\n  white-space: normal;\n  max-width: none;\n  border: none;\n}\n.rendered_html th {\n  font-weight: bold;\n}\n.rendered_html tbody tr:nth-child(odd) {\n  background: #f5f5f5;\n}\n.rendered_html tbody tr:hover {\n  background: rgba(66, 165, 245, 0.2);\n}\n.rendered_html * + table {\n  margin-top: 1em;\n}\n.rendered_html p {\n  text-align: left;\n}\n.rendered_html * + p {\n  margin-top: 1em;\n}\n.rendered_html img {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n.rendered_html * + img {\n  margin-top: 1em;\n}\n.rendered_html img,\n.rendered_html svg {\n  max-width: 100%;\n  height: auto;\n}\n.rendered_html img.unconfined,\n.rendered_html svg.unconfined {\n  max-width: none;\n}\n.rendered_html .alert {\n  margin-bottom: initial;\n}\n.rendered_html * + .alert {\n  margin-top: 1em;\n}\n[dir=\"rtl\"] .rendered_html p {\n  text-align: right;\n}\ndiv.text_cell {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n}\n@media (max-width: 540px) {\n  div.text_cell > div.prompt {\n    display: none;\n  }\n}\ndiv.text_cell_render {\n  /*font-family: \"Helvetica Neue\", Arial, Helvetica, Geneva, sans-serif;*/\n  outline: none;\n  resize: none;\n  width: inherit;\n  border-style: none;\n  padding: 0.5em 0.5em 0.5em 0.4em;\n  color: #000;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n}\na.anchor-link:link {\n  text-decoration: none;\n  padding: 0px 20px;\n  visibility: hidden;\n}\nh1:hover .anchor-link,\nh2:hover .anchor-link,\nh3:hover .anchor-link,\nh4:hover .anchor-link,\nh5:hover .anchor-link,\nh6:hover .anchor-link {\n  visibility: visible;\n}\n.text_cell.rendered .input_area {\n  display: none;\n}\n.text_cell.rendered .rendered_html {\n  overflow-x: auto;\n  overflow-y: hidden;\n}\n.text_cell.rendered .rendered_html tr,\n.text_cell.rendered .rendered_html th,\n.text_cell.rendered .rendered_html td {\n  max-width: none;\n}\n.text_cell.unrendered .text_cell_render {\n  display: none;\n}\n.text_cell .dropzone .input_area {\n  border: 2px dashed #bababa;\n  margin: -1px;\n}\n.cm-header-1,\n.cm-header-2,\n.cm-header-3,\n.cm-header-4,\n.cm-header-5,\n.cm-header-6 {\n  font-weight: bold;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n.cm-header-1 {\n  font-size: 185.7%;\n}\n.cm-header-2 {\n  font-size: 157.1%;\n}\n.cm-header-3 {\n  font-size: 128.6%;\n}\n.cm-header-4 {\n  font-size: 110%;\n}\n.cm-header-5 {\n  font-size: 100%;\n  font-style: italic;\n}\n.cm-header-6 {\n  font-size: 100%;\n  font-style: italic;\n}\n/*!\n*\n* IPython notebook webapp\n*\n*/\n@media (max-width: 767px) {\n  .notebook_app {\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n}\n#ipython-main-app {\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  height: 100%;\n}\ndiv#notebook_panel {\n  margin: 0px;\n  padding: 0px;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  height: 100%;\n}\ndiv#notebook {\n  font-size: 14px;\n  line-height: 20px;\n  overflow-y: hidden;\n  overflow-x: auto;\n  width: 100%;\n  /* This spaces the page away from the edge of the notebook area */\n  padding-top: 20px;\n  margin: 0px;\n  outline: none;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  min-height: 100%;\n}\n@media not print {\n  #notebook-container {\n    padding: 15px;\n    background-color: #fff;\n    min-height: 0;\n    -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n    box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  }\n}\n@media print {\n  #notebook-container {\n    width: 100%;\n  }\n}\ndiv.ui-widget-content {\n  border: 1px solid #ababab;\n  outline: none;\n}\npre.dialog {\n  background-color: #f7f7f7;\n  border: 1px solid #ddd;\n  border-radius: 2px;\n  padding: 0.4em;\n  padding-left: 2em;\n}\np.dialog {\n  padding: 0.2em;\n}\n/* Word-wrap output correctly.  This is the CSS3 spelling, though Firefox seems\n   to not honor it correctly.  Webkit browsers (Chrome, rekonq, Safari) do.\n */\npre,\ncode,\nkbd,\nsamp {\n  white-space: pre-wrap;\n}\n#fonttest {\n  font-family: monospace;\n}\np {\n  margin-bottom: 0;\n}\n.end_space {\n  min-height: 100px;\n  transition: height .2s ease;\n}\n.notebook_app > #header {\n  -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n}\n@media not print {\n  .notebook_app {\n    background-color: #EEE;\n  }\n}\nkbd {\n  border-style: solid;\n  border-width: 1px;\n  box-shadow: none;\n  margin: 2px;\n  padding-left: 2px;\n  padding-right: 2px;\n  padding-top: 1px;\n  padding-bottom: 1px;\n}\n.jupyter-keybindings {\n  padding: 1px;\n  line-height: 24px;\n  border-bottom: 1px solid gray;\n}\n.jupyter-keybindings input {\n  margin: 0;\n  padding: 0;\n  border: none;\n}\n.jupyter-keybindings i {\n  padding: 6px;\n}\n.well code {\n  background-color: #ffffff;\n  border-color: #ababab;\n  border-width: 1px;\n  border-style: solid;\n  padding: 2px;\n  padding-top: 1px;\n  padding-bottom: 1px;\n}\n/* CSS for the cell toolbar */\n.celltoolbar {\n  border: thin solid #CFCFCF;\n  border-bottom: none;\n  background: #EEE;\n  border-radius: 2px 2px 0px 0px;\n  width: 100%;\n  height: 29px;\n  padding-right: 4px;\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n  /* Old browsers */\n  -webkit-box-pack: end;\n  -moz-box-pack: end;\n  box-pack: end;\n  /* Modern browsers */\n  justify-content: flex-end;\n  display: -webkit-flex;\n}\n@media print {\n  .celltoolbar {\n    display: none;\n  }\n}\n.ctb_hideshow {\n  display: none;\n  vertical-align: bottom;\n}\n/* ctb_show is added to the ctb_hideshow div to show the cell toolbar.\n   Cell toolbars are only shown when the ctb_global_show class is also set.\n*/\n.ctb_global_show .ctb_show.ctb_hideshow {\n  display: block;\n}\n.ctb_global_show .ctb_show + .input_area,\n.ctb_global_show .ctb_show + div.text_cell_input,\n.ctb_global_show .ctb_show ~ div.text_cell_render {\n  border-top-right-radius: 0px;\n  border-top-left-radius: 0px;\n}\n.ctb_global_show .ctb_show ~ div.text_cell_render {\n  border: 1px solid #cfcfcf;\n}\n.celltoolbar {\n  font-size: 87%;\n  padding-top: 3px;\n}\n.celltoolbar select {\n  display: block;\n  width: 100%;\n  height: 32px;\n  padding: 6px 12px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #555555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n  width: inherit;\n  font-size: inherit;\n  height: 22px;\n  padding: 0px;\n  display: inline-block;\n}\n.celltoolbar select:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.celltoolbar select::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n.celltoolbar select:-ms-input-placeholder {\n  color: #999;\n}\n.celltoolbar select::-webkit-input-placeholder {\n  color: #999;\n}\n.celltoolbar select::-ms-expand {\n  border: 0;\n  background-color: transparent;\n}\n.celltoolbar select[disabled],\n.celltoolbar select[readonly],\nfieldset[disabled] .celltoolbar select {\n  background-color: #eeeeee;\n  opacity: 1;\n}\n.celltoolbar select[disabled],\nfieldset[disabled] .celltoolbar select {\n  cursor: not-allowed;\n}\ntextarea.celltoolbar select {\n  height: auto;\n}\nselect.celltoolbar select {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.celltoolbar select,\nselect[multiple].celltoolbar select {\n  height: auto;\n}\n.celltoolbar label {\n  margin-left: 5px;\n  margin-right: 5px;\n}\n.tags_button_container {\n  width: 100%;\n  display: flex;\n}\n.tag-container {\n  display: flex;\n  flex-direction: row;\n  flex-grow: 1;\n  overflow: hidden;\n  position: relative;\n}\n.tag-container > * {\n  margin: 0 4px;\n}\n.remove-tag-btn {\n  margin-left: 4px;\n}\n.tags-input {\n  display: flex;\n}\n.cell-tag:last-child:after {\n  content: \"\";\n  position: absolute;\n  right: 0;\n  width: 40px;\n  height: 100%;\n  /* Fade to background color of cell toolbar */\n  background: linear-gradient(to right, rgba(0, 0, 0, 0), #EEE);\n}\n.tags-input > * {\n  margin-left: 4px;\n}\n.cell-tag,\n.tags-input input,\n.tags-input button {\n  display: block;\n  width: 100%;\n  height: 32px;\n  padding: 6px 12px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #555555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 1px;\n  box-shadow: none;\n  width: inherit;\n  font-size: inherit;\n  height: 22px;\n  line-height: 22px;\n  padding: 0px 4px;\n  display: inline-block;\n}\n.cell-tag:focus,\n.tags-input input:focus,\n.tags-input button:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.cell-tag::-moz-placeholder,\n.tags-input input::-moz-placeholder,\n.tags-input button::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n.cell-tag:-ms-input-placeholder,\n.tags-input input:-ms-input-placeholder,\n.tags-input button:-ms-input-placeholder {\n  color: #999;\n}\n.cell-tag::-webkit-input-placeholder,\n.tags-input input::-webkit-input-placeholder,\n.tags-input button::-webkit-input-placeholder {\n  color: #999;\n}\n.cell-tag::-ms-expand,\n.tags-input input::-ms-expand,\n.tags-input button::-ms-expand {\n  border: 0;\n  background-color: transparent;\n}\n.cell-tag[disabled],\n.tags-input input[disabled],\n.tags-input button[disabled],\n.cell-tag[readonly],\n.tags-input input[readonly],\n.tags-input button[readonly],\nfieldset[disabled] .cell-tag,\nfieldset[disabled] .tags-input input,\nfieldset[disabled] .tags-input button {\n  background-color: #eeeeee;\n  opacity: 1;\n}\n.cell-tag[disabled],\n.tags-input input[disabled],\n.tags-input button[disabled],\nfieldset[disabled] .cell-tag,\nfieldset[disabled] .tags-input input,\nfieldset[disabled] .tags-input button {\n  cursor: not-allowed;\n}\ntextarea.cell-tag,\ntextarea.tags-input input,\ntextarea.tags-input button {\n  height: auto;\n}\nselect.cell-tag,\nselect.tags-input input,\nselect.tags-input button {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.cell-tag,\ntextarea.tags-input input,\ntextarea.tags-input button,\nselect[multiple].cell-tag,\nselect[multiple].tags-input input,\nselect[multiple].tags-input button {\n  height: auto;\n}\n.cell-tag,\n.tags-input button {\n  padding: 0px 4px;\n}\n.cell-tag {\n  background-color: #fff;\n  white-space: nowrap;\n}\n.tags-input input[type=text]:focus {\n  outline: none;\n  box-shadow: none;\n  border-color: #ccc;\n}\n.completions {\n  position: absolute;\n  z-index: 110;\n  overflow: hidden;\n  border: 1px solid #ababab;\n  border-radius: 2px;\n  -webkit-box-shadow: 0px 6px 10px -1px #adadad;\n  box-shadow: 0px 6px 10px -1px #adadad;\n  line-height: 1;\n}\n.completions select {\n  background: white;\n  outline: none;\n  border: none;\n  padding: 0px;\n  margin: 0px;\n  overflow: auto;\n  font-family: monospace;\n  font-size: 110%;\n  color: #000;\n  width: auto;\n}\n.completions select option.context {\n  color: #286090;\n}\n#kernel_logo_widget .current_kernel_logo {\n  display: none;\n  margin-top: -1px;\n  margin-bottom: -1px;\n  width: 32px;\n  height: 32px;\n}\n[dir=\"rtl\"] #kernel_logo_widget {\n  float: left !important;\n  float: left;\n}\n.modal .modal-body .move-path {\n  display: flex;\n  flex-direction: row;\n  justify-content: space;\n  align-items: center;\n}\n.modal .modal-body .move-path .server-root {\n  padding-right: 20px;\n}\n.modal .modal-body .move-path .path-input {\n  flex: 1;\n}\n#menubar {\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  margin-top: 1px;\n}\n#menubar .navbar {\n  border-top: 1px;\n  border-radius: 0px 0px 2px 2px;\n  margin-bottom: 0px;\n}\n#menubar .navbar-toggle {\n  float: left;\n  padding-top: 7px;\n  padding-bottom: 7px;\n  border: none;\n}\n#menubar .navbar-collapse {\n  clear: left;\n}\n[dir=\"rtl\"] #menubar .navbar-toggle {\n  float: right;\n}\n[dir=\"rtl\"] #menubar .navbar-collapse {\n  clear: right;\n}\n[dir=\"rtl\"] #menubar .navbar-nav {\n  float: right;\n}\n[dir=\"rtl\"] #menubar .nav {\n  padding-right: 0px;\n}\n[dir=\"rtl\"] #menubar .navbar-nav > li {\n  float: right;\n}\n[dir=\"rtl\"] #menubar .navbar-right {\n  float: left !important;\n}\n[dir=\"rtl\"] ul.dropdown-menu {\n  text-align: right;\n  left: auto;\n}\n[dir=\"rtl\"] ul#new-menu.dropdown-menu {\n  right: auto;\n  left: 0;\n}\n.nav-wrapper {\n  border-bottom: 1px solid #e7e7e7;\n}\ni.menu-icon {\n  padding-top: 4px;\n}\n[dir=\"rtl\"] i.menu-icon.pull-right {\n  float: left !important;\n  float: left;\n}\nul#help_menu li a {\n  overflow: hidden;\n  padding-right: 2.2em;\n}\nul#help_menu li a i {\n  margin-right: -1.2em;\n}\n[dir=\"rtl\"] ul#help_menu li a {\n  padding-left: 2.2em;\n}\n[dir=\"rtl\"] ul#help_menu li a i {\n  margin-right: 0;\n  margin-left: -1.2em;\n}\n[dir=\"rtl\"] ul#help_menu li a i.pull-right {\n  float: left !important;\n  float: left;\n}\n.dropdown-submenu {\n  position: relative;\n}\n.dropdown-submenu > .dropdown-menu {\n  top: 0;\n  left: 100%;\n  margin-top: -6px;\n  margin-left: -1px;\n}\n[dir=\"rtl\"] .dropdown-submenu > .dropdown-menu {\n  right: 100%;\n  margin-right: -1px;\n}\n.dropdown-submenu:hover > .dropdown-menu {\n  display: block;\n}\n.dropdown-submenu > a:after {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  display: block;\n  content: \"\\f0da\";\n  float: right;\n  color: #333333;\n  margin-top: 2px;\n  margin-right: -10px;\n}\n.dropdown-submenu > a:after.fa-pull-left {\n  margin-right: .3em;\n}\n.dropdown-submenu > a:after.fa-pull-right {\n  margin-left: .3em;\n}\n.dropdown-submenu > a:after.pull-left {\n  margin-right: .3em;\n}\n.dropdown-submenu > a:after.pull-right {\n  margin-left: .3em;\n}\n[dir=\"rtl\"] .dropdown-submenu > a:after {\n  float: left;\n  content: \"\\f0d9\";\n  margin-right: 0;\n  margin-left: -10px;\n}\n.dropdown-submenu:hover > a:after {\n  color: #262626;\n}\n.dropdown-submenu.pull-left {\n  float: none;\n}\n.dropdown-submenu.pull-left > .dropdown-menu {\n  left: -100%;\n  margin-left: 10px;\n}\n#notification_area {\n  float: right !important;\n  float: right;\n  z-index: 10;\n}\n[dir=\"rtl\"] #notification_area {\n  float: left !important;\n  float: left;\n}\n.indicator_area {\n  float: right !important;\n  float: right;\n  color: #777;\n  margin-left: 5px;\n  margin-right: 5px;\n  width: 11px;\n  z-index: 10;\n  text-align: center;\n  width: auto;\n}\n[dir=\"rtl\"] .indicator_area {\n  float: left !important;\n  float: left;\n}\n#kernel_indicator {\n  float: right !important;\n  float: right;\n  color: #777;\n  margin-left: 5px;\n  margin-right: 5px;\n  width: 11px;\n  z-index: 10;\n  text-align: center;\n  width: auto;\n  border-left: 1px solid;\n}\n#kernel_indicator .kernel_indicator_name {\n  padding-left: 5px;\n  padding-right: 5px;\n}\n[dir=\"rtl\"] #kernel_indicator {\n  float: left !important;\n  float: left;\n  border-left: 0;\n  border-right: 1px solid;\n}\n#modal_indicator {\n  float: right !important;\n  float: right;\n  color: #777;\n  margin-left: 5px;\n  margin-right: 5px;\n  width: 11px;\n  z-index: 10;\n  text-align: center;\n  width: auto;\n}\n[dir=\"rtl\"] #modal_indicator {\n  float: left !important;\n  float: left;\n}\n#readonly-indicator {\n  float: right !important;\n  float: right;\n  color: #777;\n  margin-left: 5px;\n  margin-right: 5px;\n  width: 11px;\n  z-index: 10;\n  text-align: center;\n  width: auto;\n  margin-top: 2px;\n  margin-bottom: 0px;\n  margin-left: 0px;\n  margin-right: 0px;\n  display: none;\n}\n.modal_indicator:before {\n  width: 1.28571429em;\n  text-align: center;\n}\n.edit_mode .modal_indicator:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f040\";\n}\n.edit_mode .modal_indicator:before.fa-pull-left {\n  margin-right: .3em;\n}\n.edit_mode .modal_indicator:before.fa-pull-right {\n  margin-left: .3em;\n}\n.edit_mode .modal_indicator:before.pull-left {\n  margin-right: .3em;\n}\n.edit_mode .modal_indicator:before.pull-right {\n  margin-left: .3em;\n}\n.command_mode .modal_indicator:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: ' ';\n}\n.command_mode .modal_indicator:before.fa-pull-left {\n  margin-right: .3em;\n}\n.command_mode .modal_indicator:before.fa-pull-right {\n  margin-left: .3em;\n}\n.command_mode .modal_indicator:before.pull-left {\n  margin-right: .3em;\n}\n.command_mode .modal_indicator:before.pull-right {\n  margin-left: .3em;\n}\n.kernel_idle_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f10c\";\n}\n.kernel_idle_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.kernel_idle_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.kernel_idle_icon:before.pull-left {\n  margin-right: .3em;\n}\n.kernel_idle_icon:before.pull-right {\n  margin-left: .3em;\n}\n.kernel_busy_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f111\";\n}\n.kernel_busy_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.kernel_busy_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.kernel_busy_icon:before.pull-left {\n  margin-right: .3em;\n}\n.kernel_busy_icon:before.pull-right {\n  margin-left: .3em;\n}\n.kernel_dead_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f1e2\";\n}\n.kernel_dead_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.kernel_dead_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.kernel_dead_icon:before.pull-left {\n  margin-right: .3em;\n}\n.kernel_dead_icon:before.pull-right {\n  margin-left: .3em;\n}\n.kernel_disconnected_icon:before {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  content: \"\\f127\";\n}\n.kernel_disconnected_icon:before.fa-pull-left {\n  margin-right: .3em;\n}\n.kernel_disconnected_icon:before.fa-pull-right {\n  margin-left: .3em;\n}\n.kernel_disconnected_icon:before.pull-left {\n  margin-right: .3em;\n}\n.kernel_disconnected_icon:before.pull-right {\n  margin-left: .3em;\n}\n.notification_widget {\n  color: #777;\n  z-index: 10;\n  background: rgba(240, 240, 240, 0.5);\n  margin-right: 4px;\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n}\n.notification_widget:focus,\n.notification_widget.focus {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #8c8c8c;\n}\n.notification_widget:hover {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.notification_widget:active,\n.notification_widget.active,\n.open > .dropdown-toggle.notification_widget {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.notification_widget:active:hover,\n.notification_widget.active:hover,\n.open > .dropdown-toggle.notification_widget:hover,\n.notification_widget:active:focus,\n.notification_widget.active:focus,\n.open > .dropdown-toggle.notification_widget:focus,\n.notification_widget:active.focus,\n.notification_widget.active.focus,\n.open > .dropdown-toggle.notification_widget.focus {\n  color: #333;\n  background-color: #d4d4d4;\n  border-color: #8c8c8c;\n}\n.notification_widget:active,\n.notification_widget.active,\n.open > .dropdown-toggle.notification_widget {\n  background-image: none;\n}\n.notification_widget.disabled:hover,\n.notification_widget[disabled]:hover,\nfieldset[disabled] .notification_widget:hover,\n.notification_widget.disabled:focus,\n.notification_widget[disabled]:focus,\nfieldset[disabled] .notification_widget:focus,\n.notification_widget.disabled.focus,\n.notification_widget[disabled].focus,\nfieldset[disabled] .notification_widget.focus {\n  background-color: #fff;\n  border-color: #ccc;\n}\n.notification_widget .badge {\n  color: #fff;\n  background-color: #333;\n}\n.notification_widget.warning {\n  color: #fff;\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.notification_widget.warning:focus,\n.notification_widget.warning.focus {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #985f0d;\n}\n.notification_widget.warning:hover {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.notification_widget.warning:active,\n.notification_widget.warning.active,\n.open > .dropdown-toggle.notification_widget.warning {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.notification_widget.warning:active:hover,\n.notification_widget.warning.active:hover,\n.open > .dropdown-toggle.notification_widget.warning:hover,\n.notification_widget.warning:active:focus,\n.notification_widget.warning.active:focus,\n.open > .dropdown-toggle.notification_widget.warning:focus,\n.notification_widget.warning:active.focus,\n.notification_widget.warning.active.focus,\n.open > .dropdown-toggle.notification_widget.warning.focus {\n  color: #fff;\n  background-color: #d58512;\n  border-color: #985f0d;\n}\n.notification_widget.warning:active,\n.notification_widget.warning.active,\n.open > .dropdown-toggle.notification_widget.warning {\n  background-image: none;\n}\n.notification_widget.warning.disabled:hover,\n.notification_widget.warning[disabled]:hover,\nfieldset[disabled] .notification_widget.warning:hover,\n.notification_widget.warning.disabled:focus,\n.notification_widget.warning[disabled]:focus,\nfieldset[disabled] .notification_widget.warning:focus,\n.notification_widget.warning.disabled.focus,\n.notification_widget.warning[disabled].focus,\nfieldset[disabled] .notification_widget.warning.focus {\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.notification_widget.warning .badge {\n  color: #f0ad4e;\n  background-color: #fff;\n}\n.notification_widget.success {\n  color: #fff;\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.notification_widget.success:focus,\n.notification_widget.success.focus {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #255625;\n}\n.notification_widget.success:hover {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.notification_widget.success:active,\n.notification_widget.success.active,\n.open > .dropdown-toggle.notification_widget.success {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.notification_widget.success:active:hover,\n.notification_widget.success.active:hover,\n.open > .dropdown-toggle.notification_widget.success:hover,\n.notification_widget.success:active:focus,\n.notification_widget.success.active:focus,\n.open > .dropdown-toggle.notification_widget.success:focus,\n.notification_widget.success:active.focus,\n.notification_widget.success.active.focus,\n.open > .dropdown-toggle.notification_widget.success.focus {\n  color: #fff;\n  background-color: #398439;\n  border-color: #255625;\n}\n.notification_widget.success:active,\n.notification_widget.success.active,\n.open > .dropdown-toggle.notification_widget.success {\n  background-image: none;\n}\n.notification_widget.success.disabled:hover,\n.notification_widget.success[disabled]:hover,\nfieldset[disabled] .notification_widget.success:hover,\n.notification_widget.success.disabled:focus,\n.notification_widget.success[disabled]:focus,\nfieldset[disabled] .notification_widget.success:focus,\n.notification_widget.success.disabled.focus,\n.notification_widget.success[disabled].focus,\nfieldset[disabled] .notification_widget.success.focus {\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.notification_widget.success .badge {\n  color: #5cb85c;\n  background-color: #fff;\n}\n.notification_widget.info {\n  color: #fff;\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.notification_widget.info:focus,\n.notification_widget.info.focus {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #1b6d85;\n}\n.notification_widget.info:hover {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.notification_widget.info:active,\n.notification_widget.info.active,\n.open > .dropdown-toggle.notification_widget.info {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.notification_widget.info:active:hover,\n.notification_widget.info.active:hover,\n.open > .dropdown-toggle.notification_widget.info:hover,\n.notification_widget.info:active:focus,\n.notification_widget.info.active:focus,\n.open > .dropdown-toggle.notification_widget.info:focus,\n.notification_widget.info:active.focus,\n.notification_widget.info.active.focus,\n.open > .dropdown-toggle.notification_widget.info.focus {\n  color: #fff;\n  background-color: #269abc;\n  border-color: #1b6d85;\n}\n.notification_widget.info:active,\n.notification_widget.info.active,\n.open > .dropdown-toggle.notification_widget.info {\n  background-image: none;\n}\n.notification_widget.info.disabled:hover,\n.notification_widget.info[disabled]:hover,\nfieldset[disabled] .notification_widget.info:hover,\n.notification_widget.info.disabled:focus,\n.notification_widget.info[disabled]:focus,\nfieldset[disabled] .notification_widget.info:focus,\n.notification_widget.info.disabled.focus,\n.notification_widget.info[disabled].focus,\nfieldset[disabled] .notification_widget.info.focus {\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.notification_widget.info .badge {\n  color: #5bc0de;\n  background-color: #fff;\n}\n.notification_widget.danger {\n  color: #fff;\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.notification_widget.danger:focus,\n.notification_widget.danger.focus {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #761c19;\n}\n.notification_widget.danger:hover {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.notification_widget.danger:active,\n.notification_widget.danger.active,\n.open > .dropdown-toggle.notification_widget.danger {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.notification_widget.danger:active:hover,\n.notification_widget.danger.active:hover,\n.open > .dropdown-toggle.notification_widget.danger:hover,\n.notification_widget.danger:active:focus,\n.notification_widget.danger.active:focus,\n.open > .dropdown-toggle.notification_widget.danger:focus,\n.notification_widget.danger:active.focus,\n.notification_widget.danger.active.focus,\n.open > .dropdown-toggle.notification_widget.danger.focus {\n  color: #fff;\n  background-color: #ac2925;\n  border-color: #761c19;\n}\n.notification_widget.danger:active,\n.notification_widget.danger.active,\n.open > .dropdown-toggle.notification_widget.danger {\n  background-image: none;\n}\n.notification_widget.danger.disabled:hover,\n.notification_widget.danger[disabled]:hover,\nfieldset[disabled] .notification_widget.danger:hover,\n.notification_widget.danger.disabled:focus,\n.notification_widget.danger[disabled]:focus,\nfieldset[disabled] .notification_widget.danger:focus,\n.notification_widget.danger.disabled.focus,\n.notification_widget.danger[disabled].focus,\nfieldset[disabled] .notification_widget.danger.focus {\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.notification_widget.danger .badge {\n  color: #d9534f;\n  background-color: #fff;\n}\ndiv#pager {\n  background-color: #fff;\n  font-size: 14px;\n  line-height: 20px;\n  overflow: hidden;\n  display: none;\n  position: fixed;\n  bottom: 0px;\n  width: 100%;\n  max-height: 50%;\n  padding-top: 8px;\n  -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  /* Display over codemirror */\n  z-index: 100;\n  /* Hack which prevents jquery ui resizable from changing top. */\n  top: auto !important;\n}\ndiv#pager pre {\n  line-height: 1.21429em;\n  color: #000;\n  background-color: #f7f7f7;\n  padding: 0.4em;\n}\ndiv#pager #pager-button-area {\n  position: absolute;\n  top: 8px;\n  right: 20px;\n}\ndiv#pager #pager-contents {\n  position: relative;\n  overflow: auto;\n  width: 100%;\n  height: 100%;\n}\ndiv#pager #pager-contents #pager-container {\n  position: relative;\n  padding: 15px 0px;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n}\ndiv#pager .ui-resizable-handle {\n  top: 0px;\n  height: 8px;\n  background: #f7f7f7;\n  border-top: 1px solid #cfcfcf;\n  border-bottom: 1px solid #cfcfcf;\n  /* This injects handle bars (a short, wide = symbol) for\n        the resize handle. */\n}\ndiv#pager .ui-resizable-handle::after {\n  content: '';\n  top: 2px;\n  left: 50%;\n  height: 3px;\n  width: 30px;\n  margin-left: -15px;\n  position: absolute;\n  border-top: 1px solid #cfcfcf;\n}\n.quickhelp {\n  /* Old browsers */\n  display: -webkit-box;\n  -webkit-box-orient: horizontal;\n  -webkit-box-align: stretch;\n  display: -moz-box;\n  -moz-box-orient: horizontal;\n  -moz-box-align: stretch;\n  display: box;\n  box-orient: horizontal;\n  box-align: stretch;\n  /* Modern browsers */\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n  line-height: 1.8em;\n}\n.shortcut_key {\n  display: inline-block;\n  width: 21ex;\n  text-align: right;\n  font-family: monospace;\n}\n.shortcut_descr {\n  display: inline-block;\n  /* Old browsers */\n  -webkit-box-flex: 1;\n  -moz-box-flex: 1;\n  box-flex: 1;\n  /* Modern browsers */\n  flex: 1;\n}\nspan.save_widget {\n  height: 30px;\n  margin-top: 4px;\n  display: flex;\n  justify-content: flex-start;\n  align-items: baseline;\n  width: 50%;\n  flex: 1;\n}\nspan.save_widget span.filename {\n  height: 100%;\n  line-height: 1em;\n  margin-left: 16px;\n  border: none;\n  font-size: 146.5%;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  white-space: nowrap;\n  border-radius: 2px;\n}\nspan.save_widget span.filename:hover {\n  background-color: #e6e6e6;\n}\n[dir=\"rtl\"] span.save_widget.pull-left {\n  float: right !important;\n  float: right;\n}\n[dir=\"rtl\"] span.save_widget span.filename {\n  margin-left: 0;\n  margin-right: 16px;\n}\nspan.checkpoint_status,\nspan.autosave_status {\n  font-size: small;\n  white-space: nowrap;\n  padding: 0 5px;\n}\n@media (max-width: 767px) {\n  span.save_widget {\n    font-size: small;\n    padding: 0 0 0 5px;\n  }\n  span.checkpoint_status,\n  span.autosave_status {\n    display: none;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  span.checkpoint_status {\n    display: none;\n  }\n  span.autosave_status {\n    font-size: x-small;\n  }\n}\n.toolbar {\n  padding: 0px;\n  margin-left: -5px;\n  margin-top: 2px;\n  margin-bottom: 5px;\n  box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n}\n.toolbar select,\n.toolbar label {\n  width: auto;\n  vertical-align: middle;\n  margin-right: 2px;\n  margin-bottom: 0px;\n  display: inline;\n  font-size: 92%;\n  margin-left: 0.3em;\n  margin-right: 0.3em;\n  padding: 0px;\n  padding-top: 3px;\n}\n.toolbar .btn {\n  padding: 2px 8px;\n}\n.toolbar .btn-group {\n  margin-top: 0px;\n  margin-left: 5px;\n}\n.toolbar-btn-label {\n  margin-left: 6px;\n}\n#maintoolbar {\n  margin-bottom: -3px;\n  margin-top: -8px;\n  border: 0px;\n  min-height: 27px;\n  margin-left: 0px;\n  padding-top: 11px;\n  padding-bottom: 3px;\n}\n#maintoolbar .navbar-text {\n  float: none;\n  vertical-align: middle;\n  text-align: right;\n  margin-left: 5px;\n  margin-right: 0px;\n  margin-top: 0px;\n}\n.select-xs {\n  height: 24px;\n}\n[dir=\"rtl\"] .btn-group > .btn,\n.btn-group-vertical > .btn {\n  float: right;\n}\n.pulse,\n.dropdown-menu > li > a.pulse,\nli.pulse > a.dropdown-toggle,\nli.pulse.open > a.dropdown-toggle {\n  background-color: #F37626;\n  color: white;\n}\n/**\n * Primary styles\n *\n * Author: Jupyter Development Team\n */\n/** WARNING IF YOU ARE EDITTING THIS FILE, if this is a .css file, It has a lot\n * of chance of beeing generated from the ../less/[samename].less file, you can\n * try to get back the less file by reverting somme commit in history\n **/\n/*\n * We'll try to get something pretty, so we\n * have some strange css to have the scroll bar on\n * the left with fix button on the top right of the tooltip\n */\n@-moz-keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@-webkit-keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@-moz-keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@-webkit-keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n/*properties of tooltip after \"expand\"*/\n.bigtooltip {\n  overflow: auto;\n  height: 200px;\n  -webkit-transition-property: height;\n  -webkit-transition-duration: 500ms;\n  -moz-transition-property: height;\n  -moz-transition-duration: 500ms;\n  transition-property: height;\n  transition-duration: 500ms;\n}\n/*properties of tooltip before \"expand\"*/\n.smalltooltip {\n  -webkit-transition-property: height;\n  -webkit-transition-duration: 500ms;\n  -moz-transition-property: height;\n  -moz-transition-duration: 500ms;\n  transition-property: height;\n  transition-duration: 500ms;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  height: 80px;\n}\n.tooltipbuttons {\n  position: absolute;\n  padding-right: 15px;\n  top: 0px;\n  right: 0px;\n}\n.tooltiptext {\n  /*avoid the button to overlap on some docstring*/\n  padding-right: 30px;\n}\n.ipython_tooltip {\n  max-width: 700px;\n  /*fade-in animation when inserted*/\n  -webkit-animation: fadeOut 400ms;\n  -moz-animation: fadeOut 400ms;\n  animation: fadeOut 400ms;\n  -webkit-animation: fadeIn 400ms;\n  -moz-animation: fadeIn 400ms;\n  animation: fadeIn 400ms;\n  vertical-align: middle;\n  background-color: #f7f7f7;\n  overflow: visible;\n  border: #ababab 1px solid;\n  outline: none;\n  padding: 3px;\n  margin: 0px;\n  padding-left: 7px;\n  font-family: monospace;\n  min-height: 50px;\n  -moz-box-shadow: 0px 6px 10px -1px #adadad;\n  -webkit-box-shadow: 0px 6px 10px -1px #adadad;\n  box-shadow: 0px 6px 10px -1px #adadad;\n  border-radius: 2px;\n  position: absolute;\n  z-index: 1000;\n}\n.ipython_tooltip a {\n  float: right;\n}\n.ipython_tooltip .tooltiptext pre {\n  border: 0;\n  border-radius: 0;\n  font-size: 100%;\n  background-color: #f7f7f7;\n}\n.pretooltiparrow {\n  left: 0px;\n  margin: 0px;\n  top: -16px;\n  width: 40px;\n  height: 16px;\n  overflow: hidden;\n  position: absolute;\n}\n.pretooltiparrow:before {\n  background-color: #f7f7f7;\n  border: 1px #ababab solid;\n  z-index: 11;\n  content: \"\";\n  position: absolute;\n  left: 15px;\n  top: 10px;\n  width: 25px;\n  height: 25px;\n  -webkit-transform: rotate(45deg);\n  -moz-transform: rotate(45deg);\n  -ms-transform: rotate(45deg);\n  -o-transform: rotate(45deg);\n}\nul.typeahead-list i {\n  margin-left: -10px;\n  width: 18px;\n}\n[dir=\"rtl\"] ul.typeahead-list i {\n  margin-left: 0;\n  margin-right: -10px;\n}\nul.typeahead-list {\n  max-height: 80vh;\n  overflow: auto;\n}\nul.typeahead-list > li > a {\n  /** Firefox bug **/\n  /* see https://github.com/jupyter/notebook/issues/559 */\n  white-space: normal;\n}\nul.typeahead-list  > li > a.pull-right {\n  float: left !important;\n  float: left;\n}\n[dir=\"rtl\"] .typeahead-list {\n  text-align: right;\n}\n.cmd-palette .modal-body {\n  padding: 7px;\n}\n.cmd-palette form {\n  background: white;\n}\n.cmd-palette input {\n  outline: none;\n}\n.no-shortcut {\n  min-width: 20px;\n  color: transparent;\n}\n[dir=\"rtl\"] .no-shortcut.pull-right {\n  float: left !important;\n  float: left;\n}\n[dir=\"rtl\"] .command-shortcut.pull-right {\n  float: left !important;\n  float: left;\n}\n.command-shortcut:before {\n  content: \"(command mode)\";\n  padding-right: 3px;\n  color: #777777;\n}\n.edit-shortcut:before {\n  content: \"(edit)\";\n  padding-right: 3px;\n  color: #777777;\n}\n[dir=\"rtl\"] .edit-shortcut.pull-right {\n  float: left !important;\n  float: left;\n}\n#find-and-replace #replace-preview .match,\n#find-and-replace #replace-preview .insert {\n  background-color: #BBDEFB;\n  border-color: #90CAF9;\n  border-style: solid;\n  border-width: 1px;\n  border-radius: 0px;\n}\n[dir=\"ltr\"] #find-and-replace .input-group-btn + .form-control {\n  border-left: none;\n}\n[dir=\"rtl\"] #find-and-replace .input-group-btn + .form-control {\n  border-right: none;\n}\n#find-and-replace #replace-preview .replace .match {\n  background-color: #FFCDD2;\n  border-color: #EF9A9A;\n  border-radius: 0px;\n}\n#find-and-replace #replace-preview .replace .insert {\n  background-color: #C8E6C9;\n  border-color: #A5D6A7;\n  border-radius: 0px;\n}\n#find-and-replace #replace-preview {\n  max-height: 60vh;\n  overflow: auto;\n}\n#find-and-replace #replace-preview pre {\n  padding: 5px 10px;\n}\n.terminal-app {\n  background: #EEE;\n}\n.terminal-app #header {\n  background: #fff;\n  -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n  box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);\n}\n.terminal-app .terminal {\n  width: 100%;\n  float: left;\n  font-family: monospace;\n  color: white;\n  background: black;\n  padding: 0.4em;\n  border-radius: 2px;\n  -webkit-box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.4);\n  box-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.4);\n}\n.terminal-app .terminal,\n.terminal-app .terminal dummy-screen {\n  line-height: 1em;\n  font-size: 14px;\n}\n.terminal-app .terminal .xterm-rows {\n  padding: 10px;\n}\n.terminal-app .terminal-cursor {\n  color: black;\n  background: white;\n}\n.terminal-app #terminado-container {\n  margin-top: 20px;\n}\n/*# sourceMappingURL=style.min.css.map */\n    </style>\n<style type=\"text/css\">\n    .highlight .hll { background-color: #ffffcc }\n.highlight  { background: #f8f8f8; }\n.highlight .c { color: #408080; font-style: italic } /* Comment */\n.highlight .err { border: 1px solid #FF0000 } /* Error */\n.highlight .k { color: #008000; font-weight: bold } /* Keyword */\n.highlight .o { color: #666666 } /* Operator */\n.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */\n.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */\n.highlight .cp { color: #BC7A00 } /* Comment.Preproc */\n.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */\n.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */\n.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */\n.highlight .gd { color: #A00000 } /* Generic.Deleted */\n.highlight .ge { font-style: italic } /* Generic.Emph */\n.highlight .gr { color: #FF0000 } /* Generic.Error */\n.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */\n.highlight .gi { color: #00A000 } /* Generic.Inserted */\n.highlight .go { color: #888888 } /* Generic.Output */\n.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */\n.highlight .gs { font-weight: bold } /* Generic.Strong */\n.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */\n.highlight .gt { color: #0044DD } /* Generic.Traceback */\n.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */\n.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\n.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\n.highlight .kp { color: #008000 } /* Keyword.Pseudo */\n.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\n.highlight .kt { color: #B00040 } /* Keyword.Type */\n.highlight .m { color: #666666 } /* Literal.Number */\n.highlight .s { color: #BA2121 } /* Literal.String */\n.highlight .na { color: #7D9029 } /* Name.Attribute */\n.highlight .nb { color: #008000 } /* Name.Builtin */\n.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */\n.highlight .no { color: #880000 } /* Name.Constant */\n.highlight .nd { color: #AA22FF } /* Name.Decorator */\n.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */\n.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */\n.highlight .nf { color: #0000FF } /* Name.Function */\n.highlight .nl { color: #A0A000 } /* Name.Label */\n.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\n.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */\n.highlight .nv { color: #19177C } /* Name.Variable */\n.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\n.highlight .w { color: #bbbbbb } /* Text.Whitespace */\n.highlight .mb { color: #666666 } /* Literal.Number.Bin */\n.highlight .mf { color: #666666 } /* Literal.Number.Float */\n.highlight .mh { color: #666666 } /* Literal.Number.Hex */\n.highlight .mi { color: #666666 } /* Literal.Number.Integer */\n.highlight .mo { color: #666666 } /* Literal.Number.Oct */\n.highlight .sa { color: #BA2121 } /* Literal.String.Affix */\n.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */\n.highlight .sc { color: #BA2121 } /* Literal.String.Char */\n.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */\n.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\n.highlight .s2 { color: #BA2121 } /* Literal.String.Double */\n.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */\n.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */\n.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */\n.highlight .sx { color: #008000 } /* Literal.String.Other */\n.highlight .sr { color: #BB6688 } /* Literal.String.Regex */\n.highlight .s1 { color: #BA2121 } /* Literal.String.Single */\n.highlight .ss { color: #19177C } /* Literal.String.Symbol */\n.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */\n.highlight .fm { color: #0000FF } /* Name.Function.Magic */\n.highlight .vc { color: #19177C } /* Name.Variable.Class */\n.highlight .vg { color: #19177C } /* Name.Variable.Global */\n.highlight .vi { color: #19177C } /* Name.Variable.Instance */\n.highlight .vm { color: #19177C } /* Name.Variable.Magic */\n.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */\n    </style>\n\n\n<style type=\"text/css\">\n/* Overrides of notebook CSS for static HTML export */\n.reveal {\n  font-size: 160%;\n}\n.reveal pre {\n  width: inherit;\n  padding: 0.4em;\n  margin: 0px;\n  font-family: monospace, sans-serif;\n  font-size: 80%;\n  box-shadow: 0px 0px 0px rgba(0, 0, 0, 0);\n}\n.reveal pre code {\n  padding: 0px;\n}\n.reveal section img {\n  border: 0px solid black;\n  box-shadow: 0 0 10px rgba(0, 0, 0, 0);\n}\n.reveal i {\n  font-style: normal;\n  font-family: FontAwesome;\n  font-size: 2em;\n}\n.reveal .slides {\n  text-align: left;\n}\n.reveal.fade {\n  opacity: 1;\n}\n.reveal .progress {\n  position: static;\n}\n.reveal .controls .navigate-left,\n.reveal .controls .navigate-left.enabled {\n  border-right-color: #727272;\n}\n.reveal .controls .navigate-left.enabled:hover,\n.reveal .controls .navigate-left.enabled.enabled:hover {\n  border-right-color: #dfdfdf;\n}\n.reveal .controls .navigate-right,\n.reveal .controls .navigate-right.enabled {\n  border-left-color: #727272;\n}\n.reveal .controls .navigate-right.enabled:hover,\n.reveal .controls .navigate-right.enabled.enabled:hover {\n  border-left-color: #dfdfdf;\n}\n.reveal .controls .navigate-up,\n.reveal .controls .navigate-up.enabled {\n  border-bottom-color: #727272;\n}\n.reveal .controls .navigate-up.enabled:hover,\n.reveal .controls .navigate-up.enabled.enabled:hover {\n  border-bottom-color: #dfdfdf;\n}\n.reveal .controls .navigate-down,\n.reveal .controls .navigate-down.enabled {\n  border-top-color: #727272;\n}\n.reveal .controls .navigate-down.enabled:hover,\n.reveal .controls .navigate-down.enabled.enabled:hover {\n  border-top-color: #dfdfdf;\n}\n.reveal .progress span {\n  background: #727272;\n}\ndiv.input_area {\n  padding: 0.06em;\n}\ndiv.code_cell {\n  background-color: transparent;\n}\ndiv.prompt {\n  width: 11ex;\n  padding: 0.4em;\n  margin: 0px;\n  font-family: monospace, sans-serif;\n  font-size: 80%;\n  text-align: right;\n}\ndiv.output_area pre {\n  font-family: monospace, sans-serif;\n  font-size: 80%;\n}\ndiv.output_prompt {\n  /* 5px right shift to account for margin in parent container */\n  margin: 5px 5px 0 0;\n}\ndiv.text_cell.rendered .rendered_html {\n  /* The H1 height seems miscalculated, we are just hidding the scrollbar */\n  overflow-y: hidden;\n}\na.anchor-link {\n  /* There is still an anchor, we are only hidding it */\n  display: none;\n}\n.rendered_html p {\n  text-align: inherit;\n}\n::-webkit-scrollbar\n{\n  width: 6px;\n  height: 6px;\n}\n::-webkit-scrollbar *\n{\n  background:transparent;\n}\n::-webkit-scrollbar-thumb\n{\n  background: #727272 !important;\n}\n</style>\n\n<!-- Custom stylesheet, it must be in the same directory as the html file -->\n<link rel=\"stylesheet\" href=\"custom.css\">\n\n</head>\n\n\n<body>\n\n\n<div class=\"reveal\">\n<div class=\"slides\">\n<section><section>\n<div class=\"cell border-box-sizing text_cell rendered\"><div class=\"prompt input_prompt\">\n</div><div class=\"inner_cell\">\n<div class=\"text_cell_render border-box-sizing rendered_html\">\n<h1 id=\"Introduction-to\">Introduction to<a class=\"anchor-link\" href=\"#Introduction-to\">&#182;</a></h1><h1 id=\"SEML:-Slurm-Experiment-Management-Library\"><code>SEML</code>: Slurm Experiment Management Library<a class=\"anchor-link\" href=\"#SEML:-Slurm-Experiment-Management-Library\">&#182;</a></h1>\n</div>\n</div>\n</div></section></section><section><section>\n<div class=\"cell border-box-sizing text_cell rendered\"><div class=\"prompt input_prompt\">\n</div><div class=\"inner_cell\">\n<div class=\"text_cell_render border-box-sizing rendered_html\">\n<h2 id=\"Why-SEML?\">Why <code>SEML</code>?<a class=\"anchor-link\" href=\"#Why-SEML?\">&#182;</a></h2><p>In a nutshell, <strong><code>SEML</code></strong> enables you to leverage the massive parallelization of a compute cluster without boilerplate code or having to worry about keeping track of experiments.\nThat is, it enables you to:</p>\n<ul>\n<li>very easily define hyperparameter search spaces using YAML files,</li>\n<li>run these hyperparameter configurations on a compute cluster using <code>Slurm</code>,</li>\n<li>and to track the experimental results using <code>sacred</code> and <code>MongoDB</code>.</li>\n</ul>\n\n</div>\n</div>\n</div></section></section><section><section>\n<div class=\"cell border-box-sizing text_cell rendered\"><div class=\"prompt input_prompt\">\n</div><div class=\"inner_cell\">\n<div class=\"text_cell_render border-box-sizing rendered_html\">\n<p>In addition, <strong><code>SEML</code></strong> offers many more features to make your life easier, such as</p>\n<ul>\n<li>automatically saving and loading your source code for reproducibility,</li>\n<li>collecting experiment results into a <code>Pandas</code> dataframe,</li>\n<li>easy debugging on Slurm or locally,</li>\n<li>automatically checking your experiment configurations,</li>\n<li>extending Slurm with local workers,</li>\n<li>and keeping track of resource usage (experiment runtime, RAM, etc.).</li>\n</ul>\n<p>You can even get notified on Mattermost whenever an experiment starts, completes, or fails!</p>\n\n</div>\n</div>\n</div></section></section><section><section>\n<div class=\"cell border-box-sizing text_cell rendered\"><div class=\"prompt input_prompt\">\n</div><div class=\"inner_cell\">\n<div class=\"text_cell_render border-box-sizing rendered_html\">\n<h2 id=\"How-does-it-work?\">How does it work?<a class=\"anchor-link\" href=\"#How-does-it-work?\">&#182;</a></h2><center><img src=\"./seml.pdf\"/></center><ul>\n<li><strong><code>SEML</code></strong> takes a <code>YAML</code> file containing hyperparameters and metadata about a set of experiments.</li>\n<li><strong><code>SEML</code></strong> stores each individual experiment's data as an entry in a <code>MongoDB</code> database collection.</li>\n<li>In general, each type of experiments gets their own database collection.</li>\n<li>Each individual experiment is an entry in the respective collection.</li>\n<li>A database entry is essentially a <code>JSON</code> dictionary containing (among others):<ul>\n<li>the state of the experiment,</li>\n<li>the experiment configuration (i.e., hyperparameters),</li>\n<li>the generated results, and</li>\n<li>the cached source code (by default).</li>\n</ul>\n</li>\n</ul>\n\n</div>\n</div>\n</div></section></section><section><section>\n<div class=\"cell border-box-sizing text_cell rendered\"><div class=\"prompt input_prompt\">\n</div><div class=\"inner_cell\">\n<div class=\"text_cell_render border-box-sizing rendered_html\">\n<ul>\n<li><strong><code>SEML</code></strong> takes a <code>YAML</code> file containing hyperparameters and metadata about a set of experiments.</li>\n<li><strong><code>SEML</code></strong> stores each individual experiment's data as an entry in a <code>MongoDB</code> database collection.</li>\n<li>In general, each type of experiments gets their own database collection.</li>\n<li>Each individual experiment is an entry in the respective collection.</li>\n<li>A database entry is essentially a <code>JSON</code> dictionary containing (among others):<ul>\n<li>the state of the experiment,</li>\n<li>the experiment configuration (i.e., hyperparameters),</li>\n<li>the generated results, and</li>\n<li>the cached source code (by default).</li>\n</ul>\n</li>\n</ul>\n\n</div>\n</div>\n</div>\n<div class=\"cell border-box-sizing code_cell rendered\">\n<div class=\"input\">\n<div class=\"prompt input_prompt\">In&nbsp;[&nbsp;]:</div>\n<div class=\"inner_cell\">\n    <div class=\"input_area\">\n<div class=\" highlight hl-ipython3\"><pre><span></span>\n</pre></div>\n\n    </div>\n</div>\n</div>\n\n</div></section></section>\n</div>\n</div>\n\n<script>\n\nrequire(\n    {\n      // it makes sense to wait a little bit when you are loading\n      // reveal from a cdn in a slow connection environment\n      waitSeconds: 15\n    },\n    [\n      \"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/lib/js/head.min.js\",\n      \"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/js/reveal.js\"\n    ],\n\n    function(head, Reveal){\n\n        // Full list of configuration options available here: https://github.com/hakimel/reveal.js#configuration\n        Reveal.initialize({\n            controls: true,\n            progress: true,\n            history: true,\n\n            transition: \"slide\",\n\n            // Optional libraries used to extend on reveal.js\n            dependencies: [\n                { src: \"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/lib/js/classList.js\",\n                  condition: function() { return !document.body.classList; } },\n                { src: \"https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0/plugin/notes/notes.js\",\n                  async: true,\n                  condition: function() { return !!document.body.classList; } }\n            ]\n        });\n\n        var update = function(event){\n          if(MathJax.Hub.getAllJax(Reveal.getCurrentSlide())){\n            MathJax.Hub.Rerender(Reveal.getCurrentSlide());\n          }\n        };\n\n        Reveal.addEventListener('slidechanged', update);\n\n        function setScrollingSlide() {\n            var scroll = false\n            if (scroll === true) {\n              var h = $('.reveal').height() * 0.95;\n              $('section.present').find('section')\n                .filter(function() {\n                  return $(this).height() > h;\n                })\n                .css('height', 'calc(95vh)')\n                .css('overflow-y', 'scroll')\n                .css('margin-top', '20px');\n            }\n        }\n\n        // check and set the scrolling slide every time the slide change\n        Reveal.addEventListener('slidechanged', setScrollingSlide);\n\n    }\n\n);\n</script>\n\n</body>\n\n\n</html>\n"
  },
  {
    "path": "examples/tutorial/seml.drawio",
    "content": "<mxfile host=\"Electron\" modified=\"2021-10-19T14:26:02.099Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36\" etag=\"E73Mofc1TC57IDEvkDDH\" version=\"14.6.13\" type=\"device\"><diagram id=\"Ld8WGTmILVOHCsMrV-MZ\" name=\"Seite-1\">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</diagram></mxfile>"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"seml\"\nversion = \"0.5.6\"\ndescription = \"Slurm Experiment Management Library\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nlicense = { file = \"LICENSE\" }\nauthors = [\n    { name = \"Daniel Zügner\", email = \"zuegnerd@in.tum.de\" },\n    { name = \"Johannes Gsteiger\", email = \"johannes.gasteiger@tum.de\" },\n    { name = \"Nicholas Gao\", email = \"n.gao@tum.de\" },\n    { name = \"Dominik Fuchsgruber\", email = \"d.fuchsgruber@tum.de\" },\n    { name = \"DAML Group @ TUM\" },\n]\nmaintainers = [\n    { name = \"Nicholas Gao\", email = \"n.gao@tum.de\" },\n    { name = \"Dominik Fuchsgruber\", email = \"d.fuchsgruber@tum.de\" },\n]\nclassifiers = [\"Programming Language :: Python\"]\n\ndependencies = [\n    \"numpy>=1.15; python_version < '3.13'\",\n    \"numpy>=2.1; python_version >= '3.13'\",\n    \"pymongo>=4.1\",\n    \"pandas>=2.2; python_version < '3.13'\",\n    \"pandas>=2.2.3; python_version >= '3.13'\",\n    \"sacred>=0.8.4\",\n    \"pyyaml>=5.1\",\n    \"jsonpickle>=2.2\",\n    \"munch>=4.0\",\n    \"debugpy>=1.2.1\",\n    \"requests>=2.28.1\",\n    \"typer>=0.12\",\n    \"rich>=13.0,<14.1\",\n    \"click>=8.0\",\n    \"omegaconf>=2.3.0\",\n    \"gitignore_parser>=0.1.11\",\n    \"setuptools>=69.2.0\",\n    \"importlib_resources>=5.7.0\",\n    \"typing_extensions>=4.10; python_version < '3.13'\",\n    \"typing_extensions>=4.12; python_version >= '3.13'\",\n    \"deepdiff>=7.0.1\",\n]\n\n[dependency-groups]\ndev = [\n    \"ruff>=0.6.1\",\n    \"pytest>=8.3.2\",\n    \"pre-commit>=3.8.0\",\n    \"filelock>=3.15.4\",\n    \"sshtunnel>=0.4.0\",\n    \"pyright>=1.1.407\",\n    \"tuna>=0.5.11\",\n]\nssh_forward = [\"sshtunnel>=0.4.0\", \"filelock>=3.13.3\", \"paramiko>=3.0,<4.0\"]\n\n[project.urls]\nHomepage = \"https://github.com/TUM-DAML/seml\"\nDocumentation = \"https://github.com/TUM-DAML/seml/blob/master/docs.md\"\nRepository = \"https://github.com/TUM-DAML/seml\"\nChangelog = \"https://github.com/TUM-DAML/seml/releases/latest\"\n\n[project.scripts]\nseml = \"seml.__main__:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.uv]\ndefault-groups = \"all\"\n\n[tool.ruff.format]\nquote-style = \"single\"\nline-ending = \"lf\"\n\n[tool.pyright]\ntypeCheckingMode = \"standard\"\nvenvPath = \".\"\nvenv = \".venv\"\n"
  },
  {
    "path": "src/seml/__init__.py",
    "content": "from seml.cli_utils.module_hider import AUTOCOMPLETING\n\nif AUTOCOMPLETING:\n    __version__ = '0.0.0'\nelse:\n    import importlib.metadata\n\n    from seml.evaluation import *  # noqa\n    from seml.experiment import Experiment  # noqa\n    from seml.experiment.observers import *  # noqa\n\n    __version__ = importlib.metadata.version(__package__ or __name__)\n"
  },
  {
    "path": "src/seml/__main__.py",
    "content": "#!/usr/bin/env python\nimport functools\nimport logging\nimport os\nimport sys\nfrom contextlib import nullcontext\nfrom dataclasses import dataclass\nfrom typing import (\n    Callable,\n    Dict,\n    List,\n    Optional,\n    Sequence,\n    Set,\n    Tuple,\n    TypeVar,\n)\n\nimport typer\nfrom typing_extensions import Annotated, ParamSpec\n\nfrom seml.cli_utils import AUTOCOMPLETING, cache_to_disk\nfrom seml.cli_utils.cli_states import CliStates as States\nfrom seml.document import SBatchOptions\n\nP = ParamSpec('P')\nR = TypeVar('R')\n\n_DOCS = bool(os.environ.get('_SEML_DOCS', False))\n\n\n# numexpr will log unnecessary info we don't want in our CLI\nlogging.getLogger('numexpr').setLevel(logging.ERROR)\n\n\ndef parse_dict(x: str):\n    import ast\n\n    try:\n        return ast.literal_eval(x)\n    except Exception as e:\n        logging.error(f'Could not parse dictionary: {e}\\n{x}')\n        exit(1)\n\n\nDictOption = functools.partial(\n    typer.Option,\n    metavar='DICT',\n    parser=parse_dict,\n)\n\n\n_EXPERIMENTS = '🚀 Experiments'\n_DATABASE = '📊 Database'\n_INFORMATION = '💭 Information'\n_SLURM = '🏃 Slurm'\n\n\ndef restrict_collection(require: bool = True):\n    \"\"\"Decorator to require a collection name.\"\"\"\n\n    def decorator(fun: Callable[P, R]) -> Callable[P, R]:\n        @functools.wraps(fun)\n        def wrapper(ctx: typer.Context, *args: P.args, **kwargs: P.kwargs):\n            if require and not ctx.obj['collection']:\n                raise typer.BadParameter('Please specify a collection name.', ctx=ctx)\n            elif not require and ctx.obj['collection']:\n                raise typer.BadParameter(\n                    'Please do not specify a collection name.', ctx=ctx\n                )\n            return fun(ctx, *args, **kwargs)  # type: ignore\n\n        wrapper._requires_collection = require  # type: ignore\n        return wrapper  # type: ignore\n\n    return decorator\n\n\ndef collection_free_commands(app: typer.Typer) -> List[str]:\n    \"\"\"Get the commands that do not require a collection.\"\"\"\n    return [\n        cmd.name if cmd.name else cmd.callback.__name__  # type: ignore\n        for cmd in app.registered_commands\n        if not getattr(cmd.callback, '_requires_collection', True)\n    ]\n\n\n@cache_to_disk('db_config')\ndef get_db_collections():\n    \"\"\"CLI completion for db collections.\"\"\"\n    from seml.database import (\n        get_collections_from_mongo_shell_or_pymongo,\n        get_mongodb_config,\n    )\n\n    config = get_mongodb_config()\n    return list(get_collections_from_mongo_shell_or_pymongo(**config))\n\n\ndef first_argument_completer():\n    \"\"\"CLI completition for the first argumentin SEML.\"\"\"\n    # We also add the commands that do not require a collection for autocompletion.\n    return get_db_collections() + collection_free_commands(app)\n\n\napp = typer.Typer(\n    no_args_is_help=True,\n    # Note that this is not 100% the correct chaining autocompletition\n    # but it is significantly better than nothing. Compared to the default\n    # click chaining we greedly split the arguments by any command.\n    chain=_DOCS or AUTOCOMPLETING,\n)\nYesAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-y',\n        '--yes',\n        help='Automatically confirm all dialogues with yes.',\n        is_flag=True,\n    ),\n]\nSacredIdAnnotation = Annotated[\n    Optional[int],\n    typer.Option(\n        '-id',\n        '--sacred-id',\n        help='Sacred ID (_id in the database collection) of the experiment. '\n        'Takes precedence over other filters.',\n    ),\n]\nFilterDictAnnotation = Annotated[\n    Optional[Dict],\n    DictOption(\n        '-fd',\n        '--filter-dict',\n        help='Dictionary (passed as a string, e.g. \\'{\"config.dataset\": \"cora_ml\"}\\') to filter '\n        'the experiments by.',\n    ),\n]\nBatchIdAnnotation = Annotated[\n    Optional[int],\n    typer.Option(\n        '-b',\n        '--batch-id',\n        help='Batch ID (batch_id in the database collection) of the experiments. '\n        'Experiments that were staged together have the same batch_id.',\n    ),\n]\n\n\ndef parse_optional_str_list(values: Optional[Sequence[str]]) -> List[str]:\n    if values is None:\n        return []\n    return [\n        __x.strip()\n        for _x in values\n        for __x in _x.replace(',', ' ').split()\n        if __x.strip()\n    ]\n\n\nProjectionAnnotation = Annotated[\n    List[str],\n    typer.Option(\n        '-p',\n        '--projection',\n        help='List of configuration keys, e.g., `config.model`, to additionally print.',\n        parser=str.strip,\n        callback=parse_optional_str_list,\n        metavar='KEY',\n    ),\n]\n\n_STATE_LIST = [s for states in States.values() for s in states]\nFilterStatesAnnotation = Annotated[\n    List[str],\n    typer.Option(\n        '-s',\n        '--filter-states',\n        help='List of states to filter the experiments by. If empty (\"\"), all states are considered.',\n        metavar=f'[{\"|\".join(_STATE_LIST)}]',\n        parser=lambda s: s.strip().upper(),\n        callback=parse_optional_str_list,\n    ),\n]\nSBatchOptionsAnnotation = Annotated[\n    Optional[SBatchOptions],\n    DictOption(\n        '-sb',\n        '--sbatch-options',\n        help='Dictionary (passed as a string, e.g. \\'{\"gres\": \"gpu:2\"}\\') to request two GPUs.',\n    ),\n]\nNumExperimentsAnnotation = Annotated[\n    int,\n    typer.Option(\n        '-n',\n        '--num-experiments',\n        help='Number of experiments to start. 0: all (staged) experiments ',\n    ),\n]\nNoFileOutputAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-nf',\n        '--no-file-output',\n        help=\"Do not write the experiment's output to a file.\",\n        is_flag=True,\n    ),\n]\nOutputToConsoleAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-o',\n        '--output-to-console',\n        help=\"Write the experiment's output to the console.\",\n        is_flag=True,\n    ),\n]\nStealSlurmAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-ss',\n        '--steal-slurm',\n        help=\"Local jobs 'steal' from the Slurm queue, \"\n        'i.e. also execute experiments waiting for execution via Slurm.',\n        is_flag=True,\n    ),\n]\nPostMortemAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-pm',\n        '--post-mortem',\n        help='Activate post-mortem debugging with pdb.',\n        is_flag=True,\n    ),\n]\nWorkerGPUsAnnotation = Annotated[\n    Optional[str],\n    typer.Option(\n        '-wg',\n        '--worker-gpus',\n        help='The IDs of the GPUs used by the local worker. Will be directly passed to CUDA_VISIBLE_DEVICES.',\n    ),\n]\nWorkerCPUsAnnotation = Annotated[\n    Optional[int],\n    typer.Option(\n        '-wc',\n        '--worker-cpus',\n        help='The number of CPUs used by the local worker. Will be directly passed to OMP_NUM_THREADS.',\n    ),\n]\nWorkerEnvAnnotation = Annotated[\n    Optional[Dict],\n    DictOption(\n        '-we',\n        '--worker-env',\n        help='Further environment variables to be set for the local worker.',\n    ),\n]\nPrintFullDescriptionAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-fd',\n        '--full-descriptions',\n        help='Whether to print full descriptions (possibly with line breaks).',\n        is_flag=True,\n    ),\n]\nUpdateStatusAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-u',\n        '--update-status',\n        help='Whether to update the status of experiments in the database. '\n        'This can take a while for large collections. Use only if necessary.',\n        is_flag=True,\n    ),\n]\nNoResolveDescriptionAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '--no-resolve-descriptions',\n        help='Whether to prevent using omegaconf to resolve experiment descriptions',\n        is_flag=True,\n    ),\n]\nDebugAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-d',\n        '--debug',\n        help='Run a single interactive experiment without Sacred observers and with post-mortem debugging. '\n        'Implies `--verbose --num-exps 1 --post-mortem --output-to-console`.',\n        is_flag=True,\n    ),\n]\n\nDebugServerAnnotation = Annotated[\n    bool,\n    typer.Option(\n        '-ds',\n        '--debug-server',\n        help='Run the experiment with a debug server, to which you can remotely connect with e.g. VS Code. '\n        'Implies `--debug`.',\n        is_flag=True,\n    ),\n]\n\n\ndef version_callback(value: bool):\n    if value:\n        from seml import __version__\n\n        print(__version__)\n        raise typer.Exit(0)\n\n\n@app.callback()\ndef callback(\n    ctx: typer.Context,\n    collection: Annotated[\n        str,\n        typer.Argument(\n            help='The name of the database collection to use.',\n            autocompletion=first_argument_completer,\n        ),\n    ],\n    migration_skip: Annotated[\n        bool,\n        typer.Option(\n            '--migration-skip',\n            help='Skip the migration of the database collection.',\n            is_flag=True,\n        ),\n    ] = False,\n    migration_backup: Annotated[\n        bool,\n        typer.Option(\n            '--migration-backup',\n            help='Backup the database collection before migration.',\n            is_flag=True,\n        ),\n    ] = False,\n    verbose: Annotated[\n        bool,\n        typer.Option(\n            '-v',\n            '--verbose',\n            help='Whether to print debug messages.',\n            is_flag=True,\n        ),\n    ] = False,\n    version: Annotated[\n        bool,\n        typer.Option(\n            '-V',\n            '--version',\n            help='Print the version number.',\n            is_flag=True,\n            callback=version_callback,\n        ),\n    ] = False,\n):\n    \"\"\"SEML - Slurm Experiment Management Library.\"\"\"\n    from rich.logging import RichHandler\n\n    from seml.commands.migration import migrate_collection\n    from seml.console import console\n\n    if len(logging.root.handlers) == 0:\n        logging_level = logging.NOTSET if verbose else logging.INFO\n        handler = RichHandler(\n            logging_level,\n            console=console,\n            show_path=False,\n            show_level=True,\n            show_time=False,\n        )\n        logging.basicConfig(\n            level=logging_level, format='%(message)s', handlers=[handler]\n        )\n\n    if collection:\n        migrate_collection(collection, migration_skip, migration_backup)\n\n    ctx.obj = dict(collection=collection, verbose=verbose)\n\n\n@app.command('list', rich_help_panel=_INFORMATION)\n@restrict_collection(False)\ndef list_command(\n    ctx: typer.Context,\n    pattern: Annotated[\n        str, typer.Argument(help='A regex that must match the collections to print.')\n    ] = r'.*',\n    progress: Annotated[\n        bool,\n        typer.Option(\n            '-p',\n            '--progress',\n            help='Whether to print a progress bar for iterating over collections.',\n            is_flag=True,\n        ),\n    ] = False,\n    update_status: UpdateStatusAnnotation = False,\n    full_description: PrintFullDescriptionAnnotation = False,\n):\n    \"\"\"Lists all collections in the database.\"\"\"\n    from seml.commands.print import print_collections\n\n    print_collections(\n        pattern,\n        progress=progress,\n        update_status=update_status,\n        print_full_description=full_description,\n    )\n\n\n@app.command('clean-db', rich_help_panel=_DATABASE)\ndef clean_db_command(ctx: typer.Context, yes: YesAnnotation = False):\n    \"\"\"Remove orphaned artifacts in the DB from runs which have been deleted..\"\"\"\n    from seml.database import clean_unreferenced_artifacts\n\n    clean_unreferenced_artifacts(ctx.obj['collection'], yes=yes)\n\n\n@app.command('configure')\n@restrict_collection(False)\ndef configure_command(\n    ctx: typer.Context,\n    host: Annotated[\n        Optional[str],\n        typer.Option(\n            '--host',\n            help='The host of the MongoDB server.',\n        ),\n    ] = None,\n    port: Annotated[\n        Optional[int],\n        typer.Option(\n            '--port',\n            help='The port of the MongoDB server.',\n        ),\n    ] = None,\n    database: Annotated[\n        Optional[str],\n        typer.Option(\n            '--database',\n            help='The name of the MongoDB database to use.',\n        ),\n    ] = None,\n    username: Annotated[\n        Optional[str],\n        typer.Option(\n            '--username',\n            help='The username for the MongoDB server.',\n        ),\n    ] = None,\n    password: Annotated[\n        Optional[str],\n        typer.Option(\n            '--password',\n            help='The password for the MongoDB server.',\n        ),\n    ] = None,\n    ssh_forward: Annotated[\n        bool,\n        typer.Option(\n            '-sf',\n            '--ssh-forward',\n            help='Configure SSH forwarding settings for MongoDB.',\n            is_flag=True,\n        ),\n    ] = False,\n):\n    \"\"\"\n    Configure SEML (database, argument completion, ...).\n    \"\"\"\n    from seml.commands.configure import mongodb_configure\n\n    mongodb_configure(\n        host=host,\n        port=port,\n        database=database,\n        username=username,\n        password=password,\n        setup_ssh_forward=ssh_forward,\n    )\n\n\n@app.command('start-jupyter', rich_help_panel=_SLURM)\n@restrict_collection(False)\ndef start_jupyter_command(\n    ctx: typer.Context,\n    lab: Annotated[\n        bool,\n        typer.Option(\n            '-l',\n            '--lab',\n            help='Start a jupyter-lab instance instead of jupyter notebook.',\n        ),\n    ] = False,\n    conda_env: Annotated[\n        Optional[str],\n        typer.Option(\n            '-c',\n            '--conda-env',\n            help='Start the Jupyter instance in a Conda environment.',\n        ),\n    ] = None,\n    sbatch_options: SBatchOptionsAnnotation = None,\n):\n    \"\"\"\n    Start a Jupyter slurm job. Uses SBATCH options defined in settings.py under\n    SBATCH_OPTIONS_TEMPLATES.JUPYTER\n    \"\"\"\n    from seml.commands.start import start_jupyter_job\n\n    start_jupyter_job(lab=lab, conda_env=conda_env, sbatch_options=sbatch_options)\n\n\n@app.command('cancel', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef cancel_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [*States.PENDING, *States.RUNNING],\n    wait: Annotated[\n        bool,\n        typer.Option(\n            '-w',\n            '--wait',\n            help='Wait until all jobs are properly cancelled.',\n            is_flag=True,\n        ),\n    ] = False,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Cancel the Slurm job/job step corresponding to experiments, filtered by ID or state.\n    \"\"\"\n    from seml.commands.manage import cancel_experiments\n\n    wait |= (\n        len(\n            [\n                a\n                for a in sys.argv\n                if a in command_tree(app).commands or a in command_tree(app).groups\n            ]\n        )\n        > 1\n    )\n    cancel_experiments(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_dict=filter_dict,\n        batch_id=batch_id,\n        filter_states=filter_states,\n        wait=wait,\n        yes=yes,\n    )\n\n\n@app.command('add', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef add_command(\n    ctx: typer.Context,\n    config_files: Annotated[\n        List[str],\n        typer.Argument(\n            help='Path to the YAML configuration file for the experiment.',\n            exists=True,\n            file_okay=True,\n            dir_okay=False,\n        ),\n    ],\n    no_hash: Annotated[\n        bool,\n        typer.Option(\n            '-nh',\n            '--no-hash',\n            help='By default, we use the hash of the config dictionary to filter out duplicates (by comparing all '\n            'dictionary values individually). Only disable this if you have a good reason as it is faster.',\n            is_flag=True,\n        ),\n    ] = False,\n    no_sanity_check: Annotated[\n        bool,\n        typer.Option(\n            '-ncs',\n            '--no-sanity-check',\n            help='Disable this if the check fails unexpectedly when using '\n            'advanced Sacred features or to accelerate adding.',\n            is_flag=True,\n        ),\n    ] = False,\n    no_code_checkpoint: Annotated[\n        bool,\n        typer.Option(\n            '-ncc',\n            '--no-code-checkpoint',\n            help='Disable this if you want your experiments to use the current code'\n            'instead of the code at the time of adding.',\n            is_flag=True,\n        ),\n    ] = False,\n    force: Annotated[\n        bool,\n        typer.Option(\n            '-f',\n            '--force',\n            help='Force adding the experiment even if it already exists in the database.',\n            is_flag=True,\n        ),\n    ] = False,\n    overwrite_params: Annotated[\n        Optional[Dict],\n        DictOption(\n            '-o',\n            '--overwrite-params',\n            help='Dictionary (passed as a string, e.g. \\'{\"epochs\": 100}\\') to overwrite parameters in the config.',\n        ),\n    ] = None,\n    description: Annotated[\n        Optional[str],\n        typer.Option(\n            '-d',\n            '--description',\n            help='A description for the experiment.',\n        ),\n    ] = None,\n    no_resolve_descriptions: NoResolveDescriptionAnnotation = False,\n):\n    \"\"\"\n    Add experiments to the database as defined in the configuration.\n    \"\"\"\n    from seml.commands.add import add_config_files\n\n    add_config_files(\n        ctx.obj['collection'],\n        config_files,\n        force_duplicates=force,\n        no_hash=no_hash,\n        no_sanity_check=no_sanity_check,\n        no_code_checkpoint=no_code_checkpoint,\n        overwrite_params=overwrite_params,\n        description=description,\n        resolve_descriptions=not no_resolve_descriptions,\n    )\n    get_db_collections.recompute_cache()\n\n\n@app.command('start', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef start_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    debug: DebugAnnotation = False,\n    debug_server: DebugServerAnnotation = False,\n    local: Annotated[\n        bool,\n        typer.Option(\n            '-l',\n            '--local',\n            help='Run the experiment locally instead of on a Slurm cluster.',\n            is_flag=True,\n        ),\n    ] = False,\n    no_worker: Annotated[\n        bool,\n        typer.Option(\n            '-nw',\n            '--no-worker',\n            help=\"Do not launch a local worker after setting experiments' state to PENDING.\",\n            is_flag=True,\n        ),\n    ] = False,\n    num_exps: NumExperimentsAnnotation = 0,\n    no_file_output: NoFileOutputAnnotation = False,\n    steal_slurm: StealSlurmAnnotation = False,\n    post_mortem: PostMortemAnnotation = False,\n    output_to_console: OutputToConsoleAnnotation = False,\n    worker_gpus: WorkerGPUsAnnotation = None,\n    worker_cpus: WorkerCPUsAnnotation = None,\n    worker_env: WorkerEnvAnnotation = None,\n):\n    \"\"\"\n    Fetch staged experiments from the database and run them (by default via Slurm).\n    \"\"\"\n    from seml.commands.start import start_experiments\n\n    start_experiments(\n        ctx.obj['collection'],\n        local=local,\n        sacred_id=sacred_id,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        num_exps=num_exps,\n        post_mortem=post_mortem,\n        debug=debug,\n        debug_server=debug_server,\n        output_to_console=output_to_console,\n        no_file_output=no_file_output,\n        steal_slurm=steal_slurm,\n        no_worker=no_worker,\n        set_to_pending=True,\n        worker_gpus=worker_gpus,\n        worker_cpus=worker_cpus,\n        worker_environment_vars=worker_env,\n    )\n\n\n@app.command('clean-jobs', rich_help_panel=_EXPERIMENTS, hidden=True)\n@restrict_collection()\ndef clean_jobs_command(\n    ctx: typer.Context,\n    sacred_ids: Annotated[\n        List[int],\n        typer.Argument(\n            help='Sacred IDs (_id in the database collection) of the experiments to claim.',\n        ),\n    ],\n):\n    \"\"\"\n    Cancel empty pending jobs.\n    \"\"\"\n    from seml.commands.manage import cancel_empty_pending_jobs\n\n    cancel_empty_pending_jobs(ctx.obj['collection'], *sacred_ids)\n\n\n@app.command('prepare-experiment', rich_help_panel=_EXPERIMENTS, hidden=True)\n@restrict_collection()\ndef prepare_experiment_command(\n    ctx: typer.Context,\n    sacred_id: Annotated[\n        int,\n        typer.Option(\n            '-id',\n            '--sacred-id',\n            help='Sacred ID (_id in the database collection) of the experiment. '\n            'Takes precedence over other filters.',\n        ),\n    ],\n    verbose: Annotated[\n        bool,\n        typer.Option(\n            '-v',\n            '--verbose',\n            help='Whether to print debug messages.',\n            is_flag=True,\n        ),\n    ] = False,\n    unobserved: Annotated[\n        bool,\n        typer.Option(\n            '-u',\n            '--unobserved',\n            help='Run the experiments without Sacred observers.',\n            is_flag=True,\n        ),\n    ] = False,\n    post_mortem: PostMortemAnnotation = False,\n    stored_sources_dir: Annotated[\n        Optional[str],\n        typer.Option(\n            '-ssd',\n            '--stored-sources-dir',\n            help='Load source files into this directory before starting.',\n        ),\n    ] = None,\n    debug_server: DebugServerAnnotation = False,\n):\n    \"\"\"\n    Fetch experiment from database, prepare it and print the command to execute it.\n    \"\"\"\n    from seml.commands.start import prepare_experiment\n\n    prepare_experiment(\n        ctx.obj['collection'],\n        sacred_id,\n        verbose,\n        unobserved,\n        post_mortem,\n        stored_sources_dir,\n        debug_server,\n    )\n\n\n@app.command('claim-experiment', rich_help_panel=_EXPERIMENTS, hidden=True)\n@restrict_collection()\ndef claim_experiment_command(\n    ctx: typer.Context,\n    sacred_ids: Annotated[\n        List[int],\n        typer.Argument(\n            help='Sacred IDs (_id in the database collection) of the experiments to claim.',\n        ),\n    ],\n):\n    \"\"\"\n    Claim an experiment from the database.\n    \"\"\"\n    from seml.commands.start import claim_experiment\n\n    claim_experiment(ctx.obj['collection'], sacred_ids)\n\n\n@app.command('launch-worker', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef launch_worker_command(\n    ctx: typer.Context,\n    num_exps: NumExperimentsAnnotation = 0,\n    no_file_output: NoFileOutputAnnotation = False,\n    steal_slurm: StealSlurmAnnotation = False,\n    post_mortem: PostMortemAnnotation = False,\n    debug: DebugAnnotation = False,\n    debug_server: DebugServerAnnotation = False,\n    output_to_console: OutputToConsoleAnnotation = False,\n    worker_gpus: WorkerGPUsAnnotation = None,\n    worker_cpus: WorkerCPUsAnnotation = None,\n    worker_env: WorkerEnvAnnotation = None,\n    sacred_id: SacredIdAnnotation = None,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n):\n    \"\"\"\n    Launch a local worker that runs PENDING jobs.\n    \"\"\"\n    from seml.commands.start import start_experiments\n\n    start_experiments(\n        ctx.obj['collection'],\n        local=True,\n        sacred_id=sacred_id,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        num_exps=num_exps,\n        post_mortem=post_mortem,\n        debug=debug,\n        debug_server=debug_server,\n        output_to_console=output_to_console,\n        no_file_output=no_file_output,\n        steal_slurm=steal_slurm,\n        no_worker=False,\n        set_to_pending=False,\n        worker_gpus=worker_gpus,\n        worker_cpus=worker_cpus,\n        worker_environment_vars=worker_env,\n    )\n\n\n@app.command('print-fail-trace', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef print_fail_trace_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [\n        *States.FAILED,\n        *States.KILLED,\n        *States.INTERRUPTED,\n    ],\n    projection: ProjectionAnnotation = [],\n):\n    \"\"\"\n    Prints fail traces of all failed experiments.\n    \"\"\"\n    from seml.commands.print import print_fail_trace\n\n    print_fail_trace(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        projection=projection,\n    )\n\n\n@app.command('reload-sources', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef reload_sources_command(\n    ctx: typer.Context,\n    keep_old: Annotated[\n        bool,\n        typer.Option(\n            '-k',\n            '-keep-old',\n            help='Keep the old source files in the database.',\n            is_flag=True,\n        ),\n    ] = False,\n    batch_ids: Annotated[\n        Optional[List[int]],\n        typer.Option(\n            '-b',\n            '--batch-ids',\n            help='Batch IDs (batch_id in the database collection) of the experiments. '\n            'Experiments that were staged together have the same batch_id.',\n        ),\n    ] = None,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Reload stashed source files.\n    \"\"\"\n    from seml.commands.manage import reload_sources\n\n    reload_sources(\n        ctx.obj['collection'],\n        batch_ids=batch_ids,\n        keep_old=keep_old,\n        yes=yes,\n    )\n\n\n@app.command('update-working-dir', rich_help_panel=_DATABASE)\n@restrict_collection()\ndef update_working_dir_command(\n    ctx: typer.Context,\n    working_dir: Annotated[\n        str,\n        typer.Argument(\n            help='The new working directory for the experiments.',\n            exists=True,\n            file_okay=False,\n            dir_okay=True,\n        ),\n    ],\n    batch_ids: Annotated[\n        Optional[List[int]],\n        typer.Option(\n            '-b',\n            '--batch-ids',\n            help='Batch IDs (batch_id in the database collection) of the experiments. '\n            'Experiments that were staged together have the same batch_id.',\n        ),\n    ] = None,\n):\n    \"\"\"\n    Change the working directory of experiments in case you moved the source code to a different location.\n    \"\"\"\n    from seml.database import update_working_dir\n\n    update_working_dir(\n        ctx.obj['collection'],\n        working_directory=working_dir,\n        batch_ids=batch_ids,\n    )\n\n\n@app.command('print-command', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef print_command_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = States.STAGED,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    num_exps: NumExperimentsAnnotation = 0,\n    worker_gpus: WorkerGPUsAnnotation = None,\n    worker_cpus: WorkerCPUsAnnotation = None,\n    worker_env: WorkerEnvAnnotation = None,\n    unresolved: Annotated[\n        bool,\n        typer.Option(\n            '--unresolved',\n            help='Whether to print the unresolved command.',\n            is_flag=True,\n        ),\n    ] = False,\n    no_interpolation: Annotated[\n        bool,\n        typer.Option(\n            '--no-interpolation',\n            help='Whether disable variable interpolation. Only compatible with --unresolved.',\n            is_flag=True,\n        ),\n    ] = False,\n):\n    \"\"\"\n    Print the commands that would be executed by `start`.\n    \"\"\"\n    from seml.commands.print import print_command\n\n    print_command(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        batch_id=batch_id,\n        filter_states=filter_states,\n        filter_dict=filter_dict,\n        num_exps=num_exps,\n        worker_gpus=worker_gpus,\n        worker_cpus=worker_cpus,\n        worker_environment_vars=worker_env,\n        unresolved=unresolved,\n        resolve_interpolations=not no_interpolation,\n    )\n\n\n@app.command('print-experiment', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef print_experiment_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = States.PENDING\n    + States.STAGED\n    + States.RUNNING\n    + States.FAILED\n    + States.KILLED\n    + States.INTERRUPTED\n    + States.COMPLETED,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    projection: ProjectionAnnotation = [],\n    format: Annotated[\n        str,\n        typer.Option(\n            '-F',\n            '--format',\n            help='The format in which to print the experiment document.',\n            case_sensitive=False,\n        ),\n    ] = 'yaml',\n):\n    \"\"\"\n    Print the experiment document.\n    \"\"\"\n    from seml.commands.print import print_experiment\n\n    print_experiment(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        projection=projection,\n        format=format,\n    )\n\n\n@app.command('print-output', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef print_output_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = States.RUNNING\n    + States.FAILED\n    + States.KILLED\n    + States.INTERRUPTED\n    + States.COMPLETED,\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    slurm: Annotated[\n        bool,\n        typer.Option(\n            '-sl',\n            '--slurm',\n            help='Whether to print the Slurm output instead of the experiment output.',\n            is_flag=True,\n        ),\n    ] = False,\n    head: Annotated[\n        Optional[int],\n        typer.Option(\n            '-h',\n            '--head',\n            help='Print the first n lines of the output.',\n        ),\n    ] = None,\n    tail: Annotated[\n        Optional[int],\n        typer.Option(\n            '-t',\n            '--tail',\n            help='Print the last n lines of the output.',\n        ),\n    ] = None,\n):\n    \"\"\"\n    Print the output of experiments.\n    \"\"\"\n    from seml.commands.print import print_output\n\n    print_output(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        slurm=slurm,\n        head=head,\n        tail=tail,\n    )\n\n\n@app.command('reset', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef reset_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [\n        *States.FAILED,\n        *States.KILLED,\n        *States.INTERRUPTED,\n    ],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Reset the state of experiments by setting their state to STAGED and cleaning their database entry.\n    Does not cancel Slurm jobs.\n    \"\"\"\n    from seml.commands.manage import reset_experiments\n\n    reset_experiments(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        yes=yes,\n    )\n\n\n@app.command('delete', rich_help_panel=_EXPERIMENTS)\n@restrict_collection()\ndef delete_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [\n        *States.STAGED,\n        *States.FAILED,\n        *States.KILLED,\n        *States.INTERRUPTED,\n    ],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    no_cancel: Annotated[\n        bool,\n        typer.Option(\n            '-nc',\n            '--no-cancel',\n            help='Do not cancel the experiments before deleting them.',\n            is_flag=True,\n        ),\n    ] = False,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Delete experiments by ID or state (cancels Slurm jobs first if not --no-cancel).\n    \"\"\"\n    from seml.commands.manage import delete_experiments\n\n    delete_experiments(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        batch_id=batch_id,\n        filter_dict=filter_dict,\n        yes=yes,\n        cancel=not no_cancel,\n    )\n    get_db_collections.recompute_cache()\n\n\n@app.command('drop', rich_help_panel=_DATABASE)\n@restrict_collection(False)\ndef drop_command(\n    ctx: typer.Context,\n    pattern: Annotated[\n        str, typer.Argument(help='A regex that must match the collections to print.')\n    ] = r'.*',\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Drop collections from the database.\n\n    Note: This is a dangerous operation and should only be used if you know what you are doing.\n    \"\"\"\n    from seml.commands.manage import drop_collections\n\n    drop_collections(pattern=pattern, yes=yes)\n    get_db_collections.recompute_cache()\n\n\n@app.command('detect-killed', rich_help_panel=_DATABASE)\n@restrict_collection()\ndef detect_killed_command(\n    ctx: typer.Context,\n):\n    \"\"\"\n    Detect experiments where the corresponding Slurm jobs were killed externally.\n    \"\"\"\n    from seml.commands.manage import detect_killed\n\n    detect_killed(ctx.obj['collection'])\n\n\n@app.command('status', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef status_command(\n    ctx: typer.Context,\n    update_status: UpdateStatusAnnotation = True,\n    projection: ProjectionAnnotation = [],\n):\n    \"\"\"\n    Report status of experiments in the database collection.\n    \"\"\"\n    from seml.commands.print import print_status\n\n    print_status(\n        ctx.obj['collection'], update_status=update_status, projection=projection\n    )\n\n\n@app.command('download-sources', rich_help_panel=_INFORMATION)\n@restrict_collection()\ndef download_sources_command(\n    ctx: typer.Context,\n    target_directory: Annotated[\n        str,\n        typer.Argument(\n            help='The directory where the source files should be restored.',\n            exists=False,\n            file_okay=False,\n            dir_okay=True,\n        ),\n    ],\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n):\n    \"\"\"\n    Download source files from the database to the provided path.\n    \"\"\"\n    from seml.commands.sources import download_sources\n\n    download_sources(\n        target_directory,\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        filter_dict=filter_dict,\n        batch_id=batch_id,\n    )\n\n\n@app.command('hold', rich_help_panel=_SLURM)\n@restrict_collection()\ndef hold_command(\n    ctx: typer.Context,\n    batch_id: BatchIdAnnotation = None,\n):\n    \"\"\"\n    Hold queued experiments via SLURM.\n    \"\"\"\n    from seml.commands.slurm import hold_or_release_experiments\n\n    hold_or_release_experiments(\n        True,\n        ctx.obj['collection'],\n        batch_id=batch_id,\n    )\n\n\n@app.command('release', rich_help_panel=_SLURM)\n@restrict_collection()\ndef release_command(\n    ctx: typer.Context,\n    batch_id: BatchIdAnnotation = None,\n):\n    \"\"\"\n    Release held experiments via SLURM.\n    \"\"\"\n    from seml.commands.slurm import hold_or_release_experiments\n\n    hold_or_release_experiments(\n        False,\n        ctx.obj['collection'],\n        batch_id=batch_id,\n    )\n\n\n@app.command('queue', rich_help_panel=_INFORMATION)\n@restrict_collection(False)\ndef queue_command(\n    ctx: typer.Context,\n    job_ids: List[str] = typer.Argument(\n        help='The job IDs of the experiments to get the collection for.',\n        default=None,\n    ),\n    filter_states: FilterStatesAnnotation = [*States.PENDING, *States.RUNNING],\n    check_all: Annotated[\n        bool,\n        typer.Option(\n            '-a',\n            '--all',\n            help='Whether to attempt finding the collection of the jobs of all users.',\n            is_flag=True,\n        ),\n    ] = False,\n    watch: Annotated[\n        bool,\n        typer.Option(\n            '-w',\n            '--watch',\n            help='Whether to watch the queue.',\n            is_flag=True,\n        ),\n    ] = False,\n):\n    \"\"\"\n    Prints the collections of the given job IDs. If none is specified, all jobs are considered.\n    \"\"\"\n    from seml.commands.print import print_queue\n\n    print_queue(\n        job_ids,\n        filter_by_user=not check_all,\n        filter_states=filter_states,\n        watch=watch,\n    )\n\n\napp_description = typer.Typer(\n    no_args_is_help=True,\n    help='Manage descriptions of the experiments in a collection.',\n    # chain=_AUTOCOMPLETE\n)\napp.add_typer(app_description, name='description', rich_help_panel=_EXPERIMENTS)\n\n\n@app.command('detect-duplicates', rich_help_panel=_DATABASE)\n@restrict_collection()\ndef detect_duplicates_command(\n    ctx: typer.Context,\n    filter_states: FilterStatesAnnotation = [\n        *States.STAGED,\n        *States.FAILED,\n        *States.KILLED,\n        *States.INTERRUPTED,\n    ],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n):\n    \"\"\"\n    Prints duplicate experiment configurations.\n    \"\"\"\n    from seml.commands.print import print_duplicates\n\n    print_duplicates(\n        ctx.obj['collection'],\n        filter_states=filter_states,\n        filter_dict=filter_dict,\n        batch_id=batch_id,\n    )\n\n\n@app_description.command('set')\n@restrict_collection()\ndef description_set_command(\n    ctx: typer.Context,\n    description: Annotated[\n        str,\n        typer.Argument(\n            help='The description to set.',\n        ),\n    ],\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    yes: YesAnnotation = False,\n    no_resolve_description: NoResolveDescriptionAnnotation = False,\n):\n    \"\"\"\n    Sets the description of experiment(s).\n    \"\"\"\n    from seml.commands.description import collection_set_description\n\n    collection_set_description(\n        ctx.obj['collection'],\n        description,\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        filter_dict=filter_dict,\n        batch_id=batch_id,\n        yes=yes,\n        resolve=not no_resolve_description,\n    )\n\n\n@app_description.command('delete')\n@restrict_collection()\ndef description_delete_command(\n    ctx: typer.Context,\n    sacred_id: SacredIdAnnotation = None,\n    filter_states: FilterStatesAnnotation = [],\n    filter_dict: FilterDictAnnotation = None,\n    batch_id: BatchIdAnnotation = None,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Deletes the description of experiment(s).\n    \"\"\"\n    from seml.commands.description import collection_delete_description\n\n    collection_delete_description(\n        ctx.obj['collection'],\n        sacred_id=sacred_id,\n        filter_states=filter_states,\n        filter_dict=filter_dict,\n        batch_id=batch_id,\n        yes=yes,\n    )\n\n\n@app_description.command('list')\n@restrict_collection()\ndef description_list_command(\n    ctx: typer.Context, update_status: UpdateStatusAnnotation = False\n):\n    \"\"\"\n    Lists the descriptions of all experiments.\n    \"\"\"\n    from seml.commands.description import collection_list_descriptions\n\n    collection_list_descriptions(ctx.obj['collection'], update_status=update_status)\n\n\napp_project = typer.Typer(\n    no_args_is_help=True,\n    help='Setting up new projects.',\n)\napp.add_typer(app_project, name='project')\n\n\n@app_project.command('init')\n@restrict_collection(False)\ndef init_project_command(\n    ctx: typer.Context,\n    directory: Annotated[\n        str,\n        typer.Argument(\n            help='The directory in which to initialize the project.',\n            exists=False,\n            file_okay=False,\n            dir_okay=True,\n        ),\n    ] = '.',\n    template: Annotated[\n        str,\n        typer.Option(\n            '-t',\n            '--template',\n            help='The template to use for the project. To view available templates use `seml project list-templates`.',\n        ),\n    ] = 'default',\n    project_name: Annotated[\n        Optional[str],\n        typer.Option(\n            '-n',\n            '--name',\n            help='The name of the project. (By default inferred from the directory name.)',\n        ),\n    ] = None,\n    user_name: Annotated[\n        Optional[str],\n        typer.Option(\n            '-u',\n            '--username',\n            help='The author name to use for the project. (By default inferred from $USER)',\n        ),\n    ] = None,\n    user_mail: Annotated[\n        Optional[str],\n        typer.Option(\n            '-m',\n            '--usermail',\n            help='The author email to use for the project. (By default empty.)',\n        ),\n    ] = None,\n    git_remote: Annotated[\n        Optional[str],\n        typer.Option(\n            '-r',\n            '--git-remote',\n            help='The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)',\n        ),\n    ] = None,\n    git_commit: Annotated[\n        Optional[str],\n        typer.Option(\n            '-c',\n            '--git-commit',\n            help='The exact git commit to use. May also be a tag or branch (By default latest)',\n        ),\n    ] = None,\n    yes: YesAnnotation = False,\n):\n    \"\"\"\n    Initialize a new project in the given directory.\n    \"\"\"\n    from seml.commands.project import init_project\n\n    init_project(\n        directory,\n        project_name,\n        user_name,\n        user_mail,\n        template,\n        git_remote,\n        git_commit,\n        yes,\n    )\n\n\n@app_project.command('list-templates')\n@restrict_collection(False)\ndef list_templates_command(\n    ctx: typer.Context,\n    git_remote: Annotated[\n        Optional[str],\n        typer.Option(\n            '-r',\n            '--git-remote',\n            help='The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)',\n        ),\n    ] = None,\n    git_commit: Annotated[\n        Optional[str],\n        typer.Option(\n            '-c',\n            '--git-commit',\n            help='The exact git commit to use. May also be a tag or branch (By default latest)',\n        ),\n    ] = None,\n):\n    \"\"\"\n    List available project templates.\n    \"\"\"\n    from seml.commands.project import print_available_templates\n\n    print_available_templates(git_remote, git_commit)\n\n\n@dataclass\nclass CommandTreeNode:\n    \"\"\"Compact representation of the commands (and subtyper commands) of the app\"\"\"\n\n    commands: Set[str]\n    groups: Dict[str, 'CommandTreeNode']\n\n\n@functools.lru_cache\ndef command_tree(app: typer.Typer) -> CommandTreeNode:\n    return CommandTreeNode(\n        commands={\n            cmd.name if cmd.name else cmd.callback.__name__  # type: ignore\n            for cmd in app.registered_commands\n        },\n        groups={\n            (group.name if group.name else group.callback.__name__): command_tree(  # type: ignore\n                group.typer_instance\n            )\n            for group in app.registered_groups\n        },\n    )\n\n\ndef split_args(\n    args: List[str], command_tree: CommandTreeNode, combine: bool = True\n) -> Tuple[List[List[str]], List[CommandTreeNode]]:\n    split_cmd_args: List[List[str]] = [[]]\n    cmd_stack = [command_tree]\n\n    # Chaining is only allowed in the first level of the group hierarchy, so we only\n    # split into a two level list\n    for arg in args:\n        if arg in cmd_stack[-1].groups:\n            if len(cmd_stack) == 1:  # new subtyper at the top level\n                split_cmd_args.append([arg])\n                # chaining is allowed: stack[-1] may consume further commands after its child is done consuming\n                cmd_stack.append(cmd_stack[-1].groups[arg])\n            else:\n                split_cmd_args[-1].append(arg)\n                # no chaining below the first level: stack[-1] will not consume any more commands\n                cmd_stack = cmd_stack[:-1] + [cmd_stack[-1].groups[arg]]\n        elif arg in cmd_stack[-1].commands:\n            if len(cmd_stack) == 1:  # new command at the top level\n                split_cmd_args.append([arg])\n            else:\n                split_cmd_args[-1].append(arg)\n                # no chaining below the first level: stack[-1] will not consume any more commands\n                cmd_stack.pop()\n        else:\n            split_cmd_args[-1].append(arg)\n\n    # Re-distribute shared args to each command in the first level of the hierarchy\n    if len(split_cmd_args) == 1:\n        return split_cmd_args, cmd_stack\n    shared = split_cmd_args[0]\n    chained_commands = split_cmd_args[1:]\n    # If none of the shared args contains a collection\n    # name, we add the default collection name.\n    if all(arg.startswith('-') for arg in shared):\n        shared.append('')\n    # Combine commands with the shared arguments\n    result = []\n    for split in chained_commands:\n        result.append(shared + split)\n\n    return result, cmd_stack\n\n\ndef main():\n    # We have to split the arguments manually to get proper chaining.\n    # If we were to use typer built-in chaining, lists would end the chain.\n    for args in split_args(sys.argv[1:], command_tree(app))[0]:\n        # The app will typically exit after running once.\n        # We want to run it multiple times, so we catch the SystemExit exception.\n        from seml.console import console\n\n        try:\n            cmd = args[1] if len(args) >= 2 else None\n            with (\n                console.status(f'Running command: [italic]{cmd}[/italic]')\n                if cmd\n                else nullcontext()\n            ):\n                app(args)\n        except SystemExit as e:\n            if e.code == 0:\n                continue\n            else:\n                raise e\n\n\nif __name__ == '__main__':\n    main()\n\n\n# If we are in autcompletion we must apply our parameter splitting\n# to get correct autocompletion suggestions.\nif AUTOCOMPLETING and os.environ.get('COMP_WORDS'):\n    commands, stack = split_args(\n        os.environ['COMP_WORDS'].split('\\n'), command_tree(app)\n    )\n    cword = int(os.environ['COMP_CWORD'])\n    cmd = commands[0]\n    if cword > 1:\n        # Case where we complete a command\n        # To find the right command, we must subtract the length of all previous commands\n        # let's subtract -2 everywhere for seml <collection>\n        cword -= 2\n        for cmd in commands:\n            cmd_length = len(cmd) - 2\n            # We found our current command\n            if cmd_length >= cword:\n                break\n            cword -= cmd_length\n        cword += 2  # add back the -2 we subtracted above\n    os.environ['COMP_WORDS'] = '\\n'.join(cmd)\n    os.environ['COMP_CWORD'] = str(cword)\n    # If we are not at the top level typer, we must not suggest top level commands\n    # Note: `seml collection description list <tab><tab>` does not correctly autocomplete\n    # as chaining is disabled on the app_description typer. However, if one were to enable\n    # that its assumptions about chaining differs from our assumptions about chaining.\n    app.info.chain = len(stack) == 1\n"
  },
  {
    "path": "src/seml/cli_utils/__init__.py",
    "content": "from .cache import cache_to_disk\nfrom .module_hider import AUTOCOMPLETING, ModuleHider\n\n__all__ = ['cache_to_disk', 'AUTOCOMPLETING', 'ModuleHider']\n"
  },
  {
    "path": "src/seml/cli_utils/cache.py",
    "content": "from typing import Callable, Generic, TypeVar\n\nR = TypeVar('R')\n\n\nclass DiskCachedFunction(Generic[R]):\n    def __init__(\n        self,\n        fun: Callable[[], R],\n        name: str,\n        time_to_live: float | None,\n    ):\n        self.time_to_live = time_to_live\n        self.fun = fun\n        self.name = name\n\n    @property\n    def cache_path(self):\n        import hashlib\n        import os\n        from pathlib import Path\n\n        from seml.settings import SETTINGS\n\n        user = os.environ.get('USER', 'unknown')\n        install_hash = hashlib.md5(self.name.encode('utf-8')).hexdigest()\n        file_name = f'seml_{user}_{self.name}_{install_hash}.json'\n        return Path(SETTINGS.TMP_DIRECTORY) / file_name\n\n    def __call__(self) -> R:\n        import json\n        import time\n\n        from seml.settings import SETTINGS\n\n        # Load from cache\n        # if it fails or is expired we will compute it again\n        if self.cache_path.exists():\n            try:\n                with open(self.cache_path) as f:\n                    cache = json.load(f)\n                if cache['expire'] > time.time():\n                    return cache['result']\n            except OSError:\n                pass\n            except json.JSONDecodeError:\n                pass\n        # Compute and save to cache\n        result = self.fun()\n        time_to_live = self.time_to_live or SETTINGS.AUTOCOMPLETE_CACHE_ALIVE_TIME\n        cache = {'result': result, 'expire': time.time() + time_to_live}\n        try:\n            with open(self.cache_path, 'w') as f:\n                json.dump(cache, f)\n        except OSError:\n            # If the writing fails for any reason we can just continue.\n            pass\n        return result\n\n    def clear_cache(self):\n        import os\n\n        if self.cache_path.exists():\n            try:\n                os.remove(self.cache_path)\n                return True\n            except OSError:\n                return False\n        return False\n\n    def recompute_cache(self):\n        if self.clear_cache():\n            self()\n            return True\n        return False\n\n\ndef cache_to_disk(name: str, time_to_live: float | None = None):\n    \"\"\"\n    Cache the result of a function to disk.\n\n    Parameters\n    ----------\n    name: str\n        Name of the cache file.\n    time_to_live: float\n        Time to live of the cache in seconds.\n\n    Returns\n    -------\n    A function decorator.\n    \"\"\"\n\n    def wrapper(fun: Callable[[], R]):\n        return DiskCachedFunction(fun, name, time_to_live)\n\n    return wrapper\n"
  },
  {
    "path": "src/seml/cli_utils/cli_states.py",
    "content": "# When autocompleting, we don't want to read the settings.py to retrieve the actual states.\n# Instead, we use a dummy class that behaves like a the actual states class but always returns\n# empty lists. This way we can still use the autocompletion without having to load the settings.py.\n# When seml returns type hints, we load the actual states from the settings.py.\nfrom seml.cli_utils import AUTOCOMPLETING\n\nif AUTOCOMPLETING:\n\n    class DummyStates:\n        def __getitem__(self, item):\n            return []\n\n        def __getattr__(self, item):\n            return []\n\n        def values(self):\n            return []\n\n    CliStates = DummyStates()\nelse:\n    from seml.settings import SETTINGS\n\n    CliStates = SETTINGS.STATES\n"
  },
  {
    "path": "src/seml/cli_utils/module_hider.py",
    "content": "from __future__ import annotations\n\nimport os\nimport sys\nfrom importlib.abc import Loader, MetaPathFinder\nfrom importlib.util import spec_from_loader\n\nAUTOCOMPLETING = bool(os.environ.get('_SEML_COMPLETE', False))\n\n\nclass PackageNotFoundError(Exception): ...\n\n\nclass FakeImportlibMetadata(Loader):\n    def create_module(self, spec):\n        return None\n\n    def exec_module(self, module):\n        module.PackageNotFoundError = PackageNotFoundError  # type: ignore\n\n        def version(package):\n            return '0.0.0'\n\n        module.version = version  # type: ignore\n        module.metadata = lambda name: {'version': '0.0.0'}  # type: ignore\n\n\nclass ModuleHider(MetaPathFinder):\n    def __init__(self, *hidden_modules: str, hide: bool = AUTOCOMPLETING) -> None:\n        super().__init__()\n        self.hidden_modules = set(hidden_modules)\n        self.hide = hide\n\n    def find_spec(self, fullname, path, target=None):\n        if fullname in self.hidden_modules:\n            # a special case for munch <3\n            if fullname == 'importlib_metadata':\n                return spec_from_loader(fullname, FakeImportlibMetadata())\n            raise ImportError(f'No module named {fullname}')\n\n    def __enter__(self):\n        if self.hide:\n            sys.meta_path.insert(0, self)\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if self.hide:\n            sys.meta_path.remove(self)\n"
  },
  {
    "path": "src/seml/commands/__init__.py",
    "content": ""
  },
  {
    "path": "src/seml/commands/add.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom seml.database import get_collection, get_max_in_collection\nfrom seml.document import ExperimentDoc, SemlDoc\nfrom seml.experiment.config import (\n    assemble_slurm_config_dict,\n    check_config,\n    config_get_exclude_keys,\n    generate_configs,\n    generate_named_configs,\n    read_config,\n    remove_duplicates,\n    requires_interpolation,\n    resolve_configs,\n    resolve_interpolations,\n)\nfrom seml.experiment.description import resolve_description\nfrom seml.experiment.sources import get_git_info, upload_sources\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    flatten,\n    make_hash,\n    remove_keys_from_nested,\n    to_typeddict,\n    unflatten,\n    utcnow,\n)\nfrom seml.utils.errors import ConfigError\n\nif TYPE_CHECKING:\n    from pymongo.collection import Collection\n\nStates = SETTINGS.STATES\n\n\ndef remove_existing_experiments(\n    collection: Collection[ExperimentDoc],\n    documents: list[ExperimentDoc],\n    use_hash: bool = True,\n):\n    \"\"\"Check database collection for already present entries.\n\n    Check the database collection for experiments that have the same configuration.\n    Remove the corresponding entries from the input list of configurations to prevent\n    re-running the experiments.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    documents: List[Dict]\n        The documents to filter.\n    use_hash : bool\n        Whether to use hashes (faster)\n\n    Returns\n    -------\n    filtered_configs: list of dicts\n        No longer contains configurations that are already in the database collection.\n\n    \"\"\"\n    filtered_documents: list[ExperimentDoc] = []\n    for document in documents:\n        if use_hash:\n            lookup_result = collection.find_one(\n                {'config_hash': document['config_hash']}\n            )\n        else:\n            lookup_dict = flatten(\n                {\n                    'config': remove_keys_from_nested(\n                        document['config'], document['config_unresolved'].keys()\n                    )\n                }\n            )\n            lookup_result = collection.find_one(unflatten(lookup_dict))\n        if lookup_result is None:\n            filtered_documents.append(document)\n    return filtered_documents\n\n\ndef add_configs(\n    collection: Collection[ExperimentDoc],\n    documents: list[ExperimentDoc],\n    description: str | None = None,\n    resolve_descriptions: bool = True,\n):\n    \"\"\"Put the input configurations into the database.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    documents : List[Dict]\n        The documents to add.\n    description : Optional[str], optional\n        Optional description for the experiments, by default None\n    resolve_descriptions : bool, optional\n        Whether to use omegaconf to resolve descriptions.\n    \"\"\"\n\n    if len(documents) == 0:\n        return\n\n    start_id = get_max_in_collection(collection, '_id')\n    if start_id is None:\n        start_id = 1\n    else:\n        start_id = start_id + 1\n\n    logging.info(\n        f'Adding {len(documents)} configs to the database (batch-ID {documents[0][\"batch_id\"]}).'\n    )\n\n    documents = [\n        cast(\n            ExperimentDoc,\n            {\n                **document,\n                **{\n                    '_id': start_id + idx,\n                    'status': States.STAGED[0],\n                    'add_time': utcnow(),\n                },\n            },\n        )\n        for idx, document in enumerate(documents)\n    ]\n    if description is not None:\n        for db_dict in documents:\n            db_dict['seml']['description'] = description\n        # If description is not supplied via CLI, it will already be resolved.\n        if resolve_descriptions and requires_interpolation(\n            {'description': description}, ['description']\n        ):\n            for db_dict in documents:\n                db_dict['seml']['description'] = resolve_description(\n                    db_dict['seml'].get('description', ''), db_dict\n                )\n\n    collection.insert_many(documents)\n\n\ndef add_config_files(\n    db_collection_name: str,\n    config_files: list[str],\n    force_duplicates: bool = False,\n    overwrite_params: dict | None = None,\n    no_hash: bool = False,\n    no_sanity_check: bool = False,\n    no_code_checkpoint: bool = False,\n    description: str | None = None,\n    resolve_descriptions: bool = True,\n):\n    \"\"\"Adds configuration files to the MongoDB\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to which to add\n    config_files : List[str]\n        A list of paths to configuration files (YAML) to add\n    force_duplicates : bool, optional\n        Whether to force adding configurations even if they are already in the MongoDB, by default False\n    overwrite_params : Optional[Dict], optional\n        Optional flat dict to override configuration values, by default None\n    no_hash : bool, optional\n        Whether to skip duplicate detection by computing hashes, which may result in slowdowns, by default False\n    no_sanity_check : bool, optional\n        Whether to skip feeding configuration values into the sacred experiment to detect unsupported or missing keys, by default False\n    no_code_checkpoint : bool, optional\n        Whether to not base the experiments on a copy of the current codebase, by default False\n    description : Optional[str], optional\n        Optional description for the experiments, by default None\n    resolve_descriptions : bool, optional\n        Whether to use omegaconf to resolve experiment descriptions.\n    \"\"\"\n    config_files = [os.path.abspath(file) for file in config_files]\n    for config_file in config_files:\n        add_config_file(\n            db_collection_name,\n            config_file,\n            force_duplicates,\n            overwrite_params,\n            no_hash,\n            no_sanity_check,\n            no_code_checkpoint,\n            description=description,\n            resolve_descriptions=resolve_descriptions,\n        )\n\n\ndef add_config_file(\n    db_collection_name: str,\n    config_file: str,\n    force_duplicates: bool = False,\n    overwrite_params: dict[str, Any] | None = None,\n    no_hash: bool = False,\n    no_sanity_check: bool = False,\n    no_code_checkpoint: bool = False,\n    description: str | None = None,\n    resolve_descriptions: bool = True,\n):\n    \"\"\"Adds configuration files to the MongoDB\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to which to add\n    config_files : str\n        A path to configuration file (YAML) to add\n    force_duplicates : bool, optional\n        Whether to force adding configurations even if they are already in the MongoDB, by default False\n    overwrite_params : Optional[Dict], optional\n        Optional flat dict to override configuration values, by default None\n    no_hash : bool, optional\n        Whether to skip duplicate detection by computing hashes, which may result in slowdowns, by default False\n    no_sanity_check : bool, optional\n        Whether to skip feeding configuration values into the sacred experiment to detect unsupported or missing keys, by default False\n    no_code_checkpoint : bool, optional\n        Whether to not base the experiments on a copy of the current codebase, by default False\n    description : Optional[str], optional\n        Optional description for the experiments, by default None\n    resolve_descriptions : bool, optional\n        Whether to use omegaconf to resolve descriptions\n    \"\"\"\n\n    collection = get_collection(db_collection_name)\n    seml_config, slurm_configs, experiment_config = read_config(config_file)\n\n    # Use current Anaconda environment if not specified\n    if 'conda_environment' not in seml_config:\n        seml_config['conda_environment'] = os.environ.get('CONDA_DEFAULT_ENV')\n\n    # Get git info\n    git_info = get_git_info(seml_config['executable'], seml_config['working_dir'])\n\n    # Compute batch id\n    batch_id = get_max_in_collection(collection, 'batch_id', int)\n    batch_id = 1 if batch_id is None else batch_id + 1\n\n    # Assemble the Slurm config:\n    slurm_configs = list(map(assemble_slurm_config_dict, slurm_configs))\n    if len(slurm_configs) == 0:\n        raise ConfigError('No slurm configuration found.')\n\n    configs_unresolved = generate_configs(\n        experiment_config, overwrite_params=overwrite_params\n    )\n    configs, named_configs = generate_named_configs(configs_unresolved)\n    configs = resolve_configs(\n        seml_config['executable'],\n        seml_config['conda_environment'],\n        configs,\n        named_configs,\n        seml_config['working_dir'],\n    )\n\n    # Upload source files: This also determines the batch_id\n    use_uploaded_sources = seml_config['use_uploaded_sources']\n    seml_config = to_typeddict(seml_config, SemlDoc)\n    if use_uploaded_sources and not no_code_checkpoint:\n        seml_config['source_files'] = upload_sources(seml_config, collection, batch_id)\n\n    # Create documents that can be interpolated\n    documents = [\n        cast(\n            ExperimentDoc,\n            {\n                **resolve_interpolations(\n                    {\n                        'seml': seml_config,\n                        'slurm': slurm_configs,\n                        'git': git_info,\n                        'batch_id': batch_id,  # needs to be determined now for source file uploading\n                        'config': config,\n                        'config_unresolved': config_unresolved,\n                    }\n                ),\n                'config_unresolved': config_unresolved,\n            },\n        )\n        for config, config_unresolved in zip(configs, configs_unresolved)\n    ]\n\n    if not no_sanity_check:\n        # Sanity checking uses the resolved values (after considering named configs)\n        check_config(\n            seml_config['executable'],\n            seml_config['conda_environment'],\n            [document['config'] for document in documents],\n            seml_config['working_dir'],\n        )\n\n    for document in documents:\n        document['config_hash'] = make_hash(\n            document['config'],\n            config_get_exclude_keys(document['config_unresolved']),\n        )\n\n    if not force_duplicates:\n        documents = remove_duplicates(collection, documents, use_hash=not no_hash)\n\n    # Create an index on the config hash. If the index is already present, this simply does nothing.\n    collection.create_index('config_hash')\n    # Add the configurations to the database with STAGED status.\n    if len(configs) > 0:\n        add_configs(\n            collection,\n            documents,\n            description=description,\n            resolve_descriptions=resolve_descriptions,\n        )\n"
  },
  {
    "path": "src/seml/commands/configure.py",
    "content": "from __future__ import annotations\n\nimport logging\n\nfrom seml.settings import SETTINGS\n\n\ndef prompt_ssh_forward():\n    \"\"\"\n    Prompt the user for SSH Forward settings. The output format corresponds\n    to the argument of sshtunnel.SSHTunnelForwarder.\n    \"\"\"\n    from seml.console import prompt\n\n    logging.info('Configuring SSH Forward settings.')\n    ssh_host = prompt('SSH host')\n    port = prompt('Port', default=22, type=int)\n    username = prompt('User name')\n    ssh_pkey = prompt('Path to SSH private key', default='~/.ssh/id_rsa')\n    return dict(\n        ssh_address_or_host=ssh_host,\n        ssh_port=port,\n        ssh_username=username,\n        ssh_pkey=ssh_pkey,\n    )\n\n\ndef mongodb_configure(\n    host: str | None = None,\n    port: int | None = None,\n    database: str | None = None,\n    username: str | None = None,\n    password: str | None = None,\n    setup_ssh_forward: bool = False,\n):\n    import yaml\n\n    from seml.console import prompt\n\n    if SETTINGS.DATABASE.MONGODB_CONFIG_PATH.exists() and not prompt(\n        f'MongoDB configuration {SETTINGS.DATABASE.MONGODB_CONFIG_PATH} already exists and will be overwritten.\\nContinue?',\n        type=bool,\n    ):\n        return\n    logging.info('Configuring MongoDB. Warning: Password will be stored in plain text.')\n    if host is None:\n        host = prompt('MongoDB host')\n    if port is None:\n        port = prompt('Port', default=27017, type=int)\n    if database is None:\n        database = prompt('Database name')\n    if username is None:\n        username = prompt('User name')\n    if password is None:\n        password = prompt('Password', hide_input=True)\n    file_path = SETTINGS.DATABASE.MONGODB_CONFIG_PATH\n    config: dict = dict(\n        host=host,\n        port=port,\n        database=database,\n        username=username,\n        password=password,\n    )\n    if setup_ssh_forward:\n        config['ssh_config'] = prompt_ssh_forward()\n    config_string = yaml.dump(config)\n    logging.info(\n        f'Saving the following configuration to {file_path}:\\n'\n        f'{config_string.replace(f\"{password}\", \"********\")}'\n    )\n    file_path.parent.mkdir(parents=True, exist_ok=True)\n    with open(file_path, 'w') as f:\n        f.write(config_string)\n"
  },
  {
    "path": "src/seml/commands/description.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom seml.commands.manage import detect_killed\nfrom seml.database import build_filter_dict, get_collection\nfrom seml.experiment.description import resolve_description\nfrom seml.settings import SETTINGS\nfrom seml.utils import slice_to_str, to_slices\nfrom seml.utils.errors import MongoDBError\n\nStates = SETTINGS.STATES\n\n\ndef collection_set_description(\n    db_collection_name: str,\n    description: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    filter_dict: dict | None = None,\n    batch_id: int | None = None,\n    yes: bool = False,\n    resolve: bool = True,\n):\n    \"\"\"Sets (or updates) the description of experiment(s).\n\n    Parameters\n    ----------\n    db_collection_name : str\n        Name of the collection to delete descriptions from\n    description : str\n        The description to set.\n    sacred_id : Optional[int], optional\n        If given, the id of the experiment to delete the description of. Overrides other filters, by default None\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters, by default None\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    resolve : bool, optional\n        Whether to use omegaconf to resolve descriptions\n    \"\"\"\n    from pymongo import UpdateOne\n\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=sacred_id\n    )\n    exps = list(collection.find(filter_dict, {}))\n    if len(exps) == 0 and sacred_id is not None:\n        raise MongoDBError(f'No experiment found with ID {sacred_id}.')\n    descriptions_resolved = {\n        exp['_id']: resolve_description(description, exp) if resolve else description\n        for exp in exps\n    }\n    num_to_overwrite = len(\n        list(\n            filter(\n                lambda exp: exp.get('seml', {}).get(\n                    'description', descriptions_resolved[exp['_id']]\n                )\n                != descriptions_resolved[exp['_id']],\n                exps,\n            )\n        )\n    )\n\n    if (\n        not yes\n        and num_to_overwrite >= SETTINGS.CONFIRM_THRESHOLD.DESCRIPTION_UPDATE\n        and not prompt(\n            f'{num_to_overwrite} experiment(s) have a different description. Proceed?',\n            type=bool,\n        )\n    ):\n        exit(1)\n    if len(list(filter(lambda exp: exp['status'] in States.RUNNING, exps))):\n        logging.warn(\n            f'Updating the description of {States.RUNNING[0]} experiments: This may not have an'\n            ' effect, as sacred overwrites experiments with each tick.'\n        )\n    if len(descriptions_resolved) > 0:\n        result = collection.bulk_write(\n            [\n                UpdateOne({'_id': _id}, {'$set': {'seml.description': description}})\n                for _id, description in descriptions_resolved.items()\n            ]\n        )\n        logging.info(\n            f'Updated the descriptions of {result.modified_count} experiments.'\n        )\n    else:\n        logging.info('No experiments to update.')\n\n\ndef collection_delete_description(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    filter_dict: dict[str, Any] | None = None,\n    batch_id: int | None = None,\n    yes: bool = False,\n):\n    \"\"\"Deletes the description of experiments\n\n    Parameters\n    ----------\n    db_collection_name : str\n        Name of the collection to delete descriptions from\n    sacred_id : Optional[int], optional\n        If given, the id of the experiment to delete the descriptino of. Overrides other filters, by default None\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters, by default None\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    \"\"\"\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n    update = {'$unset': {'seml.description': ''}}\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=sacred_id\n    )\n    filter_dict['seml.description'] = {'$exists': True}\n    exps = list(collection.find(filter_dict, {'seml.description': 1}))\n    if (\n        not yes\n        and len(exps) >= SETTINGS.CONFIRM_THRESHOLD.DESCRIPTION_DELETE\n        and not prompt(\n            f'Deleting descriptions of {len(exps)} experiment(s). Proceed?', type=bool\n        )\n    ):\n        exit(1)\n    result = collection.update_many(filter_dict, update)\n    logging.info(f'Deleted the descriptions of {result.modified_count} experiments.')\n\n\ndef collection_list_descriptions(db_collection_name: str, update_status: bool = False):\n    \"\"\"Lists the descriptions of experiments\n\n    Parameters\n    ----------\n    db_collection_name : str\n        Name of the collection to list descriptions from\n    update_status : bool\n        Whether to detect killed experiments\n    \"\"\"\n    from rich.align import Align\n\n    from seml.console import Table, console\n\n    collection = get_collection(db_collection_name)\n\n    # Handle status updates\n    if update_status:\n        detect_killed(db_collection_name, print_detected=False)\n    else:\n        logging.warning(\n            f'Status of {States.RUNNING[0]} experiments may not reflect if they have died or been canceled. Use the `--update-status` flag instead.'\n        )\n\n    description_slices = {\n        str(obj.get('_id', '')): {\n            'ids': to_slices(obj['ids']),  # type: ignore - these keys are added through the aggregate function.\n            'batch_ids': to_slices(obj['batch_ids']),  # type: ignore\n            'states': set(obj['states']),  # type: ignore\n        }\n        for obj in collection.aggregate(\n            [\n                {\n                    '$group': {\n                        '_id': '$seml.description',\n                        'ids': {'$addToSet': '$_id'},\n                        'batch_ids': {'$addToSet': '$batch_id'},\n                        'states': {'$addToSet': '$status'},\n                    }\n                }\n            ]\n        )\n    }\n\n    table = Table(show_header=True)\n    table.add_column('Description', justify='left')\n    table.add_column('Experiment IDs', justify='left')\n    table.add_column('Batch IDs', justify='left')\n    table.add_column('Status', justify='left')\n    for description in sorted(description_slices):\n        slices = description_slices[description]\n        table.add_row(\n            description,\n            ', '.join(map(slice_to_str, slices['ids'])),\n            ', '.join(map(slice_to_str, slices['batch_ids'])),\n            ', '.join(slices['states']),\n        )\n    console.print(Align(table, align='center'))\n"
  },
  {
    "path": "src/seml/commands/manage.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport itertools\nimport logging\nimport re\nimport subprocess\nfrom typing import TYPE_CHECKING, Any, Dict, Iterable, List, cast\n\nfrom seml.database import (\n    build_filter_dict,\n    delete_files,\n    get_collection,\n    get_database,\n    get_mongodb_config,\n)\nfrom seml.document import ExperimentDoc\nfrom seml.experiment.config import (\n    check_config,\n    config_get_exclude_keys,\n    generate_named_configs,\n    resolve_configs,\n    resolve_interpolations,\n)\nfrom seml.experiment.sources import delete_orphaned_sources, upload_sources\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    chunker,\n    make_hash,\n    s_if,\n    utcnow,\n)\nfrom seml.utils.errors import MongoDBError\nfrom seml.utils.io import tail_file\nfrom seml.utils.slurm import (\n    are_slurm_jobs_running,\n    cancel_slurm_jobs,\n    get_cluster_name,\n    get_slurm_arrays_tasks,\n    get_slurm_jobs,\n    wait_until_slurm_jobs_finished,\n)\n\nStates = SETTINGS.STATES\n\nif TYPE_CHECKING:\n    from pymongo.collection import Collection\n\n\ndef should_check_killed(filter_states: list[str] | None) -> bool:\n    \"\"\"Checks whether killed experiments should be checked\n\n    Parameters\n    ----------\n    filter_states : Optional[List[str]]\n        The states to filter on\n\n    Returns\n    -------\n    bool\n        Whether killed experiments should be checked\n    \"\"\"\n    return (\n        filter_states is not None\n        and len({*States.PENDING, *States.RUNNING, *States.KILLED} & set(filter_states))\n        > 0\n    )\n\n\ndef cancel_empty_pending_jobs(db_collection_name: str, *sacred_ids: int):\n    \"\"\"Cancels pending jobs that are not associated with any experiment\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to check for pending jobs\n    sacred_ids : int\n        The IDs of the experiments to check\n    \"\"\"\n    if len(sacred_ids) == 0:\n        raise ValueError('At least one sacred ID must be provided.')\n    collection = get_collection(db_collection_name)\n    num_pending = collection.count_documents(\n        {'status': {'$in': [*States.PENDING]}, '_id': {'$in': sacred_ids}}\n    )\n    if num_pending > 0:\n        # There are still pending experiments, we don't want to cancel the jobs.\n        return\n    pending_exps = list(collection.find({'_id': {'$in': sacred_ids}}, {'slurm'}))\n    rescheduled_exps = list(\n        collection.find(\n            {'_id': {'$in': sacred_ids}, 'status': {'$in': States.RESCHEDULED}},\n            {'execution'},\n        )\n    )\n\n    # We exclude all SLURM jobs that are needed for continuing a RESCHEDULED experiment\n    rescheduled_ids = []\n    for exp in rescheduled_exps:\n        execution = exp['execution']\n        if 'array_id' in execution:\n            rescheduled_ids.append(execution['array_id'])\n\n    array_ids = {\n        conf['array_id']\n        for exp in pending_exps\n        for conf in exp['slurm']\n        if 'array_id' in conf\n    }\n    array_ids = array_ids.difference(rescheduled_ids)\n    # Only cancel the pending jobs\n    cancel_slurm_jobs(*array_ids, state=SETTINGS.SLURM_STATES.PENDING[0])\n\n\ndef cancel_jobs_without_experiments(*slurm_array_ids: str | int):\n    \"\"\"\n    Cancels Slurm jobs that are not associated with any experiment that is still pending/running.\n\n    Parameters\n    ----------\n    slurm_array_ids : str\n        The array IDs of the Slurm jobs to check.\n    \"\"\"\n    if len(slurm_array_ids) == 0:\n        return []\n\n    canceled_ids = []\n    for array_id in slurm_array_ids:\n        try:\n            job_info = get_slurm_jobs(str(array_id))[0]\n        except subprocess.CalledProcessError:\n            # Job is not running, so we can skip this.\n            continue\n        col_name = job_info.get('Comment', None)\n        if col_name is None:\n            continue\n        collection = get_collection(col_name)\n        is_needed = (\n            collection.count_documents(\n                {\n                    'slurm': {'$elemMatch': {'array_id': array_id}},\n                    'status': {'$in': [*States.RUNNING, *States.PENDING]},\n                },\n                limit=1,\n            )\n            > 0\n        )\n        if not is_needed:\n            cancel_slurm_jobs(str(array_id))\n            canceled_ids.append(array_id)\n    return canceled_ids\n\n\ndef cancel_experiment_by_id(\n    collection: Collection[ExperimentDoc],\n    exp_id: int,\n    set_interrupted: bool = True,\n    slurm_dict: dict | None = None,\n    wait: bool = False,\n    timeout: int = SETTINGS.CANCEL_TIMEOUT,\n):\n    \"\"\"Cancels a single experiment by its id\n\n    Parameters\n    ----------\n    collection : str\n        The collection this experiment belongs to\n    exp_id : int\n        The experiment id\n    set_interrupted : bool, optional\n        Whether to set the state of the experiment to INTERRUPTED, by default True\n    slurm_dict : Optional[Dict], optional\n        Optional updates to the slurm dict of the experiments, by default None\n    wait : bool, optional\n        Whether to wait for the cancelation by checking the slurm queue, by default False\n    timeout : int, optional\n        The timeout in seconds to wait for the cancelation, by default SETTINGS.CANCEL_TIMEOUT\n    \"\"\"\n\n    exp = collection.find_one({'_id': exp_id})\n    if exp is None:\n        logging.error(f'No experiment found with ID {exp_id}.')\n        return\n\n    if slurm_dict:\n        for s_conf in exp['slurm']:\n            s_conf.update(slurm_dict)\n\n    # check if the job has been scheduled at all\n    array_ids = [conf.get('array_id', None) for conf in exp['slurm']]\n    if any(array_id is None for array_id in array_ids):\n        logging.error(f'Experiment with ID {exp_id} has not been started using Slurm.')\n        return\n\n    # check if the job has been claimed and associated with a concrete job\n    is_running = 'array_id' in exp['execution']\n    if is_running:\n        job_strings = [f'{exp[\"execution\"][\"array_id\"]}_{exp[\"execution\"][\"task_id\"]}']\n    else:\n        job_strings = list(map(str, array_ids))\n\n    # Check if slurm job exists\n    if not are_slurm_jobs_running(*job_strings):\n        logging.error(\n            f'Slurm job {job_strings} of experiment '\n            f'with ID {exp_id} is not pending/running in Slurm.'\n        )\n        return\n\n    cancel_update = {\n        '$set': {\n            'status': States.INTERRUPTED[0],\n            'stop_time': utcnow(),\n        }\n    }\n    if set_interrupted:\n        # Set the database state to INTERRUPTED\n        collection.update_one({'_id': exp_id}, cancel_update)\n\n    if is_running:\n        # Check if other experiments are running in the same job\n        filter_dict = {\n            'execution.array_id': exp['execution']['array_id'],\n            'execution.task_id': exp['execution']['task_id'],\n        }\n        filter_dict_others = {\n            **filter_dict,\n            '_id': {'$ne': exp_id},\n            'status': {'$in': [*States.RUNNING, *States.PENDING]},\n        }\n        other_exp_running = collection.count_documents(filter_dict_others) >= 1\n\n        # Cancel if no other experiments are running in the same job\n        if not other_exp_running:\n            job_str = job_strings[0]\n            cancel_slurm_jobs(job_str)\n            # Wait until the job is actually gone\n            if wait and not wait_until_slurm_jobs_finished(job_str, timeout=timeout):\n                logging.error('Job did not cancel in time.')\n                exit(1)\n\n            if set_interrupted:\n                # set state to interrupted again (might have been overwritten by Sacred in the meantime).\n                collection.update_many(filter_dict, cancel_update)\n\n    # Cancel jobs that will not execute anything\n    cancel_jobs_without_experiments(*array_ids)\n\n\ndef cancel_experiments(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n    yes: bool = False,\n    wait: bool = False,\n    confirm_threshold: int = SETTINGS.CONFIRM_THRESHOLD.CANCEL,\n    timeout: int = SETTINGS.CANCEL_TIMEOUT,\n):\n    \"\"\"Cancels experiment(s)\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to cancel experiments of\n    sacred_id : Optional[int], optional\n        ID of the experiment to delete. Overrides other filters.\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    batch_id : Optional[int], optional\n        Filter on experiment batch ids, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters on experiments, by default None\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    wait : bool, optional\n        Whether to wait for all experiments be canceled (by checking the slurm queue), by default False\n    confirm_threshold : int, optional\n        The threshold for the number of experiments to cancel before asking for confirmation, by default SETTINGS.CONFIRM_THRESHOLD.CANCEL\n    timeout : int, optional\n        The timeout in seconds to wait for the cancelation, by default SETTINGS.CANCEL_TIMEOUT\n    \"\"\"\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n    # We check whether there are slurm jobs for which after this action no\n    # RUNNING experiment remains. These slurm jobs can be killed altogether.\n    # However, it is NOT possible right now to cancel a single experiment in a Slurm job with multiple\n    # running experiments.\n    try:\n        if should_check_killed(filter_states):\n            detect_killed(db_collection_name, print_detected=False)\n\n        db_filter_dict = build_filter_dict(\n            filter_states, batch_id, filter_dict, sacred_id=sacred_id\n        )\n\n        to_cancel_arr_ids = list(collection.find(db_filter_dict, {'slurm': 1}))\n        ncancel = len(to_cancel_arr_ids)\n        if sacred_id is not None and ncancel == 0:\n            logging.error(f'No experiment found with ID {sacred_id}.')\n\n        logging.info(f'Canceling {ncancel} experiment{s_if(ncancel)}.')\n        if ncancel >= confirm_threshold:\n            if not yes and not prompt('Are you sure? (y/n)', type=bool):\n                exit(1)\n\n        running_filter = copy.deepcopy(db_filter_dict)\n        running_filter = {\n            **running_filter,\n            'execution.cluster': get_cluster_name(),\n            'execution.array_id': {'$exists': True},\n        }\n        running_exps = list(\n            collection.find(\n                running_filter,\n                {'_id': 1, 'status': 1, 'execution': 1},\n            )\n        )\n        # update database status and write the stop_time\n        # this should be done early so that the experiments are not picked up by other processes\n        cancel_update = {\n            '$set': {\n                'status': States.INTERRUPTED[0],\n                'stop_time': utcnow(),\n            }\n        }\n        collection.update_many(db_filter_dict, cancel_update)\n\n        # Cancel pending jobs that will not execute anything\n        array_ids = {\n            conf['array_id']\n            for exp in to_cancel_arr_ids\n            for conf in exp['slurm']\n            if 'array_id' in conf\n        }\n        canceled = cancel_jobs_without_experiments(*array_ids)\n\n        # set of slurm IDs in the database\n        slurm_ids = {\n            (e['execution']['array_id'], e['execution']['task_id'])\n            for e in running_exps\n        }\n        # set of experiment IDs to be canceled.\n        exp_ids = {e['_id'] for e in running_exps}\n        to_cancel = set()\n\n        # iterate over slurm IDs to check which slurm jobs can be canceled altogether\n        for a_id, t_id in slurm_ids:\n            # find experiments RUNNING under the slurm job\n            jobs_running = [\n                e\n                for e in running_exps\n                if (\n                    e['execution']['array_id'] == a_id\n                    and e['execution']['task_id'] == t_id\n                    and e['status'] in States.RUNNING\n                )\n            ]\n            running_exp_ids = {e['_id'] for e in jobs_running}\n            if len(running_exp_ids.difference(exp_ids)) == 0:\n                # there are no running jobs in this slurm job that should not be canceled.\n                to_cancel.add(f'{a_id}_{t_id}')\n\n        # cancel all Slurm jobs for which no running experiment remains.\n        if len(to_cancel) > 0:\n            chunk_size = 100\n            chunks = list(chunker(list(to_cancel), chunk_size))\n            [cancel_slurm_jobs(*chunk) for chunk in chunks]\n            # Wait until all jobs are actually stopped.\n            for chunk in chunks:\n                if wait and not wait_until_slurm_jobs_finished(*chunk, timeout=timeout):\n                    logging.error('Job did not cancel in time.')\n                    exit(1)\n\n        canceled = list(map(str, canceled + list(to_cancel)))\n        n_canceled = len(canceled)\n        if n_canceled > 0:\n            logging.info(\n                f'Canceled job{s_if(n_canceled)} with the following ID{s_if(n_canceled)}: '\n                + ', '.join(canceled)\n            )\n        # Let's repeat this in case a job cleaned itself up and overwrote the status.\n        collection.update_many(db_filter_dict, cancel_update)\n    except subprocess.CalledProcessError:\n        logging.warning(\n            'One or multiple Slurm jobs were no longer running when I tried to cancel them.'\n        )\n\n\ndef delete_experiments(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n    yes: bool = False,\n    cancel: bool = True,\n):\n    \"\"\"Deletes experiment(s).\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection name to which to delete experiments from\n    sacred_id : Optional[int], optional\n        ID of the experiment to delete. Overrides other filters.\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    batch_id : Optional[int], optional\n        Filter on experiment batch ids, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters on experiments, by default None\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    \"\"\"\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n    # Before deleting, we should first cancel the experiments that are still running.\n    if cancel:\n        cancel_states = set(States.PENDING + States.RUNNING)\n        if filter_states is not None and len(filter_states) > 0:\n            cancel_states = cancel_states.intersection(filter_states)\n\n        if len(cancel_states) > 0 and collection.find_one(\n            build_filter_dict(cancel_states, batch_id, filter_dict, sacred_id)\n        ):\n            cancel_experiments(\n                db_collection_name,\n                sacred_id,\n                list(cancel_states),\n                batch_id,\n                filter_dict,\n                yes=False,\n                confirm_threshold=1,\n                wait=True,\n            )\n\n    experiment_files_to_delete = []\n\n    filter_dict = build_filter_dict(filter_states, batch_id, filter_dict, sacred_id)\n    ndelete = collection.count_documents(filter_dict)\n    if sacred_id is not None and ndelete == 0:\n        raise MongoDBError(f'No experiment found with ID {sacred_id}.')\n    batch_ids: Iterable[ExperimentDoc] = collection.find(filter_dict, {'batch_id'})\n    batch_ids_in_del = {x.get('batch_id', -1) for x in batch_ids}\n\n    logging.info(\n        f'Deleting {ndelete} configuration{s_if(ndelete)} from database collection.'\n    )\n    if ndelete >= SETTINGS.CONFIRM_THRESHOLD.DELETE:\n        if not yes and not prompt('Are you sure? (y/n)', type=bool):\n            exit(1)\n\n    # Collect sources uploaded by sacred.\n    exp_sources_list = collection.find(\n        filter_dict, {'experiment.sources': 1, 'artifacts': 1}\n    )\n    for exp in exp_sources_list:\n        experiment_files_to_delete.extend(get_experiment_files(exp))\n    result = collection.delete_many(filter_dict)\n    if not result.deleted_count == ndelete:\n        logging.error(\n            f'Only {result.deleted_count} of {ndelete} experiments were deleted.'\n        )\n\n    # Delete sources uploaded by sacred.\n    delete_files(collection.database, experiment_files_to_delete)\n    logging.info(\n        f'Deleted {len(experiment_files_to_delete)} files associated with deleted experiments.'\n    )\n\n    if len(batch_ids_in_del) > 0:\n        # clean up the uploaded sources if no experiments of a batch remain\n        delete_orphaned_sources(collection, batch_ids_in_del)\n\n    if collection.count_documents({}) == 0:\n        collection.drop()\n\n\ndef drop_collections(\n    pattern: str, mongodb_config: dict | None = None, yes: bool = False\n):\n    \"\"\"\n    Drops collections matching the given pattern.\n\n    Parameters\n    ----------\n    pattern : str\n        The regex collection names have to match against\n    mongodb_config : dict or None\n        A configuration for the mongodb. If None, the standard config is used.\n    yes : bool\n        Whether to override confirmation prompts\n    \"\"\"\n    from seml.console import list_items, prompt\n\n    # Get the database\n    if mongodb_config is None:\n        mongodb_config = get_mongodb_config()\n    db = get_database(**mongodb_config)\n    expression = re.compile(pattern)\n    collection_names = [\n        name\n        for name in db.list_collection_names()\n        if name not in ('fs.chunks', 'fs.files') and expression.match(name)\n    ]\n    if len(collection_names) == 0:\n        logging.info('No collections found.')\n        return\n    if not yes:\n        logging.info(\n            f'The following {len(collection_names)} collection will be deleted:'\n        )\n        list_items(collection_names)\n        if not prompt('Are you sure? (y/n)', type=bool):\n            return\n    for name in collection_names:\n        delete_experiments(name, yes=True)\n\n\ndef reset_slurm_dict(exp: ExperimentDoc):\n    \"\"\"Resets the slurm dict of an experiment\n\n    Parameters\n    ----------\n    exp : Dict\n        The experiment of which to reset the slurm dict\n    \"\"\"\n    keep_slurm = set()\n    keep_slurm.update(SETTINGS.VALID_SLURM_CONFIG_VALUES)\n    for sub_conf in exp['slurm']:\n        slurm_keys = set(sub_conf.keys())\n        for key in slurm_keys - keep_slurm:\n            del sub_conf[key]\n\n        # Clean up sbatch_options dictionary\n        remove_sbatch = {'job-name', 'output', 'array', 'comment'}\n        sbatch_keys = set(sub_conf['sbatch_options'].keys())\n        for key in remove_sbatch & sbatch_keys:\n            del sub_conf['sbatch_options'][key]\n\n\ndef get_experiment_reset_op(exp: ExperimentDoc):\n    \"\"\"\n    Returns a MongoDB operation to reset an experiment.\n\n    Parameters\n    ----------\n    exp : Dict\n        The experiment to reset\n\n    Returns\n    -------\n    pymongo.operations.ReplaceOne\n        The operation to reset the experiment\n    \"\"\"\n    from pymongo import ReplaceOne\n\n    exp['status'] = States.STAGED[0]\n    # queue_time for backward compatibility.\n    keep_entries = [\n        'batch_id',\n        'status',\n        'seml',\n        'slurm',\n        'config',\n        'config_hash',\n        'add_time',\n        'queue_time',\n        'git',\n        'config_unresolved',\n    ]\n\n    # Clean up SEML dictionary\n    keep_seml = {\n        'source_files',\n        'working_dir',\n        'env',\n        SETTINGS.SEML_CONFIG_VALUE_VERSION,\n    }\n    keep_seml.update(SETTINGS.VALID_SEML_CONFIG_VALUES)\n    seml_keys = set(exp['seml'].keys())\n    for key in seml_keys - keep_seml:\n        del exp['seml'][key]\n\n    reset_slurm_dict(exp)\n\n    op = ReplaceOne(\n        {'_id': exp['_id']},\n        {entry: exp[entry] for entry in keep_entries if entry in exp},\n        upsert=False,\n    )\n    op = cast(ReplaceOne[ExperimentDoc], op)\n    return op\n\n\ndef reset_single_experiment(collection: Collection[ExperimentDoc], exp: ExperimentDoc):\n    \"\"\"Resets a single experiment\n\n    Parameters\n    ----------\n    collection : pymongo.collection.Collection\n        The collection to which the experiment belongs to\n    exp : Dict\n        The experiment dict\n    \"\"\"\n    collection.bulk_write([get_experiment_reset_op(exp)])\n\n\ndef reset_experiments(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n    yes: bool = False,\n):\n    \"\"\"Resets experiments\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The name of the collection to resets experiments from\n    sacred_id : Optional[int], optional\n        If given, the id of the experiment to reset. Overrides other filters, by default None\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters, by default None\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    \"\"\"\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n    if should_check_killed(filter_states):\n        detect_killed(db_collection_name, print_detected=False)\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=sacred_id\n    )\n    nreset = collection.count_documents(filter_dict)\n    exps = collection.find(filter_dict)\n    if sacred_id is not None and nreset == 0:\n        raise MongoDBError(f'No experiment found with ID {sacred_id}.')\n    if nreset == 0:\n        logging.info('No experiments to reset.')\n        return\n\n    logging.info(f'Resetting the state of {nreset} experiment{s_if(nreset)}.')\n    if nreset >= SETTINGS.CONFIRM_THRESHOLD.RESET:\n        if not yes and not prompt('Are you sure? (y/n)', type=bool):\n            exit(1)\n    collection.bulk_write(list(map(get_experiment_reset_op, exps)))\n\n\ndef detect_killed(db_collection_name: str, print_detected: bool = True):\n    \"\"\"Detects killed experiments by checking the slurm status\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to check killed experiments from\n    print_detected : bool, optional\n        Whether to print how many killed experiments have been detected, by default True\n    \"\"\"\n    from pymongo import UpdateMany, UpdateOne\n\n    collection = get_collection(db_collection_name)\n    cluster = get_cluster_name()\n    exps = collection.find(\n        {\n            'status': {'$in': [*States.PENDING, *States.RUNNING, *States.RESCHEDULED]},\n            'execution.cluster': cluster,  # only check experiments that are running on the current cluster\n            # Previously we only checked for started experiments by including the following line:\n            # 'host': {'$exists': True},  # only check experiments that have been started\n            # Though, this does not catch the case where a user cancels pending experiments with scancel.\n            # I (Nicholas) am not 100% sure about the implications of removing the check but it at least\n            # resolves the issue around manually canceled jobs.\n        }\n    )\n    running_jobs = get_slurm_arrays_tasks(True)\n    nkilled = 0\n    trace_updates = []\n    interrupted_exps = []\n    killed_exps = []\n    for exp in exps:\n        # detect whether the experiment is running in slurm\n        arr_id = exp['execution'].get('array_id', -1)\n        task_id = exp['execution'].get('task_id', -1)\n        is_running = arr_id in running_jobs and (\n            any(task_id in r for r in running_jobs[arr_id][0])\n            or task_id in running_jobs[arr_id][1]\n        )\n        # detect whether any job that could execute it is pending\n        array_ids = {conf.get('array_id') for conf in exp['slurm']}\n        # Any of these jobs may still pull the experiment and run it\n        is_pending = (\n            any(array_id in running_jobs for array_id in array_ids)\n            and 'array_id' not in exp['execution']\n        )\n\n        if not is_running and not is_pending:\n            if 'stop_time' in exp:\n                # the experiment is already over but failed to report properly\n                interrupted_exps.append(exp['_id'])\n            else:\n                # the experiment was externally killed\n                nkilled += 1\n                killed_exps.append(exp['_id'])\n                if output_file := exp['seml'].get('output_file'):\n                    try:\n                        fail_trace = tail_file(output_file, n=4)\n                        trace_updates.append(\n                            UpdateOne(\n                                {'_id': exp['_id']},\n                                {'$set': {'fail_trace': fail_trace}},\n                            )\n                        )\n                    except OSError:\n                        # If the experiment is canceled before starting (e.g. when still queued), there is not output file.\n                        logging.debug(f'File {output_file} could not be read.')\n                else:\n                    logging.debug(f'Output file not found in experiment {exp[\"_id\"]}.')\n    updates = [\n        UpdateMany(\n            {'_id': {'$in': interrupted_exps}},\n            {'$set': {'status': States.INTERRUPTED[0]}},\n        ),\n        UpdateMany(\n            {'_id': {'$in': killed_exps}},\n            {'$set': {'status': States.KILLED[0]}},\n        ),\n    ] + trace_updates\n    collection.bulk_write(updates)\n    if print_detected:\n        logging.info(f'Detected {nkilled} externally killed experiment{s_if(nkilled)}.')\n\n\ndef get_experiment_files(experiment: ExperimentDoc) -> list[str]:\n    \"\"\"Gets the file ids of files associated with an experiment\n\n    Parameters\n    ----------\n    experiment : Dict\n        The experiment dict\n\n    Returns\n    -------\n    List[str]\n        A list of file ids\n    \"\"\"\n    experiment_files = []\n    if 'experiment' in experiment:\n        if 'sources' in experiment['experiment']:\n            exp_sources = experiment['experiment']['sources']\n            experiment_files.extend([x[1] for x in exp_sources])\n    if 'artifacts' in experiment:\n        experiment_files.extend([x['file_id'] for x in experiment['artifacts']])\n    return experiment_files\n\n\ndef reload_sources(\n    db_collection_name: str,\n    batch_ids: list[int] | None = None,\n    keep_old: bool = False,\n    yes: bool = False,\n):\n    \"\"\"Reloads the sources of experiment(s)\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to reload sources from\n    batch_ids : Optional[List[int]], optional\n        Filter on the batch ids, by default None\n    keep_old : bool, optional\n        Whether to keep old source files in the fs, by default False\n    yes : bool, optional\n        Whether to override confirmation prompts, by default False\n    resolve : bool, optional\n        Whether to re-resolve the config values\n    \"\"\"\n    from importlib.metadata import version\n\n    from bson import ObjectId\n    from deepdiff import DeepDiff\n    from pymongo import UpdateOne\n\n    from seml.console import prompt\n\n    collection = get_collection(db_collection_name)\n\n    if batch_ids is not None and len(batch_ids) > 0:\n        filter_dict = {'batch_id': {'$in': list(batch_ids)}}\n    else:\n        filter_dict = {}\n    db_results = list(\n        collection.find(\n            filter_dict,\n            {\n                'batch_id',\n                'seml',\n                'config',\n                'status',\n                'config_unresolved',\n                'config_hash',\n            },\n        )\n    )\n    id_to_document: dict[int, list[ExperimentDoc]] = {}\n    for bid, documents in itertools.groupby(db_results, lambda x: x['batch_id']):\n        id_to_document[bid] = list(documents)\n    states = {x['status'] for x in db_results}\n\n    if any([s in (States.RUNNING + States.PENDING + States.COMPLETED) for s in states]):\n        logging.info(\n            'Some of the experiments is still in RUNNING, PENDING or COMPLETED.'\n        )\n        if not yes and not prompt('Are you sure? (y/n)', type=bool):\n            exit(1)\n\n    for batch_id, documents in id_to_document.items():\n        seml_config = documents[0]['seml']\n\n        version_seml_config = seml_config.get(\n            SETTINGS.SEML_CONFIG_VALUE_VERSION, (0, 0, 0)\n        )\n        version_str = '.'.join(map(str, version_seml_config))\n        if version_str != version('seml'):\n            logging.warn(\n                f'Batch {batch_id} was added with seml version \"{version_str}\" '\n                f'which mismatches the current version {version(\"seml\")}'\n            )\n\n        if 'working_dir' not in seml_config or not seml_config['working_dir']:\n            logging.error(f'Batch {batch_id}: No source files to refresh.')\n            continue\n\n        if any(\n            document.get('config_unresolved', None) is None for document in documents\n        ):\n            logging.warn(\n                f'Batch {batch_id}: Some experiments do not have an unresolved configuration. '\n                'The resolved configuration \"config\" will be used for resolution instead.'\n            )\n        configs_unresolved = [\n            document.get('config_unresolved', document['config'])\n            for document in documents\n        ]\n        configs, named_configs = generate_named_configs(configs_unresolved)\n        configs = resolve_configs(\n            seml_config['executable'],\n            seml_config['conda_environment'],\n            configs,\n            named_configs,\n            seml_config['working_dir'],\n        )\n\n        # If the seed was explicited, it should be kept for the new resolved config when reloading resources\n        for config, config_unresolved in zip(configs, configs_unresolved):\n            if SETTINGS.CONFIG_KEY_SEED in configs_unresolved:\n                config[SETTINGS.CONFIG_KEY_SEED] = config_unresolved[\n                    SETTINGS.CONFIG_KEY_SEED\n                ]\n\n        new_documents = [\n            resolve_interpolations(\n                {\n                    **{**document, 'config': config},\n                    'config_unresolved': config_unresolved,\n                }\n            )\n            for document, config, config_unresolved in zip(\n                documents, configs, configs_unresolved\n            )\n        ]\n\n        # determine which documents to udpate\n        updates = []\n        for old_doc, new_doc in zip(documents, new_documents):\n            use_hash = 'config_hash' in old_doc\n            # these config fields are populated if the experiment ran\n            runtime_fields = {\n                k: old_doc['config'][k]\n                for k in ['db_collection', 'overwrite', 'seed']\n                if k in old_doc['config']\n            }\n            new = dict(\n                config=new_doc['config'] | runtime_fields,\n                config_unresolved=new_doc['config_unresolved'],\n            )\n            # compare new to old config\n            if use_hash:\n                new['config_hash'] = make_hash(\n                    new_doc['config'],\n                    config_get_exclude_keys(new_doc['config_unresolved']),\n                )\n                update = new['config_hash'] != old_doc['config_hash']\n            else:\n                diff = DeepDiff(new['config'], old_doc['config'], ignore_order=True)\n                update = bool(diff)\n            # Create mongodb update\n            if update:\n                updates.append(UpdateOne({'_id': old_doc['_id']}, {'$set': new}))\n        if len(updates) > 0:\n            result = collection.bulk_write(updates)\n            logging.info(\n                f'Batch {batch_id}: Resolved configurations of {result.matched_count} experiments against new source files ({result.modified_count} changed).'\n            )\n        else:\n            logging.info(f'Batch {batch_id}: No experiment configurations changed.')\n\n        # Check whether the configurations aligns with the current source code\n        check_config(\n            seml_config['executable'],\n            seml_config['conda_environment'],\n            [document['config'] for document in documents],\n            seml_config['working_dir'],\n        )\n\n        # Find the currently used source files\n        db = collection.database\n        fs_filter_dict = {\n            'metadata.batch_id': batch_id,\n            'metadata.collection_name': f'{collection.name}',\n            'metadata.deprecated': {'$exists': False},\n        }\n        current_source_files = db['fs.files'].find(filter_dict, '_id')\n        current_ids = [x['_id'] for x in current_source_files]\n        fs_filter_dict = {'_id': {'$in': current_ids}}\n        # Deprecate them\n        db['fs.files'].update_many(\n            fs_filter_dict, {'$set': {'metadata.deprecated': True}}\n        )\n        try:\n            # Try to upload the new ones\n            source_files = upload_sources(seml_config, collection, batch_id)\n        except Exception as e:\n            # If it fails we reconstruct the old ones\n            logging.error(\n                f'Batch {batch_id}: Source import failed. Restoring old files.'\n            )\n            db['fs.files'].update_many(\n                fs_filter_dict, {'$unset': {'metadata.deprecated': ''}}\n            )\n            raise e\n\n        try:\n            # Try to assign the new ones to the experiments\n            filter_dict = {'batch_id': batch_id}\n            collection.update_many(\n                filter_dict, {'$set': {'seml.source_files': source_files}}\n            )\n            logging.info(f'Batch {batch_id}: Successfully reloaded source code.')\n        except Exception as e:\n            logging.error(f'Batch {batch_id}: Failed to set new source files.')\n            # Delete new source files from DB\n            delete_files(db, [x[1] for x in source_files])\n            raise e\n\n        # Delete the old source files\n        if not keep_old:\n            fs_filter_dict = {\n                'metadata.batch_id': batch_id,\n                'metadata.collection_name': f'{collection.name}',\n                'metadata.deprecated': True,\n            }\n            source_files_old = [\n                cast(ObjectId, x['_id'])\n                for x in db['fs.files'].find(fs_filter_dict, {'_id'})\n            ]\n            delete_files(db, source_files_old)\n\n\ndef detect_duplicates(\n    db_collection_name: str, filter_dict: dict | None = None\n) -> list[set[int]]:\n    \"\"\"Finds duplicate configurations based on their hashes.\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to check\n\n    Returns\n    -------\n    List[Set[int]]\n        All duplicate experiments.\n    \"\"\"\n    collection = get_collection(db_collection_name)\n    pipeline = [\n        {\n            '$group': {\n                '_id': '$config_hash',\n                'ids': {'$addToSet': '$_id'},\n                'count': {'$sum': 1},\n            }\n        },\n        {\n            '$match': {\n                'count': {'$gt': 1},\n            }\n        },\n    ]\n    if filter_dict is not None:\n        pipeline = [{'$match': filter_dict}] + pipeline\n    duplicates = collection.aggregate(pipeline)\n    # the aggregate type is wrongly typed.\n    duplicates = cast(List[Dict[str, Any]], duplicates)\n    return [set(duplicate['ids']) for duplicate in duplicates]\n"
  },
  {
    "path": "src/seml/commands/migration.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING, Protocol\n\nfrom seml.database import get_collection\nfrom seml.document import ExperimentDoc\nfrom seml.settings import SETTINGS\nfrom seml.utils import s_if, smaller_than_version_filter, utcnow\nfrom seml.utils.slurm import get_cluster_name\n\nif TYPE_CHECKING:\n    from pymongo.collection import Collection\n\nStates = SETTINGS.STATES\n\n\ndef migrate_collection(db_collection_name: str, skip: bool, backup: bool):\n    \"\"\"\n    Migrate the given collection to be compatible with the current SEML version.\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The name of the collection to migrate.\n    skip : bool\n        Whether to skip the migration.\n    backup : bool\n        Whether to create a backup of the collection before migrating.\n    \"\"\"\n    from seml.console import prompt\n\n    skip = SETTINGS.MIGRATION.SKIP or skip\n    yes = SETTINGS.MIGRATION.YES\n    backup = SETTINGS.MIGRATION.BACKUP or backup\n    backup_tmp = SETTINGS.MIGRATION.BACKUP_TMP\n\n    if skip:\n        return\n\n    collection = get_collection(db_collection_name)\n    migrations = [migration_cls(collection) for migration_cls in _MIGRATIONS]\n    backed_up = False\n    for migration in migrations:\n        if not migration.requires_migration():\n            continue\n\n        if not migration.is_silent():\n            logging.warning(\n                f\"The collection '{db_collection_name}' needs a {migration.name()} migration to work with newer SEML versions.\\n\"\n                'If you wish to make backups, do not proceed and call `seml --migration-backup <collection> <command>`.\\n'\n                'To skip migration, use `seml --migration-skip <collection> <command>`.'\n            )\n            if not yes and not prompt('Do you want to proceed? (y/n)', type=bool):\n                logging.error('Aborted migration.')\n                exit(1)\n\n        if not backed_up and backup and not migration.is_silent():\n            backup_name = backup_tmp.format(\n                collection=db_collection_name, time=utcnow().strftime('%Y%m%d_%H%M%S')\n            )\n            if backup_name in collection.database.list_collection_names():\n                logging.error(f'Backup collection {backup_name} already exists.')\n                exit(1)\n            collection.aggregate([{'$match': {}}, {'$out': backup_name}])\n            logging.info(\n                f\"Backed up collection '{db_collection_name}' to '{backup_name}'.\"\n            )\n            backed_up = True\n\n        n_updated = migration.migrate()\n        if not migration.is_silent():\n            logging.info(\n                f'Successfully migrated {n_updated} experiment{s_if(n_updated)}.'\n            )\n\n\nclass Migration(Protocol):\n    def __init__(self, collection: Collection[ExperimentDoc]): ...\n    def requires_migration(self) -> bool: ...\n    def migrate(self) -> int: ...\n    def name(self) -> str: ...\n    def is_silent(self) -> bool: ...\n\n\nclass Migration04To05Slurm(Migration):\n    collection: Collection[ExperimentDoc]\n    db_filter = {'slurm': {'$not': {'$type': 'array'}}}\n\n    def __init__(self, collection: Collection[ExperimentDoc]):\n        self.collection = collection\n\n    def is_silent(self):\n        return False\n\n    def requires_migration(self):\n        return self.collection.count_documents(self.db_filter, limit=1) > 0\n\n    def name(self):\n        return 'SLURM config'\n\n    def migrate(self):\n        # Check if there are still experiments running\n        # If so, we cannot migrate the SLURM configuration\n        db_filter_running = {\n            **self.db_filter,\n            'status': {'$in': [*States.PENDING, *States.RUNNING]},\n        }\n        if self.collection.count_documents(db_filter_running) > 0:\n            logging.error(\n                'Cannot migrate SLURM configuration while there are still experiments running.\\n'\n                'Please wait until all experiments have finished.'\n            )\n            exit(1)\n\n        # Move slurm array to execution field\n        db_filter_executed = {**self.db_filter, 'slurm.array_id': {'$exists': True}}\n        self.collection.update_many(\n            db_filter_executed,\n            {\n                '$set': {\n                    'execution': {\n                        'cluster': get_cluster_name(),\n                        'slurm.output_file': '$slurm.output_file',\n                        'array_id': '$slurm.array_id',\n                        'task_id': '$slurm.task_id',\n                    }\n                }\n            },\n        )\n        # Convert slurm field to array\n        n_updated = self.collection.update_many(\n            self.db_filter,\n            [{'$set': {'slurm': ['$slurm']}}],\n        ).modified_count\n        return n_updated\n\n\nclass Migration05Version(Migration):\n    collection: Collection[ExperimentDoc]\n    db_filter = {\n        '$or': [\n            {f'seml.{SETTINGS.SEML_CONFIG_VALUE_VERSION}': {'$exists': False}},\n            {\n                f'seml.{SETTINGS.SEML_CONFIG_VALUE_VERSION}': {\n                    '$not': {'$type': 'array'}\n                }\n            },\n            smaller_than_version_filter((0, 5, 0)),\n        ]\n    }\n\n    def __init__(self, collection: Collection[ExperimentDoc]):\n        self.collection = collection\n\n    def is_silent(self):\n        return True\n\n    def requires_migration(self):\n        return self.collection.count_documents(self.db_filter, limit=1) > 0\n\n    def name(self):\n        return 'Version'\n\n    def migrate(self):\n        n_updated = self.collection.update_many(\n            self.db_filter,\n            {'$set': {f'seml.{SETTINGS.SEML_CONFIG_VALUE_VERSION}': [0, 5, 0]}},\n        ).modified_count\n        return n_updated\n\n\n_MIGRATIONS: tuple[type[Migration], ...] = (\n    Migration04To05Slurm,\n    Migration05Version,\n)\n"
  },
  {
    "path": "src/seml/commands/print.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nimport re\nimport time\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING, Any, Dict, List, Sequence, cast\n\nfrom seml.commands.manage import detect_duplicates, detect_killed, should_check_killed\nfrom seml.database import (\n    build_filter_dict,\n    get_collection,\n    get_database,\n    get_mongodb_config,\n)\nfrom seml.document import ExperimentDoc\nfrom seml.experiment.command import (\n    get_command_from_exp,\n    get_config_overrides,\n    get_environment_variables,\n    get_shell_command,\n)\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    find_jupyter_host,\n    flatten,\n    get_from_nested,\n    resolve_projection_path_conflicts,\n    s_if,\n    slice_to_str,\n    to_hashable,\n    to_slices,\n)\nfrom seml.utils.io import tail_file\nfrom seml.utils.slurm import get_cluster_name, get_slurm_jobs\n\nif TYPE_CHECKING:\n    import pymongo.database\n\nStates = SETTINGS.STATES\n\n\ndef print_fail_trace(\n    db_collection_name: str,\n    sacred_id: int | None,\n    filter_states: list[str] | None,\n    batch_id: int | None,\n    filter_dict: dict | None,\n    projection: list[str] | None = None,\n):\n    \"\"\"Convenience function that prints the fail trace of experiments\n\n    Parameters\n    ----------\n    db_collection_name : str\n        Name of the collection to print traces of\n    sacred_id : Optional[int]\n        Optional filter on the experiment ID\n    filter_states : Optional[List[str]]\n        Optional filter on the experiment states\n    batch_id : Optional[int]\n        Optional filter on the experiment batch ID\n    filter_dict : Optional[Dict]\n        Optional filters\n    projection : Optional[List[str]]\n        Additional values to print per failed experiment, by default None\n    \"\"\"\n    from rich.align import Align\n    from rich.console import ConsoleRenderable, Group\n    from rich.panel import Panel\n    from rich.rule import Rule\n    from rich.text import Text\n\n    from seml.console import Table, console\n\n    detect_killed(db_collection_name, print_detected=False)\n    collection = get_collection(db_collection_name)\n    if projection is None:\n        projection = []\n    mongo_db_projection = resolve_projection_path_conflicts(\n        {\n            '_id': 1,\n            'status': 1,\n            'execution.array_id': 1,\n            'execution.task_id': 1,\n            'fail_trace': 1,\n            'seml.description': 1,\n            'batch_id': 1,\n            **{key: 1 for key in projection},\n        }\n    )\n\n    filter_dict = build_filter_dict(filter_states, batch_id, filter_dict, sacred_id)\n    exps: list[ExperimentDoc] = list(collection.find(filter_dict, mongo_db_projection))\n    for exp in exps:\n        exp_id = exp.get('_id')\n        status = exp.get('status')\n        batch_id = exp.get('batch_id')\n        slurm_array_id = exp.get('execution', {}).get('array_id', None)\n        slurm_task_id = exp.get('execution', {}).get('task_id', None)\n        fail_trace = exp.get('fail_trace', [])\n        description = exp.get('seml', {}).get('description', None)\n        header = (\n            f'Experiment ID {exp_id}, '\n            f'Batch ID {batch_id}, '\n            f'Status: \"{status}\", '\n            f'Slurm Array-Task id: {slurm_array_id}-{slurm_task_id}'\n        )\n\n        renderables: list[ConsoleRenderable | str] = []\n        if description is not None:\n            text_description = Text()\n            text_description.append('Description: ', style='bold magenta')\n            text_description.append(description)\n            renderables += [text_description, Rule(Text('Fail-Trace', style='bold'))]\n\n        if isinstance(fail_trace, str):\n            fail_trace = fail_trace.splitlines()\n        renderables.append(''.join(['\\t' + line for line in fail_trace] + []).strip())\n        if len(projection) > 0:\n            table_projection = Table(show_header=False)\n            projection_keys_flat = [\n                key\n                for key in flatten(exp)\n                if any(key.startswith(p) for p in projection)\n            ]\n            for key in projection_keys_flat:\n                table_projection.add_row(key, str(get_from_nested(exp, key)))\n            renderables += [\n                Rule(Text('Projection', style='bold')),\n                Align(table_projection, align='left'),\n            ]\n        panel = Panel(\n            Group(*renderables),\n            title=console.render_str(header, highlight=True),\n            highlight=True,\n            border_style='red',\n        )\n        console.print(panel)\n    logging.info(f'Printed the fail traces of {len(exps)} experiment(s).')\n\n\ndef print_status(\n    db_collection_name: str,\n    update_status: bool = True,\n    projection: list[str] | None = None,\n):\n    \"\"\"Prints the status of an experiment collection\n\n    Parameters\n    ----------\n    db_collection_name : str\n        Name of the collection to print status of\n    update_status : bool, optional\n        Whehter to detect killed experiments, by default True\n    projection : Optional[List[str]], optional\n        Additional attributes from the MongoDB to print, by default None\n    \"\"\"\n\n    from rich.align import Align\n    from rich.table import Column\n\n    from seml.console import Table, console\n\n    collection = get_collection(db_collection_name)\n\n    # Handle status updates\n    if update_status:\n        detect_killed(db_collection_name, print_detected=False)\n    else:\n        logging.warning(\n            f'Status of {States.RUNNING[0]} experiments may not reflect if they have died or been canceled. Use the `--update-status` flag instead.'\n        )\n\n    if projection is None:\n        projection = []\n    projection = list(resolve_projection_path_conflicts({key: 1 for key in projection}))\n\n    result = collection.aggregate(\n        [\n            {\n                '$group': {\n                    '_id': '$status',\n                    'ids': {'$addToSet': '$_id'},\n                    'batch_ids': {'$addToSet': '$batch_id'},\n                    'descriptions': {'$addToSet': '$seml.description'},\n                    'count': {'$sum': 1},\n                    **{\n                        f'projection_{idx}': {'$addToSet': f'${key}'}\n                        for idx, key in enumerate(projection)\n                    },\n                }\n            }\n        ]\n    )\n    # the aggregate function is wrongly typed.\n    result = cast(List[Dict[str, Any]], list(result))\n    result = sorted(result, key=lambda x: list(States.keys()).index(x['_id']))\n    show_descriptions = any(len(row['descriptions']) > 0 for row in result)\n    # Unpack the (nested) projections\n    # We keep prefixes encoded as ${id} to preserve the order of the projection keys\n    result_projection: list[dict[str, set]] = []\n    for record in result:\n        result_projection.append(defaultdict(set))\n        for idx, key in enumerate(projection):\n            for values in record[f'projection_{idx}']:\n                for x, y in flatten({f'${idx}': values}).items():\n                    result_projection[-1][x].add(to_hashable(y))\n    projection_columns = sorted({k for record in result_projection for k in record})\n    # For the column headers, we replace ${id} with the projection key\n    columns = []\n    for projection_column in projection_columns:\n        match = re.match(r'.*\\$([0-9]+)(\\..*|$)', projection_column)\n        assert match is not None\n        projection_key_idx = int(match.groups()[0])\n        columns.append(\n            projection_column.replace(\n                f'${projection_key_idx}', projection[projection_key_idx]\n            )\n        )\n    duplicate_experiment_ids = {\n        experiment_id\n        for dups in detect_duplicates(db_collection_name)\n        for experiment_id in dups\n    }\n\n    if show_descriptions:\n        columns.insert(0, 'Descriptions')\n    table = Table(\n        Column('Status', justify='left', footer='Total'),\n        Column(\n            'Count',\n            justify='left',\n            footer=str(sum(record['count'] for record in result)),\n        ),\n        Column('Experiment IDs', justify='left'),\n        Column('Batch IDs', justify='left'),\n        Column('Duplicates', footer=str(len(duplicate_experiment_ids))),\n        *[Column(key, justify='left') for key in columns],\n        show_header=True,\n        show_footer=len(result) > 1,\n    )\n    for record, record_projection in zip(result, result_projection):\n        row = [\n            record['_id'],\n            str(record['count']),\n            ', '.join(map(slice_to_str, to_slices(record['ids']))),\n            ', '.join(map(slice_to_str, to_slices(record['batch_ids']))),\n            str(len(set(record['ids']) & duplicate_experiment_ids)),\n        ]\n        if show_descriptions:\n            row.append(\n                ', '.join(\n                    [f'\"{description}\"' for description in record['descriptions']]\n                    if len(record['descriptions']) > 1\n                    else record['descriptions']\n                )\n            )\n        row += [\n            ', '.join(map(str, record_projection.get(key, {})))\n            for key in projection_columns\n        ]\n        table.add_row(*row)\n    console.print(Align(table, align='center'))\n\n\ndef print_collections(\n    pattern: str,\n    mongodb_config: dict | None = None,\n    progress: bool = False,\n    list_empty: bool = False,\n    update_status: bool = False,\n    print_full_description: bool = False,\n):\n    \"\"\"\n    Prints a tabular version of multiple collections and their states (without resolving RUNNING experiments that may have been canceled manually).\n\n    Parameters\n    ----------\n    pattern : str\n        The regex collection names have to match against\n    mongodb_config : dict or None\n        A configuration for the mongodb. If None, the standard config is used.\n    progress : bool\n        Whether to use a progress bar for fetching\n    list_empty : bool\n        Whether to list collections that have no documents associated with any state\n    update_status : bool\n        Whether to update the status of experiments by checking log files. This may take a while.\n    print_full_description : bool\n        Whether to print full descriptions (wrap-arround) or truncate the descriptions otherwise.\n    \"\"\"\n    import pandas as pd\n    from rich.align import Align\n    from rich.table import Column\n\n    from seml.console import Table, console, track\n\n    # Get the database\n    if mongodb_config is None:\n        mongodb_config = get_mongodb_config()\n    db = get_database(**mongodb_config)\n    expression = re.compile(pattern)\n    collection_names = [\n        name\n        for name in db.list_collection_names()\n        if name not in ('fs.chunks', 'fs.files') and expression.match(name)\n    ]\n    # Handle status updates\n    if update_status:\n        for collection in collection_names:\n            detect_killed(collection, print_detected=False)\n    else:\n        logging.warning(\n            f'Status of {States.RUNNING[0]} experiments may not reflect if they have died or been canceled. Use the `--update-status` flag instead.'\n        )\n\n    # Count the number of experiments in each state\n    name_to_counts: dict[str, dict[str, int]] = defaultdict(\n        lambda: {state: 0 for state in States.keys()}\n    )\n    name_to_descriptions = defaultdict(str)\n    it = track(collection_names, disable=not progress)\n\n    inv_states = {v: k for k, states in States.items() for v in states}\n    show_description = False\n    for collection_name in it:\n        counts_by_status = db[collection_name].aggregate(\n            [\n                {\n                    '$group': {\n                        '_id': '$status',\n                        '_count': {'$sum': 1},\n                        'description': {'$addToSet': '$seml.description'},\n                    }\n                }\n            ]\n        )\n        # the aggregate function is wrongly typed.\n        counts_by_status = cast(List[Dict[str, Any]], list(counts_by_status))\n        descriptions = db[collection_name].aggregate(\n            [{'$group': {'_id': '$seml.description'}}]\n        )\n        # the aggregate function is wrongly typed.\n        descriptions = cast(List[Dict[str, Any]], list(descriptions))\n        descriptions = [\n            result['_id'] for result in descriptions if result['_id'] is not None\n        ]\n        name_to_counts[collection_name].update(\n            {\n                inv_states[result['_id']]: result['_count']\n                for result in counts_by_status\n                if result['_id'] in inv_states\n            }\n        )\n        if len(descriptions) > 1:\n            descriptions = [f'\"{description}\"' for description in descriptions]\n        if len(descriptions) > 0:\n            show_description = True\n        name_to_descriptions[collection_name] = ', '.join(descriptions)\n\n    if len(name_to_counts) == 0:\n        logging.info(f'Found no collection matching \"{pattern}\"!')\n        return\n\n    df = pd.DataFrame.from_dict(name_to_counts, dtype=int).transpose()  # type: ignore\n    # Remove empty collections\n    if not list_empty:\n        df = df[df.sum(axis=1) > 0]\n    # sort rows and columns\n    df = df.sort_index()[list(States.keys())]\n    # add a column with the total\n    df['Total'] = df.sum(axis=1)\n    # I don't know why but pyright thinks it could be a numpy array.\n    df = cast(pd.DataFrame, df)\n\n    totals = df.sum(axis=0)\n    max_len = max(map(len, collection_names))\n    columns = [\n        Column('Collection', justify='left', footer='Total', min_width=max_len),\n    ] + [\n        Column(state.capitalize(), justify='right', footer=str(totals[state]))\n        for state in df.columns\n    ]\n    if show_description:\n        columns.append(\n            Column(\n                'Description(s)',\n                justify='left',\n                max_width=console.width - max_len - sum(map(len, df.columns)) + 1,\n                no_wrap=not print_full_description,\n                overflow='ellipsis',\n            )\n        )\n    table = Table(*columns, show_footer=df.shape[0] > 1)\n    for collection_name, row in df.iterrows():\n        tab_row = [collection_name, *[str(x) for x in row.to_list()]]\n        if show_description:\n            tab_row.append(name_to_descriptions[collection_name])\n        table.add_row(*tab_row)\n    # For some reason the table thinks the terminal is larger than it is\n    to_print = Align(table, align='center', width=console.width - max_len + 1)\n    console.print(to_print, soft_wrap=True)\n\n\ndef print_duplicates(\n    db_collection_name: str,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n):\n    \"\"\"Detects and lists duplicate experiment configurations\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to detect duplicates in\n    filter_states : Optional[List[str]], optional\n        Optional filter on states, by default None\n    batch_id : Optional[int], optional\n        Optional filter on batch IDs, by default None\n    filter_dict : Optional[Dict], optional\n        Optional additional user filters, by default None\n    \"\"\"\n    from rich.panel import Panel\n    from rich.text import Text\n\n    from seml.console import console\n\n    if should_check_killed(filter_states):\n        detect_killed(db_collection_name, print_detected=False)\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=None\n    )\n    duplicates = detect_duplicates(db_collection_name, filter_dict)\n    num_duplicates = sum(map(len, duplicates))\n    sorted_duplicates = sorted(list(map(lambda d: tuple(sorted(d)), duplicates)))\n    panel = Panel(\n        Text.assemble(\n            ('Duplicate experiment ID groups: ', 'bold'),\n            (', '.join(map(str, sorted_duplicates))),\n        ),\n        title=console.render_str(\n            f'Found {num_duplicates} duplicate experiment configurations ({len(duplicates)} groups)'\n        ),\n        highlight=True,\n        border_style='red',\n    )\n    console.print(panel)\n\n\ndef print_experiment(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n    projection: list[str] | None = None,\n    format: str = 'yaml',\n):\n    \"\"\"\n    Prints the details of an experiment.\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to print the experiment from\n    sacred_id : Optional[int], optional\n        The ID of the experiment to print, by default None\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters, by default None\n    projection : Optional[List[str]], optional\n        Additional values to print per experiment, by default all are printed\n    format: Literal['json', 'yaml'], optional\n        The format to print the experiment in, by default 'yaml'\n    \"\"\"\n    import yaml\n    from rich import print_json\n    from rich.syntax import Syntax\n\n    from seml.console import Heading, console, pause_live_widget\n\n    filter_dict = build_filter_dict(filter_states, batch_id, filter_dict, sacred_id)\n    collection = get_collection(db_collection_name)\n    if projection is None or len(projection) == 0:\n        proj = {}\n    else:\n        proj = {'_id': 1, 'batch_id': 1, **{p: 1 for p in projection}}\n    experiments: list[ExperimentDoc] = list(collection.find(filter_dict, proj))\n\n    if len(experiments) == 0:\n        logging.info('No experiment found to print.')\n        return\n\n    def json_print_fn(exp):\n        print_json(data=exp, skip_keys=True, default=str)\n\n    def yaml_print_fn(exp):\n        from seml.utils.yaml import YamlDumper\n\n        yaml_str = yaml.dump(exp, indent=2, default_flow_style=None, Dumper=YamlDumper)\n        syntax = Syntax(\n            yaml_str.strip(),  # remove trailing newline\n            lexer='yaml',\n            background_color='default',\n        )\n        console.print(syntax)\n\n    format = format.lower()\n    if format == 'json':\n        print_fn = json_print_fn\n    elif format == 'yaml':\n        print_fn = yaml_print_fn\n    else:\n        raise ValueError(f'Unknown format: {format}')\n\n    with pause_live_widget():\n        for exp in experiments:\n            console.print(Heading(f'Experiment {exp[\"_id\"]} (batch {exp[\"batch_id\"]})'))\n            # id and batch_id are already printed in the header.\n            to_print = dict(exp)\n            del to_print['_id']\n            del to_print['batch_id']\n            print_fn(to_print)\n\n\ndef print_output(\n    db_collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n    slurm: bool = False,\n    head: int | None = None,\n    tail: int | None = None,\n):\n    \"\"\"\n    Prints the output of experiments\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to print the output of\n    sacred_id : Optional[int], optional\n        The ID of the experiment to print the output of, by default None\n    filter_states : Optional[List[str]], optional\n        Filter on experiment states, by default None\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    filter_dict : Optional[Dict], optional\n        Additional filters, by default None\n    slurm : bool, optional\n        Whether to print the Slurm output instead of the experiment output, by default False\n    head : Optional[int], optional\n        The number of lines to print from the beginning of the output, by default None\n    tail : Optional[int], optional\n        The number of lines to print from the end of the output, by default None\n    \"\"\"\n    from seml.console import Heading, console, pause_live_widget\n\n    assert tail is None or head is None, 'Cannot specify both tail and head.'\n\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=sacred_id\n    )\n    collection = get_collection(db_collection_name)\n    experiments = collection.find(\n        filter_dict,\n        {\n            'seml.output_file': 1,\n            '_id': 1,\n            'batch_id': 1,\n            'captured_out': 1,\n            'execution': 1,\n        },\n    )\n\n    def print_head(file: str):\n        with open(file, newline='', errors='replace') as f:\n            for i, line in enumerate(f):\n                console.print(line[:-1], end=line[-1])\n                if head is not None and i >= head:\n                    break\n\n    def print_tail(file: str, tail: int):\n        console.print(tail_file(file, n=tail))\n\n    def print_all(file: str):\n        with open(file, newline='', errors='replace') as f:\n            for line in f:\n                console.print(line[:-1], end=line[-1])\n\n    count = 0\n    for exp in experiments:\n        count += 1\n        console.print(Heading(f'Experiment {exp[\"_id\"]} (batch {exp[\"batch_id\"]})'))\n        with pause_live_widget():\n            # Select output file\n            out_file = exp['seml'].get('output_file')\n            if out_file is None or slurm:\n                if 'execution' not in exp:\n                    logging.error(f'Experiment {exp[\"_id\"]} has not been executed yet.')\n                    continue\n                if not slurm:\n                    logging.info(\n                        f'No experiment output file found for experiment {exp[\"_id\"]}. '\n                        'Using Slurm output instead.'\n                    )\n                if 'slurm_output_file' in exp['execution']:\n                    out_file = exp['execution']['slurm_output_file']\n                else:\n                    logging.error(\n                        f'No Slurm output file found for experiment {exp[\"_id\"]}.'\n                    )\n                    continue\n            # Actually read\n            try:\n                if head:\n                    print_head(out_file)\n                elif tail:\n                    print_tail(out_file, tail)\n                else:\n                    print_all(out_file)\n                console.print()  # new line\n            except OSError:\n                logging.info(f'File {out_file} could not be read.')\n                if 'captured_out' in exp and exp['captured_out']:\n                    logging.info('Captured output from DB:')\n                    console.print(exp['captured_out'])\n                else:\n                    logging.error('No output available.')\n\n    if count == 0:\n        logging.info('No experiments found.')\n\n\ndef generate_queue_table(\n    db: pymongo.database.Database,\n    job_ids: Sequence[str] | None,\n    filter_states: Sequence[str] | None,\n    filter_by_user: bool = True,\n):\n    \"\"\"\n    Generates a table of the SEML collections of Slurm jobs.\n\n    Parameters\n    ----------\n    job_ids : List[str]\n        The job IDs to check\n    filter_by_user : bool, optional\n        Whether to only check jobs by the current user, by default True.\n\n    Returns\n    -------\n    Align\n        The table of the SEML collections of Slurm jobs.\n    \"\"\"\n    from rich.align import Align\n\n    from seml.console import Table\n\n    _NO_COLLECTION = 'No collection found'\n\n    if job_ids:\n        job_infos = get_slurm_jobs(*job_ids)\n    else:\n        job_infos = get_slurm_jobs()\n\n    # Find the collections\n    all_collections = set(db.list_collection_names())\n\n    collection_to_jobs: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list)\n    states = set()\n    collections = set()\n    for job in job_infos:\n        state = job['JobState']\n        if filter_states is not None and state not in filter_states:\n            continue\n\n        user_id = job.get('UserId', '').split('(')[0]\n        if filter_by_user and user_id != os.environ['USER']:\n            continue\n\n        collection = job.get('Comment', None)\n        if not (collection and collection in all_collections):\n            collection = _NO_COLLECTION\n            if job['JobName'] == 'jupyter':\n                collection = 'Jupyter'\n                url, known_host = find_jupyter_host(job['StdOut'], False)\n                if known_host is not None:\n                    collection = f'Jupyter ({url})'\n\n        collection_to_jobs[(collection, state)].append(job)\n        states.add(job['JobState'])\n        collections.add(collection)\n\n    # Print the collections\n    sorted_states = sorted(states)\n    table = Table(\n        'Collection',\n        *map(str.title, sorted_states),\n        show_header=True,\n    )\n    cluster_name = get_cluster_name()\n\n    def format_job(job_info, db_col_name):\n        if job_info is None:\n            return ''\n        nodelist = job_info['NodeList']\n        array_id = job_info.get('ArrayJobId', job_info['JobId'])\n        task_id = job_info.get('ArrayTaskId', None)\n        if task_id:\n            if any(x in task_id for x in ',-'):\n                task_id = f'[{task_id}]'\n            job_id = f'{array_id}_{task_id}'\n        else:\n            job_id = array_id\n        # In some SLURM versions, the NodeList is '(null)' when the job is not running\n        # in other it's simply not set.\n        suffix = ''\n        is_running = nodelist and nodelist != '(null)'\n        if db_col_name == _NO_COLLECTION:\n            if job_name := job_info.get('JobName'):\n                suffix = f', [green]{job_name}[/green]'\n        else:\n            if is_running:\n                # Running\n                collection = get_collection(db_col_name)\n                experiments = collection.find(\n                    {\n                        'execution.cluster': cluster_name,\n                        'execution.array_id': int(array_id),\n                        'execution.task_id': int(task_id) if task_id else None,\n                    },\n                    {'_id': 1},\n                )\n                if ids := [exp['_id'] for exp in experiments]:\n                    suffix = f', Id{s_if(len(ids))}: {\"|\".join(map(str, ids))}'\n\n        if is_running:\n            return f'[bright_blue]{job_id}[/bright_blue] ({job_info[\"RunTime\"]}, {nodelist}{suffix})'\n        else:\n            return f'[bright_blue]{job_id}[/bright_blue] ({job_info.get(\"Reason\", \"\")}{suffix})'\n\n    for col in sorted(collections):\n        row = [col]\n        for state in sorted_states:\n            jobs = collection_to_jobs[(col, state)]\n            row.append('\\n'.join(map(format_job, jobs, [col] * len(jobs))))\n        table.add_row(*row)\n\n    if len(collections) == 0:\n        return Align('No jobs found.', align='center')\n    return Align(table, align='center')\n\n\ndef print_queue(\n    job_ids: Sequence[str] | None,\n    filter_states: Sequence[str] | None,\n    filter_by_user: bool,\n    watch: bool,\n):\n    \"\"\"\n    Prints the SEML collections of Slurm jobs.\n\n    Parameters\n    ----------\n    job_ids : Optional[Sequence[str]], optional\n        The job IDs to check, by default None (None meaning all jobs)\n    filter_by_user : bool, optional\n        Whether to only check jobs by the current user, by default True\n    \"\"\"\n    from rich.live import Live\n\n    from seml.console import console, pause_live_widget\n\n    mongodb_config = get_mongodb_config()\n    db = get_database(**mongodb_config)\n\n    def generate_table_fn():\n        return generate_queue_table(db, job_ids, filter_states, filter_by_user)\n\n    table = generate_table_fn()\n    if watch:\n        console.clear()\n        with pause_live_widget():\n            with Live(table, refresh_per_second=0.5) as live:\n                while True:\n                    time.sleep(2)\n                    live.update(generate_table_fn())\n    else:\n        console.print(table)\n\n\ndef print_command(\n    db_collection_name: str,\n    sacred_id: int | None,\n    batch_id: int | None,\n    filter_states: list[str],\n    filter_dict: dict | None,\n    num_exps: int,\n    worker_gpus: str | None = None,\n    worker_cpus: int | None = None,\n    worker_environment_vars: dict | None = None,\n    unresolved: bool = False,\n    resolve_interpolations: bool = True,\n):\n    import rich\n\n    from seml.console import Heading, console\n\n    collection = get_collection(db_collection_name)\n\n    filter_dict = build_filter_dict(filter_states, batch_id, filter_dict, sacred_id)\n\n    env_dict = get_environment_variables(\n        worker_gpus, worker_cpus, worker_environment_vars\n    )\n\n    orig_level = logging.root.level\n    logging.root.setLevel(logging.NOTSET)\n\n    exps_list = list(collection.find(filter_dict, limit=num_exps))\n    if len(exps_list) == 0:\n        return\n\n    exp = exps_list[0]\n    _, exe, config = get_command_from_exp(\n        exp,\n        collection.name,\n        verbose=logging.root.level <= logging.DEBUG,\n        unobserved=True,\n        post_mortem=False,\n        unresolved=unresolved,\n        resolve_interpolations=resolve_interpolations,\n    )\n    _, exe, vscode_config = get_command_from_exp(\n        exp,\n        collection.name,\n        verbose=logging.root.level <= logging.DEBUG,\n        unobserved=True,\n        post_mortem=False,\n        use_json=True,\n        unresolved=unresolved,\n        resolve_interpolations=resolve_interpolations,\n    )\n    env = exp['seml'].get('conda_environment')\n\n    console.print(Heading('First experiment'))\n    logging.info(f'Executable: {exe}')\n    if env is not None:\n        logging.info(f'Anaconda environment: {env}')\n\n    console.print(Heading('Arguments for VS Code debugger'))\n    rich.print_json(data=['with', '--debug'] + vscode_config)\n    console.print(Heading('Arguments for PyCharm debugger'))\n    print('with --debug ' + get_config_overrides(config))\n\n    console.print(Heading('Command for post-mortem debugging'))\n    interpreter, exe, config = get_command_from_exp(\n        exps_list[0],\n        collection.name,\n        verbose=logging.root.level <= logging.DEBUG,\n        unobserved=True,\n        post_mortem=True,\n        unresolved=unresolved,\n        resolve_interpolations=resolve_interpolations,\n    )\n    print(get_shell_command(interpreter, exe, config, env=env_dict))\n\n    console.print(Heading('Command for remote debugging'))\n    interpreter, exe, config = get_command_from_exp(\n        exps_list[0],\n        collection.name,\n        verbose=logging.root.level <= logging.DEBUG,\n        unobserved=True,\n        debug_server=True,\n        print_info=False,\n        unresolved=unresolved,\n        resolve_interpolations=resolve_interpolations,\n    )\n    print(get_shell_command(interpreter, exe, config, env=env_dict))\n\n    console.print(Heading('All raw commands'))\n    logging.root.setLevel(orig_level)\n    for exp in exps_list:\n        interpreter, exe, config = get_command_from_exp(\n            exp,\n            collection.name,\n            verbose=logging.root.level <= logging.DEBUG,\n            unresolved=unresolved,\n            resolve_interpolations=resolve_interpolations,\n        )\n        print(get_shell_command(interpreter, exe, config, env=env_dict))\n"
  },
  {
    "path": "src/seml/commands/project.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nfrom contextlib import contextmanager\nfrom pathlib import Path\n\nfrom seml.settings import SETTINGS\n\n\ndef init_project(\n    directory: str | Path = '.',\n    project_name: str | None = None,\n    user_name: str | None = None,\n    user_mail: str | None = None,\n    template: str = 'default',\n    git_remote: str | None = None,\n    git_commit: str | None = None,\n    yes: bool = False,\n):\n    \"\"\"\n    Initialize a new project in the given directory.\n\n    Parameters\n    ----------\n    directory : Union[str, Path]\n        The directory to initialize the project in.\n    project_name : Optional[str]\n        The name of the project. If not given, the name of the directory is used.\n    user_name : Optional[str]\n        The name of the user. If not given, the environment variable USER is used.\n    user_mail : Optional[str]\n        The email of the user. If not given, ''.\n    template : str\n        The template to use for the project.\n    git_repo : Optional[str]\n        The URL of the git repository to use.\n    git_commit : Optional[str]\n        The commit to use.\n    git_branch : Optional[str]\n        The branch to use.\n    yes : bool\n        If True, no confirmation is asked before initializing the project.\n    \"\"\"\n    from gitignore_parser import parse_gitignore  # type: ignore\n\n    from seml.console import prompt\n\n    if directory is None:\n        directory = Path()\n    directory = Path(directory).absolute()\n\n    # Ensure that the directory exists\n    if not directory.exists():\n        directory.mkdir(parents=True)\n\n    # Ensure that its state is okay\n    if any(directory.glob('**/*')) and not yes:\n        if not prompt(\n            f'Directory \"{directory}\" is not empty. Are you sure you want to initialize a new project here? (y/n)',\n            type=bool,\n        ):\n            exit(1)\n\n    with checkout_template_repo(git_remote, git_commit) as tmp_dir:\n        template_path = tmp_dir / 'templates' / template\n        if not template_path.exists():\n            logging.error(f'Template \"{template}\" does not exist')\n            exit(1)\n\n        logging.info(\n            f'Initializing project in \"{directory}\" using template \"{template}\".'\n        )\n\n        if project_name is None:\n            project_name = directory.name\n        if user_name is None:\n            user_name = os.getenv('USER', os.getenv('USERNAME', 'user'))\n        if user_mail is None:\n            user_mail = 'my@mail.com'\n        format_map = dict(\n            project_name=project_name, user_name=user_name, user_mail=user_mail\n        )\n\n        gitignore_path = template_path / '.gitignore'\n        if gitignore_path.exists():\n            ignore_file = parse_gitignore(gitignore_path)  # type: ignore\n        else:\n\n            def ignore_file(file_path: str):\n                return False\n\n        # Copy files one-by-one\n        for src in template_path.glob('**/*'):\n            # skip files ignored by .gitignore\n            if ignore_file(str(src)):\n                continue\n            # construct destination\n            file_name = src.relative_to(template_path)\n            target_file_name = Path(str(file_name).format_map(format_map))\n            dst = directory / target_file_name\n            # Create directories\n            if src.is_dir():\n                if not dst.exists():\n                    dst.mkdir()\n            elif not dst.exists():\n                # For templates fill in variables\n                if src.suffix.endswith('.template'):\n                    dst = dst.with_suffix(src.suffix.replace('.template', ''))\n                    dst.write_text(src.read_text().format_map(format_map))\n                else:\n                    # Other files copy directly\n                    dst.write_bytes(src.read_bytes())\n    logging.info('Project initialized successfully')\n\n\n@contextmanager\ndef checkout_template_repo(\n    git_remote: str | None = None, git_commit: str | None = None\n):\n    \"\"\"\n    Context manager to clone the template repository. The cloned repository\n    is deleted after the context is left.\n\n    Parameters\n    ----------\n    git_remote : Optional[str]\n        The git remote to use.\n    git_commit : Optional[str]\n        The git commit to use.\n    \"\"\"\n    import tempfile\n\n    from git import Repo\n\n    if git_remote is None:\n        git_remote = SETTINGS.TEMPLATE_REMOTE\n\n    with tempfile.TemporaryDirectory(dir=SETTINGS.TMP_DIRECTORY) as tmp_dir:\n        try:\n            repo = Repo.clone_from(git_remote, tmp_dir)\n            if git_commit is not None:\n                repo.head.reference = repo.commit(git_commit)  # type: ignore\n                repo.head.reset(index=True, working_tree=True)\n        except Exception as e:\n            logging.error(\n                f'Failed to clone git repository \"{git_remote}\" to \"{tmp_dir}\"'\n            )\n            logging.error(e)\n            exit(1)\n        yield Path(repo.working_dir)\n\n\ndef get_available_templates(\n    git_remote: str | None = None, git_commit: str | None = None\n) -> list[str]:\n    \"\"\"\n    Return a list of available templates.\n\n    Parameters\n    ----------\n    git_remote : Optional[str]\n        The git remote to use.\n    git_commit : Optional[str]\n        The git commit to use.\n\n    Returns\n    -------\n    List[str]\n        A list of available templates.\n    \"\"\"\n    with checkout_template_repo(git_remote, git_commit) as repo:\n        return [template.name for template in (repo / 'templates').iterdir()]\n\n\ndef print_available_templates(\n    git_remote: str | None = None, git_commit: str | None = None\n):\n    \"\"\"\n    Print the available templates.\n\n    Parameters\n    ----------\n    git_remote : Optional[str]\n        The git remote to use.\n    git_commit : Optional[str]\n        The git commit to use.\n    \"\"\"\n    result = 'Available templates:'\n    for template in get_available_templates(git_remote, git_commit):\n        result += f'\\n  - {template}'\n    logging.info(result)\n"
  },
  {
    "path": "src/seml/commands/slurm.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport subprocess\n\nfrom seml.commands.manage import detect_killed\nfrom seml.database import build_filter_dict, get_collection\nfrom seml.settings import SETTINGS\nfrom seml.utils import s_if\n\n\ndef hold_or_release_experiments(\n    hold: bool,\n    db_collection_name: str,\n    batch_id: int | None = None,\n):\n    \"\"\"\n    Holds or releases experiments that are currently in the SLURM queue.\n\n    Parameters\n    ----------\n    hold : bool\n        Whether to hold or release the experiments\n    db_collection_name : str\n        The collection to hold or release experiments from\n    batch_id : Optional[int], optional\n        Filter on the batch ID of experiments, by default None\n    \"\"\"\n    import shlex\n\n    detect_killed(db_collection_name, False)\n\n    filter_dict = build_filter_dict([*SETTINGS.STATES.PENDING], batch_id, None, None)\n    collection = get_collection(db_collection_name)\n    experiments = list(collection.find(filter_dict, {'slurm': 1}))\n\n    arrays = set()\n    for exp in experiments:\n        for slurm in exp['slurm']:\n            if (\n                'array_id' not in slurm\n            ):  # Skip experiments that are not in the SLURM queue\n                continue\n            arrays.add(slurm['array_id'])\n\n    opteration = 'hold' if hold else 'release'\n    subprocess.run(\n        f'scontrol {opteration} {shlex.quote(\" \".join(map(str, arrays)))}',\n        shell=True,\n        check=True,\n        stdout=subprocess.DEVNULL,\n    )\n    # User feedback\n    op_name = 'Held' if hold else 'Released'\n    n_exp = len(experiments)\n    n_job = len(arrays)\n    logging.info(\n        f'{op_name} {n_exp} experiment{s_if(n_exp)} in {n_job} job{s_if(n_job)} ({\",\".join(map(str, arrays))}).'\n    )\n"
  },
  {
    "path": "src/seml/commands/sources.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\n\nfrom seml.database import build_filter_dict, get_collection\nfrom seml.experiment.sources import load_sources_from_db\nfrom seml.settings import SETTINGS\n\nStates = SETTINGS.STATES\n\n\ndef download_sources(\n    target_directory: str,\n    collection_name: str,\n    sacred_id: int | None = None,\n    filter_states: list[str] | None = None,\n    batch_id: int | None = None,\n    filter_dict: dict | None = None,\n):\n    \"\"\"\n    Restore source files from the database to the provided path. This is a helper function for the CLI.\n\n    Parameters\n    ----------\n    target_directory: str\n        The directory where the source files should be restored.\n    collection_name: str\n        The name of the MongoDB collection.\n    sacred_id: int\n        The ID of the Sacred experiment.\n    filter_states: List[str]\n        The states of the experiments to filter.\n    batch_id: int\n        The ID of the batch.\n    filter_dict: Dict\n        Additional filter dictionary.\n    \"\"\"\n    from seml.console import prompt\n\n    filter_dict = build_filter_dict(\n        filter_states, batch_id, filter_dict, sacred_id=sacred_id\n    )\n    collection = get_collection(collection_name)\n    experiments = list(collection.find(filter_dict))\n    batch_ids = {exp['batch_id'] for exp in experiments}\n\n    if len(batch_ids) > 1:\n        logging.error(\n            f'Multiple source code versions found for batch IDs: {batch_ids}.'\n        )\n        logging.error('Please specify the target experiment more concretely.')\n        exit(1)\n\n    exp = experiments[0]\n    target_directory = os.path.expandvars(os.path.expanduser(target_directory))\n    if not os.path.exists(target_directory):\n        os.mkdir(target_directory)\n    if os.listdir(target_directory):\n        logging.warning(\n            f'Target directory \"{target_directory}\" is not empty. '\n            f'Files may be overwritten.'\n        )\n        if not prompt('Are you sure you want to continue? (y/n)', type=bool):\n            exit(0)\n\n    load_sources_from_db(exp, collection, target_directory)\n    logging.info(f'Source files restored to \"{target_directory}\".')\n"
  },
  {
    "path": "src/seml/commands/start.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport logging\nimport math\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport time\nimport uuid\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Sequence\n\nfrom seml.commands.manage import cancel_experiment_by_id, reset_slurm_dict\nfrom seml.database import build_filter_dict, get_collection\nfrom seml.document import ExecutionDoc, ExperimentDoc, SBatchOptions\nfrom seml.experiment.command import (\n    get_command_from_exp,\n    get_environment_variables,\n    get_shell_command,\n)\nfrom seml.experiment.config import check_slurm_config\nfrom seml.experiment.sources import load_sources_from_db\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    assert_package_installed,\n    find_jupyter_host,\n    load_text_resource,\n    merge_dicts,\n    s_if,\n    working_directory,\n)\nfrom seml.utils.errors import ArgumentError, ConfigError\nfrom seml.utils.slurm import (\n    get_cluster_name,\n    get_current_slurm_array_id,\n    get_current_slurm_job_id,\n    get_slurm_jobs,\n)\n\nif TYPE_CHECKING:\n    from pymongo.collection import Collection\n\nStates = SETTINGS.STATES\nSlurmStates = SETTINGS.SLURM_STATES\n\n\ndef get_experiment_environment(experiment: ExperimentDoc):\n    env = {}\n    if exp_env := experiment['seml'].get('env'):\n        env = dict(exp_env)\n    env = {**env, **SETTINGS.EXPERIMENT.ENVIRONMENT}\n    return env\n\n\ndef get_and_make_output_dir_path(config: ExperimentDoc):\n    with working_directory(config['seml']['working_dir']):\n        output_dir = config['seml'].get('output_dir', '.')\n        output_dir_path = Path(output_dir).expanduser().resolve()\n        try:\n            output_dir_path.mkdir(parents=True, exist_ok=True)\n        except PermissionError:\n            raise ConfigError(\n                f\"Output directory '{output_dir_path}' is not writable. \"\n                'Please choose a different directory.'\n            )\n    return str(output_dir_path)\n\n\ndef get_exp_name(exp_config: ExperimentDoc, db_collection_name: str):\n    return exp_config['seml'].get('name', db_collection_name)\n\n\ndef set_slurm_job_name(\n    sbatch_options: SBatchOptions,\n    name: str,\n    exp: ExperimentDoc,\n    db_collection_name: str,\n):\n    if 'job-name' in sbatch_options:\n        raise ConfigError(\n            \"Can't set sbatch `job-name` parameter explicitly. \"\n            'Use `name` parameter instead and SEML will do that for you.'\n        )\n    job_name = f'{name}_{exp[\"batch_id\"]}'\n    sbatch_options['job-name'] = job_name\n    if sbatch_options.get('comment', db_collection_name) != db_collection_name:\n        raise ConfigError(\n            \"Can't set sbatch `comment` parameter explicitly. \"\n            'SEML will do that for you and set it to the collection name.'\n        )\n    sbatch_options['comment'] = db_collection_name\n\n\ndef create_slurm_options_string(slurm_options: SBatchOptions, srun: bool = False):\n    \"\"\"\n    Convert a dictionary with sbatch_options into a string that can be used in a bash script.\n\n    Parameters\n    ----------\n    slurm_options: Dictionary containing the sbatch options.\n    srun: Construct options for an srun command instead of an sbatch script.\n\n    Returns\n    -------\n    slurm_options_str: sbatch option string.\n    \"\"\"\n    if srun:\n        option_structure = ' {prepend}{key}={value}'\n    else:\n        option_structure = '#SBATCH {prepend}{key}={value}\\n'\n\n    slurm_options_str = ''\n    for key, value_raw in slurm_options.items():\n        prepend = '-' if len(key) == 1 else '--'\n        if key in ['partition', 'p'] and isinstance(value_raw, list):\n            value = ','.join(value_raw)\n        else:\n            value = value_raw\n        slurm_options_str += option_structure.format(\n            prepend=prepend, key=key, value=value\n        )\n    slurm_options_str += option_structure.format(\n        prepend='--', key='export', value='ALL'\n    )\n    return slurm_options_str\n\n\ndef start_sbatch_job(\n    collection: Collection[ExperimentDoc],\n    exp_array: Sequence[ExperimentDoc],\n    slurm_options_id: int,\n    sbatch_options: SBatchOptions,\n    unobserved: bool = False,\n    name: str | None = None,\n    output_path: str = '.',\n    output_to_file: bool = True,\n    max_simultaneous_jobs: int | None = None,\n    experiments_per_job: int = 1,\n    debug_server: bool = False,\n):\n    \"\"\"Run a list of experiments as a job on the Slurm cluster.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    exp_array: List[dict]\n        List of chunks of experiments to run. Each chunk is a list of experiments.\n    unobserved: bool\n        Disable all Sacred observers (nothing written to MongoDB).\n    name: str\n        Job name, used by Slurm job and output file.\n    output_dir_path: str\n        Directory (relative to home directory) where to store the slurm output files.\n    sbatch_options: dict\n        A dictionary that contains options for #SBATCH, e.g. {'mem': 8000} to limit the job's memory to 8,000 MB.\n    max_simultaneous_jobs: int\n        Maximum number of Slurm jobs running simultaneously.\n    debug_server: bool\n        Run jobs with a debug server.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    from tempfile import NamedTemporaryFile\n\n    seml_conf = exp_array[0]['seml']\n\n    # Set Slurm job array options\n    num_tasks = math.ceil(len(exp_array) / experiments_per_job)\n    sbatch_options['array'] = f'0-{num_tasks - 1}'\n    if max_simultaneous_jobs is not None:\n        sbatch_options['array'] += f'%{max_simultaneous_jobs}'\n\n    # Set Slurm output parameter\n    if 'output' in sbatch_options:\n        raise ConfigError(\n            \"Can't set sbatch `output` Parameter explicitly. SEML will do that for you.\"\n        )\n    elif not output_to_file:\n        slurm_output_file = '/dev/null'\n    else:\n        slurm_output_file = f'{output_path}/{name}_%A_%a.out'\n        # Ensure that the output path exists\n        Path(slurm_output_file).parent.mkdir(exist_ok=True)\n\n    reschedule_output_file = f'{output_path}/{name}_%A_%a.reschedule'\n    Path(reschedule_output_file).parent.mkdir(exist_ok=True)\n    reschedule_request_file = f'{reschedule_output_file}.request'\n    reschedule_timeout = seml_conf.get('reschedule_timeout')\n    reschedule_signal_directive = ''\n    if reschedule_timeout:\n        reschedule_signal_directive = f'#SBATCH --signal=B:USR1@{reschedule_timeout}'\n\n    sbatch_options['output'] = slurm_output_file\n    sbatch_options['job-name'] = name\n\n    # Construct srun options if experiments_per_job > 1\n    check_slurm_config(experiments_per_job, sbatch_options)\n    srun_str = '' if experiments_per_job > 1 else 'srun '\n    # Construct sbatch options string\n    env = get_experiment_environment(exp_array[0])\n    sbatch_options_str = create_slurm_options_string(sbatch_options, False)\n\n    # Construct list with all experiment IDs\n    expid_strings = f'{\" \".join([str(exp[\"_id\"]) for exp in exp_array])}'\n\n    with_sources = 'source_files' in seml_conf\n    use_conda_env = seml_conf.get('conda_environment')\n    working_dir = seml_conf.get('working_dir', '${{SLURM_SUBMIT_DIR}}')\n\n    # Build arguments for the prepare_experiment script\n    prepare_args = ''\n    if with_sources:\n        prepare_args += ' --stored-sources-dir $tmpdir'\n    if logging.root.level <= logging.DEBUG:\n        prepare_args += ' --verbose'\n    if unobserved:\n        prepare_args += ' --unobserved'\n    if debug_server:\n        prepare_args += ' --debug-server'\n\n    variables = {\n        'sbatch_options': sbatch_options_str,\n        'working_dir': working_dir,\n        'use_conda_env': str(use_conda_env is not None).lower(),\n        'conda_env': seml_conf.get('conda_environment', '') if use_conda_env else '',\n        'exp_ids': expid_strings,\n        'with_sources': str(with_sources).lower(),\n        'db_collection_name': collection.name,\n        'prepare_args': prepare_args,\n        'tmp_directory': SETTINGS.TMP_DIRECTORY,\n        'experiments_per_job': str(experiments_per_job),\n        'maybe_srun': srun_str,\n        'reschedule_file': reschedule_output_file,\n        'reschedule_request_file': reschedule_request_file,\n        'reschedule_signal_directive': reschedule_signal_directive,\n    }\n    variables = {\n        **variables,\n        'setup_command': SETTINGS.SETUP_COMMAND.format(**variables),\n        'end_command': SETTINGS.END_COMMAND.format(**variables),\n    }\n    # Construct Slurm script\n    template = load_text_resource('templates/slurm/slurm_template.sh')\n    script = template.format(**variables)\n\n    # Dump the prepared script to a temporary file\n    with NamedTemporaryFile('w', dir=SETTINGS.TMP_DIRECTORY) as f:\n        f.write(script)\n        f.flush()\n\n        # Sbatch the script\n        try:\n            output = subprocess.run(\n                f'sbatch {f.name}', shell=True, check=True, capture_output=True, env=env\n            ).stdout\n        except subprocess.CalledProcessError as e:\n            logging.error(\n                f\"Could not start Slurm job via sbatch. Here's the sbatch error message:\\n\"\n                f'{e.stderr.decode(\"utf-8\")}'\n            )\n            exit(1)\n\n    # Now we just update the mongodb. So, if we are in unobserved mode, we can stop here.\n    if unobserved:\n        return\n\n    slurm_array_job_id = int(output.split(b' ')[-1])\n    slurm_output_file = slurm_output_file.replace('%A', str(slurm_array_job_id))\n    reschedule_output_file = reschedule_output_file.replace(\n        '%A', str(slurm_array_job_id)\n    )\n    cluster_name = get_cluster_name()\n    collection.update_many(\n        {'_id': {'$in': [exp['_id'] for exp in exp_array]}},\n        {\n            '$set': {\n                'status': States.PENDING[0],\n                f'slurm.{slurm_options_id}.array_id': slurm_array_job_id,\n                f'slurm.{slurm_options_id}.num_tasks': num_tasks,\n                f'slurm.{slurm_options_id}.output_files_template': slurm_output_file,\n                f'slurm.{slurm_options_id}.reschedule_file': reschedule_output_file,\n                f'slurm.{slurm_options_id}.sbatch_options': sbatch_options,\n                'execution.cluster': cluster_name,\n            }\n        },\n    )\n    return slurm_array_job_id\n\n\ndef start_srun_job(\n    collection: Collection[ExperimentDoc],\n    exp: ExperimentDoc,\n    srun_options: SBatchOptions,\n    seml_arguments: Sequence[str],\n):\n    \"\"\"Run a list of experiments as a job on the Slurm cluster.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    exp: dict\n        Experiment to run.\n    unobserved: bool\n        Disable all Sacred observers (nothing written to MongoDB).\n    srun_options: dict\n        A dictionary that contains arguments for srun, e.g. {'mem': 8000} to limit the job's memory to 8,000 MB.\n    seml_arguments: list\n        A list that contains arguments for seml, e.g. ['--debug-server']\n\n    Returns\n    -------\n    None\n    \"\"\"\n    from seml.console import pause_live_widget\n\n    with pause_live_widget():\n        # Construct srun options string\n        # srun will run 2 processes in parallel when ntasks is not specified. Probably because of hyperthreading.\n        if 'ntasks' not in srun_options:\n            srun_options['ntasks'] = 1\n        env = get_experiment_environment(exp)\n        srun_options_str = create_slurm_options_string(srun_options, True)\n\n        # Set command args for job inside Slurm\n        cmd_args = f'--local --sacred-id {exp[\"_id\"]} '\n        cmd_args += ' '.join(seml_arguments)\n\n        cmd = f'srun{srun_options_str} seml {collection.name} start {cmd_args}'\n        try:\n            subprocess.run(cmd, shell=True, check=True, env=env)\n        except subprocess.CalledProcessError as e:\n            if e.stderr:\n                logging.error(\n                    f\"Could not start Slurm job via srun. Here's the sbatch error message:\\n\"\n                    f'{e.stderr.decode(\"utf-8\")}'\n                )\n            else:\n                logging.error('Could not start Slurm job via srun.')\n            exit(1)\n\n\ndef start_local_job(\n    collection: Collection[ExperimentDoc],\n    exp: ExperimentDoc,\n    unobserved: bool = False,\n    post_mortem: bool = False,\n    output_dir_path: str = '.',\n    output_to_console: bool = False,\n    debug_server: bool = False,\n):\n    \"\"\"Run an experiment locally.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    exp: dict\n        Experiment to run.\n    unobserved: bool\n        Disable all Sacred observers (nothing written to MongoDB).\n    post_mortem: bool\n        Activate post-mortem debugging.\n    output_dir_path: str\n        Write the output to a file in `output_dir` given by the SEML config or in the current directory.\n    output_to_console:\n        Pipe all output (stdout and stderr) to the console.\n    debug_server: bool\n        Run job with a debug server.\n\n    Returns\n    -------\n    True if job was executed successfully; False if it failed; None if job was not started because the database entry\n    was not in the PENDING state.\n    \"\"\"\n    from seml.console import pause_live_widget\n\n    use_stored_sources = 'source_files' in exp['seml']\n\n    interpreter, exe, config = get_command_from_exp(\n        exp,\n        collection.name,\n        verbose=logging.root.level <= logging.DEBUG,\n        unobserved=unobserved,\n        post_mortem=post_mortem,\n        debug_server=debug_server,\n    )\n    cmd = get_shell_command(interpreter, exe, config)\n\n    wd_path = exp['seml']['working_dir'] if use_stored_sources else '.'\n    with working_directory(wd_path):\n        success = True\n        temp_dir = None\n        output_file = ''\n        try:\n            seml_config = exp['seml']\n\n            if use_stored_sources:\n                temp_dir = os.path.join(SETTINGS.TMP_DIRECTORY, str(uuid.uuid4()))\n                os.mkdir(temp_dir, mode=0o700)\n                load_sources_from_db(exp, collection, to_directory=temp_dir)\n                # To support `src` layouts we should add the src directory to the PYTHONPATH.\n                # https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/\n                src_temp_dir = os.path.join(temp_dir, 'src')\n                env = get_experiment_environment(exp)\n                env = {**env, 'PYTHONPATH': f'{temp_dir}:{src_temp_dir}:$PYTHONPATH'}\n                temp_exe = os.path.join(temp_dir, exe)\n                # update the command to use the temp dir\n                cmd = get_shell_command(interpreter, temp_exe, config, env=env)\n\n            if output_dir_path:\n                exp_name = get_exp_name(exp, collection.name)\n                output_file = f'{output_dir_path}/{exp_name}_{exp[\"_id\"]}.out'\n                if not unobserved:\n                    collection.update_one(\n                        {'_id': exp['_id']}, {'$set': {'seml.output_file': output_file}}\n                    )\n                if output_to_console:\n                    # redirect output to logfile AND output to console. See https://stackoverflow.com/a/34604684.\n                    # Alternatively, we could go with subprocess.Popen, but this could conflict with pdb.\n                    cmd = f'{cmd} 2>&1 | tee -a {output_file}'\n\n            if seml_config.get('conda_environment') is not None:\n                cmd = (\n                    f'. $(conda info --base)/etc/profile.d/conda.sh '\n                    f'&& conda activate {seml_config[\"conda_environment\"]} '\n                    f'&& {cmd} '\n                    f'&& conda deactivate'\n                )\n\n            if not unobserved:\n                execution = ExecutionDoc(cluster='local')  # type: ignore\n                if 'SLURM_JOBID' in os.environ:\n                    execution['array_id'] = int(os.environ['SLURM_JOBID'])\n                    execution['task_id'] = 0\n                collection.update_one(\n                    {'_id': exp['_id']},\n                    {'$set': {'execution': execution}},\n                )\n\n            logging.debug(f'Running the following command:\\n {cmd}')\n\n            if output_dir_path:\n                if output_to_console:\n                    # Let's pause the live widget so we can actually see the output.\n                    with pause_live_widget():\n                        subprocess.run(cmd, shell=True, check=True)\n                else:  # redirect output to logfile\n                    with open(output_file, 'w') as log_file:\n                        subprocess.run(\n                            cmd,\n                            shell=True,\n                            stderr=log_file,\n                            stdout=log_file,\n                            check=True,\n                        )\n            else:\n                with pause_live_widget():\n                    subprocess.run(cmd, shell=True, check=True)\n\n        except subprocess.CalledProcessError:\n            success = False\n        except OSError:\n            logging.error(f'Log file {output_file} could not be written.')\n            # Since Sacred is never called in case of I/O error, we need to set the experiment state manually.\n            if not unobserved:\n                collection.update_one(\n                    filter={'_id': exp['_id']},\n                    update={'$set': {'status': States.FAILED[0]}},\n                )\n            success = False\n        finally:\n            if use_stored_sources and temp_dir is not None:\n                # clean up temp directory\n                shutil.rmtree(temp_dir)\n\n    return success\n\n\ndef chunk_list(exps: Sequence[ExperimentDoc]):\n    \"\"\"\n    Divide experiments by batch id as these will be submitted jointly.\n    This assumes constant Slurm settings per batch (which should be the case if MongoDB wasn't edited manually).\n\n    Parameters\n    ----------\n    exps: list[dict]\n        List of dictionaries containing the experiment settings as saved in the MongoDB\n\n    Returns\n    -------\n    exp_chunks: list\n    \"\"\"\n    from collections import defaultdict\n\n    exp_chunks: dict[int, list[ExperimentDoc]] = defaultdict(list)\n    for exp in exps:\n        exp_chunks[exp['batch_id']].append(exp)\n    return list(exp_chunks.values())\n\n\ndef prepare_staged_experiments(\n    collection: Collection[ExperimentDoc],\n    filter_dict: dict[str, Any] | None = None,\n    num_exps: int = 0,\n    set_to_pending: bool = True,\n):\n    \"\"\"\n    Load experiments from the input MongoDB collection, and prepare them for running.\n    If the filter_dict contains no status or ID, we filter the status by STAGED.\n    If set_to_pending is True, we set their status to PENDING.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection with STAGED experiments.\n    filter_dict: dict\n        Optional dict with custom database filters.\n    num_exps: int\n        Only set <num_exps> experiments' state to PENDING. If 0, set all STAGED experiments to PENDING.\n    set_to_pending: bool\n        Whether to update the database entries to status PENDING.\n\n    Returns\n    -------\n    The filtered list of database entries.\n    \"\"\"\n    if filter_dict is None:\n        filter_dict = {}\n\n    query_dict = copy.deepcopy(filter_dict)\n    if '_id' not in query_dict and 'status' not in query_dict:\n        query_dict['status'] = {'$in': States.STAGED}\n\n    experiments: list[ExperimentDoc] = list(collection.find(query_dict, limit=num_exps))\n\n    if set_to_pending:\n        update_dict = {'$set': {'status': States.PENDING[0]}}\n\n        if num_exps > 0:\n            # Set only those experiments to PENDING which will be run.\n            collection.update_many(\n                {'_id': {'$in': [e['_id'] for e in experiments]}}, update_dict\n            )\n        else:\n            collection.update_many(query_dict, update_dict)\n\n        nexps_set = len(experiments)\n        logging.info(f'Setting {nexps_set} experiment{s_if(nexps_set)} to pending.')\n\n    return experiments\n\n\ndef add_to_slurm_queue(\n    collection: Collection[ExperimentDoc],\n    exps_list: Sequence[ExperimentDoc],\n    unobserved: bool = False,\n    post_mortem: bool = False,\n    output_to_file: bool = True,\n    output_to_console: bool = False,\n    srun: bool = False,\n    debug_server: bool = False,\n):\n    \"\"\"\n    Send the input list of experiments to the Slurm system for execution.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    exps_list: list of dicts\n        The list of database entries corresponding to experiments to be executed.\n    unobserved: bool\n        Whether to suppress observation by Sacred observers.\n    post_mortem: bool\n        Activate post-mortem debugging.\n    output_to_file: bool\n        Whether to capture output in a logfile.\n    output_to_console: bool\n        Whether to capture output in the console. This is currently not supported for Slurm jobs and will raise an\n        error if set to True.\n    srun: bool\n        Run jobs interactively via srun instead of using sbatch.\n    debug_server: bool\n        Run jobs with a debug server.\n\n    Returns\n    -------\n    None\n    \"\"\"\n\n    nexps = len(exps_list)\n    exp_arrays = chunk_list(exps_list)\n    narrays = 0\n    array_ids = []\n\n    for exp_array in exp_arrays:\n        slurm_options = exp_array[0]['slurm']\n        default_sbatch_options = slurm_options[0]['sbatch_options']\n        job_name = get_exp_name(exp_array[0], collection.name)\n        if srun:\n            set_slurm_job_name(\n                default_sbatch_options,\n                job_name,\n                exp_array[0],\n                collection.name,\n            )\n            assert len(exp_array) == 1\n            seml_arguments = []\n            seml_arguments.append('--debug')\n            if post_mortem:\n                seml_arguments.append('--post-mortem')\n            if output_to_console:\n                seml_arguments.append('--output-to-console')\n            if not output_to_file:\n                seml_arguments.append('--no-file-output')\n            if debug_server:\n                seml_arguments.append('--debug-server')\n            start_srun_job(\n                collection,\n                exp_array[0],\n                srun_options=default_sbatch_options,\n                seml_arguments=seml_arguments,\n            )\n            narrays += 1\n        else:\n            output_dir_path = get_and_make_output_dir_path(exp_array[0])\n            assert not post_mortem\n            for slurm_options_id, slurm_option in enumerate(slurm_options):\n                set_slurm_job_name(\n                    slurm_option['sbatch_options'],\n                    job_name,\n                    exp_array[0],\n                    collection.name,\n                )\n                array_id = start_sbatch_job(\n                    collection,\n                    exp_array,\n                    slurm_options_id,\n                    slurm_option['sbatch_options'],\n                    unobserved,\n                    name=job_name,\n                    output_path=output_dir_path,\n                    output_to_file=output_to_file,\n                    max_simultaneous_jobs=slurm_option.get('max_simultaneous_jobs'),\n                    experiments_per_job=slurm_option.get('experiments_per_job', 1),\n                    debug_server=debug_server,\n                )\n                array_ids.append(array_id)\n                narrays += 1\n    if nexps == 0:\n        logging.info('No experiments to run.')\n    else:\n        logging.info(\n            f'Started {nexps} experiment{s_if(nexps)} in '\n            f'{narrays} Slurm job array{s_if(narrays)}: {\", \".join(map(str, array_ids))}'\n        )\n\n\ndef check_compute_node():\n    if os.uname()[1] in SETTINGS.LOGIN_NODE_NAMES:\n        raise ArgumentError(\n            'Refusing to run a compute experiment on a login node. '\n            'Please use Slurm or a compute node.'\n        )\n\n\ndef start_local_worker(\n    collection: Collection[ExperimentDoc],\n    num_exps: int = 0,\n    filter_dict: dict[str, Any] | None = None,\n    unobserved: bool = False,\n    post_mortem: bool = False,\n    steal_slurm: bool = False,\n    output_to_console: bool = False,\n    output_to_file: bool = True,\n    gpus: str | None = None,\n    cpus: int | None = None,\n    environment_variables: dict[str, str] | None = None,\n    debug_server: bool = False,\n):\n    \"\"\"\n    Start a local worker on the current machine that pulls PENDING experiments from the database and executes them.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    num_exps: int\n        The maximum number of experiments run by this worker before terminating.\n    filter_dict: dict\n        Optional dict with custom database filters.\n    unobserved: bool\n        Whether to suppress observation by Sacred observers.\n    post_mortem: bool\n        Activate post-mortem debugging.\n    steal_slurm: bool\n        If True, the local worker will also execute jobs waiting for execution in Slurm.\n    output_to_console: bool\n        Whether to capture output in the console.\n    output_to_file: bool\n        Whether to capture output in a logfile.\n    gpus: str\n        Comma-separated list of GPU IDs to be used by this worker (e.g., \"2,3\"). Will be passed to CUDA_VISIBLE_DEVICES.\n    cpus: int\n        Number of CPU cores to be used by this worker. If None, use all cores.\n    environment_variables: dict\n        Optional dict of additional environment variables to be set.\n    debug_server: bool\n        Run jobs with a debug server.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    from rich.progress import Progress\n\n    from seml.console import pause_live_widget, prompt\n\n    check_compute_node()\n\n    if 'SLURM_JOBID' in os.environ:\n        node_str = subprocess.run(\n            'squeue -j ${SLURM_JOBID} -O nodelist:1000',\n            shell=True,\n            check=True,\n            capture_output=True,\n        ).stdout\n        node_id = node_str.decode('utf-8').split('\\n')[1].strip()\n        logging.info(f'SLURM assigned me the node(s): {node_id}')\n\n    if num_exps > 0:\n        logging.info(\n            f'Starting local worker thread that will run up to {num_exps} experiment{s_if(num_exps)}, '\n            f'or until no pending experiments remain.'\n        )\n    else:\n        logging.info(\n            'Starting local worker thread that will run experiments until no pending experiments remain.'\n        )\n        num_exps = int(1e30)\n\n    os.environ.update(get_environment_variables(gpus, cpus, environment_variables))\n\n    num_exceptions = 0\n    jobs_counter = 0\n\n    exp_query: dict[str, Any] = {}\n    if not unobserved:\n        exp_query['status'] = {'$in': States.PENDING}\n    if not steal_slurm:\n        exp_query['slurm'] = {'$elemMatch': {'array_id': {'$exists': False}}}\n\n    if filter_dict:\n        exp_query.update(filter_dict)\n\n    with pause_live_widget():\n        with Progress(auto_refresh=False) as progress:\n            task = progress.add_task('Running experiments...', total=None)\n            while collection.count_documents(exp_query) > 0 and jobs_counter < num_exps:\n                if unobserved:\n                    exp = collection.find_one(exp_query)\n                else:\n                    exp = collection.find_one_and_update(\n                        exp_query,\n                        {\n                            '$set': {\n                                'status': States.RUNNING[0],\n                                'execution.cluster': 'local',\n                            }\n                        },\n                    )\n                if exp is None:\n                    continue\n                if 'array_id' in exp.get('execution', {}):\n                    # Clean up MongoDB entry\n                    slurm_ids = {\n                        'array_id': exp['execution']['array_id'],\n                        'task_id': exp['execution']['task_id'],\n                    }\n                    reset_slurm_dict(exp)\n                    collection.replace_one({'_id': exp['_id']}, exp, upsert=False)\n\n                    # Cancel Slurm job; after cleaning up to prevent race conditions\n                    if prompt(\n                        f'SLURM is currently executing experiment {exp[\"_id\"]}, do you want to cancel the SLURM job?',\n                        type=bool,\n                    ):\n                        cancel_experiment_by_id(\n                            collection,\n                            exp['_id'],\n                            set_interrupted=False,\n                            slurm_dict=slurm_ids,\n                        )\n\n                progress.console.print(\n                    f'current id : {exp[\"_id\"]}, failed={num_exceptions}/{jobs_counter} experiments'\n                )\n\n                # Add newline if we need to avoid tqdm's output\n                if (\n                    debug_server\n                    or output_to_console\n                    or logging.root.level <= logging.DEBUG\n                ):\n                    print(file=sys.stderr)\n\n                if output_to_file:\n                    output_dir_path = get_and_make_output_dir_path(exp)\n                else:\n                    output_dir_path = None\n                try:\n                    success = start_local_job(\n                        collection=collection,\n                        exp=exp,\n                        unobserved=unobserved,\n                        post_mortem=post_mortem,\n                        output_dir_path=str(output_dir_path),\n                        output_to_console=output_to_console,\n                        debug_server=debug_server,\n                    )\n                    if success is False:\n                        num_exceptions += 1\n                except KeyboardInterrupt:\n                    logging.info('Caught KeyboardInterrupt signal. Aborting.')\n                    exit(1)\n                jobs_counter += 1\n                progress.advance(task)\n                # tq.set_postfix(current_id=exp['_id'], failed=f\"{num_exceptions}/{jobs_counter} experiments\")\n\n\ndef start_experiments(\n    db_collection_name: str,\n    local: bool,\n    sacred_id: int | None,\n    batch_id: int | None,\n    filter_dict: dict[str, Any] | None,\n    num_exps: int,\n    post_mortem: bool,\n    debug: bool,\n    debug_server: bool,\n    output_to_console: bool,\n    no_file_output: bool,\n    steal_slurm: bool,\n    no_worker: bool,\n    set_to_pending: bool = True,\n    worker_gpus: str | None = None,\n    worker_cpus: int | None = None,\n    worker_environment_vars: dict[str, str] | None = None,\n):\n    output_to_file = not no_file_output\n    launch_worker = not no_worker\n\n    if debug or debug_server:\n        num_exps = 1\n        unobserved = True\n        post_mortem = True\n        output_to_console = True\n        srun = True\n        logging.root.setLevel(logging.DEBUG)\n    else:\n        unobserved = False\n        srun = False\n\n    if local:\n        check_compute_node()\n\n    if not local:\n        local_kwargs = {\n            '--no-worker': no_worker,\n            '--steal-slurm': steal_slurm,\n            '--worker-gpus': worker_gpus,\n            '--worker-cpus': worker_cpus,\n            '--worker-environment-vars': worker_environment_vars,\n        }\n        for key, val in local_kwargs.items():\n            if val:\n                raise ArgumentError(\n                    f\"The argument '{key}' only works in local mode, not in Slurm mode.\"\n                )\n    if not local and not srun:\n        non_sbatch_kwargs = {\n            '--post-mortem': post_mortem,\n            '--output-to-console': output_to_console,\n        }\n        for key, val in non_sbatch_kwargs.items():\n            if val:\n                raise ArgumentError(\n                    f\"The argument '{key}' does not work in regular Slurm mode. \"\n                    \"Remove the argument or use '--debug'.\"\n                )\n\n    if unobserved:\n        set_to_pending = False\n\n    filter_dict = build_filter_dict([], batch_id, filter_dict, sacred_id)\n\n    collection = get_collection(db_collection_name)\n\n    staged_experiments = prepare_staged_experiments(\n        collection=collection,\n        filter_dict=filter_dict,\n        num_exps=num_exps,\n        set_to_pending=set_to_pending and local,\n    )\n\n    if not staged_experiments:\n        logging.info('No experiments to run.')\n        return\n    if debug_server:\n        use_stored_sources = 'source_files' in staged_experiments[0]['seml']\n        if use_stored_sources:\n            raise ArgumentError(\n                'Cannot use a debug server with source code that is loaded from the MongoDB. '\n                'Use the `--no-code-checkpoint` option when adding the experiment.'\n            )\n\n    if not local:\n        add_to_slurm_queue(\n            collection=collection,\n            exps_list=staged_experiments,\n            unobserved=unobserved,\n            post_mortem=post_mortem,\n            output_to_file=output_to_file,\n            output_to_console=output_to_console,\n            srun=srun,\n            debug_server=debug_server,\n        )\n    elif launch_worker:\n        start_local_worker(\n            collection=collection,\n            num_exps=num_exps,\n            filter_dict=filter_dict,\n            unobserved=unobserved,\n            post_mortem=post_mortem,\n            steal_slurm=steal_slurm,\n            output_to_console=output_to_console,\n            output_to_file=output_to_file,\n            gpus=worker_gpus,\n            cpus=worker_cpus,\n            environment_variables=worker_environment_vars,\n            debug_server=debug_server,\n        )\n\n\ndef start_jupyter_job(\n    sbatch_options: SBatchOptions | None = None,\n    conda_env: str | None = None,\n    lab: bool = False,\n):\n    if lab:\n        assert_package_installed(\n            'jupyterlab',\n            '`start-jupyter --lab` requires `jupyterlab` (e.g. `pip install jupyterlab`)',\n        )\n    else:\n        assert_package_installed(\n            'notebook',\n            '`start-jupyter` requires `notebook` (e.g. `pip install notebook`)',\n        )\n\n    sbatch_options = sbatch_options if sbatch_options is not None else {}\n    sbatch_options_merged = SETTINGS.SLURM_DEFAULT['sbatch_options']\n    sbatch_options_merged.update(SETTINGS.SBATCH_OPTIONS_TEMPLATES.JUPYTER)\n    sbatch_options_merged.update(sbatch_options)\n    # Construct sbatch options string\n    sbatch_options_str = create_slurm_options_string(sbatch_options_merged)\n\n    template = load_text_resource('templates/slurm/jupyter_template.sh')\n\n    script = template.format(\n        sbatch_options=sbatch_options_str,\n        use_conda_env=str(conda_env is not None).lower(),\n        conda_env=conda_env,\n        notebook_or_lab=' notebook' if not lab else '-lab',\n    )\n\n    path = os.path.join(SETTINGS.TMP_DIRECTORY, f'{uuid.uuid4()}.sh')\n    with open(path, 'w') as f:\n        f.write(script)\n\n    try:\n        output = subprocess.run(\n            f'sbatch {path}', shell=True, check=True, capture_output=True\n        ).stdout\n    except subprocess.CalledProcessError as e:\n        logging.error(\n            f\"Could not start Slurm job via sbatch. Here's the sbatch error message:\\n\"\n            f'{e.stderr.decode(\"utf-8\")}'\n        )\n        os.remove(path)\n        exit(1)\n    os.remove(path)\n\n    slurm_array_job_id = int(output.split(b' ')[-1])\n    logging.info(f'Queued Jupyter instance in Slurm job with ID {slurm_array_job_id}.')\n\n    job_output = subprocess.run(\n        f'scontrol show job {slurm_array_job_id} -o',\n        shell=True,\n        check=True,\n        capture_output=True,\n    ).stdout\n    job_output_results = job_output.decode('utf-8').split(' ')\n    job_info_dict = {\n        x.split('=')[0]: x.split('=')[1] for x in job_output_results if '=' in x\n    }\n    log_file = job_info_dict['StdOut']\n\n    logging.info(f\"The job's log-file is '{log_file}'.\")\n    logging.info(\n        'Waiting for start-up to fetch the machine and port of the Jupyter instance... '\n        '(ctrl-C to cancel fetching)'\n    )\n\n    while job_info_dict['JobState'] in SlurmStates.PENDING:\n        job_output = subprocess.run(\n            f'scontrol show job {slurm_array_job_id} -o',\n            shell=True,\n            check=True,\n            capture_output=True,\n        ).stdout\n        job_output_results = job_output.decode('utf-8').split(' ')\n        job_info_dict = {\n            x.split('=')[0]: x.split('=')[1] for x in job_output_results if '=' in x\n        }\n        time.sleep(1)\n    if job_info_dict['JobState'] not in SlurmStates.RUNNING:\n        logging.error(\n            f\"Slurm job failed. See log-file '{log_file}' for more information.\"\n        )\n        exit(1)\n\n    logging.info('Slurm job is running. Jupyter instance is starting up...')\n\n    # Obtain list of hostnames to addresses\n    url_str, known_host = find_jupyter_host(log_file, True)\n    if known_host is None:\n        logging.error(\n            f\"Could not fetch the host and port of the Jupyter instance. Here's the raw output: \\n\"\n            f'{url_str}'\n        )\n        exit(1)\n    if not known_host:\n        logging.warning('Host unknown to SLURM.')\n    logging.info(f\"Start-up completed. The Jupyter instance is running at '{url_str}'.\")\n    logging.info(f\"To stop the job, run 'scancel {slurm_array_job_id}'.\")\n\n\ndef get_experiment_to_prepare(\n    collection: Collection[ExperimentDoc],\n    exp_id: int,\n    unobserved: bool,\n):\n    \"\"\"\n    Retrieves the experiment the pending experiment with the given ID from the database and sets its state to RUNNING.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    exp_id: int\n        The ID of the experiment to retrieve.\n    unobserved: bool\n        Whether to suppress observation by Sacred observers.\n\n    Returns\n    -------\n    The experiment document if it was found and set to RUNNING, None otherwise.\n    \"\"\"\n    if unobserved:\n        # If the experiment has no observer, we just pull the configuration but never update the database.\n        return collection.find_one({'_id': exp_id})\n    # This returns the document as it was BEFORE the update. So we first have to check whether its state was\n    # PENDING. This is to avoid race conditions, since find_one_and_update is an atomic operation.\n    slurm_array_id, slurm_task_id = get_current_slurm_array_id()\n    if slurm_array_id is not None and slurm_task_id is not None:\n        # We're running in SLURM.\n        # Check if the job executing is this one.\n        cluster_name = get_cluster_name()\n        job_filter = {\n            '_id': exp_id,\n            'execution.array_id': int(slurm_array_id),\n            'execution.task_id': int(slurm_task_id),\n            'execution.cluster': cluster_name,\n        }\n        # Either take the experiment if it is pending or if it is the one being executed.\n        # The latter case is important for multi-node jobs.\n        return collection.find_one(job_filter)\n    # Steal slurm case\n    return collection.find_one({'_id': exp_id})\n\n\ndef claim_experiment(db_collection_name: str, exp_ids: Sequence[int]):\n    \"\"\"\n    Claim an experiment for execution by setting its state to RUNNING.\n\n    Parameters\n    ----------\n    db_collection_name: str\n        The name of the MongoDB collection containing the experiments.\n    exp_ids: Sequence[int]\n        The IDs of the experiments to claim.\n\n    Exit Codes\n    ----------\n    0: Experiment claimed successfully\n    3: Experiment not in the database\n\n    Stdout\n    -------\n    The ID of the claimed experiment.\n    \"\"\"\n    collection = get_collection(db_collection_name)\n    array_id, task_id = get_current_slurm_array_id()\n    exp = None\n    if array_id is not None and task_id is not None:\n        # We are running in slurm\n        array_id, task_id = int(array_id), int(task_id)\n        cluster_name = get_cluster_name()\n        update = {\n            'execution.array_id': array_id,\n            'execution.task_id': task_id,\n            'execution.cluster': cluster_name,\n        }\n        # First, we check whether this SLURM job is responsible for a RESCHEDULED experiment\n        # If so, we claim this first\n        exp = collection.find_one_and_update(\n            {\n                '_id': {'$in': list(exp_ids)},\n                'status': {'$in': States.RESCHEDULED},\n                'execution.array_id': array_id,\n                'execution.task_id': task_id,\n                'execution.cluster': cluster_name,\n            },\n            {'$set': {'status': States.RUNNING[0], **update}},\n            {'_id': 1, 'slurm': 1},\n        )\n        # Only after we have checked that this job is not responsible for a RESCHEDULED experiment\n        # can we pick up a pending one.\n        if exp is None:\n            exp = collection.find_one_and_update(\n                {\n                    '_id': {'$in': list(exp_ids)},\n                    'status': {'$in': States.PENDING},\n                },\n                {'$set': {'status': States.RUNNING[0], **update}},\n                {'_id': 1, 'slurm': 1},\n            )\n        if exp is None:\n            exit(3)\n        # Set slurm output file\n        for s_conf in exp['slurm']:\n            if s_conf['array_id'] == array_id:\n                output_file = s_conf['output_files_template']\n                output_file = output_file.replace('%a', str(task_id))\n                reschedule_file = s_conf.get('reschedule_file', '')\n                reschedule_file = reschedule_file.replace('%a', str(task_id))\n                collection.update_one(\n                    {'_id': exp['_id']},\n                    {\n                        '$set': {\n                            'execution.slurm_output_file': output_file,\n                            'execution.reschedule_file': reschedule_file,\n                        }\n                    },\n                )\n    else:\n        # Steal slurm\n        exp = collection.find_one_and_update(\n            {'_id': {'$in': list(exp_ids)}, 'status': {'$in': States.PENDING}},\n            {'$set': {'status': States.RUNNING[0], 'execution.cluster': 'local'}},\n            {'_id': 1},\n        )\n        if exp is None:\n            exit(3)\n    print(exp['_id'])\n    exit(0)\n\n\ndef prepare_experiment(\n    db_collection_name: str,\n    exp_id: int,\n    verbose: bool,\n    unobserved: bool,\n    post_mortem: bool,\n    stored_sources_dir: str | None,\n    debug_server: bool,\n):\n    \"\"\"\n    Prepare an experiment for execution by printing the command that should be executed.\n    If stored_sources_dir is set, the source files are loaded from the database and stored in the directory.\n\n    Parameters\n    ----------\n    db_collection_name: str\n        The name of the MongoDB collection containing the experiments.\n    exp_id: int\n        The ID of the experiment to prepare.\n    verbose: bool\n        Whether to print the command verbosely.\n    unobserved: bool\n        Whether to suppress observation by Sacred observers.\n    post_mortem: bool\n        Activate post-mortem debugging.\n    stored_sources_dir: str\n        The directory where the source files are stored.\n    debug_server: bool\n        Run job with a debug server.\n\n    Exit Codes\n    ----------\n    0: Preparation successful\n    3: Experiment is not in the database\n    4: Experiment is in the database but not in the PENDING state\n\n    Returns\n    -------\n    None\n    \"\"\"\n    from sacred.randomness import get_seed\n\n    from seml.utils.multi_process import (\n        is_local_main_process,\n        is_main_process,\n        is_running_in_multi_process,\n    )\n\n    # This process should only be executed once per node, so if we are not the main\n    # process per node, we directly exit.\n    if not is_local_main_process():\n        exit(0)\n\n    collection = get_collection(db_collection_name)\n    exp = get_experiment_to_prepare(collection, exp_id, unobserved)\n\n    if exp is None:\n        # These exit codes will be handled in the bash script\n        if collection.count_documents({'_id': exp_id}) == 0:\n            exit(4)\n        else:\n            exit(3)\n\n    if stored_sources_dir:\n        os.makedirs(stored_sources_dir, exist_ok=True)\n        if not os.listdir(stored_sources_dir):\n            assert 'source_files' in exp['seml'], (\n                '--stored-sources-dir is set but no source files are stored in the database.'\n            )\n            load_sources_from_db(exp, collection, to_directory=stored_sources_dir)\n\n    # The remaining part (updateing MongoDB & printing the python command) is only executed by the main process.\n    if not is_main_process():\n        exit(0)\n\n    # If we run in a multi task environment, we want to make sure that the seed is fixed once and\n    # all tasks start with the same seed. Otherwise, one could not reproduce the experiment as the\n    # seed would change on the child nodes. It is up to the user to distribute seeds if needed.\n    if is_running_in_multi_process():\n        if SETTINGS.CONFIG_KEY_SEED not in exp['config']:\n            exp['config'][SETTINGS.CONFIG_KEY_SEED] = get_seed()\n\n    # Let's generate a output file\n    output_dir = get_and_make_output_dir_path(exp)\n    try:\n        slurm_id = get_current_slurm_job_id()\n        assert slurm_id is not None, 'No SLURM job ID found.'\n        job_info = get_slurm_jobs(slurm_id)[0]\n        name = job_info['JobName']\n        array_id, task_id = get_current_slurm_array_id()\n        name = f'{name}_{array_id}_{task_id}'\n    except Exception:\n        name = str(uuid.uuid4())\n    output_file = f'{output_dir}/{name}_{exp[\"_id\"]}.out'\n\n    # Let's apply the reschedule updates, if we are rescheduling\n    if 'reschedule_config_update' in exp:\n        reschedule_update = exp['reschedule_config_update']\n        assert isinstance(reschedule_update, dict), (\n            f'Encountered faulty type {type(reschedule_update)} for reschedule_update in database.'\n        )\n        exp['config'] = merge_dicts(exp['config'], reschedule_update)\n\n    interpreter, exe, config = get_command_from_exp(\n        exp,\n        db_collection_name,\n        verbose=verbose,\n        unobserved=unobserved,\n        post_mortem=post_mortem,\n        debug_server=debug_server,\n    )\n    cmd = get_shell_command(interpreter, exe, config)\n    cmd_unresolved = get_shell_command(\n        *get_command_from_exp(\n            exp,\n            db_collection_name,\n            verbose=verbose,\n            unobserved=unobserved,\n            post_mortem=post_mortem,\n            debug_server=debug_server,\n            unresolved=True,\n        )\n    )\n    updates = {\n        'seml.command': cmd,\n        'seml.command_unresolved': cmd_unresolved,\n        'seml.output_file': output_file,\n    }\n\n    if stored_sources_dir:\n        temp_dir = stored_sources_dir\n        # Store the temp dir for debugging purposes\n        updates['seml.temp_dir'] = temp_dir\n        cmd = get_shell_command(interpreter, os.path.join(temp_dir, exe), config)\n\n    if not unobserved:\n        collection.update_one({'_id': exp['_id']}, {'$set': updates})\n\n    # Print the command to be ran.\n    print(f'{cmd} >> {output_file} 2>&1')\n    # We exit with 0 to signal that the preparation was successful.\n    exit(0)\n"
  },
  {
    "path": "src/seml/console/__init__.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport os\nfrom contextlib import contextmanager\nfrom typing import Sequence\n\nimport rich\nimport rich.box\nimport rich.progress\nimport rich.table\nfrom rich.console import Console\nfrom rich.padding import Padding\nfrom rich.rule import Rule\nfrom typer import prompt as typer_prompt\n\ntry:\n    terminal_width = os.get_terminal_size().columns\nexcept OSError:\n    from seml.settings import SETTINGS\n\n    terminal_width = SETTINGS.EXPERIMENT.TERMINAL_WIDTH\n\nconsole = Console(\n    width=terminal_width,\n)\n\nTable = functools.partial(\n    rich.table.Table,\n    collapse_padding=True,\n    show_lines=False,\n    show_edge=False,\n    box=rich.box.SIMPLE,\n    row_styles=['none', 'dim'],\n    padding=(\n        0,\n        0,\n    ),\n    highlight=True,\n)\n\n\n@contextmanager\ndef pause_live_widget():\n    prev_live = console._live\n    if prev_live:\n        prev_live.stop()\n    console.clear_live()\n    yield\n    if prev_live:\n        prev_live.start()\n\n\n@functools.wraps(rich.progress.track)\ndef track(*args, **kwargs):\n    \"\"\"\n    Wrapper for `rich.progress.track` that uses the global console instance and\n    avoids creating empty ipython widgets in jupyter notebooks.\n\n    Parameters\n    ----------\n    *args : Any\n        Positional arguments to pass to `rich.progress.track`.\n    **kwargs : Any\n        Keyword arguments to pass to `rich.progress.track`.\n    \"\"\"\n    # Directly return the sequence if the track is disabled. This avoids empty\n    # ipywidgets in jupyter instances.\n    if kwargs.get('disable', False):\n        if len(args) == 0:\n            yield from kwargs['sequence']\n        else:\n            yield from args[0]\n        return\n\n    if console not in kwargs:\n        kwargs['console'] = console\n\n    # Since there can only be one live instance at a time, we first need to stop\n    # the previous one and then restart it after the new one is done.\n    with pause_live_widget():\n        yield from rich.progress.track(*args, **kwargs)\n\n\n@functools.wraps(typer_prompt)\ndef prompt(*args, **kwargs):\n    with pause_live_widget():\n        return typer_prompt(*args, **kwargs)\n\n\ndef Heading(text: str):\n    \"\"\"\n    Convenience function to create a seperator in the console.\n\n    Parameters\n    ----------\n    text : str\n        The text to display in the seperator.\n    \"\"\"\n    return Padding(Rule(text, style='red'), pad=(0, 0, 0, 0))\n\n\ndef list_items(items: Sequence[str]):\n    \"\"\"\n    Print a list of items in columns, using as many columns as possible\n    while keeping the items readable. This will look similar to bash autcompletition suggestions.\n\n    Parameters\n    ----------\n    items : Sequence[str]\n        The items to print.\n    \"\"\"\n    # Calculate available width\n    available_width = console.width\n\n    # Determine the number of columns based on available width\n    max_len = max(len(s) for s in items)\n    num_columns = max(1, available_width // (max_len + 1))\n\n    # Create a table with the calculated number of columns\n    table = rich.table.Table.grid(expand=True, pad_edge=True)\n    for i in range(num_columns):\n        table.add_column()\n\n    # Add suggestions to the table, distributing them across columns\n    for i in range(0, len(items), num_columns):\n        table.add_row(*items[i : i + num_columns])\n\n    # Print the table\n    console.print(table)\n"
  },
  {
    "path": "src/seml/database.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport logging\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Iterable, TypeVar, overload\n\nfrom seml.document import ExperimentDoc\nfrom seml.settings import SETTINGS\nfrom seml.utils import Hashabledict, s_if\nfrom seml.utils.errors import MongoDBError\nfrom seml.utils.ssh_forward import get_forwarded_mongo_client\n\nif TYPE_CHECKING:\n    import pymongo\n    import pymongo.collection\n    import pymongo.database\n    from bson import ObjectId\n\nStates = SETTINGS.STATES\n\n\n# The private method only accepts hashable dicts such that we can use it as a cache key.\n@functools.lru_cache(1)\ndef _get_collection(collection_name: str, mongodb_config: Hashabledict | None = None):\n    if mongodb_config is None:\n        config = get_mongodb_config()\n    else:\n        config = dict(mongodb_config)\n    db = get_database(**config)\n    return db[collection_name]\n\n\ndef get_collection(collection_name: str, mongodb_config: dict[str, Any] | None = None):\n    if mongodb_config is not None:\n        mongodb_config = Hashabledict(mongodb_config)\n    return _get_collection(collection_name, mongodb_config)\n\n\ndef get_mongo_client(\n    db_name: str,\n    host: str,\n    port: int,\n    username: str,\n    password: str,\n    ssh_config: dict[str, Any] | None = None,\n    **kwargs,\n) -> pymongo.MongoClient[ExperimentDoc]:\n    import pymongo\n\n    if ssh_config is not None:\n        client = get_forwarded_mongo_client(\n            db_name, username, password, ssh_config, **kwargs\n        )\n    else:\n        client = pymongo.MongoClient[ExperimentDoc](\n            host,\n            int(port),\n            username=username,\n            password=password,\n            authSource=db_name,\n            **kwargs,\n        )\n    return client\n\n\ndef get_database(\n    db_name: str, host: str, port: int, username: str, password: str, **kwargs\n):\n    db = get_mongo_client(db_name, host, port, username, password, **kwargs)[db_name]\n    return db\n\n\ndef get_collections_from_mongo_shell_or_pymongo(\n    db_name: str, host: str, port: int, username: str, password: str, **kwargs\n) -> list[str]:\n    \"\"\"Gets all collections in the database by first using the mongo shell and if that fails uses pymongo.\n\n    Args:\n        db_name (str): the name of the database\n        host (str): the name of the host\n        port (int): the port at which to access the mongodb\n        username (str): the username\n        password (str): the password\n\n    Returns:\n        List[str]: all collections in the database\n    \"\"\"\n    import subprocess\n\n    cmd = (\n        f\"mongo -u '{username}' --authenticationDatabase '{db_name}' {host}:{port}/{db_name} -p {password} \"\n        \"--eval 'db.getCollectionNames().forEach(function(f){print(f)})' --quiet\"\n    )\n    try:\n        output = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL)\n        collection_names = output.decode('utf-8').split('\\n')\n    except (subprocess.CalledProcessError, SyntaxError):\n        db = get_database(db_name, host, port, username, password, **kwargs)\n        collection_names = db.list_collection_names()\n    return [name for name in collection_names if name not in ('fs.chunks', 'fs.files')]\n\n\n@functools.cache\ndef get_mongodb_config(path: str | Path = SETTINGS.DATABASE.MONGODB_CONFIG_PATH):\n    \"\"\"Read the MongoDB connection configuration.\n\n    Reads the file at the provided path or otherwise {SETTINGS.DATABASE.MONGODB_CONFIG_PATH} to get\n        - database host\n        - database port\n        - database name\n        - username\n        - password\n        - directConnection (Optional)\n        - ssh_config (Optional)\n\n    Default path is $HOME/.config/seml/mongodb.config.\n\n    Config file should be in the format:\n    username: <your_username>\n    password: <your_password>\n    port: <port>\n    database: <database_name>\n    host: <host>\n    directConnection: <bool> (Optional)\n    ssh_config: <dict> (Optional)\n      ssh_address_or_host: <the url of the jump host>\n      ssh_pkey: <the ssh host key>\n      ssh_username: <username for jump host>\n      retries_max: <number of retries to establish shh tunnel, default 6> (Optional)\n      retries_delay: <initial wait time for exponential retry, default 1> (Optional)\n      lock_file: <lockfile to avoid establishing ssh tunnel parallely, default `~/seml_ssh.lock`> (Optional)\n      lock_timeout: <timeout for aquiring lock, default 30> (Optional)\n      ** further arguments passed to `SSHTunnelForwarder` (see https://github.com/pahaz/sshtunnel)\n\n    Returns\n    -------\n    dict\n        Contains the MongoDB config as detailed above.\n\n    \"\"\"\n    import yaml\n\n    access_dict = {}\n    config_str = '\\nPlease run `seml configure` to provide your credentials.'\n    path = Path(path)\n\n    if not path.exists():\n        raise MongoDBError(\n            f\"MongoDB credentials could not be read at '{path}'.{config_str}\"\n        )\n\n    with open(str(path)) as conf:\n        access_dict = yaml.safe_load(conf)\n\n    required_entries = ['username', 'password', 'port', 'host', 'database']\n    for entry in required_entries:\n        if entry not in access_dict:\n            raise MongoDBError(f\"No {entry} found in '{path}'.{config_str}\")\n\n    db_port = access_dict['port']\n    db_name = access_dict['database']\n    db_host = access_dict['host']\n    db_username = access_dict['username']\n    db_password = access_dict['password']\n    # False is the default value for PyMongo > 4.0\n    db_direct = (\n        access_dict['directConnection'] == 'True'\n        if 'directConnection' in access_dict\n        else False\n    )\n\n    cfg = {\n        'password': db_password,\n        'username': db_username,\n        'host': db_host,\n        'db_name': db_name,\n        'port': db_port,\n        'directConnection': db_direct,\n    }\n\n    if 'ssh_config' not in access_dict:\n        return cfg\n\n    cfg['ssh_config'] = access_dict['ssh_config']\n    cfg['ssh_config']['remote_bind_address'] = (db_host, db_port)\n    cfg['directConnection'] = True\n\n    return cfg\n\n\ndef build_filter_dict(\n    filter_states: Iterable[str] | None,\n    batch_id: int | None,\n    filter_dict: dict[str, Any] | None,\n    sacred_id: int | None = None,\n):\n    \"\"\"\n    Construct a dictionary to be used for filtering a MongoDB collection.\n\n    Parameters\n    ----------\n    filter_states: list\n        The states to filter for, e.g. [\"RUNNING\", \"PENDING\"].\n    batch_id: int\n        The batch ID to filter.\n    filter_dict: dict\n        The filter dict passed via the command line. Values in here take precedence over values passed via\n        batch_id or filter_states.\n    sacred_id: int\n        Sacred ID (_id in the database collection) of the experiment. Takes precedence over other filters.\n\n    Returns\n    -------\n    filter_dict: dict\n    \"\"\"\n\n    if sacred_id is not None:\n        return {'_id': sacred_id}\n\n    if filter_dict is None:\n        filter_dict = {}\n\n    if filter_states is not None:\n        filter_states = list(filter_states)\n\n    if filter_states is not None and len(filter_states) > 0:\n        if 'status' not in filter_dict:\n            filter_dict['status'] = {'$in': filter_states}\n        else:\n            logging.warning(\n                f\"'status' was defined in the filter dictionary passed via the command line (-f): \"\n                f'{filter_dict[\"status\"]} AND --status was set to {filter_states}. '\n                f\"I'm using the value passed via -f.\"\n            )\n\n    if batch_id is not None:\n        if 'batch_id' not in filter_dict:\n            filter_dict['batch_id'] = batch_id\n        else:\n            logging.warning(\n                f\"'batch_id' was defined in the filter dictionary passed via the command line (-f): \"\n                f'{filter_dict[\"status\"]} AND --batch-id was set to {filter_states}. '\n                f\"I'm using the value passed via -f.\"\n            )\n    return filter_dict\n\n\nT = TypeVar('T')\n\n\n@overload\ndef get_max_in_collection(\n    collection: pymongo.collection.Collection[ExperimentDoc],\n    field: str,\n    cls: type[T],\n) -> T | None: ...\n\n\n@overload\ndef get_max_in_collection(\n    collection: pymongo.collection.Collection[ExperimentDoc],\n    field: str,\n    cls: None = None,\n) -> Any | None: ...\n\n\ndef get_max_in_collection(\n    collection: pymongo.collection.Collection[ExperimentDoc],\n    field: str,\n    cls: type[T] | None = None,\n) -> T | Any | None:\n    \"\"\"\n    Find the maximum value in the input collection for the input field.\n    Parameters\n    ----------\n    collection\n    field\n\n    Returns\n    -------\n    max_val: the maximum value in the field.\n    \"\"\"\n    import pymongo\n\n    if result := collection.find_one(\n        projection={field: 1}, sort=[(field, pymongo.DESCENDING)], limit=1\n    ):\n        var = result.get(field)\n        if cls is not None and not isinstance(var, cls):\n            raise ValueError(f'Expected {cls}, got {type(var)}')\n        return var\n    return None\n\n\ndef upload_file(\n    filename: str,\n    db_collection: pymongo.collection.Collection[ExperimentDoc] | str,\n    batch_id: int,\n    filetype: str,\n):\n    \"\"\"\n    Upload a source file to the MongoDB.\n    Parameters\n    ----------\n    filename: str\n    db_collection: Collection\n    batch_id: int\n    filetype: str\n\n    Returns\n    -------\n    file_id: ID of the inserted file, or None if there was an error.\n    \"\"\"\n    import gridfs\n\n    if isinstance(db_collection, str):\n        db_collection = get_collection(db_collection)\n\n    db = db_collection.database\n    fs = gridfs.GridFS(db)\n    try:\n        with open(filename, 'rb') as f:\n            db_filename = f'file://{db_collection.name}/{batch_id}/{filename}'\n            file_id = fs.put(\n                f,\n                filename=db_filename,\n                metadata={\n                    'collection_name': db_collection.name,\n                    'batch_id': batch_id,\n                    'type': filetype,\n                },\n            )\n            return file_id\n    except OSError:\n        logging.error(f'IOError: could not read {filename}')\n    return None\n\n\ndef upload_file_mt(x: tuple[str, str, int, str]) -> ObjectId | None:\n    \"\"\"\n    A multithreading wrapper for upload_file.\n    \"\"\"\n    return upload_file(*x)\n\n\ndef delete_files(\n    database: pymongo.database.Database[ExperimentDoc],\n    file_ids: Iterable[ObjectId | str],\n):\n    file_ids = list(file_ids)\n    if len(file_ids) == 0:\n        return\n    # This does the same as GridFs(database).delete(file_id), but for multiple files\n    database.fs.files.delete_many({'_id': {'$in': file_ids}})\n    database.fs.chunks.delete_many({'files_id': {'$in': file_ids}})\n\n\ndef clean_unreferenced_artifacts(\n    db_collection_name: str | None = None, yes: bool = False\n):\n    \"\"\"\n    Delete orphaned artifacts from the database. That is, artifacts that were generated by experiments, but whose\n    experiment's database entry has been removed. This leads to storage accumulation, and this function cleans this\n    excess storage.\n    Parameters\n    ----------\n    db_collection_name: str, optional\n        Database collection to be scanned. If None, all collections will be scanned.\n    yes: bool\n        Whether to automatically confirm the deletion dialog.\n    Returns\n    -------\n    None\n    \"\"\"\n    from seml.console import prompt, track\n\n    all_collections = not bool(db_collection_name)\n    if all_collections:\n        config = get_mongodb_config()\n        db = get_database(**config)\n        collection_names = db.list_collection_names()\n    else:\n        collection = get_collection(db_collection_name)\n        db = collection.database\n        collection_names = [collection.name]\n    collection_names = set(collection_names)\n    collection_blacklist = {'fs.chunks', 'fs.files'}\n    collection_names = collection_names - collection_blacklist\n\n    referenced_files: set[ObjectId] = set()\n    tq = track(collection_names)\n    logging.info('Scanning collections for orphaned artifacts...')\n    for collection_name in tq:\n        collection = db[collection_name]\n        experiments = list(\n            collection.find(\n                {}, {'artifacts': 1, 'experiment.sources': 1, 'seml.source_files': 1}\n            )\n        )\n        for exp in experiments:\n            if 'artifacts' in exp:\n                try:\n                    referenced_files.update({x[1] for x in exp['artifacts']})\n                except KeyError:\n                    referenced_files.update({x['file_id'] for x in exp['artifacts']})\n            if 'experiment' in exp and 'sources' in exp['experiment']:\n                referenced_files.update({x[1] for x in exp['experiment']['sources']})\n            if 'seml' in exp and 'source_files' in exp['seml']:\n                referenced_files.update({x[1] for x in exp['seml']['source_files']})\n\n    all_files_in_db = list(\n        db['fs.files'].find({}, {'_id': 1, 'filename': 1, 'metadata': 1})\n    )\n    filtered_file_ids: set[ObjectId] = set()\n    for file in all_files_in_db:\n        if 'filename' in file:\n            filename = file['filename']\n            file_collection = None\n            if (\n                filename.startswith('file://')\n                and 'metadata' in file\n                and file['metadata']\n            ):\n                # seml-uploaded source\n                metadata = file['metadata']\n                file_collection = metadata.get('collection_name')\n            elif filename.startswith('artifact://'):\n                # artifact uploaded by Sacred\n                filename = filename[11:]\n                file_collection = filename.split('/')[0]\n            if (\n                file_collection is not None and file_collection in collection_names\n            ) or all_collections:\n                # only delete files corresponding to collections we want to clean\n                filtered_file_ids.add(file['_id'])\n\n    not_referenced_artifacts = filtered_file_ids - referenced_files\n    n_delete = len(not_referenced_artifacts)\n    if n_delete == 0:\n        logging.info('No unreferenced artifacts found.')\n        return\n\n    logging.info(\n        f'Deleting {n_delete} unreferenced artifact{s_if(n_delete)} from database {db.name}. WARNING: This cannot be undone! Artifacts/files might have been inserted to MongoDB manually or by tools other than seml/sacred. They will be deleted.'\n    )\n    if not yes and not prompt('Are you sure? (y/n)', type=bool):\n        exit(1)\n    logging.info('Deleting unreferenced artifacts...')\n    delete_files(db, not_referenced_artifacts)\n    logging.info(\n        f'Successfully deleted {n_delete} unreferenced artifact{s_if(n_delete)}.'\n    )\n\n\ndef update_working_dir(\n    db_collection_name: str,\n    working_directory: str,\n    batch_ids: list[int] | None = None,\n):\n    \"\"\"Changes the working directory of experiments in the database.\n\n    Parameters\n    ----------\n    db_collection_name : str\n        The collection to change the working directory in.\n    working_directory: str\n        The new working directory.\n    batch_ids : Optional[List[int]], optional\n        Filter on the batch ids, by default None\n    \"\"\"\n\n    collection = get_collection(db_collection_name)\n    if batch_ids is not None and len(batch_ids) > 0:\n        filter_dict = {'batch_id': {'$in': list(batch_ids)}}\n    else:\n        filter_dict = {}\n\n    working_directory = str(Path(working_directory).expanduser().resolve())\n\n    update_result = collection.update_many(\n        filter_dict,\n        {'$set': {'seml.working_dir': working_directory}},\n    )\n    logging.info(\n        f'Updated the working directory of {update_result.modified_count} experiments.'\n    )\n"
  },
  {
    "path": "src/seml/document.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, List, Optional, Union\n\nfrom typing_extensions import NotRequired, Required, TypeAlias, TypedDict\n\nif TYPE_CHECKING:\n    from datetime import datetime\n\n    from bson import ObjectId\n\n\nVersion: TypeAlias = List[Union[int, str]]\n\n\nclass SemlDocBase(TypedDict, total=False):\n    \"\"\"\n    All configurations of the `seml` block that are shared between the configuration file\n    and the MongoDB entry\n\n    Attributes\n    ----------\n    conda_environment : str | None\n        The conda environment to use. None will use the current environment.\n    description : str, optional\n        The description of the experiment.\n    executable : str\n        The executable python script to run. The path is relative to the working_dir or project_root_dir.\n    name: str, optional\n        The name of the experiment. Will be used for the slurm job. If None, the collection name will be used.\n    output_dir : str\n        The output directory of the experiment. The path is relative to the project root directory.\n    stash_all_py_files : bool\n        Whether to stash all python files or not. Otherwise, only imported files will be stashed.\n    \"\"\"\n\n    conda_environment: Required[str | None]\n    executable: Required[str]\n    output_dir: str\n    description: str\n    name: str\n    stash_all_py_files: bool\n    reschedule_timeout: int | None\n\n\nclass SemlFileConfig(SemlDocBase, total=False):\n    \"\"\"\n    The configuration of the `seml` block in the configuration file.\n\n    Attributes\n    ----------\n    project_root_dir : str\n        The root directory of the project.\n    \"\"\"\n\n    project_root_dir: str\n    working_dir: str\n\n\nclass SemlDoc(SemlDocBase, total=False):\n    \"\"\"\n    output_file: str\n        The output file of the experiment where the output is stored.\n    source_files: list[tuple[str, ObjectId]]\n        The source files of the experiment. The first entry is the relative path to the source file.\n        The second entry is the ObjectId of the file in the database.\n    version: Version\n        The version of seml which was used to create the experiment.\n    working_dir: str\n        The working directory of the experiment. This is an absolute path that must exist on all machines.\n    command: str\n        The CLI command that has been executed to run the experiment.\n    command_unresolved: str\n        The CLI command with unresolved named configurations that runs the experiment.\n    temp_dir: str\n        The temporary directory which has been used to restore source files from the DB.\n    env: dict[str, str]\n        The environment variables that were used to run the experiment.\n    \"\"\"\n\n    output_file: str\n    source_files: list[tuple[str, ObjectId]]\n    version: Required[Version]\n    working_dir: Required[str]\n    # Runtime populated\n    command: str | None\n    command_unresolved: str | None\n    temp_dir: str | None\n    env: dict[str, str] | None\n\n\nclass SemlConfig(SemlDoc, total=False):\n    \"\"\"\n    Intermediate configuration of the `seml` block in the configuration file.\n\n    Attributes\n    ----------\n    version: Version\n        The version of seml which was used to create the experiment.\n    use_uploaded_sources : bool\n        Whether to use the uploaded sources or not.\n    working_dir: str\n        The working directory of the experiment. This is an absolute path that must exist on all machines.\n    \"\"\"\n\n    use_uploaded_sources: Required[bool]\n\n\n# For job-name we must use the functional syntax\nSBatchOptions = TypedDict(\n    'SBatchOptions',\n    {\n        'array': str,\n        'comment': str,\n        'cpus_per_task': int,\n        'gres': str,\n        'job-name': Optional[str],\n        'mem': str,\n        'nodes': int,\n        'ntasks': int,\n        'partition': str,\n        'output': str,\n        'time': str,\n    },\n    total=False,\n)\n\n\nclass SlurmConfig(TypedDict):\n    \"\"\"\n    The valid configuration for the SLURM block in a seml config yaml file.\n\n    Attributes\n    ----------\n    experiments_per_job : int\n        The number of experiments to run in parallel per Slurm job.\n    sbatch_options_template : str | None, optional\n        The template for the sbatch options. If None, the options will be set directly. Templates must be defined via the settings.py.\n    sbatch_options : SBatchOptions\n        The sbatch options for the SLURM job.\n    \"\"\"\n\n    experiments_per_job: int\n    sbatch_options_template: NotRequired[str | None]\n    sbatch_options: SBatchOptions\n\n\nclass SlurmDoc(SlurmConfig):\n    \"\"\"\n    The slurm block of a document retrieved from the database.\n\n    Attributes\n    ----------\n    array_id : int\n        The array ID of the SLURM job.\n    num_tasks : int\n        The number of tasks in the SLURM job.\n    output_files_template : str\n        The template for the output files. The template must contain the placeholders {array_id} and {task_id}.\n    reschedule_file : str\n        The path to the reschedule file for this SLURM job. This is set regardless of whether an experiment\n        is actually executed by this SLURM job, i.e. whether the job manages to claim the experiment.\n    \"\"\"\n\n    array_id: int\n    num_tasks: int\n    output_files_template: str\n    reschedule_file: str\n\n\nclass GitDoc(TypedDict):\n    \"\"\"\n    The git block of a document retrieved from the database.\n\n    Attributes\n    ----------\n    path: str\n        Path at the remote repository, e.g., git@github.com:TUM-DAML/seml.git\n    commit: str\n        The commit hash at the time of submission.\n    dirty: bool\n        Whether the repository was dirty at the time of submission.\n    \"\"\"\n\n    path: str\n    commit: str\n    dirty: bool\n\n\nclass ExecutionDoc(TypedDict):\n    \"\"\"\n    The execution block of a document retrieved from the database.\n\n    Attributes\n    ----------\n    cluster: str\n        The name of the Slurm cluster on which the experiment has been scheduled on.\n    array_id: int\n        The array ID of the SLURM job.\n    task_id: int\n        The task ID of the SLURM job.\n    slurm_output_file: str\n        The output file of the SLURM job.\n    reschedule_file: str\n        The reschedule file of the SLURM job. This is only set once an experiment is actually\n        executed by the SLURM job.\n    \"\"\"\n\n    cluster: str\n    array_id: int\n    task_id: int\n    slurm_output_file: str\n    reschedule_file: str\n\n\nclass SacredExperimentDoc(TypedDict):\n    \"\"\"\n    The sacred block of a document retrieved from the database. Populated by sacred.\n    TODO: Honestly no idea what this is good for, it looks like it's not correctly populated. 🤷‍♂️\n    \"\"\"\n\n    base_dir: str\n    dependencies: list[str]\n    mainfile: str\n    name: str\n    repositories: list[str]\n    sources: list[tuple[str, ObjectId]]\n\n\nclass GPUDoc(TypedDict):\n    \"\"\"\n    The gpu block in the gpus block of the host document. Populated by sacred.\n\n    Attributes\n    ----------\n    model: str\n        The model of the GPU.\n    total_emmory: int\n        The total memory of the GPU in MB.\n    persistence_mode: bool\n        Whether the persistence mode is enabled.\n    \"\"\"\n\n    model: str\n    total_emmory: int\n    persistence_mode: bool\n\n\nclass GPUsDoc(TypedDict):\n    \"\"\"\n    The gpus block of a document retrieved from the database. Populated by sacred.\n\n    Attributes\n    ----------\n    driver_version: str\n        The version of the GPU driver.\n    gpus: list[GPUDoc]\n        The list of GPUs used for the experiment.\n    \"\"\"\n\n    driver_version: str\n    gpus: list[GPUDoc]\n\n\nclass HostDoc(TypedDict):\n    \"\"\"\n    The host block of a document retrieved from the database. Populated by sacred.\n\n    Attributes\n    ----------\n    hostname: str\n        The hostname of the machine.\n    os: list[str]\n        The operating system of the machine.\n        Typically a list, e.g., ['Linux', 'Linux-5.15.0-107-generic-x86_64-with-glibc2.35'] is returned for Ubuntu 22.04.\n    python_version: str\n        The version of the Python interpreter.\n    cpu: str\n        The model of the CPU.\n    gpus: GPUsDoc\n        The GPUs used for this experiment.\n    ENV: dict[str, str]\n        The environment variables that are overwritten during the experiment.\n        TODO: This doesn't look properly populated\n    \"\"\"\n\n    hostname: str\n    os: list[str]\n    python_version: str\n    cpu: str\n    gpus: GPUsDoc\n    ENV: dict[str, str]\n\n\nclass MetaDoc(TypedDict):\n    \"\"\"\n    The meta block of a document retrieved from the database. Populated by sacred.\n\n    Attributes\n    ----------\n    command: str\n        The command that has been executed.\n        TODO: This doesn't look properly populated.\n    options: dict[str, Any]\n        TODO: Figure out what this one does.\n    named_configs: list[str]\n        The named configurations that have been used.\n        TODO: This will always be empty since we populate them at add time.\n    config_updates: dict[str, Any]\n        The configuration updates that have been used.\n    \"\"\"\n\n    command: str\n    options: dict[str, Any]\n    named_configs: list[str]\n    config_updates: dict[str, Any]\n\n\nclass StatDoc(TypedDict):\n    \"\"\"\n    Statics of the time and memory usage of a process. Used in StatsDoc.\n\n    Attributes\n    ----------\n    user_time: float\n        The user time of the process.\n    system_time: float\n        The system time of the process.\n    max_memory_bytes: int\n        The maximum memory usage of the process in bytes.\n    \"\"\"\n\n    user_time: float\n    system_time: float\n    max_memory_bytes: int\n\n\nclass GPUStatDoc(TypedDict):\n    \"\"\"\n    Attributes\n    ----------\n    gpu_max_memory_bytes: int\n        The maximum number of bytes allocated at the GPU.\n    \"\"\"\n\n    gpu_max_memory_bytes: int\n\n\nclass StatsDoc(TypedDict):\n    real_time: float\n    self: StatDoc\n    children: StatDoc\n    pytorch: NotRequired[GPUStatDoc]\n    tensorflow: NotRequired[GPUStatDoc]\n\n\n# One could technically set total=False and properly check if attributes exist. However, this is frequently redundant with the MongoDB query!\n# This could be changed in the future. This applies to many other places as well.\nclass ExperimentDoc(TypedDict, total=True):\n    \"\"\"\n    The document retrieved from the database.\n\n    Attributes\n    ----------\n    These attributes should always exist:\n    _id: int\n        The ID of the document. Set by sacred.\n    add_time: datetime\n        The time at which this experiment has been added to the collection.\n    batch_id: int\n        ID of the batch which this experiment belongs to.\n    config: dict[str, Any]\n        The configuration of the experiment after resolving named configurations.\n    config_hash: str\n        A hash of the config to identify duplicates.\n    config_unresolved: dict[str, Any]\n        The configuration of the experiment before resolving named configurations.\n    git: GitDoc | None\n        Information about the git status at the time of staging.\n    seml: SemlDoc\n        Seml specific information about source codes, working directories and commands.\n    slurm: list[SlurmDoc]\n        A list of Slurm configurations for the experiment. For each configuration a\n        separate Slurm job will be started and the job will be assigned on the first come\n        first serve basis.\n    statis: str\n        The status of the experiment.\n\n\n    The following are set during runtime:\n    artifacts: list\n        A list of artifacts that are created during runtime and are stored in the mongodb.\n    captured_out: str\n        The captured output of the experiment. This is only popultaed if SETTINGS.EXPERIMENT.CAPUTRE_OUPUT=True.\n    command: str\n        The command that has been executed. Populated by sacred.\n        TODO: This is likely incorrectly populated.\n    execution: ExecutionDoc\n        Information about the Slurm cluster and job that executed this experiment.\n    experiment: SacredExperimentDoc\n        Sacred's experiment information. Populated by sacred.\n    fail_trace: list[str]\n        A list of lines of the stack trace if the experiment failed.\n    format: str\n        The Observer format.\n    heartbeat: datetime\n        The last heartbeat of the experiment.\n    host: HostDoc\n        Information about the host that executed the experiment. Populated by sacred.\n    info: dict\n        Additional optional information that is populated during runtime.\n    meta: MetaDoc\n        Meta information about the experiment. Populated by sacred.\n    resources: list\n        TODO: Unknown - populated by sacred.\n    result: Any\n        The result of the experiment.\n    start_time: datetime\n        The time at which the experiment has been started.\n    stats: StatsDoc\n        The statistics about runtime and memory consumption of the experiment.\n    stop_time: datetime\n        The time at which the experiment has been stopped.\n    reschedule_config_update: dict[str, Any] | None\n        If the experiment has been rescheduled, this contains the configuration update\n        that must be applied during rescheduling.\n    \"\"\"\n\n    # Set at init\n    _id: Required[int]\n    add_time: Required[datetime]\n    batch_id: Required[int]\n    config: Required[dict[str, Any]]\n    config_hash: Required[str]\n    config_unresolved: Required[dict[str, Any]]\n    git: Required[GitDoc | None]\n    seml: Required[SemlDoc]\n    slurm: Required[list[SlurmDoc]]\n    status: Required[str]\n\n    # Set during runtime\n    artifacts: list\n    captured_out: str\n    command: str\n    execution: ExecutionDoc\n    experiment: SacredExperimentDoc\n    fail_trace: list[str]\n    format: str\n    heartbeat: datetime\n    host: HostDoc\n    info: dict\n    meta: MetaDoc\n    resources: list\n    result: Any\n    start_time: datetime\n    stats: StatsDoc\n    stop_time: datetime\n    reschedule_config_update: dict[str, Any] | None\n\n\nclass ExperimentConfig(TypedDict, total=False, closed=False):\n    # We have this base class to represent also sub-configurations.\n    # TODO: properly type the configurations files.\n    fixed: Any\n    grid: Any\n    random: Any\n    __extra_items__: dict[str, ExperimentConfig]\n\n\nclass SemlExperimentFile(ExperimentConfig, total=False, closed=True):\n    seml: SemlFileConfig\n    slurm: list[SlurmConfig]\n"
  },
  {
    "path": "src/seml/evaluation.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom copy import deepcopy\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    List,\n    Literal,\n    Sequence,\n    cast,\n    overload,\n)\n\nfrom seml.database import get_collection\nfrom seml.document import ExperimentDoc\nfrom seml.settings import SETTINGS\n\nif TYPE_CHECKING:\n    import pandas as pd\n\nStates = SETTINGS.STATES\n\n__all__ = ['get_results']\n\n\ndef parse_jsonpickle(db_entry: ExperimentDoc):\n    import json\n\n    import jsonpickle\n    import jsonpickle.ext.numpy as jsonpickle_numpy\n\n    jsonpickle_numpy.register_handlers()\n    try:\n        p = jsonpickle.pickler.Pickler(keys=False)\n        parsed = jsonpickle.loads(json.dumps(db_entry, default=p.flatten), keys=False)\n        parsed = cast(ExperimentDoc, parsed)\n    except IndexError:\n        parsed = db_entry\n    return parsed\n\n\n@overload\ndef get_results(\n    db_collection_name: str,\n    fields: Sequence[str] | dict[str, Any] | None = ...,\n    *,\n    to_data_frame: Literal[False] = ...,\n    mongodb_config: dict[str, Any] | None = ...,\n    states: Sequence[str] | None = ...,\n    filter_dict: dict[str, Any] | None = ...,\n    parallel: bool = ...,\n    progress: bool = ...,\n) -> list[ExperimentDoc]: ...\n\n\n@overload\ndef get_results(\n    db_collection_name: str,\n    fields: Sequence[str] | dict[str, Any] | None = ...,\n    *,\n    to_data_frame: Literal[True],\n    mongodb_config: dict[str, Any] | None = ...,\n    states: Sequence[str] | None = ...,\n    filter_dict: dict[str, Any] | None = ...,\n    parallel: bool = ...,\n    progress: bool = ...,\n) -> pd.DataFrame: ...\n\n\ndef get_results(\n    db_collection_name: str,\n    fields: Sequence[str] | dict[str, Any] | None = None,\n    *,\n    to_data_frame: bool = False,\n    mongodb_config: dict[str, Any] | None = None,\n    states: Sequence[str] | None = None,\n    filter_dict: dict[str, Any] | None = None,\n    parallel: bool = False,\n    progress: bool = True,\n) -> list[ExperimentDoc] | pd.DataFrame:\n    \"\"\"\n    Get experiment results from the MongoDB.\n    Parameters\n    ----------\n    db_collection_name: str\n        Name of the MongoDB collection.\n    fields: list (optional).\n        Database attributes to extract. Default: ['config', 'result'].\n    to_data_frame: bool, default: False\n        Whether to convert the results into a Pandas DataFrame.\n    mongodb_config: dict (optional)\n        MongoDB credential dictionary. If None, uses the credentials specified by `seml configure`.\n    states: list of strings (optional)\n        Extract only experiments with certain states. Default: ['COMPLETED'].\n    filter_dict: dict (optional)\n        Custom dictionary for filtering results from the MongoDB.\n    parallel: bool, default: False\n        If True, unserialize entries in parallel. Use for very large experiment collections.\n    progress: bool, default: False\n        If True, show a progress bar.\n\n    Returns\n    -------\n\n    \"\"\"\n    import functools\n\n    import pandas as pd\n\n    from seml.console import track\n\n    if fields is None:\n        fields = ['config', 'result']\n\n    if states is None:\n        states = States.COMPLETED\n\n    if filter_dict is None:\n        filter_dict = {}\n\n    track = functools.partial(track, disable=not progress)\n\n    collection = get_collection(\n        db_collection_name,\n        mongodb_config=mongodb_config,\n    )\n\n    if len(states) > 0:\n        if 'status' in filter_dict:\n            logging.warning(\n                \"'states' argument is not empty and will overwrite 'filter_dict['status']'.\"\n            )\n        filter_dict = deepcopy(filter_dict)\n        filter_dict['status'] = {'$in': states}\n\n    cursor = collection.find(filter_dict, fields)\n    results = [x for x in track(cursor, total=collection.count_documents(filter_dict))]\n\n    if parallel:\n        from multiprocessing import Pool\n\n        with Pool() as p:\n            parsed = list(track(p.imap(parse_jsonpickle, results), total=len(results)))\n        parsed = cast(List[ExperimentDoc], parsed)\n    else:\n        parsed = [parse_jsonpickle(entry) for entry in track(results)]\n    if to_data_frame:\n        parsed = pd.json_normalize(parsed, sep='.')  # type: ignore\n    return parsed\n"
  },
  {
    "path": "src/seml/experiment/__init__.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from .experiment import Experiment  # noqa\nelse:\n\n    def Experiment(*args, **kwargs):\n        # THis proxy class is used to avoid importing the real thing\n        from .experiment import Experiment as SemlExperiment\n\n        return SemlExperiment(*args, **kwargs)\n"
  },
  {
    "path": "src/seml/experiment/command.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import Any, List, Sequence, cast\n\nfrom seml.document import ExperimentDoc\nfrom seml.experiment.config import generate_named_configs\nfrom seml.experiment.config import (\n    resolve_interpolations as resolve_config_interpolations,\n)\nfrom seml.settings import SETTINGS\nfrom seml.utils.errors import ArgumentError, MongoDBError\nfrom seml.utils.network import find_free_port\n\n\ndef _generate_debug_attach_url(ip_address: str, port: int):\n    import json\n    import urllib.parse\n\n    launch_config = {\n        'type': 'debugpy',\n        'request': 'attach',\n        'connect': {'host': ip_address, 'port': port},\n        'pathMappings': [{'localRoot': '${workspaceFolder}', 'remoteRoot': '.'}],\n    }\n    launch_config = urllib.parse.quote(json.dumps(launch_config))\n    return f'vscode://fabiospampinato.vscode-debug-launcher/launch?args={launch_config}'\n\n\ndef get_environment_variables(\n    gpus: str | None = None,\n    cpus: int | None = None,\n    environment_variables: dict[str, str] | None = None,\n):\n    if environment_variables is None:\n        environment_variables = {}\n\n    if gpus is not None:\n        if isinstance(gpus, list):\n            raise ArgumentError(\n                'Received an input of type list to set CUDA_VISIBLE_DEVICES. '\n                'Please pass a string for input \"gpus\", '\n                'e.g. \"1,2\" if you want to use GPUs with IDs 1 and 2.'\n            )\n        environment_variables['CUDA_VISIBLE_DEVICES'] = str(gpus)\n    if cpus is not None:\n        environment_variables['OMP_NUM_THREADS'] = str(cpus)\n    return environment_variables\n\n\ndef get_config_overrides(config: Sequence[str]):\n    import shlex\n\n    return ' '.join(map(shlex.quote, config))\n\n\ndef get_shell_command(\n    interpreter: str,\n    exe: str,\n    config: Sequence[str],\n    env: dict[str, str] | None = None,\n):\n    import shlex\n\n    config_overrides = get_config_overrides(config)\n\n    if env is None or len(env) == 0:\n        return f'{interpreter} {exe} with {config_overrides}'\n    else:\n        env_overrides = ' '.join(\n            f'{key}={shlex.quote(val)}' for key, val in env.items()\n        )\n\n        return f'{env_overrides} {interpreter} {exe} with {config_overrides}'\n\n\ndef value_to_string(value: Any, use_json: bool = False):\n    from seml.utils.json import PythonEncoder\n\n    # We need the json encoding for vscode due to https://github.com/microsoft/vscode/issues/91578\n    # Once this bug has been fixed we should only rely on `repr` and remove this code.\n    if use_json:\n        return PythonEncoder().encode(value)\n    else:\n        return repr(value)\n\n\ndef get_command_from_exp(\n    exp: ExperimentDoc,\n    db_collection_name: str,\n    verbose: bool = False,\n    unobserved: bool = False,\n    post_mortem: bool = False,\n    debug: bool = False,\n    debug_server: bool = False,\n    print_info: bool = True,\n    use_json: bool = False,\n    unresolved: bool = False,\n    resolve_interpolations: bool = True,\n):\n    from seml.console import console\n\n    if 'executable' not in exp['seml']:\n        raise MongoDBError(\n            f'No executable found for experiment {exp[\"_id\"]}. Aborting.'\n        )\n    exe = exp['seml']['executable']\n\n    named_configs: list[str]\n    if unresolved:\n        config_unresolved = exp.get('config_unresolved', exp['config'])\n        config, named_configs = tuple(\n            zip(*generate_named_configs([config_unresolved]))\n        )[0]\n        # Variable interpolation in unresolved and named configs\n\n        if resolve_interpolations:\n            import uuid\n\n            key_named_configs = str(uuid.uuid4())\n            interpolated = resolve_config_interpolations(\n                {\n                    **exp,\n                    'config_unresolved': config_unresolved,\n                    key_named_configs: named_configs,\n                },\n                allow_interpolation_keys=list(SETTINGS.ALLOW_INTERPOLATION_IN)\n                + ['config_unresolved', key_named_configs],\n            )\n            interpolated = cast(ExperimentDoc, interpolated)\n\n            config = {\n                k: v\n                for k, v in interpolated['config_unresolved'].items()\n                if not k.startswith(SETTINGS.NAMED_CONFIG.PREFIX)\n            }\n            named_configs = cast(List[str], interpolated[key_named_configs])\n        else:\n            config = {\n                k: v\n                for k, v in config_unresolved.items()\n                if not k.startswith(SETTINGS.NAMED_CONFIG.PREFIX)\n            }\n    else:\n        assert resolve_interpolations, (\n            'In resolved configs, interpolations are automatically resolved'\n        )\n        config = exp['config']\n        named_configs = []\n\n    config['db_collection'] = db_collection_name\n    if not unobserved:\n        config['overwrite'] = exp['_id']\n\n    # We encode values with `repr` such that we can decode them with `eval`. While `shlex.quote`\n    # may cause messy commands with lots of single quotes JSON doesn't match Python 1:1, e.g.,\n    # boolean values are lower case in JSON (true, false) but start with capital letters in Python.\n    config_strings = [\n        f'{key}={value_to_string(val, use_json)}' for key, val in config.items()\n    ]\n    config_strings += named_configs\n\n    # TODO (?): Variable interpolation for unresolved CLI calls\n\n    if not verbose:\n        config_strings.append('--force')\n    if unobserved:\n        config_strings.append('--unobserved')\n    if post_mortem:\n        config_strings.append('--pdb')\n    if debug:\n        config_strings.append('--debug')\n\n    if debug_server:\n        ip_address, port = find_free_port()\n        if print_info:\n            logging.info(\n                f\"Starting debug server with IP '{ip_address}' and port '{port}'. \"\n                f'Experiment will wait for a debug client to attach.'\n            )\n            attach_link = _generate_debug_attach_url(ip_address, port)\n\n            logging.info(\n                \"If you are using VSCode, you can use the 'Debug Launcher' extension to attach:\"\n            )\n            console.out(attach_link)\n\n        interpreter = (\n            f'python -m debugpy --listen {ip_address}:{port} --wait-for-client'\n        )\n    else:\n        interpreter = 'python'\n\n    return interpreter, exe, config_strings\n"
  },
  {
    "path": "src/seml/experiment/config.py",
    "content": "from __future__ import annotations\n\nimport ast\nimport copy\nimport functools\nimport itertools  # type: ignore - N.Gao: I don't get this error\nimport logging\nimport numbers\nimport os\nimport warnings\nfrom pathlib import Path\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Dict,\n    Iterable,\n    Mapping,\n    Sequence,\n    TypeVar,\n    cast,\n)\n\nfrom seml.document import (\n    ExperimentConfig,\n    ExperimentDoc,\n    SBatchOptions,\n    SemlConfig,\n    SemlDocBase,\n    SemlExperimentFile,\n    SemlFileConfig,\n    SlurmConfig,\n)\nfrom seml.experiment.parameters import (\n    cartesian_product_zipped_dict,\n    generate_grid,\n    sample_random_configs,\n    zipped_dict,\n)\nfrom seml.experiment.sources import import_exe\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    Hashabledict,\n    drop_typeddict_difference,\n    flatten,\n    merge_dicts,\n    remove_keys_from_nested,\n    s_if,\n    to_typeddict,\n    unflatten,\n    working_directory,\n)\nfrom seml.utils.errors import ConfigError, ExecutableError\n\nif TYPE_CHECKING:\n    import sacred\n    from pymongo.collection import Collection\n\nRESERVED_KEYS = ['grid', 'fixed', 'random']\n\n\ndef unpack_config(config):\n    config = convert_parameter_collections(config)\n    children = {}\n    reserved_dict = {}\n    for key, value in config.items():\n        if not isinstance(value, dict):\n            continue\n\n        if key not in RESERVED_KEYS:\n            children[key] = value\n        else:\n            if key == 'random':\n                if 'samples' not in value:\n                    raise ConfigError(\n                        'Random parameters must specify \"samples\", i.e. the number of random samples.'\n                    )\n                reserved_dict[key] = value\n            else:\n                reserved_dict[key] = value\n    return reserved_dict, children\n\n\ndef extract_parameter_set(input_config: dict, key: str):\n    flattened_dict = flatten(input_config.get(key, {}))\n    keys = flattened_dict.keys()\n    if key != 'fixed':\n        keys = [\n            '.'.join(k.split('.')[:-1])\n            for k in keys\n            if flattened_dict[k] != 'parameter_collection'\n        ]\n    return set(keys)\n\n\ndef convert_parameter_collections(input_config: dict):\n    flattened_dict = flatten(input_config)\n    parameter_collection_keys = [\n        k for k in flattened_dict.keys() if flattened_dict[k] == 'parameter_collection'\n    ]\n    if len(parameter_collection_keys) > 0:\n        logging.warning(\n            'Parameter collections are deprecated. Use dot-notation for nested parameters instead.'\n        )\n    while len(parameter_collection_keys) > 0:\n        k = parameter_collection_keys[0]\n        del flattened_dict[k]\n        # sub1.sub2.type ==> # sub1.sub2\n        k = '.'.join(k.split('.')[:-1])\n        parameter_collections_params = [\n            param_key for param_key in flattened_dict.keys() if param_key.startswith(k)\n        ]\n        for p in parameter_collections_params:\n            if f'{k}.params' in p:\n                new_key = p.replace(f'{k}.params', k)\n                if new_key in flattened_dict:\n                    raise ConfigError(\n                        f'Could not convert parameter collections due to key collision: {new_key}.'\n                    )\n                flattened_dict[new_key] = flattened_dict[p]\n                del flattened_dict[p]\n        parameter_collection_keys = [\n            k\n            for k in flattened_dict.keys()\n            if flattened_dict[k] == 'parameter_collection'\n        ]\n    return unflatten(flattened_dict)\n\n\ndef standardize_config(config: dict):\n    config = unflatten(flatten(config), levels=[0])\n    out_dict = {}\n    for k in RESERVED_KEYS:\n        if k == 'fixed':\n            out_dict[k] = config.get(k, {})\n        else:\n            out_dict[k] = unflatten(config.get(k, {}), levels=[-1])\n    return out_dict\n\n\ndef invert_config(config: dict):\n    reserved_sets = [(k, set(config.get(k, {}).keys())) for k in RESERVED_KEYS]\n    inverted_config = {}\n    for k, params in reserved_sets:\n        for p in params:\n            L = inverted_config.get(p, [])\n            L.append(k)\n            inverted_config[p] = L\n    return inverted_config\n\n\ndef detect_duplicate_parameters(\n    inverted_config: dict,\n    sub_config_name: str | None = None,\n    ignore_keys: dict[str, Any] | None = None,\n):\n    if ignore_keys is None:\n        ignore_keys = {'random': ('seed', 'samples')}\n\n    duplicate_keys = []\n    for p, L in inverted_config.items():\n        if len(L) > 1:\n            if 'random' in L and p in ignore_keys['random']:\n                continue\n            duplicate_keys.append((p, L))\n\n    if len(duplicate_keys) > 0:\n        if sub_config_name:\n            raise ConfigError(\n                f'Found duplicate keys in sub-config {sub_config_name}: '\n                f'{duplicate_keys}'\n            )\n        else:\n            raise ConfigError(f'Found duplicate keys: {duplicate_keys}')\n\n    start_characters = {x[0] for x in inverted_config.keys()}\n    buckets = {\n        k: {x for x in inverted_config.keys() if x.startswith(k)}\n        for k in start_characters\n    }\n\n    if sub_config_name:\n        error_str = (\n            f'Conflicting parameters in sub-config {sub_config_name}, most likely '\n            'due to ambiguous use of dot-notation in the config dict. Found '\n            \"parameter '{p1}' in dot-notation starting with other parameter \"\n            \"'{p2}', which is ambiguous.\"\n        )\n    else:\n        error_str = (\n            'Conflicting parameters, most likely '\n            'due to ambiguous use of dot-notation in the config dict. Found '\n            \"parameter '{p1}' in dot-notation starting with other parameter \"\n            \"'{p2}', which is ambiguous.\"\n        )\n\n    for k in buckets.keys():\n        for p1, p2 in itertools.combinations(buckets[k], r=2):\n            if p1.startswith(\n                f'{p2}.'\n            ):  # with \".\" after p2 to catch cases like \"test\" and \"test1\", which are valid.\n                raise ConfigError(error_str.format(p1=p1, p2=p2))\n            elif p2.startswith(f'{p1}.'):\n                raise ConfigError(error_str.format(p1=p1, p2=p2))\n\n\ndef generate_configs(experiment_config, overwrite_params=None):\n    \"\"\"Generate parameter configurations based on an input configuration.\n\n    Input is a nested configuration where on each level there can be 'fixed', 'grid', and 'random' parameters.\n\n    In essence, we take the cartesian product of all the `grid` parameters and take random samples for the random\n    parameters. The nested structure makes it possible to define different parameter spaces e.g. for different datasets.\n    Parameter definitions lower in the hierarchy overwrite parameters defined closer to the root.\n\n    For each leaf configuration we take the maximum of all num_samples values on the path since we need to have the same\n    number of samples for each random parameter.\n\n    For each configuration of the `grid` parameters we then create `num_samples` configurations of the random\n    parameters, i.e. leading to `num_samples * len(grid_configurations)` configurations.\n\n    See Also `examples/example_config.yaml` and the example below.\n\n    Parameters\n    ----------\n    experiment_config: dict\n        Dictionary that specifies the \"search space\" of parameters that will be enumerated. Should be\n        parsed from a YAML file.\n    overwrite_params: Optional[dict]\n        Flat dictionary that overwrites configs. Resulting duplicates will be removed.\n\n    Returns\n    -------\n    all_configs: list of dicts\n        Contains the individual combinations of the parameters.\n\n\n    \"\"\"\n\n    reserved, next_level = unpack_config(experiment_config)\n    reserved = standardize_config(reserved)\n    if not any([len(reserved.get(k, {})) > 0 for k in RESERVED_KEYS]):\n        raise ConfigError(\n            'No parameters defined under grid, fixed, or random in the config file.'\n        )\n    level_stack = [('', next_level)]\n    config_levels = [reserved]\n    final_configs = []\n\n    detect_duplicate_parameters(invert_config(reserved), None)\n\n    while len(level_stack) > 0:\n        current_sub_name, sub_vals = level_stack.pop(0)\n        sub_config, sub_levels = unpack_config(sub_vals)\n        if current_sub_name != '' and not any(\n            [len(sub_config.get(k, {})) > 0 for k in RESERVED_KEYS]\n        ):\n            raise ConfigError(\n                f'No parameters defined under grid, fixed, or random in sub-config {current_sub_name}.'\n            )\n        sub_config = standardize_config(sub_config)\n        config_above = config_levels.pop(0)\n\n        inverted_sub_config = invert_config(sub_config)\n        detect_duplicate_parameters(inverted_sub_config, current_sub_name)\n\n        inverted_config_above = invert_config(config_above)\n        redefined_parameters = set(inverted_sub_config.keys()).intersection(\n            set(inverted_config_above.keys())\n        )\n\n        if len(redefined_parameters) > 0:\n            logging.info(\n                f\"Parameters {redefined_parameters} are redefined in sub-config '{current_sub_name}'.\\n\"\n                'Definitions in sub-configs override more general ones.'\n            )\n            config_above = copy.deepcopy(config_above)\n            for p in redefined_parameters:\n                sections = inverted_config_above[p]\n                for s in sections:\n                    del config_above[s][p]\n\n        config = merge_dicts(config_above, sub_config)\n\n        if len(sub_levels) == 0:\n            final_configs.append((current_sub_name, config))\n\n        for sub_name, sub_vals in sub_levels.items():\n            new_sub_name = (\n                f'{current_sub_name}.{sub_name}' if current_sub_name != '' else sub_name\n            )\n            level_stack.append((new_sub_name, sub_vals))\n            config_levels.append(config)\n\n    all_configs = []\n    for subconfig_name, conf in final_configs:\n        conf = standardize_config(conf)\n        random_params = conf.get('random', {})\n        fixed_params = flatten(conf.get('fixed', {}))\n        grid_params = conf.get('grid', {})\n\n        grids = [generate_grid(v, parent_key=k) for k, v in grid_params.items()]\n        grid_configs = dict([sub for item in grids for sub in item])\n        grouped_configs = zipped_dict(grid_configs)\n        grid_product = list(cartesian_product_zipped_dict(grouped_configs))\n\n        with_fixed = [{**d, **fixed_params} for d in grid_product]\n        if len(random_params) > 0:\n            num_samples = random_params['samples']\n            root_seed = random_params.get('seed', None)\n            random_sampled = sample_random_configs(\n                flatten(random_params), seed=root_seed, samples=num_samples\n            )\n            with_random = [\n                {**grid, **random} for grid in with_fixed for random in random_sampled\n            ]\n        else:\n            with_random = with_fixed\n        all_configs.extend(with_random)\n\n    # Cast NumPy integers to normal integers since PyMongo doesn't like them\n    all_configs = [\n        {\n            k: int(v)\n            if isinstance(v, numbers.Integral) and not isinstance(v, bool)\n            else v\n            for k, v in config.items()\n        }\n        for config in all_configs\n    ]\n\n    if overwrite_params is not None:\n        all_configs = [merge_dicts(config, overwrite_params) for config in all_configs]\n        base_length = len(all_configs)\n        # We use a dictionary instead a set because dictionary keys are ordered as of Python 3\n        all_configs = list({Hashabledict(**config): None for config in all_configs})\n        new_length = len(all_configs)\n        if base_length != new_length:\n            diff = base_length - new_length\n            logging.warning(\n                f'Parameter overwrite caused {diff} identical configs. Duplicates were removed.'\n            )\n\n    all_configs = [unflatten(conf) for conf in all_configs]\n    return all_configs\n\n\ndef generate_named_config(named_config_dict: dict) -> list[str]:\n    \"\"\"Generates a sequence of named configs that is resolved by sacred in-order\n\n    Parameters\n    ----------\n    named_config_dict : Dict\n        Flattened configuration before parsing the named configurations.\n\n    Returns\n    -------\n    List[str]\n        A sequence of named configuration in the order that is defined by the `named_config_dict` input\n    \"\"\"\n    # Parse named config names and priorities\n    names, priorities = {}, {}\n    for k, v in named_config_dict.items():\n        if k.startswith(SETTINGS.NAMED_CONFIG.PREFIX):\n            if isinstance(v, str):\n                v = dict(name=str(v))\n            if not isinstance(v, Dict):\n                raise ConfigError(\n                    f'Named configs must be given as '\n                    f'{SETTINGS.NAMED_CONFIG.PREFIX}{\"{identifier}\"}: str | '\n                    '{\"name\": str, \"priority\": int}'\n                )\n            for attribute, value in v.items():\n                if attribute == SETTINGS.NAMED_CONFIG.KEY_NAME:\n                    if not isinstance(value, str):\n                        raise ConfigError(\n                            f'Named config names should be strings, not {value} ({value.__class__})'\n                        )\n                    names[k] = value\n                elif attribute == SETTINGS.NAMED_CONFIG.KEY_PRIORITY:\n                    try:\n                        value = int(value)\n                    except (ValueError, TypeError):\n                        raise ConfigError(\n                            f'Named config priorities should be non-negative integers, not {value} ({value.__class__})'\n                        )\n                    priorities[k] = value\n                else:\n                    raise ConfigError(\n                        f'Named configs only have the attributes {[SETTINGS.NAMED_CONFIG.KEY_NAME, SETTINGS.NAMED_CONFIG.KEY_PRIORITY]}'\n                    )\n    for idx in priorities:\n        if idx not in names:\n            raise ConfigError(\n                f'Defined a priority but not a name for named config {idx}'\n            )\n    return [\n        names[idx]\n        for idx in sorted(\n            names, key=lambda idx: (priorities.get(idx, float('inf')), names[idx])\n        )\n    ]\n\n\ndef generate_named_configs(configs: list[dict]) -> tuple[list[dict], list[list[str]]]:\n    \"\"\"From experiment configurations, generates both the config updates as well as the named configs in the order specified.\n\n    Parameters\n    ----------\n    configs : List[Dict]\n        Input configurations.\n\n    Returns\n    -------\n    List[Dict]\n        For each input configuration, the output configuration that does not contain named configuration specifiers anymore.\n\n    List[List[str]]]\n        For each input configuration, the sequence of named configurations in the order specified.\n    \"\"\"\n    result_configs, result_named_configs = [], []\n    for config in configs:\n        result_configs.append(\n            {\n                k: v\n                for k, v in config.items()\n                if not k.startswith(SETTINGS.NAMED_CONFIG.PREFIX)\n            }\n        )\n        result_named_configs.append(generate_named_config(config))\n    return result_configs, result_named_configs\n\n\n@functools.lru_cache\ndef load_config_dict(cfg_name: str):\n    \"\"\"\n    Wrapper around sacred internal function to load a configuration file.\n    This wrapper is cached to avoid loading the same file multiple times.\n\n    Parameters\n    ----------\n    cfg_name : str\n        Path to the configuration file.\n\n    Returns\n    -------\n    ConfigDict\n        The configuration dictionary.\n    \"\"\"\n    from sacred.config.config_dict import ConfigDict\n    from sacred.config.config_files import load_config_file\n\n    return ConfigDict(load_config_file(cfg_name))\n\n\n_SCAFFOLD_KEYS = (\n    'config_updates',\n    'named_configs_to_use',\n    'config',\n    'fallback',\n    'presets',\n    'fixture',\n    'logger',\n    'seed',\n    'rnd',\n    'config_mods',\n    'summaries',\n)\n\n\ndef _get_scaffold_state(scaffolding):\n    return {\n        k2: {k: getattr(scaffold, k) for k in _SCAFFOLD_KEYS}\n        for k2, scaffold in scaffolding.items()\n    }\n\n\ndef _set_scaffold_state(scaffolding, state):\n    from copy import copy\n\n    for k2, scaffold in scaffolding.items():\n        for k, v in state[k2].items():\n            setattr(scaffold, k, copy(v))\n\n\ndef _sacred_create_configs(\n    exp: sacred.Experiment,\n    configs: list[dict],\n    named_configs: Sequence[Sequence[str]] | None = None,\n) -> list[dict]:\n    \"\"\"Creates configs from an experiment and update values. This is done by re-implementing sacreds `sacred.initialize.create_run`\n    method. Doing this is significantly faster, but it can be out-of-sync with sacred's current implementation.\n\n    Parameters\n    ----------\n    exp : sacred.Experiment\n        The sacred experiment to create configs for\n    configs : List[Dict]\n        Configuration updates for each experiment\n    named_configs : Optional[List[Tuple[str]]], optional\n        Named configs for each experiment, by default ()\n\n    Returns\n    -------\n    Dict\n        The updated configurations containing all values derived from the experiment.\n    \"\"\"\n    from copy import deepcopy\n\n    from sacred.config.utils import undogmatize\n    from sacred.initialize import (\n        Scaffold,\n        create_scaffolding,\n        distribute_config_updates,\n        distribute_presets,\n        gather_ingredients_topological,\n        get_configuration,\n        get_scaffolding_and_config_name,\n    )\n    from sacred.utils import (\n        convert_to_nested_dict,\n        iterate_flattened,\n        join_paths,\n        recursive_update,\n        set_by_dotted_path,\n    )\n\n    from seml.console import track\n\n    def run_named_config(scaffold: Scaffold, cfg_name: str):\n        # This version of sacred.initialize.Scaffold.run_named_config uses our\n        # cached version of load_config_dict. This is necessary to avoid loading the same file multiple times.\n        if os.path.isfile(cfg_name):\n            nc = load_config_dict(cfg_name)\n            cfg = nc(\n                fixed=scaffold.get_config_updates_recursive(),\n                preset=scaffold.presets,\n                fallback=scaffold.fallback,\n            )\n            return undogmatize(cfg)\n        return scaffold.run_named_config(cfg_name)\n\n    configs_resolved = []\n    if named_configs is None:\n        named_configs = [()] * len(configs)\n    sorted_ingredients = gather_ingredients_topological(exp)\n    scaffolding = create_scaffolding(exp, sorted_ingredients)\n    init_state = deepcopy(_get_scaffold_state(scaffolding))\n    # get all split non-empty prefixes sorted from deepest to shallowest\n    prefixes = sorted(\n        [s.split('.') for s in scaffolding if s != ''],\n        reverse=True,\n        key=lambda p: len(p),\n    )\n\n    for config, named_config in track(\n        list(zip(configs, named_configs)),\n        description='Resolving configurations',\n        disable=len(configs) < SETTINGS.CONFIG_RESOLUTION_PROGRESS_BAR_THRESHOLD,\n    ):\n        # The following code is adapted from sacred directly: This results in a significant speedup\n        # as we only care about the config but not about creating runs, however it is more error prone\n        # to changes to sacred\n        _set_scaffold_state(scaffolding, init_state)\n        # --------- configuration process -------------------\n        # Phase 1: Config updates\n        config_updates = convert_to_nested_dict(config)\n        distribute_config_updates(prefixes, scaffolding, config_updates)\n\n        # Phase 2: Named Configs\n        for ncfg in named_config:\n            scaff, cfg_name = get_scaffolding_and_config_name(ncfg, scaffolding)\n            scaff.gather_fallbacks()\n            ncfg_updates = run_named_config(scaff, cfg_name)\n            distribute_presets(scaff.path, prefixes, scaffolding, ncfg_updates)\n            for ncfg_key, value in iterate_flattened(ncfg_updates):\n                set_by_dotted_path(\n                    config_updates, join_paths(scaff.path, ncfg_key), value\n                )\n\n        distribute_config_updates(prefixes, scaffolding, config_updates)\n\n        # Phase 3: Normal config scopes\n        for scaffold in scaffolding.values():\n            scaffold.gather_fallbacks()\n            scaffold.set_up_config()\n\n            # update global config\n            config = get_configuration(scaffolding)\n            # run config hooks\n            config_hook_updates = scaffold.run_config_hooks(\n                config, exp.default_command, None\n            )\n            recursive_update(scaffold.config, config_hook_updates)\n\n        # Phase 4: finalize seeding\n        for scaffold in reversed(list(scaffolding.values())):\n            scaffold.set_up_seed()  # partially recursive\n\n        config_resolved = get_configuration(scaffolding)\n        configs_resolved.append(\n            remove_keys_from_nested(config_resolved, config_get_exclude_keys(config))\n        )\n    return configs_resolved\n\n\ndef resolve_configs(\n    executable: str,\n    conda_env: str | None,\n    configs: list[dict],\n    named_configs: list[list[str]],\n    working_dir: str,\n) -> list[dict]:\n    \"\"\"Resolves configurations by adding keys that are only added when the experiment is run to the MongoDB\n\n    Parameters\n    ----------\n    executable : str\n        Path to the executable\n    conda_env : str\n        Which conda environment to use\n    configs : List[Dict]\n        All experiment configurations\n    named_configs : List[str]\n        For each experiment, the named configurations to use.\n    working_dir : str\n        Which working directory to use\n\n    Returns\n    -------\n    List[Dict]\n        Resolved configurations\n    \"\"\"\n    import sacred\n\n    from seml.experiment.experiment import Experiment\n\n    exp_module = import_exe(executable, conda_env, working_dir)\n\n    # Extract experiment from module\n    exps = [\n        v\n        for k, v in exp_module.__dict__.items()\n        if isinstance(v, (sacred.Experiment, Experiment))\n    ]\n    if len(exps) == 0:\n        raise ExecutableError(\n            f\"Found no Sacred experiment. Something is wrong in '{executable}'.\"\n        )\n    elif len(exps) > 1:\n        raise ExecutableError(\n            f\"Found more than 1 Sacred experiment in '{executable}'. \"\n            f\"Can't resolve configs.\"\n        )\n    exp = exps[0]\n    if not isinstance(exp, Experiment):\n        logging.warning(\n            'The use of sacred.Experiment is deprecated. Please use seml.Experiment instead.\\n'\n            'seml.Experiment already includes typical MongoDB observer and logging setups.\\n'\n            'Please familiar yourself with the new API and adjust your code accordingly.\\n'\n            'See https://github.com/TUM-DAML/seml/blob/master/examples/example_experiment.py'\n        )\n    with working_directory(working_dir):\n        return _sacred_create_configs(exp, configs, named_configs)\n\n\ndef check_config(\n    executable: str, conda_env: str | None, configs: list[dict], working_dir: str\n):\n    \"\"\"Check if the given configs are consistent with the Sacred experiment in the given executable.\n\n    Parameters\n    ----------\n    executable: str\n        The Python file containing the experiment.\n    conda_env: str\n        The experiment's Anaconda environment.\n    configs: List[Dict]\n        Contains the parameter configurations.\n    working_dir : str\n        The current working directory.\n    \"\"\"\n    import sacred\n    import sacred.initialize\n    import sacred.utils\n\n    exp_module = import_exe(executable, conda_env, working_dir)\n\n    # Extract experiment from module\n    exps = [\n        v for k, v in exp_module.__dict__.items() if isinstance(v, sacred.Experiment)\n    ]\n    if len(exps) == 0:\n        raise ExecutableError(\n            f\"Found no Sacred experiment. Something is wrong in '{executable}'.\"\n        )\n    elif len(exps) > 1:\n        raise ExecutableError(\n            f\"Found more than 1 Sacred experiment in '{executable}'. \"\n            f\"Can't check parameter configs. Disable via --no-sanity-check.\"\n        )\n    exp = exps[0]\n\n    empty_run = sacred.initialize.create_run(\n        exp, exp.default_command, config_updates=None, named_configs=()\n    )\n\n    captured_args = {\n        sacred.utils.join_paths(cf.prefix, n)\n        for cf in exp.captured_functions\n        for n in cf.signature.arguments\n    }\n\n    config_keys_empty_run = set(flatten(empty_run.config).keys())\n\n    for config in configs:\n        config_flat = flatten(config)\n        config_keys_added = set(config_flat.keys()).difference(config_keys_empty_run)\n\n        # Check for unused arguments\n        for conf in sorted(config_keys_added):\n            if not (set(sacred.utils.iter_prefixes(conf)) & captured_args):\n                raise sacred.utils.ConfigAddedError(\n                    conf,\n                    config={\n                        k: v for k, v in config_flat.items() if k in config_keys_added\n                    },\n                )\n\n        # Check for missing arguments\n        options = empty_run.config.copy()\n        options.update(config)\n        options.update({k: None for k in sacred.utils.ConfigAddedError.SPECIAL_ARGS})\n        try:\n            empty_run.main_function.signature.construct_arguments(\n                (), {}, options, False\n            )\n        except sacred.utils.MissingConfigError as e:\n            logging.error(str(e))\n            exit(1)\n\n\ndef restore(flat):\n    \"\"\"\n    Restore more complex data that Python's json can't handle (e.g. Numpy arrays).\n    Copied from sacred.serializer for performance reasons.\n    \"\"\"\n    import json\n\n    import jsonpickle\n\n    return jsonpickle.decode(json.dumps(flat), keys=True)\n\n\ndef _convert_value(value):\n    \"\"\"\n    Parse string as python literal if possible and fallback to string.\n    Copied from sacred.arg_parser for performance reasons.\n    \"\"\"\n\n    try:\n        return restore(ast.literal_eval(value))\n    except (ValueError, SyntaxError):\n        # use as string if nothing else worked\n        return value\n\n\ndef convert_values(val: Any):\n    if isinstance(val, dict):\n        for key, inner_val in val.items():\n            val[key] = convert_values(inner_val)\n    elif isinstance(val, list):\n        for i, inner_val in enumerate(val):\n            val[i] = convert_values(inner_val)\n    elif isinstance(val, str):\n        return _convert_value(val)\n    return val\n\n\ndef read_config(config_path: str | Path):\n    import yaml\n\n    from seml import __version__\n    from seml.utils.yaml import YamlUniqueLoader\n\n    with open(config_path) as conf:\n        config_dict = cast(\n            SemlExperimentFile, convert_values(yaml.load(conf, Loader=YamlUniqueLoader))\n        )\n\n    if 'seml' not in config_dict:\n        raise ConfigError(\"Please specify a 'seml' dictionary.\")\n\n    seml_conf = config_dict['seml']\n\n    for k in seml_conf.keys():\n        if k not in SETTINGS.VALID_SEML_CONFIG_VALUES:\n            raise ConfigError(f'{k} is not a valid value in the `seml` config block.')\n\n    if SETTINGS.SEML_CONFIG_VALUE_VERSION in seml_conf:\n        raise ConfigError(\n            f'Using {SETTINGS.SEML_CONFIG_VALUE_VERSION} in the `seml` config block is prohibited.'\n        )\n\n    version_array = [(int(x) if x.isdecimal() else x) for x in __version__.split('.')]\n    executable, working_dir, output_dir, use_uploaded_sources = (\n        determine_executable_and_working_dir(config_path, seml_conf)\n    )\n    seml = to_typeddict(seml_conf, SemlDocBase)\n    seml.update(executable=executable)\n    seml = SemlConfig(\n        **seml,\n        version=version_array,\n        working_dir=working_dir,\n        use_uploaded_sources=use_uploaded_sources,\n        env=dict(os.environ),\n    )\n    if output_dir is not None:\n        seml['output_dir'] = output_dir\n\n    # Get list of slurm configs\n    slurm_list: list[SlurmConfig] = config_dict.get('slurm', [])\n\n    # Check for deprecated `slurm` dictionary\n    if isinstance(slurm_list, dict):\n        warnings.warn('`slurm` is expected to be a list of slurm configurations.')\n        slurm_list = [cast(SlurmConfig, slurm_list)]\n\n    if slurm_list is None:\n        slurm_list: list[SlurmConfig] = []\n\n    # Sanity check\n    for slurm_conf in slurm_list:\n        for k in slurm_conf.keys():\n            if k not in SETTINGS.VALID_SLURM_CONFIG_VALUES:\n                raise ConfigError(\n                    f'{k} is not a valid value in the `slurm` config block.'\n                )\n        if slurm_conf.get('sbatch_options', None) is None:\n            slurm_conf['sbatch_options'] = {}\n\n    # If we have no config, we should add one\n    if len(slurm_list) == 0:\n        slurm_list.append(SlurmConfig(experiments_per_job=1, sbatch_options={}))\n\n    # Remove unnecessary keys from config_dict\n    config_dict = drop_typeddict_difference(\n        config_dict, SemlExperimentFile, ExperimentConfig\n    )\n    return seml, slurm_list, config_dict\n\n\ndef determine_executable_and_working_dir(\n    config_path: str | Path, seml_dict: SemlFileConfig\n):\n    \"\"\"\n    Determine the working directory of the project and chdir into the working directory.\n    Parameters\n    ----------\n    config_path: Path to the config file\n    seml_dict: SEML config dictionary\n\n    Returns\n    -------\n    None\n    \"\"\"\n    config_dir = str(Path(config_path).expanduser().resolve().parent)\n    working_dir = config_dir\n    if 'executable' not in seml_dict:\n        raise ConfigError('Please specify an executable path for the experiment.')\n    executable = seml_dict['executable']\n    with working_directory(working_dir):\n        executable_relative_to_config = os.path.exists(executable)\n    executable_relative_to_project_root = False\n    if 'project_root_dir' in seml_dict:\n        with working_directory(config_dir):\n            working_dir = str(\n                Path(seml_dict['project_root_dir']).expanduser().resolve()\n            )\n        use_uploaded_sources = True\n        with working_directory(working_dir):  # use project root as base dir from now on\n            executable_relative_to_project_root = os.path.exists(executable)\n        del seml_dict['project_root_dir']  # from now on we use only the working dir\n    else:\n        use_uploaded_sources = False\n        logging.warning(\n            \"'project_root_dir' not defined in seml config. Source files will not be saved in MongoDB.\"\n        )\n    if not (executable_relative_to_config or executable_relative_to_project_root):\n        raise ExecutableError('Could not find the executable.')\n    with working_directory(working_dir):\n        executable = str(Path(executable).expanduser().resolve())\n        if executable_relative_to_project_root:\n            executable = str(Path(executable).relative_to(working_dir))\n        else:\n            executable = str(Path(executable).relative_to(config_dir))\n\n        if 'output_dir' in seml_dict:\n            output_dir = seml_dict['output_dir']\n        else:\n            output_dir = None\n    return executable, working_dir, output_dir, use_uploaded_sources\n\n\ndef remove_prepended_dashes(param_dict: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"\n    Returns a new dictionary where all keys that start with a dash are stripped of the dash.\n\n    Parameters\n    ----------\n    param_dict : Dict[str, Any]\n        The dictionary to remove the dashes from.\n\n    Returns\n    -------\n    Dict[str, Any]\n        The dictionary with the dashes removed.\n    \"\"\"\n    new_dict = {}\n    for k, v in param_dict.items():\n        if k.startswith('--'):\n            new_dict[k[2:]] = v\n        elif k.startswith('-'):\n            new_dict[k[1:]] = v\n        else:\n            new_dict[k] = v\n    return new_dict\n\n\ndef config_get_exclude_keys(config_unresolved: dict | None = None) -> list[str]:\n    \"\"\"Gets the key that should be excluded from identifying a config. These should\n    e.g. not be used in hashing\n\n    Parameters\n    ----------\n    config_unresolved : Dict\n        the configuration before resolution by sacred\n\n    Returns\n    -------\n    List[str]\n        keys that do not identify the config\n    \"\"\"\n    exclude_keys = list(SETTINGS.CONFIG_EXCLUDE_KEYS)\n    if config_unresolved is None:\n        return exclude_keys\n    if SETTINGS.CONFIG_KEY_SEED not in config_unresolved:\n        # The seed will only be included (e.g. for hashing) if explicited in the unresolved configuration\n        exclude_keys.append(SETTINGS.CONFIG_KEY_SEED)\n    return exclude_keys\n\n\ndef create_starts_with_regex(*strings: str):\n    \"\"\"\n    Creates a regex pattern that matches the start of a string with any of the given strings.\n\n    Parameters\n    ----------\n    strings : List[str]\n        The strings to match the start of.\n\n    Returns\n    -------\n    re.Pattern\n        The compiled regex pattern.\n    \"\"\"\n    import re\n\n    if len(strings) == 0:\n        # Match not x and x -> always False\n        return re.compile(r'^(?!x)x$')\n\n    # Escape special characters in each string\n    escaped_strings = [re.escape(s) for s in set(strings)]\n    # Join the strings with '|' to create an OR pattern\n    pattern = '|'.join(escaped_strings)\n    # Add '^' to ensure the match is at the start of the string\n    regex = f'^({pattern})'\n    return re.compile(regex)\n\n\ndef requires_interpolation(\n    document: Mapping[str, Any],\n    allow_interpolation_keys: Iterable[str] = SETTINGS.ALLOW_INTERPOLATION_IN,\n) -> bool:\n    r\"\"\"\n    Check if a document requires variable interpolation. This is done by checking if\n    any value matches the regex: .*(?<!\\\\)\\${.+}.*\n\n    Parameters\n    ----------\n    document : Dict\n        The document to check\n    allow_interpolation_keys : List[str]\n        All keys that should be permitted to do variable interpolation. Other keys are taken from the unresolved config.\n\n    Returns\n    -------\n    bool\n        True if the document requires variable interpolation\n    \"\"\"\n    import re\n\n    flat_dict = flatten(document)\n    # Find a ${...} pattern that is not preceded by a backslash\n    pattern = re.compile(r'.*(?<!\\\\)\\${.+}.*')\n    key_pattern = create_starts_with_regex(*allow_interpolation_keys)\n\n    def check_interpolation(key, value):\n        # These instructions are ordered by cost\n        if not isinstance(value, str):\n            return False\n        if not pattern.match(value):\n            return False\n        return key_pattern.match(key)\n\n    return any(map(check_interpolation, flat_dict.keys(), flat_dict.values()))\n\n\ndef escape_non_interpolated_dollars(\n    document: Mapping[str, Any],\n    allow_interpolation_keys: Iterable[str] = SETTINGS.ALLOW_INTERPOLATION_IN,\n) -> dict[str, Any]:\n    r\"\"\"\n    Escapes all dollar signs that are not part of a variable interpolation.\n\n    Parameters\n    ----------\n    document : Dict\n        The document to escape.\n\n    Returns\n    -------\n    Dict\n        The escaped document\n    \"\"\"\n    from seml.utils import unflatten\n\n    flat_doc = flatten(document)\n    key_pattern = create_starts_with_regex(*allow_interpolation_keys)\n    for key, value in flat_doc.items():\n        if isinstance(value, str) and not key_pattern.match(key):\n            value = value.replace(r'${', r'\\${')\n            flat_doc[key] = value\n    return unflatten(flat_doc)\n\n\nT = TypeVar('T', bound=Mapping[str, Any])\n\n\ndef resolve_interpolations(\n    document: T,\n    allow_interpolation_keys: Iterable[str] = SETTINGS.ALLOW_INTERPOLATION_IN,\n) -> T:\n    \"\"\"Resolves variable interpolation using `OmegaConf`\n\n    Parameters\n    ----------\n    documents : Dict\n        The document to resolve.\n    allow_interpolations_in : List[str]\n        All keys that should be permitted to do variable interpolation. Other keys are taken from the unresolved config.\n\n    Returns\n    -------\n    Dict\n        The resolved document\n    \"\"\"\n    allow_interpolation_keys = set(allow_interpolation_keys)\n    if not requires_interpolation(document, allow_interpolation_keys):\n        return document\n\n    from omegaconf import OmegaConf\n\n    to_resolve_doc = escape_non_interpolated_dollars(document, allow_interpolation_keys)\n    key_pattern = create_starts_with_regex(*allow_interpolation_keys)\n    resolved = cast(\n        T,\n        OmegaConf.to_container(\n            OmegaConf.create(to_resolve_doc, flags={'allow_objects': True}),\n            resolve=True,\n        ),\n    )\n    resolved_flat = {\n        key: value for key, value in flatten(resolved).items() if key_pattern.match(key)\n    }\n    unresolved_flat = {\n        key: value\n        for key, value in flatten(document).items()\n        if not key_pattern.match(key)\n    }\n    resolved_keys = set(resolved_flat.keys())\n    unresolved_keys = set(unresolved_flat.keys())\n    assert resolved_keys.isdisjoint(unresolved_keys), (\n        f'Overlap between unresolved and resolved dicts: {resolved_keys.intersection(unresolved_keys)}'\n    )\n    resolved = unflatten({**resolved_flat, **unresolved_flat})\n    return cast(T, resolved)\n\n\ndef remove_duplicates_in_list(documents: Sequence[ExperimentDoc], use_hash: bool):\n    \"\"\"\n    Returns a new list of ExperimentDoc where all elements are unique.\n\n    Parameters\n    ----------\n    documents: Sequence[ExperimentDoc]\n        The documents to filter.\n    use_hash : bool\n        Whether to use hashes (faster)\n\n    Returns\n    -------\n    List[ExperimentDoc]\n        List of unique documents.\n    \"\"\"\n    if not use_hash:\n        # slow duplicate detection without hashes\n        unique_documents, unique_keys = [], set()\n        for document in documents:\n            key = Hashabledict(\n                **remove_keys_from_nested(\n                    document['config'],\n                    config_get_exclude_keys(document['config_unresolved']),\n                )\n            )\n            if key not in unique_keys:\n                unique_documents.append(document)\n                unique_keys.add(key)\n        documents = unique_documents\n    else:\n        # fast duplicate detection using hashing.\n        documents_dict = {document['config_hash']: document for document in documents}\n        documents = list(documents_dict.values())\n    return documents\n\n\ndef remove_duplicates_in_db(\n    collection: Collection[ExperimentDoc],\n    documents: Sequence[ExperimentDoc],\n    use_hash: bool,\n):\n    \"\"\"Check database collection for already present entries.\n\n    Check the database collection for experiments that have the same configuration.\n    Remove the corresponding entries from the input list of configurations to prevent\n    re-running the experiments.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    documents: List[Dict]\n        The documents to filter.\n    use_hash : bool\n        Whether to use hashes (faster)\n\n    Returns\n    -------\n    filtered_configs: list of dicts\n        No longer contains configurations that are already in the database collection.\n\n    \"\"\"\n    if use_hash:\n        hashes = [document['config_hash'] for document in documents]\n        db_hashes = set(\n            collection.find({'config_hash': {'$in': hashes}}).distinct('config_hash')\n        )\n        return [doc for doc in documents if doc['config_hash'] not in db_hashes]\n\n    filtered_documents: list[ExperimentDoc] = []\n    for document in documents:\n        lookup_dict = flatten(\n            {\n                'config': remove_keys_from_nested(\n                    document['config'], document['config_unresolved'].keys()\n                )\n            }\n        )\n        lookup_result = collection.find_one(unflatten(lookup_dict))\n        if lookup_result is None:\n            filtered_documents.append(document)\n    return filtered_documents\n\n\ndef remove_duplicates(\n    collection: Collection[ExperimentDoc] | None,\n    documents: Sequence[ExperimentDoc],\n    use_hash: bool = True,\n):\n    \"\"\"\n    Returns a new list of documents that do not contain duplicates in the database or within the input list.\n\n    Parameters\n    ----------\n    collection: pymongo.collection.Collection\n        The MongoDB collection containing the experiments.\n    documents: Sequence[ExperimentDoc]\n        The documents to filter.\n    use_hash : bool\n        Whether to use hashes (faster)\n\n    Returns\n    -------\n    filtered_configs: list of ExperimentDoc\n        No longer contains configurations that are already in the database collection.\n    \"\"\"\n    if len(documents) == 0:\n        return list(documents)\n    n_total = len(documents)\n\n    # First, check for duplicates withing the experiment configurations from the file.\n    documents = remove_duplicates_in_list(documents, use_hash)\n    n_unique = len(documents)\n    if n_unique != n_total:\n        logging.info(\n            f'{n_total - n_unique} of {n_total} experiment{s_if(n_total)} were '\n            f'duplicates. Adding only the {n_unique} unique configurations.'\n        )\n    # Now, check for duplicate configurations in the database.\n    if collection is not None:\n        documents = remove_duplicates_in_db(collection, documents, use_hash)\n        n_unique_and_not_in_db = len(documents)\n        if n_unique_and_not_in_db != n_unique:\n            logging.info(\n                f'{n_unique - n_unique_and_not_in_db} of {n_unique} '\n                f'experiment{s_if(n_unique)} were already found in the database. They were not added again.'\n            )\n    return documents\n\n\ndef check_slurm_config(experiments_per_job: int, sbatch_options: SBatchOptions):\n    if not (\n        (\n            sbatch_options.get('nodes', 1) == 1\n            and sbatch_options.get('N', 1) == 1  # short for --nodes\n            and sbatch_options.get('ntasks', 1) == 1\n            and sbatch_options.get('n', 1) == 1  # short for --ntasks\n            and sbatch_options.get('ntasks-per-node', 1) == 1\n            and sbatch_options.get('ntasks-per-gpu', 1) == 1\n            and sbatch_options.get('ntasks-per-socket', 1) == 1\n        )\n        or experiments_per_job == 1\n    ):\n        raise ConfigError(\n            'Cannot run multiple experiments per job with multiple nodes or tasks per node.'\n        )\n\n\ndef assemble_slurm_config_dict(experiment_slurm_config: SlurmConfig):\n    \"\"\"\n    Realize inheritance for the slurm configuration, with the following relationship:\n    Default -> Template -> Experiment\n\n    Parameters\n    ----------\n    experiment_slurm_config: The slurm experiment configuration as returned by the function read_config\n\n    Returns\n    -------\n    slurm_config\n\n    \"\"\"\n    # Rename\n    slurm_config = experiment_slurm_config\n    # Assemble the Slurm config:\n    # Basis config is the default config. This can be overridden by the sbatch_options_template.\n    # And this in turn can be overridden by the sbatch config defined in the experiment .yaml file.\n    slurm_config_base = copy.deepcopy(SETTINGS.SLURM_DEFAULT)\n\n    # Check for and use sbatch options template\n    sbatch_options_template = slurm_config.get('sbatch_options_template', None)\n    if sbatch_options_template is not None:\n        if sbatch_options_template not in SETTINGS.SBATCH_OPTIONS_TEMPLATES:\n            raise ConfigError(\n                f\"sbatch options template '{sbatch_options_template}' not found in settings.py.\"\n            )\n        slurm_config_base['sbatch_options'] = merge_dicts(\n            slurm_config_base['sbatch_options'],\n            SETTINGS.SBATCH_OPTIONS_TEMPLATES[sbatch_options_template],\n        )\n\n    # Integrate experiment specific config\n    slurm_config = merge_dicts(slurm_config_base, slurm_config)\n\n    slurm_config['sbatch_options'] = cast(\n        SBatchOptions,\n        remove_prepended_dashes(cast(Dict[str, Any], slurm_config['sbatch_options'])),\n    )\n\n    # Check that ntasks and experiments_per_job are mutually exclusive\n    sbatch_options = slurm_config['sbatch_options']\n    check_slurm_config(slurm_config.get('experiments_per_job', 1), sbatch_options)\n    return slurm_config\n"
  },
  {
    "path": "src/seml/experiment/description.py",
    "content": "from __future__ import annotations\n\nfrom typing import Mapping\n\nfrom seml.experiment.config import (\n    escape_non_interpolated_dollars,\n    requires_interpolation,\n)\n\n\ndef resolve_description(description: str, config: Mapping) -> str:\n    import uuid\n\n    from omegaconf import OmegaConf\n\n    if not requires_interpolation({'seml.description': description}):\n        return description\n\n    # omegaconf can only resolve dicts that refers to its own values\n    # so we add the description string to the config\n    config = escape_non_interpolated_dollars(config, [])\n    key = str(uuid.uuid4())\n    omg_config = OmegaConf.create(\n        {key: description, **config}, flags={'allow_objects': True}\n    )\n    return OmegaConf.to_container(omg_config, resolve=True)[key]  # type: ignore\n"
  },
  {
    "path": "src/seml/experiment/experiment.py",
    "content": "import datetime\nimport logging\nimport resource\nimport sys\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import (\n    TYPE_CHECKING,\n    Callable,\n    List,\n    Optional,\n    ParamSpec,\n    Sequence,\n    Union,\n    cast,\n)\n\nfrom pymongo.collection import Collection\n\nfrom seml.database import States, get_collection\nfrom seml.document import ExperimentDoc\nfrom seml.experiment.observers import create_mongodb_observer\nfrom seml.settings import SETTINGS\nfrom seml.utils.multi_process import is_main_process\n\n# These are only used for type hints\nif TYPE_CHECKING:\n    from sacred import Experiment as ExperimentBase\n    from sacred import Ingredient\n    from sacred.commandline_options import CLIOption\n    from sacred.host_info import HostInfoGetter\n    from sacred.utils import PathType\nfrom sacred import SETTINGS as SACRED_SETTINGS\nfrom sacred import Experiment as ExperimentBase\nfrom sacred.config.utils import (\n    dogmatize,\n    recursive_fill_in,\n    undogmatize,\n)\nfrom sacred.utils import SacredInterrupt\n\n\nclass LoggerOptions(Enum):\n    NONE = None\n    DEFAULT = 'default'\n    RICH = 'rich'\n\n\nclass RescheduleInterrupt(SacredInterrupt):\n    STATUS = States.RESCHEDULED[0]\n\n\nclass Experiment(ExperimentBase):\n    _reschedule_watch_path: Path | None\n    _reschedule_hook_registered: bool\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        ingredients: Sequence['Ingredient'] = (),\n        interactive: bool = False,\n        base_dir: Optional['PathType'] = None,\n        additional_host_info: Optional[List['HostInfoGetter']] = None,\n        additional_cli_options: Optional[Sequence['CLIOption']] = None,\n        save_git_info: bool = True,\n        add_mongodb_observer: bool = True,\n        logger: Optional[Union[LoggerOptions, str]] = LoggerOptions.RICH,\n        capture_output: Optional[bool] = None,\n        collect_stats: bool = True,\n    ):\n        super().__init__(\n            name=name,\n            ingredients=ingredients,\n            interactive=interactive,\n            base_dir=base_dir,\n            additional_host_info=additional_host_info,\n            additional_cli_options=additional_cli_options,\n            save_git_info=save_git_info,\n        )\n        self.capture_output = capture_output\n        if add_mongodb_observer:\n            self.configurations.append(MongoDbObserverConfig(self))\n        self.configurations.append(ClearObserverForMultiTaskConfig(self))\n        if logger:\n            setup_logger(self, LoggerOptions(logger))\n        if collect_stats:\n            self.post_run_hook(lambda _run: _collect_exp_stats(_run))\n\n        self._reschedule_watch_path = None\n        self._reschedule_hook_registered = False\n\n    def run(\n        self,\n        command_name: Optional[str] = None,\n        config_updates: Optional[dict] = None,\n        named_configs: Sequence[str] = (),\n        info: Optional[dict] = None,\n        meta_info: Optional[dict] = None,\n        options: Optional[dict] = None,\n    ):\n        if (\n            not SETTINGS.EXPERIMENT.CAPTURE_OUTPUT and not self.capture_output\n        ) or self.capture_output is False:\n            SACRED_SETTINGS.CAPTURE_MODE = 'no'  # type: ignore\n        return super().run(\n            command_name=command_name,\n            config_updates=config_updates,\n            named_configs=named_configs,\n            info=info,\n            meta_info=meta_info,\n            options=options,\n        )\n\n    P = ParamSpec('P')\n\n    def reschedule_hook(self, f: Callable[P, dict]) -> Callable[P, None]:\n        \"\"\"Decorator to register the reschedule hook.\n\n        In case of a required rescheduling, the decorated function will be called.\n        It should return a config dictionary that will be used to update the current\n        configuration before submitting the rescheduled job.\n\n        Parameters\n        ----------\n        f: Callable[P, dict]\n            User-defined function to be called when rescheduling is triggered.\n\n        Returns\n        -------\n        Callable[P, None]\n            Wrapped function that checks for rescheduling and calls the user-defined function.\n\n\n        Example\n        -------\n\n        ```python\n        from seml import Experiment\n        ex = Experiment()\n\n        @ex.reschedule_hook\n        def reschedule(step: int):\n            print(f'Reschedule triggered at step {step}.')\n            return {'checkpoint': step}\n\n        @ex.automain\n        def run(n_steps: int, checkpoint: int | None = None):\n            if checkpoint is not None:\n                print(f'Resuming from checkpoint: {checkpoint}')\n            for step in range(checkpoint or 0, n_steps):\n                reschedule(step)\n                print(f'Processing step {step + 1}/{n_steps}')\n            print('Experiment completed successfully.')\n        ```\n        \"\"\"\n\n        self._reschedule_hook_registered = True\n\n        def _reschedule_hook(*args, **kwargs):\n            if not is_main_process():\n                return\n\n            if not self._ensure_reschedule_hook_ready():\n                return\n\n            assert self._reschedule_watch_path is not None\n            if not Path(self._reschedule_watch_path).exists():\n                return\n\n            # If yes, call the user-defined function\n            logging.info('Caught reschedule signal, calling reschedule_hook.')\n            new_config = f(*args, **kwargs)\n            assert isinstance(new_config, dict), (\n                'Reschedule hook must return a configuration dictionary.'\n                f' Got type:  + {type(new_config)}'\n            )\n            logging.info(f'Reschedule hook returned new configuration: {new_config}')\n\n            # Add the new_config to the database\n            self._add_reschedule_config_to_db(new_config)\n            self._touch_reschedule_request_file()\n\n            raise RescheduleInterrupt\n\n        return _reschedule_hook\n\n    def _ensure_reschedule_hook_ready(self) -> bool:\n        # Exit, in case the hook is already disabled\n        if not self._reschedule_hook_registered:\n            return False\n\n        # Exit, if already initialized\n        if self._reschedule_watch_path is not None:\n            return True\n\n        # Initialize the reschedule hook\n        exp = self._get_exp_document_from_db()\n        if exp is None:\n            logging.warning(\n                'Failed to retrieve experiment document from database.'\n                ' Disabling reschedule hook.'\n            )\n            self._reschedule_hook_registered = False\n            return False\n\n        if self._is_local_execution(exp):\n            logging.info('Experiment is executed locally. Reschedule hook is ignored.')\n            self._reschedule_hook_registered = False\n            return False\n\n        if not self._is_reschedule_timeout_configured(exp):\n            logging.info(\n                'No reschedule timeout configured for this experiment.'\n                ' Disabling reschedule hook.'\n            )\n            self._reschedule_hook_registered = False\n            return False\n\n        reschedule_path = self._get_reschedule_signal_file(exp)\n        logging.info(\n            'Found signal path for reschedule hook.'\n            f' Watching for file: {reschedule_path.as_posix()}'\n        )\n        self._reschedule_watch_path = reschedule_path\n        return True\n\n    def _add_reschedule_config_to_db(self, config: dict):\n        run = self.current_run\n        assert run is not None\n        for observer in run.observers:\n            if hasattr(observer, 'set_reschedule_config_update'):\n                logging.info(\n                    'Adding reschedule configuration update to the database observer.'\n                )\n                observer.set_reschedule_config_update(config)\n\n    def _get_db_collection(self) -> Optional[Collection[ExperimentDoc]]:\n        assert self.current_run is not None\n        db_collection = self.current_run.config.get('db_collection')\n        if db_collection is None:\n            logging.warning(\n                'Failed to retrieve `db_collection`'\n                ' Was the reschedule_hook called outside a SEML-managed experiment?'\n            )\n            return None\n        db_collection = get_collection(db_collection)\n        return db_collection\n\n    def _get_exp_id(self) -> Optional[int]:\n        assert self.current_run is not None\n        exp_id = self.current_run.config.get('overwrite')\n        if exp_id is None:\n            logging.warning(\n                'Failed to retrieve experiment ID from `overwrite` field.'\n                ' Is this execution unobserved?'\n            )\n        return exp_id\n\n    def _get_exp_document_from_db(self) -> Optional[ExperimentDoc]:\n        db_collection = self._get_db_collection()\n        exp_id = self._get_exp_id()\n        if db_collection is None or exp_id is None:\n            return None\n        exp = db_collection.find_one(exp_id)\n        if exp is None:\n            logging.warning(\n                f'No experiment with ID {exp_id} found in database.'\n                ' Cannot retrieve experiment document.'\n            )\n        return exp\n\n    @staticmethod\n    def _is_reschedule_timeout_configured(exp: ExperimentDoc) -> bool:\n        timeout = exp.get('seml', {}).get('reschedule_timeout')\n        if not timeout:\n            return False\n        return True\n\n    @staticmethod\n    def _is_local_execution(exp: ExperimentDoc) -> bool:\n        cluster = exp.get('execution', {}).get('cluster')\n        return cluster == 'local'\n\n    @staticmethod\n    def _get_reschedule_signal_file(exp: ExperimentDoc) -> Path:\n        reschedule_path = exp.get('execution', {}).get('reschedule_file')\n        assert reschedule_path is not None, (\n            'No SLURM reschedule file recorded for this experiment in the database.'\n        )\n        return Path(reschedule_path)\n\n    def _touch_reschedule_request_file(self) -> None:\n        assert self._reschedule_watch_path is not None\n        request_path = Path(f'{self._reschedule_watch_path}.request')\n        request_path.parent.mkdir(parents=True, exist_ok=True)\n        request_path.touch(exist_ok=True)\n        logging.info('Created reschedule request file %s', request_path.as_posix())\n\n\nclass MongoDbObserverConfig:\n    def __init__(self, experiment: Experiment):\n        self.experiment = experiment\n\n    def __call__(self, fixed=None, preset=None, fallback=None):\n        from sacred.config.config_summary import ConfigSummary\n        from sacred.config.custom_containers import DogmaticDict\n\n        result = cast(DogmaticDict, dogmatize(fixed or {}))\n        defaults = dict(overwrite=None, db_collection=None)\n        recursive_fill_in(result, defaults)\n        recursive_fill_in(result, preset or {})\n        added = result.revelation()\n        config_summary = ConfigSummary(added, result.modified, result.typechanges)\n        config_summary.update(undogmatize(result))\n\n        if config_summary['db_collection'] is not None and is_main_process():\n            self.experiment.observers.append(\n                create_mongodb_observer(\n                    config_summary['db_collection'],\n                    overwrite=config_summary['overwrite'],\n                )\n            )\n        return config_summary\n\n\nclass ClearObserverForMultiTaskConfig:\n    def __init__(self, experiment: Experiment):\n        self.experiment = experiment\n\n    def __call__(self, fixed=None, preset=None, fallback=None):\n        from sacred.config.config_summary import ConfigSummary\n        from sacred.config.custom_containers import DogmaticDict\n\n        result = cast(DogmaticDict, dogmatize(fixed or {}))\n        defaults = dict(overwrite=None, db_collection=None)\n        recursive_fill_in(result, defaults)\n        recursive_fill_in(result, preset or {})\n        added = result.revelation()\n        config_summary = ConfigSummary(added, result.modified, result.typechanges)\n        config_summary.update(undogmatize(result))\n\n        # We only want observers on the main process\n        if not is_main_process():\n            self.experiment.observers.clear()\n        return config_summary\n\n\ndef setup_logger(\n    ex: 'ExperimentBase',\n    logger_option: LoggerOptions = LoggerOptions.RICH,\n    level: Optional[Union[str, int]] = None,\n):\n    \"\"\"\n    Set up logger for experiment.\n\n    Parameters\n    ----------\n    ex: sacred.Experiment\n    Sacred experiment to set the logger of.\n    level: str or int\n    Set the threshold for the logger to this level. Default is logging.INFO.\n\n    Returns\n    -------\n    None\n\n    \"\"\"\n    if hasattr(ex, 'logger') and ex.logger:\n        logging.warn(\n            'Logger already set up for this experiment.\\n'\n            'The new seml.experiment.Experiment class already includes the logger setup.\\n'\n            'Either remove the explicit call to setup_logger or disable the logger setup in the Experiment constructor.'\n        )\n        return\n    if logger_option is LoggerOptions.NONE:\n        return\n    logger = logging.getLogger()\n    logger.handlers = []\n    if level is None:\n        if is_main_process():\n            level = logging.INFO\n        else:\n            level = logging.ERROR\n    if logger_option is LoggerOptions.RICH:\n        from rich.logging import RichHandler\n\n        from seml.console import console\n\n        logger.addHandler(\n            RichHandler(\n                level,\n                console=console,\n                show_time=True,\n                show_level=True,\n                log_time_format='[%X]',\n            )\n        )\n    elif logger_option is LoggerOptions.DEFAULT:\n        ch = logging.StreamHandler()\n        formatter = logging.Formatter(\n            fmt='%(asctime)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%d %H:%M:%S'\n        )\n        ch.setFormatter(formatter)\n        logger.addHandler(ch)\n    logger.setLevel(level)\n    ex.logger = logger  # type: ignore\n\n\ndef _collect_exp_stats(run):\n    \"\"\"\n    Collect information such as CPU user time, maximum memory usage,\n    and maximum GPU memory usage and save it in the MongoDB.\n\n    Parameters\n    ----------\n    run: Sacred run\n        Current Sacred run.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    exp_id = run.config['overwrite']\n    if exp_id is None or run.unobserved:\n        return\n\n    stats = {}\n\n    stats['real_time'] = (datetime.datetime.utcnow() - run.start_time).total_seconds()\n\n    stats['self'] = {}\n    stats['self']['user_time'] = resource.getrusage(resource.RUSAGE_SELF).ru_utime\n    stats['self']['system_time'] = resource.getrusage(resource.RUSAGE_SELF).ru_stime\n    stats['self']['max_memory_bytes'] = (\n        1024 * resource.getrusage(resource.RUSAGE_SELF).ru_maxrss\n    )\n    stats['children'] = {}\n    stats['children']['user_time'] = resource.getrusage(\n        resource.RUSAGE_CHILDREN\n    ).ru_utime\n    stats['children']['system_time'] = resource.getrusage(\n        resource.RUSAGE_CHILDREN\n    ).ru_stime\n    stats['children']['max_memory_bytes'] = (\n        1024 * resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss\n    )\n\n    if 'torch' in sys.modules:\n        import torch  # type: ignore\n\n        stats['pytorch'] = {}\n        if torch.cuda.is_available():\n            stats['pytorch']['gpu_max_memory_bytes'] = torch.cuda.max_memory_allocated()\n\n    if 'tensorflow' in sys.modules:\n        import tensorflow as tf  # type: ignore\n\n        stats['tensorflow'] = {}\n        if int(tf.__version__.split('.')[0]) < 2:\n            if tf.test.is_gpu_available():\n                with tf.Session() as sess:\n                    stats['tensorflow']['gpu_max_memory_bytes'] = int(\n                        sess.run(tf.contrib.memory_stats.MaxBytesInUse())\n                    )\n        else:\n            if len(tf.config.experimental.list_physical_devices('GPU')) >= 1:\n                if int(tf.__version__.split('.')[1]) >= 5:\n                    stats['tensorflow']['gpu_max_memory_bytes'] = (\n                        tf.config.experimental.get_memory_info('GPU:0')['peak']\n                    )\n                else:\n                    logging.info(\n                        'SEML stats: There is no way to get actual peak GPU memory usage in TensorFlow 2.0-2.4.'\n                    )\n\n    collection = get_collection(run.config['db_collection'])\n    collection.update_one({'_id': exp_id}, {'$set': {'stats': stats}})\n\n\ndef collect_exp_stats(run):\n    logging.warn(\n        'seml.collect_exp_stats is deprecated.\\n'\n        'Use seml.experiment.Experiment instead of sacred.Experiment.\\n'\n        'seml.experiment.Experiment already includes the statistics collection.\\n'\n        'See https://github.com/TUM-DAML/seml/blob/master/examples/example_experiment.py'\n    )\n    _collect_exp_stats(run)\n\n\n__all__ = [\n    'setup_logger',\n    'collect_exp_stats',\n    'Experiment',\n]\n"
  },
  {
    "path": "src/seml/experiment/mattermost_observer.py",
    "content": "from __future__ import annotations\n\nimport json\nimport re\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any\n\nfrom bson import json_util\nfrom sacred.config.config_files import load_config_file\nfrom sacred.observers.base import RunObserver, td_format\n\nfrom seml.utils import utcnow\nfrom seml.utils.json import NumpyEncoder\n\n\ndef to_local_timezone(dtime: datetime):\n    return dtime.replace(tzinfo=timezone.utc).astimezone(tz=None)\n\n\nclass MattermostObserver(RunObserver):\n    \"\"\"\n    Based on Sacred's Slack observer: https://github.com/IDSIA/sacred/blob/master/sacred/observers/slack.py\n    Sends a message to Mattermost upon completion/failing of an experiment.\n    \"\"\"\n\n    @classmethod\n    def from_config(cls, filename):\n        \"\"\"\n        Create a MattermostObserver from a given configuration file.\n\n        The file can be in any format supported by Sacred\n        (.json, .pickle, [.yaml]).\n        It has to specify a ``webhook_url`` and can optionally set\n        ``bot_name``, ``icon``, ``completed_text``, ``interrupted_text``, and\n        ``failed_text``.\n        \"\"\"\n        return cls(**load_config_file(filename))  # type: ignore\n\n    def __init__(\n        self,\n        webhook_url,\n        channel=None,\n        bot_name='sacredbot',\n        icon=':angel:',\n        notify_on_started=False,\n        notify_on_completed=True,\n        notify_on_failed=True,\n        notify_on_interrupted=False,\n        heartbeat_interval=None,\n        started_text=None,\n        completed_text=None,\n        failed_text=None,\n        interrupted_text=None,\n        heartbeat_text=None,\n        convert_utc_to_local_timezone=True,\n    ):\n        \"\"\"\n        Create a Sacred observer that will send notifications to Mattermost.\n        Parameters\n        ----------\n        webhook_url: str\n            The webhook for the bot account.\n        channel: str\n            The channel to which to send notifications. To send direct messages, set to @username.\n        bot_name: str\n            The name of the bot.\n        icon: str\n            The icon of the bot.\n        notify_on_started: bool\n            Whether to send a notification when the experiment starts.\n        notify_on_completed: bool\n            Whether to send a notification upon completion.\n        notify_on_failed: bool\n            Whether to send a notification when the experiment fails.\n        notify_on_interrupted: bool\n            Whether to send a notification when the experiment is interrupted.\n        heartbeat_interval: str\n            String in the format D-hh:mm indicating how often to send heartbeat notifications. If None, send no\n            notifications.\n        started_text: str\n            Text to be sent when the experiment starts. If None, this default will be used:\n            \":hourglass_flowing_sand: *{experiment[name]}* \"\n            \"started on host `{host_info[hostname]}` at _{start_time}_.\"\n        completed_text: str\n            Text to be sent upon completion. If None, this default will be used:\n            \":white_check_mark: *{experiment[name]}* \"\n            \"completed after _{elapsed_time}_ with result: \\n```json\\n{result}\\n````\\n\"\n        failed_text: str\n            Text to be sent upon failure. If None, this default will be used:\n            \":x: *{experiment[name]}* failed after \" \"_{elapsed_time}_ with `{error}`\"\n        interrupted_text: str\n            Text to be sent upon interruption. If None, this default will be used:\n            \":warning: *{experiment[name]}* \" \"interrupted after _{elapsed_time}_\"\n        heartbeat_text: str\n            Text to be sent to notify that the experiment is still running. If None, this default will be used:\n            \":heartpulse: *{experiment[name]}* has been up and running for \" \"_{elapsed_time}_. \"\n            \"Current info dict: \\n```json\\n{info}\\n```\\n\"\n            \"Next heartbeat will be sent in about _{heartbeat_interval}_, i.e., on _{next_heartbeat_date}_.\"\n        convert_utc_to_local_timezone: bool\n            Whether to convert UTC times to local timezone in the notifications.\n        \"\"\"\n\n        self.webhook_url = webhook_url\n        self.bot_name = bot_name\n        self.icon = icon\n        self.completed_text = completed_text or (\n            ':white_check_mark: *{experiment[name]}* '\n            'completed after _{elapsed_time}_ with result: \\n```json\\n{result}\\n````\\n'\n        )\n        self.started_text = started_text or (\n            ':hourglass_flowing_sand: *{experiment[name]}* '\n            'started on host `{host_info[hostname]}` at _{start_time}_.'\n        )\n        self.interrupted_text = interrupted_text or (\n            ':warning: *{experiment[name]}* interrupted after _{elapsed_time}_'\n        )\n        self.failed_text = failed_text or (\n            ':x: *{experiment[name]}* failed after _{elapsed_time}_ with `{error}`'\n        )\n        self.heartbeat_text = heartbeat_text or (\n            ':heartpulse: *{experiment[name]}* has been up and running for '\n            '_{elapsed_time}_. '\n            'Current info dict: \\n```json\\n{info}\\n```\\n'\n            'Next heartbeat will be sent in about _{heartbeat_interval}_, i.e., on _{next_heartbeat_date}_.'\n        )\n\n        self._run: dict[str, Any] | None = None\n        self.channel = channel\n\n        self.notify_on_completed = notify_on_completed\n        self.notify_on_failed = notify_on_failed\n        self.notify_on_interrupted = notify_on_interrupted\n        self.notify_on_started = notify_on_started\n        self.notify_on_heartbeat = False\n        self.last_heartbeat_notification = utcnow()\n        self.convert_utc_to_local_timezone = convert_utc_to_local_timezone\n\n        self.heartbeat_interval = None\n        if heartbeat_interval is not None:\n            # unfortunately datetime.strptime() doesn't work with timedeltas, so we parse the date ourselves:\n            pattern = re.compile('([0-9]+)-([0-9]+):([0-9]+)')\n            match = pattern.match(heartbeat_interval)\n            assert match is not None, 'Invalid heartbeat_interval format. Use D-hh:mm.'\n            days, hours, minutes = match.groups()\n            self.heartbeat_interval = timedelta(\n                days=int(days), hours=int(hours), minutes=int(minutes)\n            )\n            self.notify_on_heartbeat = True\n\n    @property\n    def run(self):\n        assert self._run is not None, 'No run has been started.'\n        return self._run\n\n    def started_event(\n        self, ex_info, command, host_info, start_time: datetime, config, meta_info, _id\n    ):\n        import requests\n\n        if self.convert_utc_to_local_timezone:\n            start_time = to_local_timezone(start_time)\n\n        self._run = {\n            '_id': _id,\n            'config': config,\n            'start_time': start_time,\n            'experiment': ex_info,\n            'command': command,\n            'host_info': host_info,\n        }\n        if self.heartbeat_interval is not None:\n            self.run['heartbeat_interval'] = td_format(self.heartbeat_interval)\n            self.last_heartbeat_notification = start_time\n\n        if not self.notify_on_started:\n            return\n\n        data = {\n            'username': self.bot_name,\n            'icon_emoji': self.icon,\n            'text': self.get_started_text(),\n        }\n\n        if self.channel is not None:\n            data['channel'] = self.channel\n        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}\n        requests.post(self.webhook_url, data=json.dumps(data), headers=headers)\n\n    def get_completed_text(self):\n        return self.completed_text.format(**self.run)\n\n    def get_started_text(self):\n        return self.started_text.format(**self.run)\n\n    def get_interrupted_text(self):\n        return self.interrupted_text.format(**self.run)\n\n    def get_failed_text(self):\n        return self.failed_text.format(**self.run)\n\n    def get_heartbeat_text(self):\n        return self.heartbeat_text.format(**self.run)\n\n    def completed_event(self, stop_time, result):\n        import requests\n\n        if self.completed_text is None or not self.notify_on_completed:\n            return\n\n        if self.convert_utc_to_local_timezone:\n            stop_time = to_local_timezone(stop_time)\n\n        self.run['result'] = json.dumps(result, indent=4, cls=NumpyEncoder)\n        self.run['stop_time'] = stop_time\n        self.run['elapsed_time'] = td_format(stop_time - self.run['start_time'])\n\n        data = {\n            'username': self.bot_name,\n            'icon_emoji': self.icon,\n            'text': self.get_completed_text(),\n        }\n        if self.channel is not None:\n            data['channel'] = self.channel\n        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}\n        requests.post(self.webhook_url, data=json.dumps(data), headers=headers)\n\n    def interrupted_event(self, interrupt_time, status):\n        import requests\n\n        if self.interrupted_text is None or not self.notify_on_interrupted:\n            return\n\n        if self.convert_utc_to_local_timezone:\n            interrupt_time = to_local_timezone(interrupt_time)\n\n        self.run['status'] = status\n        self.run['interrupt_time'] = interrupt_time\n        self.run['elapsed_time'] = td_format(interrupt_time - self.run['start_time'])\n\n        data = {\n            'username': self.bot_name,\n            'icon_emoji': self.icon,\n            'text': self.get_interrupted_text(),\n        }\n        if self.channel is not None:\n            data['channel'] = self.channel\n        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}\n        requests.post(self.webhook_url, data=json.dumps(data), headers=headers)\n\n    def failed_event(self, fail_time, fail_trace):\n        import requests\n\n        if self.failed_text is None or not self.notify_on_failed:\n            return\n        if self.convert_utc_to_local_timezone:\n            fail_time = to_local_timezone(fail_time)\n\n        self.run['fail_trace'] = '\\n'.join(fail_trace)\n        self.run['error'] = fail_trace[-1].strip()\n        self.run['fail_time'] = fail_time\n        self.run['elapsed_time'] = td_format(fail_time - self.run['start_time'])\n\n        data = {\n            'username': self.bot_name,\n            'icon_emoji': self.icon,\n            'text': self.get_failed_text(),\n        }\n        if self.channel is not None:\n            data['channel'] = self.channel\n        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}\n        requests.post(self.webhook_url, data=json.dumps(data), headers=headers)\n\n    def heartbeat_event(self, info, captured_out, beat_time: datetime, result):\n        import requests\n\n        if (\n            self.heartbeat_text is None\n            or not self.notify_on_heartbeat\n            or self.heartbeat_interval is None\n        ):\n            return\n\n        if self.convert_utc_to_local_timezone:\n            beat_time = to_local_timezone(beat_time)\n\n        if beat_time < self.last_heartbeat_notification + self.heartbeat_interval:\n            return\n\n        next_heartbeat_notification = beat_time + self.heartbeat_interval\n        self.run['next_heartbeat_date'] = datetime.strftime(\n            next_heartbeat_notification, '%Y-%m-%d %H:%M'\n        )\n        self.run['elapsed_time'] = td_format(beat_time - self.run['start_time'])\n        self.run['info'] = json.dumps(info, indent=4, default=json_util.default)\n\n        data = {\n            'username': self.bot_name,\n            'icon_emoji': self.icon,\n            'text': self.get_heartbeat_text(),\n        }\n        if self.channel is not None:\n            data['channel'] = self.channel\n        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}\n        requests.post(self.webhook_url, data=json.dumps(data), headers=headers)\n        self.last_heartbeat_notification = beat_time\n"
  },
  {
    "path": "src/seml/experiment/observers.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nfrom typing import Any\n\nfrom seml.database import get_mongo_client, get_mongodb_config\nfrom seml.settings import SETTINGS\nfrom seml.utils import assert_package_installed, warn_multiple_calls\n\n__all__ = [\n    'create_mongodb_observer',\n    'create_slack_observer',\n    'create_neptune_observer',\n    'create_file_storage_observer',\n    'add_to_file_storage_observer',\n    'create_mattermost_observer',\n]\n\n\n@warn_multiple_calls(\n    'Created {num_calls} MongoDB observers.\\n'\n    'This might not be intended.\\n'\n    'seml.experiment.Experiment creates one by default.\\n'\n    'Either disable the default observer (seml.experiment.Experiment(add_mongodb_observer=False)) \\n'\n    'or remove the explicit call to create_mongodb_observer.'\n)\ndef create_mongodb_observer(\n    collection: str,\n    mongodb_config: dict[str, Any] | None = None,\n    overwrite: int | None = None,\n):\n    \"\"\"Create a MongoDB observer for a Sacred experiment\n\n    Parameters\n    ----------\n    collection: str\n        Name of the collection in the database to write the results to.\n    mongodb_config: dict\n        Dictionary containing the connection details to the MongoDB. See get_mongodb_config().\n    overwrite: int\n        ID of an experiment to overwrite, e.g. a staged or failed experiment.\n\n    Returns\n    -------\n    observer: MongoObserver\n    \"\"\"\n    from sacred.observers import MongoObserver as SacredMongoObserver\n\n    class MongoObserver(SacredMongoObserver):\n        def set_reschedule_config_update(self, reschedule_config_update: dict):\n            if not isinstance(getattr(self, 'run_entry', None), dict):\n                logging.debug(\n                    'MongoObserver.run_entry not initialized yet. '\n                    'Ignoring set_reschedule_config_update call.'\n                )\n                return\n            assert isinstance(self.run_entry, dict)\n            self.run_entry['reschedule_config_update'] = reschedule_config_update\n\n    if mongodb_config is None:\n        mongodb_config = get_mongodb_config()\n\n    observer = MongoObserver(\n        client=get_mongo_client(**mongodb_config),\n        collection=collection,\n        db_name=mongodb_config['db_name'],\n        overwrite=overwrite,\n    )\n    return observer\n\n\ndef create_file_storage_observer(\n    runs_folder_name: str, basedir: str | None = None, **kwargs\n):\n    from sacred.observers import FileStorageObserver\n\n    if basedir is None:\n        basedir = SETTINGS.OBSERVERS.FILE.DEFAULT_BASE_DIR\n        logging.info(\n            f'Starting file observer in location {basedir}/{runs_folder_name}. To change the default base '\n            f'directory, modify entry SETTINGS.OBSERVERS.FILE.DEFAULT_BASE_DIR in seml/settings.py.'\n        )\n    else:\n        logging.info(\n            f'Starting file observer in location {basedir}/{runs_folder_name}.'\n        )\n    observer = FileStorageObserver(f'{basedir}/{runs_folder_name}', **kwargs)\n    return observer\n\n\ndef add_to_file_storage_observer(\n    file: str, experiment, delete_local_file: bool = False\n):\n    \"\"\"\n\n    Parameters\n    ----------\n    file: str\n        Path to file to add to the file storage observer.\n    experiment: sacred.experiment.Experiment\n        The Sacred Experiment containing the FileStorageObserver.\n    delete_local_file: bool, default: False\n        If True, delete the local file after copying it to the FileStorageObserver.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    has_file_observer = False\n    for obs in experiment.current_run.observers:\n        if 'FileStorageObserver' in str(type(obs)):\n            obs.artifact_event(\n                name=None,\n                filename=file,\n            )\n            has_file_observer = True\n    if not has_file_observer:\n        logging.warning(\n            \"'add_to_file_storage_observer' was called but found no FileStorageObserver for the experiment.\"\n        )\n    if delete_local_file:\n        os.remove(file)\n\n\ndef create_slack_observer(webhook: str | None = None):\n    from sacred.observers import SlackObserver\n\n    slack_obs = None\n    if webhook is None:\n        webhook = SETTINGS.get('OBSERVERS', {}).get('SLACK', {}).get('WEBHOOK')\n    slack_obs = SlackObserver(webhook)\n\n    if slack_obs is None:\n        logging.warning('Failed to create Slack observer.')\n    return slack_obs\n\n\ndef create_mattermost_observer(webhook=None, channel=None, **kwargs):\n    \"\"\"\n    Create a Mattermost observer, which sends notifications via Mattermost.\n\n    Parameters\n    ----------\n    webhook: str\n        The webhook of the Mattermost instance. If you don't know this, ask your Mattermost administrator.\n    channel: str\n        The channel to send notifications to. This should usually be  @your.username (starting with '@').\n    kwargs: dict\n        Keyword arguments that are passed to the MattermostObserver. See MattermostObserver.__init__ for details.\n\n    Returns\n    -------\n    The observer.\n\n    \"\"\"\n    from seml.experiment.mattermost_observer import MattermostObserver\n\n    if channel is None:\n        if 'OBSERVERS' in SETTINGS and 'MATTERMOST' in SETTINGS.OBSERVERS:\n            if channel is None and 'DEFAULT_CHANNEL' in SETTINGS.OBSERVERS.MATTERMOST:\n                channel = SETTINGS.OBSERVERS.MATTERMOST.DEFAULT_CHANNEL\n    if webhook is None:\n        if 'OBSERVERS' in SETTINGS and 'MATTERMOST' in SETTINGS.OBSERVERS:\n            if channel is None and 'DEFAULT_CHANNEL' in SETTINGS.OBSERVERS.MATTERMOST:\n                channel = SETTINGS.OBSERVERS.MATTERMOST.DEFAULT_CHANNEL\n            if 'WEBHOOK' in SETTINGS.OBSERVERS.MATTERMOST:\n                webhook = SETTINGS.OBSERVERS.MATTERMOST.WEBHOOK\n        else:\n            raise ValueError('No webhook provided and none found in settings.py.')\n\n    mattermost_observer = MattermostObserver(webhook, channel=channel, **kwargs)\n    return mattermost_observer\n\n\ndef create_neptune_observer(\n    project_name, api_token=None, source_extensions=['**/*.py', '**/*.yaml', '**/*.yml']\n):\n    assert_package_installed(\n        'neptunecontrib',\n        'Could not import neptunecontrib. Install via `pip install neptune-contrib`.',\n    )\n    from neptunecontrib.monitoring.sacred import NeptuneObserver  # type: ignore\n\n    if api_token is None:\n        if 'OBSERVERS' in SETTINGS and 'NEPTUNE' in SETTINGS.OBSERVERS:\n            if 'AUTH_TOKEN' in SETTINGS.OBSERVERS.NEPTUNE:\n                api_token = SETTINGS.OBSERVERS.NEPTUNE.AUTH_TOKEN\n                # Ignore example token setting\n                if api_token == 'YOUR_AUTH_TOKEN':\n                    api_token = None\n\n    if api_token is None:\n        logging.info(\n            'No API token for Neptune provided. Trying to use environment variable NEPTUNE_API_TOKEN.'\n        )\n    neptune_obs = NeptuneObserver(\n        api_token=api_token,\n        project_name=project_name,\n        source_extensions=source_extensions,\n    )\n    return neptune_obs\n"
  },
  {
    "path": "src/seml/experiment/parameters.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport random\nimport uuid\nfrom typing import Any, DefaultDict, Sequence\n\nfrom seml.utils import unflatten\nfrom seml.utils.errors import ConfigError\n\n\ndef sample_random_configs(\n    random_block: dict[str, Any], samples: int = 1, seed: int | None = None\n):\n    \"\"\"\n    Sample random configurations from the specified search space.\n\n    Parameters\n    ----------\n    random_config: dict\n        dict where each key is a parameter and the value defines how the random sample is drawn. The samples will be\n        drawn using the function sample_parameter.\n    samples: int\n        The number of samples to draw per parameter\n    seed: int or None\n        The seed to use when drawing the parameter value. Defaults to None.\n\n    Returns\n    -------\n    random_configurations: list of dicts\n        List of dicts, where each dict gives a value for all parameters defined in the input random_config dict.\n\n    \"\"\"\n\n    if len(random_block) == 0:\n        return [{}]\n\n    rdm_keys = [k for k in random_block.keys() if k not in ['samples', 'seed']]\n    random_config = {k: random_block[k] for k in rdm_keys}\n    random_parameter_dicts = unflatten(random_config, levels=-1)\n    random_samples = [\n        sample_parameter(random_parameter_dicts[k], samples, seed, parent_key=k)\n        for k in random_parameter_dicts.keys()\n    ]\n    random_samples = dict([sub for item in random_samples for sub in item])\n    random_configurations = [\n        {k: v[ix] for k, v in random_samples.items()} for ix in range(samples)\n    ]\n\n    return random_configurations\n\n\ndef sample_parameter(\n    parameter: dict[str, Any],\n    samples: int,\n    seed: int | None = None,\n    parent_key: str = '',\n):\n    \"\"\"\n    Generate random samples from the specified parameter.\n\n    The parameter types are inspired from https://github.com/hyperopt/hyperopt/wiki/FMin. When implementing new types,\n    please make them compatible with the hyperopt nomenclature so that we can switch to hyperopt at some point.\n\n    Parameters\n    ----------\n    parameter: dict\n        Defines the type of parameter. Dict must include the key \"type\" that defines how the parameter will be sampled.\n        Supported types are\n            - choice: Randomly samples <samples> entries (with replacement) from the list in parameter['options']\n            - uniform: Uniformly samples between 'min' and 'max' as specified in the parameter dict.\n            - loguniform:  Uniformly samples in log space between 'min' and 'max' as specified in the parameter dict.\n            - randint: Randomly samples integers between 'min' (included) and 'max' (excluded).\n    samples: int\n        Number of samples to draw for the parameter.\n    seed: int\n        The seed to use when drawing the parameter value. Defaults to None.\n    parent_key: str\n        The key to prepend the parameter name with. Used for nested parameters, where we here create a flattened version\n        where e.g. {'a': {'b': 11}, 'c': 3} becomes {'a.b': 11, 'c': 3}\n\n    Returns\n    -------\n    return_items: tuple(str, np.array or list)\n        tuple of the parameter name and a 1-D list/array of the samples drawn for the parameter.\n\n    \"\"\"\n    import numpy as np\n\n    if 'type' not in parameter:\n        raise ConfigError(f'No type found in parameter {parameter}')\n    return_items = []\n    allowed_keys = ['seed', 'type']\n    if 'seed' in parameter:\n        np.random.seed(parameter['seed'])\n    elif seed is not None:\n        np.random.seed(seed)\n\n    param_type = parameter['type']\n\n    if param_type == 'choice':\n        choices = parameter['options']\n        allowed_keys.append('options')\n        sampled_values = [random.choice(choices) for _ in range(samples)]\n        return_items.append((parent_key, sampled_values))\n\n    elif param_type == 'uniform':\n        min_val = parameter['min']\n        max_val = parameter['max']\n        allowed_keys.extend(['min', 'max'])\n        sampled_values = np.random.uniform(min_val, max_val, samples)\n        return_items.append((parent_key, sampled_values))\n\n    elif param_type == 'loguniform':\n        if parameter['min'] <= 0:\n            raise ConfigError('Cannot take log of values <= 0')\n        min_val = np.log(parameter['min'])\n        max_val = np.log(parameter['max'])\n        allowed_keys.extend(['min', 'max'])\n        sampled_values = np.exp(np.random.uniform(min_val, max_val, samples))\n        return_items.append((parent_key, sampled_values))\n\n    elif param_type == 'randint':\n        min_val = int(parameter['min'])\n        max_val = int(parameter['max'])\n        allowed_keys.extend(['min', 'max'])\n        sampled_values = np.random.randint(min_val, max_val, samples)\n        return_items.append((parent_key, sampled_values))\n\n    elif param_type == 'randint_unique':\n        min_val = int(parameter['min'])\n        max_val = int(parameter['max'])\n        allowed_keys.extend(['min', 'max'])\n        sampled_values = np.random.choice(\n            np.arange(min_val, max_val), samples, replace=False\n        )\n        return_items.append((parent_key, sampled_values))\n\n    elif param_type == 'parameter_collection':\n        sub_items = [\n            sample_parameter(\n                v, parent_key=f'{parent_key}.{k}', seed=seed, samples=samples\n            )\n            for k, v in parameter['params'].items()\n        ]\n        return_items.extend([sub_item for item in sub_items for sub_item in item])\n\n    else:\n        raise ConfigError(f'Parameter type {param_type} not implemented.')\n\n    if param_type != 'parameter_collection':\n        extra_keys = set(parameter.keys()).difference(set(allowed_keys))\n        if len(extra_keys) > 0:\n            raise ConfigError(\n                f\"Unexpected keys in parameter definition. Allowed keys for type '{param_type}' are \"\n                f'{allowed_keys}. Unexpected keys: {extra_keys}'\n            )\n    return return_items\n\n\ndef generate_grid(parameter: dict[str, Any], parent_key: str = ''):\n    \"\"\"\n    Generate a grid of parameter values from the input configuration.\n\n    Parameters\n    ----------\n    parameter: dict\n        Defines the type of parameter. Options for parameter['type'] are\n            - choice: Expects a list of options in paramter['options'], which will be returned.\n            - range: Expects 'min', 'max', and 'step' keys with values in the dict that are used as\n                     np.arange(min, max, step)\n            - uniform: Generates the grid using np.linspace(min, max, num, endpoint=True)\n            - loguniform: Uniformly samples 'num' points in log space (base 10) between 'min' and 'max'\n            - parameter_collection: wrapper around a dictionary of parameters (of the types above); we call this\n              function recursively on each of the sub-parameters.\n    parent_key: str\n        The key to prepend the parameter name with. Used for nested parameters, where we here create a flattened version\n        where e.g. {'a': {'b': 11}, 'c': 3} becomes {'a.b': 11, 'c': 3}\n\n    Returns\n    -------\n    return_items: tuple(str, tuple(list, str))\n        Name of the parameter and tuple with list containing the grid values for this parameter and zip id.\n\n    \"\"\"\n    import numpy as np\n\n    if 'type' not in parameter:\n        raise ConfigError(f'No type found in parameter {parameter}')\n\n    param_type = parameter['type']\n    allowed_keys = ['type', 'zip_id']\n\n    return_items: list[tuple[str, Any]] = []\n\n    if param_type == 'choice':\n        values = parameter['options']\n        allowed_keys.append('options')\n        return_items.append((parent_key, values))\n\n    elif param_type == 'range':\n        min_val = parameter['min']\n        max_val = parameter['max']\n        step = parameter['step']\n        allowed_keys.extend(['min', 'max', 'step'])\n        values = list(np.arange(min_val, max_val, step))\n        return_items.append((parent_key, values))\n\n    elif param_type == 'uniform':\n        min_val = parameter['min']\n        max_val = parameter['max']\n        num = int(parameter['num'])\n        allowed_keys.extend(['min', 'max', 'num'])\n        values = list(np.linspace(min_val, max_val, num, endpoint=True))\n        return_items.append((parent_key, values))\n\n    elif param_type == 'loguniform':\n        min_val = parameter['min']\n        max_val = parameter['max']\n        num = int(parameter['num'])\n        allowed_keys.extend(['min', 'max', 'num'])\n        values = np.logspace(np.log10(min_val), np.log10(max_val), num, endpoint=True)\n        return_items.append((parent_key, values))\n\n    elif param_type == 'parameter_collection':\n        sub_items = [\n            generate_grid(v, parent_key=f'{parent_key}.{k}')\n            for k, v in parameter['params'].items()\n        ]\n        return_items.extend([sub_item for item in sub_items for sub_item in item])\n\n    else:\n        raise ConfigError(f'Parameter {param_type} not implemented.')\n\n    if param_type != 'parameter_collection':\n        extra_keys = set(parameter.keys()).difference(set(allowed_keys))\n        if len(extra_keys) > 0:\n            raise ConfigError(\n                f\"Unexpected keys in parameter definition. Allowed keys for type '{param_type}' are \"\n                f'{allowed_keys}. Unexpected keys: {extra_keys}'\n            )\n\n    zip_id = parameter.get('zip_id', str(uuid.uuid4()))\n    return_items = [(item[0], (item[1], zip_id)) for item in return_items]\n    return return_items\n\n\ndef zipped_dict(input_dict: dict[str, tuple[Sequence, str]]):\n    \"\"\"Zips dictionaries of type:\n    {\n        'element1': (values, zip_id),\n        ...\n    }\n    to\n    {\n        'zip_id1': {\n            'element1': values\n            'element2': values\n        },\n        ...\n    }\n\n    Args:\n        input_dict (dict[str, tuple(list, str)]): unzipped dictionary\n\n    Returns:\n        dict[str, dict[str, list]]: zipped dictionary\n    \"\"\"\n    # Zip by zip_id attribute\n    zipped_dict: dict[str, dict[str, Sequence]] = DefaultDict(dict)\n    for k, (val, zip_id) in input_dict.items():\n        zipped_dict[zip_id][k] = val\n\n    # Check that parameters in within a bundle have the same number of configurations.\n    for k, bundle in zipped_dict.items():\n        if len({len(x) for x in bundle.values()}) != 1:\n            raise ConfigError(\n                f\"Parameters with zip_id '{k}' have different number of configurations!\"\n            )\n    return zipped_dict\n\n\ndef cartesian_product_zipped_dict(zipped_dict: dict[str, dict[str, Sequence]]):\n    \"\"\"Compute the Cartesian product of the ziped input dictionary values.\n    Parameters\n    ----------\n    zipped_dict: dict of dicts of lists\n\n    Returns\n    -------\n    list of dicts\n        Cartesian product of the lists in the input dictionary.\n\n    \"\"\"\n    zip_lengths = {\n        k: len(next(iter(bundle.values()))) for k, bundle in zipped_dict.items()\n    }\n\n    for idx in itertools.product(*[range(k) for k in zip_lengths.values()]):\n        yield {\n            key: values[i]\n            for zip_id, i in zip(zipped_dict, idx)\n            for key, values in zipped_dict[zip_id].items()\n        }\n"
  },
  {
    "path": "src/seml/experiment/sources.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport importlib\nimport logging\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Iterable\n\nfrom seml.database import delete_files, upload_file_mt\nfrom seml.document import ExperimentDoc, GitDoc, SemlDoc\nfrom seml.settings import SETTINGS\nfrom seml.utils import (\n    assert_package_installed,\n    is_local_file,\n    s_if,\n    working_directory,\n)\nfrom seml.utils.errors import ExecutableError, MongoDBError\n\nif TYPE_CHECKING:\n    from pymongo.collection import Collection\n\nStates = SETTINGS.STATES\n\n\n@functools.lru_cache(maxsize=1)\ndef import_exe(executable: str, conda_env: str | None, working_dir: str):\n    \"\"\"Import the given executable file.\n\n    Parameters\n    ----------\n    executable: str\n        The Python file containing the experiment.\n    conda_env: str\n        The experiment's Anaconda environment.\n\n    Returns\n    -------\n    The module of the imported executable.\n\n    \"\"\"\n    # Check if current environment matches experiment environment\n    if conda_env is not None and conda_env != os.environ.get('CONDA_DEFAULT_ENV'):\n        logging.warning(\n            f\"Current Anaconda environment does not match the experiment's environment ('{conda_env}').\"\n        )\n\n    with working_directory(working_dir):\n        # Get experiment as module (which causes Sacred not to start ex.automain)\n        exe_path = str(Path(executable).expanduser().resolve())\n        sys.path.insert(0, os.path.dirname(exe_path))\n        orig_handlers = logging.root.handlers\n        orig_loglevel = logging.root.level\n        exe_module = importlib.import_module(\n            os.path.splitext(os.path.basename(executable))[0]\n        )\n\n    if exe_module.__file__ != exe_path:\n        logging.error(\n            f'Imported module path\\n\"{exe_module.__file__}\" does not match executable path\\n'\n            f'\"{exe_path}\".\\nIs the executable file name \"{os.path.basename(executable)}\" '\n            f'a package name required by seml, e.g., \"numpy.py\"? '\n            f'If yes, this case it not supported; please rename your script.\\n'\n            f'Otherwise, you can also skip source file uploading and configuration sanity checking '\n            f'py passing \"--no-code-checkpoint\" and \"--no-sanity-check\" to seml.'\n        )\n        exit(1)\n    logging.root.handlers = orig_handlers\n    logging.root.setLevel(orig_loglevel)\n    del sys.path[0]\n\n    return exe_module\n\n\ndef get_imported_sources(\n    executable, root_dir, conda_env, working_dir, stash_all_py_files: bool\n) -> set[str]:\n    \"\"\"Get the sources imported by the given executable.\n\n    Args:\n        executable (_type_): The Python file containing the experiment.\n        root_dir (_type_): The root directory of the experiment.\n        conda_env (_type_): The experiment's Anaconda environment.\n        working_dir (_type_): The working directory of the experiment.\n        stash_all_py_files (_type_): Whether to stash all .py files in the working directory.\n\n    Returns:\n        List[str]: The sources imported by the given executable.\n    \"\"\"\n    import_exe(executable, conda_env, working_dir)\n    root_path = Path(root_dir).expanduser().resolve()\n\n    sources = set()\n    source_added = True\n    while source_added:\n        source_added = False\n        for name, mod in list(sys.modules.items()):\n            if mod is None:\n                continue\n            filename = getattr(mod, '__file__', None)\n            if not filename:\n                continue\n            filename = os.path.abspath(filename)\n            # Check if the file is a local file and not in site-packages (i.e., an installed package)\n            if filename not in sources and is_local_file(filename, root_path):\n                sources.add(filename)\n                source_added = True\n\n    if stash_all_py_files:\n        for file in Path(working_dir).glob('**/*.py'):\n            # This check ensures that we don't add files from site-packages.\n            if is_local_file(file, root_path):\n                sources.add(str(file))\n\n    return sources\n\n\ndef upload_sources(\n    seml_config: SemlDoc, collection: Collection[ExperimentDoc], batch_id: int\n):\n    from concurrent.futures import ThreadPoolExecutor as Pool\n\n    with working_directory(seml_config['working_dir']):\n        root_dir = str(Path(seml_config['working_dir']).expanduser().resolve())\n\n        sources = get_imported_sources(\n            seml_config['executable'],\n            root_dir=root_dir,\n            conda_env=seml_config['conda_environment'],\n            working_dir=seml_config['working_dir'],\n            stash_all_py_files=seml_config.get('stash_all_py_files', False),\n        )\n        executable_abs = str(Path(seml_config['executable']).expanduser().resolve())\n\n        if executable_abs not in sources:\n            raise ExecutableError(\n                f'Executable {executable_abs} was not found in the source code files to upload.'\n            )\n\n        uploaded_files = []\n\n        with Pool() as p:\n            file_ids = p.map(\n                upload_file_mt,\n                [(s, collection.name, batch_id, 'source_file') for s in sources],\n            )\n            for s, file_id in zip(sources, file_ids):\n                source_path = Path(s)\n                uploaded_files.append((str(source_path.relative_to(root_dir)), file_id))\n    return uploaded_files\n\n\ndef get_git_info(filename: str, working_dir: str):\n    \"\"\"\n    Get the git commit info.\n    See https://github.com/IDSIA/sacred/blob/c1c19a58332368da5f184e113252b6b0abc8e33b/sacred/dependencies.py#L400\n\n    Parameters\n    ----------\n    filename: str\n\n    Returns\n    -------\n    path: str\n        The base path of the repository\n    commit: str\n        The commit hash\n    is_dirty: bool\n        True if there are uncommitted changes in the repository\n    \"\"\"\n    assert_package_installed(\n        'git', 'Cannot import git (pip install GitPython). Not saving git status.'\n    )\n    from git import InvalidGitRepositoryError, Repo\n\n    with working_directory(working_dir):\n        directory = os.path.dirname(filename)\n\n        try:\n            repo = Repo(directory, search_parent_directories=True)\n        except InvalidGitRepositoryError:\n            return None\n        try:\n            path = repo.remote().url\n        except ValueError:\n            path = 'git:/' + str(repo.working_dir)\n        commit = repo.head.commit.hexsha\n    return GitDoc(path=path, commit=commit, dirty=repo.is_dirty())\n\n\ndef load_sources_from_db(\n    experiment: ExperimentDoc,\n    collection: Collection[ExperimentDoc],\n    to_directory: str | Path,\n):\n    import gridfs\n\n    db = collection.database\n    fs = gridfs.GridFS(db)\n    if 'source_files' not in experiment['seml']:\n        raise MongoDBError(\n            f'No source files found for experiment with ID {experiment[\"_id\"]}.'\n        )\n    source_files = experiment['seml']['source_files']\n    target_directory = Path(to_directory)\n    for path, _id in source_files:\n        out_path = target_directory / path\n        # only current user can read, write, or execute\n        out_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)\n        db_file = fs.find_one(_id)\n        if db_file is None:\n            raise MongoDBError(\n                f\"Could not find source file with ID '{_id}' for experiment with ID {experiment['_id']}.\"\n            )\n        with open(out_path, 'wb') as f:\n            f.write(db_file.read())\n\n\ndef delete_batch_sources(collection: Collection[ExperimentDoc], batch_id: int):\n    from bson import ObjectId\n\n    db = collection.database\n    filter_dict = {\n        'metadata.batch_id': batch_id,\n        'metadata.collection_name': collection.name,\n    }\n    source_files = list(\n        map(ObjectId, db['fs.files'].find(filter_dict, {'_id'}).distinct('_id'))\n    )\n    if len(source_files) > 0:\n        logging.info(\n            f'Deleting {len(source_files)} source file{s_if(len(source_files))} corresponding '\n            f'to batch {batch_id} in collection {collection.name}.'\n        )\n        delete_files(db, source_files)\n\n\ndef delete_orphaned_sources(\n    collection: Collection[ExperimentDoc], batch_ids: Iterable[int] | None = None\n):\n    if batch_ids is not None:\n        # check for empty batches within list of batch ids\n        filter_dict = {'batch_id': {'$in': list(batch_ids)}}\n        batch_ids = set(batch_ids)\n    else:\n        # check for any empty batches\n        filter_dict = {}\n        batch_ids = set()\n    db_results = collection.find(filter_dict, {'batch_id'})\n    remaining_batch_ids = {x['batch_id'] for x in db_results}\n    empty_batch_ids = batch_ids - remaining_batch_ids\n    for b_id in empty_batch_ids:\n        delete_batch_sources(collection, b_id)\n"
  },
  {
    "path": "src/seml/settings.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom runpy import run_path\nfrom typing import Any, Dict, List, Mapping, cast\n\nimport typer\nfrom typing_extensions import TypeVar\n\nfrom seml.cli_utils.module_hider import ModuleHider\nfrom seml.document import SBatchOptions, SlurmConfig\nfrom seml.utils import merge_dicts\n\n# The YAML, json import is rather slow\nwith ModuleHider(\n    'yaml',\n    'json',\n    'simplejson',\n    'importlib_metadata',\n    'importlib.metadata',\n):\n    from munch import munchify\n\nT = TypeVar('T', default=Any)\n\n\nclass SettingsDict(Mapping[str, T]):\n    def __getattr__(self, name: str) -> T: ...\n\n\nclass DatabaseSettings(SettingsDict):\n    MONGODB_CONFIG_PATH: Path\n\n\nclass States(SettingsDict[List[str]]):\n    STAGED: list[str]\n    PENDING: list[str]\n    RUNNING: list[str]\n    RESCHEDULED: list[str]\n    FAILED: list[str]\n    KILLED: list[str]\n    INTERRUPTED: list[str]\n    COMPLETED: list[str]\n\n\nclass SlurmStates(SettingsDict[List[str]]):\n    PENDING: list[str]\n    RUNNING: list[str]\n    PAUSED: list[str]\n    INTERRUPTED: list[str]\n    FAILED: list[str]\n    COMPLETED: list[str]\n    ACTIVE: list[str]\n\n\nclass FileObserverSettings(SettingsDict[Any]):\n    DEFAULT_BASE_DIR: str\n\n\nclass SlackObserverSettings(SettingsDict[str]):\n    WEBHOOK: str\n\n\nclass MattermostObserverSettings(SettingsDict[str]):\n    WEBHOOK: str\n    DEFAULT_CHANNEL: str\n\n\nclass NetupeObserverSettings(SettingsDict[str]):\n    AUTH_TOKEN: str\n\n\nclass ObserverSettings(SettingsDict[Dict[str, str]]):\n    FILE: FileObserverSettings\n    SLACK: SlackObserverSettings\n    NEPTUNE: NetupeObserverSettings\n    MATTERMOST: MattermostObserverSettings\n\n\nclass NamedConfigSettings(SettingsDict[str]):\n    PREFIX: str\n    KEY_NAME: str\n    KEY_PRIORITY: str\n\n\nclass ConfirmThresholdSettings(SettingsDict[int]):\n    DELETE: int\n    RESET: int\n    CANCEL: int\n    DESCRIPTION_DELETE: int\n    DESCRIPTION_UPDATE: int\n\n\nclass ExperimentSettings(SettingsDict):\n    CAPTURE_OUTPUT: bool\n    TERMINAL_WIDTH: int\n    ENVIRONMENT: dict[str, str]\n\n\nclass MigrationSettings(SettingsDict):\n    SKIP: bool\n    YES: bool\n    BACKUP: bool\n    BACKUP_TMP: str\n\n\nclass SSHForwardSettings(SettingsDict):\n    LOCK_FILE: str\n    RETRIES_MAX: int\n    RETRIES_DELAY: int\n    LOCK_TIMEOUT: int\n    HEALTH_CHECK_INTERVAL: int\n\n\nclass Settings(SettingsDict):\n    USER_SETTINGS_PATH: Path\n    TMP_DIRECTORY: str\n    TEMPLATE_REMOTE: str\n    DATABASE: DatabaseSettings\n    SLURM_DEFAULT: SlurmConfig\n    SBATCH_OPTIONS_TEMPLATES: SettingsDict[SBatchOptions]\n    STATES: States\n    SLURM_STATES: SlurmStates\n    VALID_SEML_CONFIG_VALUES: list[str]\n    SEML_CONFIG_VALUE_VERSION: str\n    VALID_SLURM_CONFIG_VALUES: list[str]\n    LOGIN_NODE_NAMES: list[str]\n    OBSERVERS: ObserverSettings\n    CONFIG_EXCLUDE_KEYS: list[str]\n    CONFIG_KEY_SEED: str\n    ALLOW_INTERPOLATION_IN: list[str]\n    NAMED_CONFIG: NamedConfigSettings\n    CONFIRM_THRESHOLD: ConfirmThresholdSettings\n    EXPERIMENT: ExperimentSettings\n    MIGRATION: MigrationSettings\n    CANCEL_TIMEOUT: int\n    CONFIG_RESOLUTION_PROGRESS_BAR_THRESHOLD: int\n    AUTOCOMPLETE_CACHE_ALIVE_TIME: int\n    SETUP_COMMAND: str\n    END_COMMAND: str\n    SSH_FORWARD: SSHForwardSettings\n\n\nSETTINGS: Settings\n\n__all__ = ('SETTINGS',)\n\nAPP_DIR = Path(typer.get_app_dir('seml'))\n\nSETTINGS = cast(\n    Settings,\n    munchify(\n        {\n            # Location of user-specific settings.py file containing a SETTINGS dict.\n            # With this dict you can change anything that is set here, conveniently from your home directory.\n            # Default: $HOME/.config/seml/settings.py\n            'USER_SETTINGS_PATH': APP_DIR / 'settings.py',\n            # Directory which is used on the compute nodes to dump scripts and Python code.\n            # Only change this if you know what you're doing.\n            'TMP_DIRECTORY': '/tmp',\n            'TEMPLATE_REMOTE': 'https://github.com/TUM-DAML/seml-templates.git',\n            'DATABASE': {\n                # location of the MongoDB config. Default: $HOME/.config/seml/monogdb.config\n                'MONGODB_CONFIG_PATH': APP_DIR / 'mongodb.config'\n            },\n            'SLURM_DEFAULT': {\n                'experiments_per_job': 1,\n                'sbatch_options': {\n                    'time': '0-08:00',\n                    'nodes': 1,\n                    'cpus-per-task': 1,\n                    'mem': '8G',\n                },\n            },\n            'SBATCH_OPTIONS_TEMPLATES': {\n                # This is a special template used for `seml jupyter`\n                'JUPYTER': {\n                    'cpus-per-task': 2,\n                    'mem': '16G',\n                    'gres': 'gpu:1',\n                    'qos': 'interactive',\n                    'job-name': 'jupyter',\n                    'output': 'jupyter-%j.out',\n                    'partition': 'gpu_gtx1080',\n                },\n                # Extend this with your custom templates.\n                'GPU': {\n                    'cpus-per-task': 2,\n                    'mem': '16G',\n                    'gres': 'gpu:1',\n                },\n            },\n            'STATES': {\n                'STAGED': ['STAGED', 'QUEUED'],  # QUEUED for backward compatibility\n                'PENDING': ['PENDING'],\n                'RUNNING': ['RUNNING'],\n                'FAILED': ['FAILED'],\n                'KILLED': ['KILLED'],\n                'INTERRUPTED': ['INTERRUPTED'],\n                'COMPLETED': ['COMPLETED'],\n                'RESCHEDULED': ['RESCHEDULED'],\n            },\n            'SLURM_STATES': {\n                'PENDING': [\n                    'PENDING',\n                    'CONFIGURING',\n                    'REQUEUE_FED',\n                    'REQUEUE_HOLD',\n                    'REQUEUED',\n                    'RESIZING',\n                ],\n                'RUNNING': [\n                    'RUNNING',\n                    'SIGNALING',\n                ],  # Python code can still be executed while in SIGNALING\n                'PAUSED': ['STOPPED', 'SUSPENDED', 'SPECIAL_EXIT'],\n                'INTERRUPTED': ['CANCELLED'],  # Caused by user command\n                'FAILED': [\n                    'FAILED',\n                    'BOOT_FAIL',\n                    'DEADLINE',\n                    'NODE_FAIL',\n                    'OUT_OF_MEMORY',\n                    'PREEMPTED',\n                    'REVOKED',\n                    'TIMEOUT',\n                ],\n                # REVOKED is not failed, but would need code that handles multi-cluster operation\n                'COMPLETED': ['COMPLETED', 'COMPLETING', 'STAGE_OUT'],\n            },\n            'VALID_SEML_CONFIG_VALUES': [\n                'executable',\n                'name',\n                'output_dir',\n                'conda_environment',\n                'project_root_dir',\n                'description',\n                'stash_all_py_files',\n                'reschedule_timeout',\n            ],\n            'SEML_CONFIG_VALUE_VERSION': 'version',\n            'VALID_SLURM_CONFIG_VALUES': [\n                'experiments_per_job',\n                'max_simultaneous_jobs',\n                'sbatch_options_template',\n                'sbatch_options',\n            ],\n            'LOGIN_NODE_NAMES': ['fs'],\n            'OBSERVERS': {\n                'NEPTUNE': {\n                    'AUTH_TOKEN': 'YOUR_AUTH_TOKEN',\n                },\n                'SLACK': {\n                    'WEBHOOK': 'YOUR_WEBHOOK',\n                },\n                'MATTERMOST': {\n                    'WEBHOOK': 'YOUR_WEBHOOK',\n                    'DEFAULT_CHANNEL': 'YOUR_DEFAULT_CHANNEL',\n                },\n            },\n            'CONFIG_EXCLUDE_KEYS': [\n                '__doc__',\n                'db_collection',\n                'overwrite',\n            ],  # keys that will be excluded from resolved configurations, sacred for some reason captures the docstring attribute\n            # Which key is treated as the experiment seed\n            'CONFIG_KEY_SEED': 'seed',\n            'ALLOW_INTERPOLATION_IN': [\n                'seml.description',\n                'config',\n            ],  # in which fields to allow variable interpolation\n            'NAMED_CONFIG': {\n                'PREFIX': '+',  # prefix for all named configuration parameters\n                'KEY_NAME': 'name',  # key that identifies the name of a named config\n                'KEY_PRIORITY': 'priority',  # key that identifies the priority of a named config\n            },\n            'CONFIRM_THRESHOLD': {\n                'DELETE': 10,\n                'RESET': 10,\n                'CANCEL': 10,\n                'DESCRIPTION_DELETE': 10,\n                'DESCRIPTION_UPDATE': 10,\n            },\n            'EXPERIMENT': {\n                'CAPTURE_OUTPUT': False,  # whether to capture the output of the experiment in the database\n                'TERMINAL_WIDTH': 80,  # width of the terminal for rich output\n                'ENVIRONMENT': {},  # Additional environment variables to set for the experiment - these override existing ones\n            },\n            'MIGRATION': {\n                'SKIP': False,  # always ignore migrations, changing this most likely breaks compatibility!\n                'YES': False,  # always confirm migrations\n                'BACKUP': False,  # always backup the database before running migrations\n                'BACKUP_TMP': '{collection}_backup_{time}',  # format for backup collections\n            },\n            'CANCEL_TIMEOUT': 60,  # wait up to 60s for canceling an experiment\n            'CONFIG_RESOLUTION_PROGRESS_BAR_THRESHOLD': 25,\n            'AUTOCOMPLETE_CACHE_ALIVE_TIME': 60 * 60 * 24,  # one day\n            'SETUP_COMMAND': '',\n            'END_COMMAND': '',\n            'SSH_FORWARD': {\n                'LOCK_FILE': '/tmp/seml_ssh_forward.lock',\n                'RETRIES_MAX': 6,\n                'RETRIES_DELAY': 1,\n                'LOCK_TIMEOUT': 30,\n                'HEALTH_CHECK_INTERVAL': 10,\n            },\n        },\n    ),\n)\n\n# Load user settings\nif SETTINGS.USER_SETTINGS_PATH.exists():\n    user_settings_source = run_path(str(SETTINGS.USER_SETTINGS_PATH))\n    SETTINGS = cast(\n        Settings,\n        munchify(merge_dicts(SETTINGS, user_settings_source['SETTINGS'])),  # type: ignore\n    )\n\nSETTINGS.SLURM_STATES.ACTIVE = (\n    SETTINGS.SLURM_STATES.PENDING\n    + SETTINGS.SLURM_STATES.RUNNING\n    + SETTINGS.SLURM_STATES.PAUSED\n)\n"
  },
  {
    "path": "src/seml/templates/slurm/jupyter_template.sh",
    "content": "#!/bin/bash\n{sbatch_options}\n\n# Move either to project root dir or the config file path.\ncd ${{SLURM_SUBMIT_DIR}}\n\n# Print job information\necho \"Starting job ${{SLURM_JOBID}}\"\necho \"SLURM assigned me the node(s): $(squeue -j ${{SLURM_JOBID}} -O nodelist:1000 | tail -n +2 | sed -e 's/[[:space:]]*$//')\"\n\n# Activate Anaconda environment\nif {use_conda_env}; then\n    CONDA_BASE=$(conda info --base)\n    source $CONDA_BASE/etc/profile.d/conda.sh\n    conda activate {conda_env}\nfi\n\n# Fixes Jupyter bug with read/write permissions https://github.com/jupyter/notebook/issues/1318\nexport XDG_RUNTIME_DIR=\"\"\njupyter{notebook_or_lab} --no-browser --ip=\"*\"\n"
  },
  {
    "path": "src/seml/templates/slurm/slurm_template.sh",
    "content": "#!/bin/bash\n{sbatch_options}\n{reschedule_signal_directive}\n#SBATCH --open-mode=append\n\n# Execute optional bash commands\n{setup_command}\n\n# Move either to project root dir or the config file path.\ncd {working_dir}\n\n# Print job information\necho \"Starting job ${{SLURM_ARRAY_JOB_ID}}_${{SLURM_ARRAY_TASK_ID}}\"\necho \"SLURM assigned me the node(s): ${{SLURM_NODELIST}}\"\n\n# Process signal for rescheduling\n_res_template=\"{reschedule_file}\"\nreschedule_file=\"${{_res_template//%A/${{SLURM_ARRAY_JOB_ID}}}}\"\nreschedule_file=\"${{reschedule_file//%a/${{SLURM_ARRAY_TASK_ID}}}}\"\n_req_template=\"{reschedule_request_file}\"\nreschedule_request_file=\"${{_req_template//%A/${{SLURM_ARRAY_JOB_ID}}}}\"\nreschedule_request_file=\"${{reschedule_request_file//%a/${{SLURM_ARRAY_TASK_ID}}}}\"\necho \"Touching file ${{reschedule_file}} before timeout for rescheduling.\"\ntrap \"touch ${{reschedule_file}}\" USR1\n\n# Activate Anaconda environment\nif {use_conda_env}; then\n    CONDA_BASE=$(conda info --base)\n    source $CONDA_BASE/etc/profile.d/conda.sh\n    conda activate {conda_env}\nfi\n\n# List with all experiment IDs\nall_exp_ids=({exp_ids})\n\nprocess_ids=() # list of all process ids\nexp_ids=() # list of all sacred ids\ntmp_dirs=() # list of temporary directories\n# Start experiments in separate processes\nfor i in $(seq 1 {experiments_per_job}); do\n    # Claim an experiment, this is separate from the experiment preparation\n    # to avoid race conditions and handle multi-process experiments well.\n    exp_id=$(seml {db_collection_name} claim-experiment ${{all_exp_ids[@]}})\n    if [ $? -eq 3 ]; then\n        echo \"WARNING: No more experiments to run.\"\n        break\n    fi\n    exp_ids+=($exp_id)\n\n    # Create directory for the source files in MongoDB\n    if {with_sources}; then\n        tmpdir=$(mktemp -d -p \"{tmp_directory}\" seml_sources.XXXXXXXX) || {{\n            (>&2 echo \"ERROR: Could not create temporary directory for source files under {tmp_directory}.\"); exit 1; }}\n        # Prepend the temp dir and potential src paths to $PYTHONPATH so it will be used by python.\n        # https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/\n        exp_pypath=\"$tmpdir:$tmpdir/src:$PYTHONPATH\"\n        tmp_dirs+=($tmpdir)\n    fi\n\n    # Prepare the epxeriment\n    cmd=$({maybe_srun}seml {db_collection_name} prepare-experiment -id ${{exp_id}}{prepare_args})\n\n    # Check if the preparation was successful\n    ret=$?\n    if [ $ret -eq 0 ]; then\n        # This experiment works and will be started.\n        {maybe_srun}bash -c \"PYTHONPATH=$exp_pypath $cmd\" &\n        process_ids+=($!)\n    elif [ $ret -eq 3 ]; then\n        echo \"ERROR: Experiment with id ${{exp_id}} got claimed by this job but is not associated correctly.\"\n    elif [ $ret -eq 4 ]; then\n        (>&2 echo \"ERROR: Experiment with id ${{exp_id}} not found in the database.\")\n    fi\ndone\n\n# Kill unnecessary jobs\nseml {db_collection_name} clean-jobs ${{all_exp_ids[@]}}\n\n# Print process information\necho \"Experiments are running under the following process IDs:\"\nnum_it=${{#process_ids[@]}}\nfor ((i=0; i<$num_it; i++)); do\n    echo \"Experiment ID: ${{exp_ids[$i]}}\tProcess ID: ${{process_ids[$i]}}\"\ndone\necho\n\n# Wait for all experiments to finish\nwait\n\n# Delete temporary source files\nif {with_sources}; then\n    for tmpdir in ${{tmp_dirs[@]}}; do\n        srun rm -rf $tmpdir\n    done\nfi\n\n# Execute optional bash commands\n{end_command}\n\n# Delete reschedule signal file and requeue this job if requested\nif [ -f ${{reschedule_file}} ] && [ -f ${{reschedule_request_file}} ]; then\n    rm -f ${{reschedule_file}} ${{reschedule_request_file}}\n    echo \"Rescheduling job ${{SLURM_ARRAY_JOB_ID}}_${{SLURM_ARRAY_TASK_ID}}\"\n    scontrol requeue ${{SLURM_ARRAY_JOB_ID}}_${{SLURM_ARRAY_TASK_ID}}\nelse\n    rm -f ${{reschedule_file}} ${{reschedule_request_file}}\nfi\n\n"
  },
  {
    "path": "src/seml/utils/__init__.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport functools\nimport logging\nimport os\nimport time\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Generator,\n    Hashable,\n    Iterable,\n    Mapping,\n    Sequence,\n    TypeVar,\n    cast,\n    overload,\n)\n\n\ndef s_if(n: int) -> str:\n    return '' if n == 1 else 's'\n\n\ndef unflatten(\n    dictionary: dict,\n    sep: str = '.',\n    recursive: bool = False,\n    levels: int | Sequence[int] | None = None,\n):\n    \"\"\"\n    Turns a flattened dict into a nested one, e.g. {'a.b':2, 'c':3} becomes {'a':{'b': 2}, 'c': 3}\n    From https://stackoverflow.com/questions/6037503/python-unflatten-dict.\n\n    Parameters\n    ----------\n    dictionary: dict to be un-flattened\n    sep: separator with which the nested keys are separated\n    recursive: bool, default: False\n        Whether to also un-flatten sub-dictionaries recursively. NOTE: if recursive is True, there can be key\n        collisions, e.g.: {'a.b': 3, 'a': {'b': 5}}. In these cases, keys which are later in the insertion order\n        overwrite former ones, i.e. the example above returns {'a': {'b': 5}}.\n    levels: int or list of ints (optional).\n        If specified, only un-flatten the desired levels. E.g., if levels= [0, -1], then {'a.b.c.d': 111} becomes\n        {'a': {'b.c': {'d': 111}}}.\n\n    Returns\n    -------\n    result_dict: the nested dictionary.\n    \"\"\"\n    import collections.abc\n\n    duplicate_key_warning_str = (\n        'Duplicate key detected in recursive dictionary unflattening. '\n        \"Overwriting previous entries of '{}'.\"\n    )\n\n    if levels is not None:\n        if isinstance(levels, collections.abc.Sequence):\n            levels = list(levels)\n        else:\n            levels = [levels]\n        if len(levels) == 0:\n            raise ValueError(\n                'Need at least one level to unflatten when levels != None.'\n            )\n        if not isinstance(levels[0], int):\n            raise TypeError(\n                f'Levels must be list or set of integers, got type {type(levels[0])}.'\n            )\n\n    result_dict = dict()\n    for key, value in dictionary.items():\n        if isinstance(value, dict) and recursive:\n            value = unflatten(value, sep=sep, recursive=True, levels=levels)\n\n        parts = key.split(sep)\n        if levels is not None:\n            key_levels = levels.copy()\n            for ix in range(len(key_levels)):\n                if key_levels[ix] < 0:\n                    new_ix = len(parts) + key_levels[ix] - 1\n                    if (\n                        key_levels[ix] == -1\n                    ):  # special case so that indexing with -1 never throws an error.\n                        new_ix = max(0, new_ix)\n                    if new_ix < 0:\n                        raise IndexError(\n                            f'Dictionary key level out of bounds. ({new_ix} < 0).'\n                        )\n                    key_levels[ix] = new_ix\n                if key_levels[ix] >= len(parts):\n                    raise IndexError(\n                        f'Dictionary key level {key_levels[ix]} out of bounds for size {len(parts)}.'\n                    )\n            key_levels = sorted(key_levels)\n\n            key_levels = list(set(key_levels))\n            new_parts = []\n            ix_current = 0\n            for level in key_levels:\n                new_parts.append(sep.join(parts[ix_current : level + 1]))\n                ix_current = level + 1\n\n            if ix_current < len(parts):\n                new_parts.append(sep.join(parts[ix_current::]))\n            parts = new_parts\n\n        d = result_dict\n        # Index the existing dictionary in a nested way via the separated key levels. Create empty dicts if necessary.\n        for part in parts[:-1]:\n            if part not in d:\n                d[part] = dict()\n            elif not isinstance(d[part], dict):\n                # Here we have a case such as: {'a.b': ['not_dict'], 'a': {'b': {'c': 111}}}\n                # Since later keys overwrite former ones, we replace the value for {'a.b'} with {'c': 111}.\n                logging.warning(duplicate_key_warning_str.format(part))\n                d[part] = dict()\n            # Select the sub-dictionary for the key level.\n            d = d[part]\n        last_key = parts[-1]\n        if last_key in d:\n            if isinstance(value, dict):\n                intersection = set(d[last_key].keys()).intersection(value.keys())\n                if len(intersection) > 0:\n                    logging.warning(duplicate_key_warning_str.format(last_key))\n                # Merge dictionaries, overwriting any existing values for duplicate keys.\n                d[last_key] = merge_dicts(d[last_key], value)\n            else:\n                logging.warning(duplicate_key_warning_str.format(last_key))\n                d[last_key] = value\n        else:\n            d[last_key] = value\n    return result_dict\n\n\ndef flatten(dictionary: Mapping[str, Any], parent_key: str = '', sep: str = '.'):\n    \"\"\"\n    Flatten a nested dictionary, e.g. {'a':{'b': 2}, 'c': 3} becomes {'a.b':2, 'c':3}.\n    From https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys\n\n    Parameters\n    ----------\n    dictionary: dict to be flattened\n    parent_key: string to prepend the key with\n    sep: level separator\n\n    Returns\n    -------\n    flattened dictionary.\n    \"\"\"\n    import collections.abc\n\n    items = []\n    for k, v in dictionary.items():\n        k = str(k)\n        new_key = parent_key + sep + k if parent_key else k\n        if isinstance(v, collections.abc.MutableMapping):\n            # This covers the edge case that someone supplies an empty dictionary as parameter\n            if len(v) == 0:\n                items.append((new_key, v))\n            else:\n                items.extend(flatten(v, new_key, sep=sep).items())\n        else:\n            items.append((new_key, v))\n    return dict(items)\n\n\ndef get_from_nested(d: Mapping[str, Any], key: str, sep: str = '.') -> Any:\n    \"\"\"Gets a value from an unflattened dict, e.g. allows to use strings like `config.data` on a nesteddict\n\n    Parameters\n    ----------\n    d : Dict\n        The dict from which to get\n    key : str\n        A path to the value, separated by `sep`\n    sep : str, optional\n        The separator for levels in the nested dict, by default '.'\n\n    Returns\n    -------\n    Any\n        The nested value\n    \"\"\"\n    for k in key.split(sep):\n        d = d[k]\n    return d\n\n\ndef list_is_prefix(first: Sequence, second: Sequence) -> bool:\n    return len(first) <= len(second) and all(x1 == x2 for x1, x2 in zip(first, second))\n\n\ndef resolve_projection_path_conflicts(\n    projection: dict[str, bool | int], sep: str = '.'\n) -> dict[str, bool]:\n    \"\"\"Removes path conflicts in a MongoDB projection dict. E.g. if you pass the dict\n    `{'config' : 1, 'config.dataset' : 1}`, MongoDB will throw an error. This method will ensure that\n    always the \"bigger\" projection is returned, i.e. `\"config\"` in the aforementioned example.\n    Note that this resolution will not work if you pass e.g. `{'config' : 1, 'config.dataset' : 0}`.\n\n    Parameters\n    ----------\n    projection : Dict[str, bool]\n        The projection to resolve\n    sep : str, optional\n        The separator for nested config values, by default '.'\n\n    Returns\n    -------\n    Dict[str, bool]\n        The resolved projection\n    \"\"\"\n    result: dict[tuple[str, ...], bool] = {}\n    for k, v in projection.items():\n        k = tuple(k.split(sep))\n        add_k = True\n        for other in list(result.keys()):\n            if list_is_prefix(k, other):\n                # If `k` is a prefix of any path in `result`, this path will be removed\n                if result[other] != v:\n                    raise ValueError(\n                        f'Can not resolve projection {(k, v), (other, result[other])}'\n                    )\n                del result[other]\n            elif list_is_prefix(other, k):\n                # If any other path in `result` is a prefix of `k` we do not add k\n                if result[other] != v:\n                    raise ValueError(\n                        f'Can not resolve projection {(k, v), (other, result[other])}'\n                    )\n                add_k = False\n        if add_k:\n            result[k] = bool(v)\n    return {sep.join(k): v for k, v in result.items()}\n\n\nS = TypeVar('S', bound=Sequence)\n\n\ndef chunker(seq: S, size: int) -> Generator[S]:\n    \"\"\"\n    Chunk a list into chunks of size `size`.\n    From\n    https://stackoverflow.com/questions/434287/what-is-the-most-pythonic-way-to-iterate-over-a-list-in-chunks\n\n    Parameters\n    ----------\n    seq: input list\n    size: size of chunks\n\n    Returns\n    -------\n    The list of lists of size `size`\n    \"\"\"\n    yield from (cast(S, seq[pos : pos + size]) for pos in range(0, len(seq), size))\n\n\nD = TypeVar('D', bound=Mapping)\n\n\n@overload\ndef merge_dicts(dict1: D, dict2: D) -> D: ...\n\n\n# NG: I don't have a good idea how to type this properly.\n# The idea is that if the two types are identical, we\n# return the same type (for TypedDicts). Otherwise, we\n# want to return just a dict without any assumptions.\n@overload\ndef merge_dicts(dict1: dict, dict2: dict) -> dict: ...  # type: ignore\n\n\ndef merge_dicts(dict1: Mapping, dict2: Mapping) -> Mapping:\n    \"\"\"Recursively merge two dictionaries.\n\n    Values in dict2 override values in dict1. If dict1 and dict2 contain a dictionary as a\n    value, this will call itself recursively to merge these dictionaries.\n    This does not modify the input dictionaries (creates an internal copy).\n\n    Parameters\n    ----------\n    dict1: dict\n        First dict.\n    dict2: dict\n        Second dict. Values in dict2 will override values from dict1 in case they share the same key.\n\n    Returns\n    -------\n    return_dict: dict\n        Merged dictionaries.\n\n    \"\"\"\n    if not isinstance(dict1, dict):\n        raise ValueError(f'Expecting dict1 to be dict, found {type(dict1)}.')\n    if not isinstance(dict2, dict):\n        raise ValueError(f'Expecting dict2 to be dict, found {type(dict2)}.')\n\n    return_dict = copy.deepcopy(dict1)\n\n    for k, v in dict2.items():\n        if k not in dict1:\n            return_dict[k] = v\n        else:\n            if isinstance(v, dict) and isinstance(dict1[k], dict):\n                return_dict[k] = merge_dicts(dict1[k], dict2[k])\n            else:\n                return_dict[k] = dict2[k]\n\n    return return_dict\n\n\ndef remove_keys_from_nested(d: dict, keys: Iterable[str] = ()) -> dict:\n    \"\"\"Removes keys from a nested dictionary\n\n    Parameters\n    ----------\n    d : Dict\n        the dict to remove keys from\n    keys : List[str], optional\n        the keys to remove, by default []. Prefixes are also allowed.\n\n    Returns\n    -------\n    Dict\n        a copy of the dict without the values in `keys`\n    \"\"\"\n    return unflatten(\n        {\n            k: v\n            for k, v in flatten(d).items()\n            if not any(k.startswith(key) for key in keys)\n        }\n    )\n\n\ndef make_hash(d: dict, exclude_keys: Sequence[str] = ()):\n    \"\"\"\n    Generate a hash for the input dictionary.\n    From: https://stackoverflow.com/a/22003440\n    Parameters\n    ----------\n    d : Dict\n        The dictionary to hash\n    exclude_keys : List[str]\n        Keys to not hash.\n\n    Returns\n    -------\n    hash (hex encoded) of the input dictionary.\n    \"\"\"\n    import hashlib\n    import json\n\n    return hashlib.md5(\n        json.dumps(remove_keys_from_nested(d, exclude_keys), sort_keys=True).encode(\n            'utf-8'\n        )\n    ).hexdigest()\n\n\nclass Hashabledict(dict):\n    def __hash__(self):  # type: ignore - I don't think we can satisfy this. This is indeed a hack.\n        import json\n\n        return hash(json.dumps(self, sort_keys=True))\n\n\n@contextmanager\ndef working_directory(path: Path | str):\n    \"\"\"\n    Context manager to temporarily change the working directory.\n\n    Parameters\n    ----------\n    path: Path\n        Path to the new working directory.\n    \"\"\"\n    origin = Path().absolute()\n    try:\n        os.chdir(path)\n        yield\n    finally:\n        os.chdir(origin)\n\n\ndef to_slices(items: list[int]) -> list[tuple[int, int]]:\n    \"\"\"\n    Convert a list of integers to a list of slices.\n\n    Parameters\n    ----------\n    items: List[int]\n        List of integers.\n\n    Returns\n    -------\n    List[Tuple[int, int]]\n        List of slices.\n    \"\"\"\n    slices = []\n    if len(items) == 0:\n        return slices\n    items = sorted(items)\n    start, end = items[0], items[0]\n    for i in items[1:]:\n        if i == end + 1:\n            end = i\n        else:\n            slices.append((start, end))\n            start, end = i, i\n    # last slice\n    slices.append((start, end))\n    return slices\n\n\ndef slice_to_str(s: tuple[int, int]) -> str:\n    \"\"\"\n    Convert a slice to a string.\n\n    Parameters\n    ----------\n    s: Tuple[int, int]\n        The slice.\n\n    Returns\n    -------\n    str\n        The slice as a string.\n    \"\"\"\n    if s[0] == s[1]:\n        return str(s[0])\n    else:\n        return f'{s[0]}-{s[1]}'\n\n\ndef to_hashable(x: Any) -> Any:\n    \"\"\"Returns a hashable representation of an object. Currently supports dicts and other iterables (which will be\n    transformed into tuples)\n\n    Parameters\n    ----------\n    x : Any\n        the object to transform\n\n    Returns\n    -------\n    Any\n        the hashable representation\n    \"\"\"\n    if isinstance(x, Hashable):\n        return x\n    elif isinstance(x, Dict):\n        Hashabledict((k, to_hashable(v)) for k, v in x.items())\n    elif isinstance(x, Iterable):\n        return tuple(map(to_hashable, x))\n    else:\n        raise ValueError(f'{x} of type {type(x)} is not hashable.')\n\n\nT = TypeVar('T', bound=Callable)\n\n\ndef warn_multiple_calls(warning: str, warn_after: int = 1):\n    \"\"\"\n    Decorator to warn if a function is called multiple times.\n\n    Parameters\n    ----------\n    warning: str\n        The warning message.\n    warn_after: int\n        The number of calls after which to warn.\n    \"\"\"\n\n    def decorator(f: T) -> T:\n        num_calls = 0\n\n        @functools.wraps(f)\n        def wrapper(*args, **kwargs):\n            nonlocal num_calls\n            num_calls += 1\n            if num_calls > warn_after:\n                logging.warning(warning.format(num_calls=num_calls))\n            return f(*args, **kwargs)\n\n        return cast(T, wrapper)\n\n    return decorator\n\n\ndef load_text_resource(path: str | Path):\n    \"\"\"\n    Read a text resource from the package.\n\n    Parameters\n    ----------\n    path: str | Path\n        Path to the resource.\n\n    Returns\n    -------\n    str\n        The resource content.\n    \"\"\"\n    path = Path(path)\n    try:\n        import importlib.resources\n\n        full_path = importlib.resources.files('seml') / path  # type: ignore\n    except (AttributeError, ImportError):\n        # Python 3.8\n        import importlib_resources\n\n        full_path = importlib_resources.files('seml') / path\n\n    with open(str(full_path)) as inp:\n        return inp.read()\n\n\ndef assert_package_installed(package: str, error: str):\n    \"\"\"\n    Assert that a package is installed.\n\n    Parameters\n    ----------\n    package: str\n        The package name.\n    \"\"\"\n    import importlib\n\n    try:\n        importlib.import_module(package)\n    except ImportError:\n        logging.error(error)\n        exit(1)\n\n\ndef find_jupyter_host(\n    log_file: str | Path, wait: bool\n) -> tuple[str | None, bool | None]:\n    \"\"\"\n    Extracts the hostname from the jupyter log file and returns the URL.\n\n    Parameters\n    ----------\n    log_file: str | Path\n        The path to the log file.\n    wait: bool\n        Whether to wait until the jupyter server is running.\n\n    Returns\n    -------\n    Optional[str]\n        The URL of the jupyter server.\n    Optional[bool]\n        Whether the hostname is known. If None is returned, an error occured.\n    \"\"\"\n    import subprocess\n\n    hosts_str = subprocess.run(\n        'sinfo -h -o \"%N|%o\"', shell=True, check=True, capture_output=True\n    ).stdout.decode('utf-8')\n    hosts = {\n        h.split('|')[0]: h.split('|')[1] for h in hosts_str.split('\\n') if len(h) > 1\n    }\n    # Wait until jupyter is running\n    if wait:\n        log_file_contents = ''\n        while ' is running at' not in log_file_contents:\n            if os.path.exists(log_file):\n                with open(log_file) as f:\n                    log_file_contents = f.read()\n            time.sleep(0.5)\n    else:\n        if not os.path.exists(log_file):\n            return None, None\n        with open(log_file) as f:\n            log_file_contents = f.read()\n        if ' is running at' not in log_file_contents:\n            return None, None\n    # Determine hostname\n    JUPYTER_LOG_HOSTNAME_PREFIX = 'SLURM assigned me the node(s): '\n    hostname = (\n        [x for x in log_file_contents.split('\\n') if JUPYTER_LOG_HOSTNAME_PREFIX in x][\n            0\n        ]\n        .split(':')[1]\n        .strip()\n    )\n    if hostname in hosts:\n        hostname = hosts[hostname]\n        known_host = True\n    else:\n        known_host = False\n    # Obtain general URL\n    log_file_split = log_file_contents.split('\\n')\n    url_lines = [x for x in log_file_split if 'http' in x]\n    url = url_lines[0].split(' ')\n    url_str = None\n    for s in url:\n        if s.startswith('http://') or s.startswith('https://'):\n            url_str = s\n            break\n    if url_str is None:\n        return log_file_contents, None\n    url_str = hostname + ':' + url_str.split(':')[-1]\n    url_str = url_str.rstrip('/')\n    if url_str.endswith('/lab'):\n        url_str = url_str[:-4]\n    return url_str, known_host\n\n\n@functools.cache\ndef get_virtual_env_path():\n    \"\"\"\n    Get the path to the virtual environment.\n\n    Returns\n    -------\n    str\n        The path to the virtual environment.\n    \"\"\"\n    if path := os.environ.get('VIRTUAL_ENV', os.environ.get('CONDA_PREFIX')):\n        return Path(path).expanduser().resolve()\n    return None\n\n\ndef is_local_file(\n    filename: str | Path,\n    root_dir: str | Path,\n    ignore_site_packages_folder: bool = True,\n):\n    \"\"\"\n    See https://github.com/IDSIA/sacred/blob/master/sacred/dependencies.py\n    Parameters\n    ----------\n    filename\n    root_dir\n\n    Returns\n    -------\n    bool\n    \"\"\"\n    import site\n\n    file_path = Path(filename).expanduser().resolve()\n    root_path = Path(root_dir).expanduser().resolve()\n    # We do the simple check first to avoid the expensive loop check\n    # Check if file lies within the root directory\n    if not file_path.is_relative_to(root_path):\n        return False\n    # Reject all files that are in some-diretory called `site-packages`\n    if not ignore_site_packages_folder and 'site-packages' in str(file_path):\n        return False\n    if (venv := get_virtual_env_path()) and file_path.is_relative_to(venv):\n        return False\n    # Check if the file is in any environment site-packages\n    for site_dir in map(Path, site.getsitepackages()):\n        if file_path.is_relative_to(site_dir):\n            return False\n    # We are in the root_dir and not in any site-packages\n    return True\n\n\ndef smaller_than_version_filter(version: tuple[int, int, int]):\n    \"\"\"\n    Returns a mongodb filter that selects experiments where the version number\n    is small or equal to the supplied version.\n\n    Parameters\n    ----------\n    version: Tuple[int, int, int]\n        The version number to compare to.\n\n    Returns\n    -------\n    Dict\n        The filter.\n    \"\"\"\n    from seml.settings import SETTINGS\n\n    version_prefix = f'seml.{SETTINGS.SEML_CONFIG_VALUE_VERSION}'\n    return {\n        '$or': [\n            {f'{version_prefix}.0': {'$lt': version[0]}},\n            {\n                f'{version_prefix}.0': {'$eq': version[0]},\n                f'{version_prefix}.1': {'$lt': version[1]},\n            },\n            {\n                f'{version_prefix}.0': {'$eq': version[0]},\n                f'{version_prefix}.1': {'$eq': version[1]},\n                f'{version_prefix}.2': {'$lt': version[2]},\n            },\n        ]\n    }\n\n\ndef utcnow():\n    \"\"\"\n    Wrapper around datetime.datetime.now(datetime.UTC) but supports older python versions.\n\n    Returns\n    -------\n    datetime.datetime\n        The current datetime.\n    \"\"\"\n    import datetime\n\n    try:\n        return datetime.datetime.now(datetime.UTC)  # type: ignore - here the type checker may fail in old python version\n    except AttributeError:\n        return datetime.datetime.utcnow()\n\n\nTD = TypeVar('TD', bound=Mapping[str, Any])\n\n\ndef to_typeddict(d: Mapping[str, Any], cls: type[TD], missing_ok: bool = True) -> TD:\n    \"\"\"\n    Returns a new TypedDict where only keys that are in the class type are kept.\n\n    If the class has an `__extra_items__` attribute, the input dict is returned as is.\n\n    If one wants to explicitly drop the delta between two typed dicts, use `cast_and_drop`.\n\n    Parameters\n    ----------\n    d: Mapping[str, Any]\n        The object to cast.\n    cls: type[TD]\n        The target class.\n    missing_ok: bool, default: True\n        Whether to allow missing keys in `d`.\n\n    Returns\n    -------\n    TD\n        The new object.\n    \"\"\"\n    from copy import deepcopy\n\n    if getattr(cls, '__extra_items__', None):\n        return cast(TD, deepcopy(d))\n\n    result = dict()\n    for key in cls.__annotations__:\n        if key in d:\n            result[key] = d[key]\n        elif not missing_ok:\n            raise ValueError(f'Missing key {key} in {d}.')\n    return cast(TD, result)\n\n\nTD1 = TypeVar('TD1', bound=Mapping[str, Any])\nTD2 = TypeVar('TD2', bound=Mapping[str, Any])\n\n\ndef drop_typeddict_difference(obj: TD1, cls: type[TD1], cls2: type[TD2]) -> TD2:\n    \"\"\"\n    Returns a new TypedDict where all keys that cls has but cls2 does not have are dropped.\n\n    Parameters\n    ----------\n    obj: TD1\n        The object to cast.\n    cls: type[TD1]\n        The current class.\n    cls2: type[TD2]\n        The target class.\n\n    Returns\n    -------\n    TD2\n        The new object.\n    \"\"\"\n    from copy import deepcopy\n\n    result = dict(deepcopy(obj))\n    to_drop = [key for key in cls.__annotations__ if key not in cls2.__annotations__]\n    for k in to_drop:\n        if k in result:\n            del result[k]\n    return result  # type: ignore\n"
  },
  {
    "path": "src/seml/utils/errors.py",
    "content": "from __future__ import annotations\n\n\nclass InputError(SystemExit):\n    \"\"\"Parent class for input errors that don't print a stack trace.\"\"\"\n\n    pass\n\n\nclass ConfigError(InputError):\n    \"\"\"Raised when the something is wrong in the config\"\"\"\n\n    def __init__(self, message='The config file contains an error.'):\n        super().__init__(f'CONFIG ERROR: {message}')\n\n\nclass ExecutableError(InputError):\n    \"\"\"Raised when the something is wrong with the Python executable\"\"\"\n\n    def __init__(self, message='The Python executable has a problem.'):\n        super().__init__(f'EXECUTABLE ERROR: {message}')\n\n\nclass MongoDBError(InputError):\n    \"\"\"Raised when the something is wrong with the MongoDB\"\"\"\n\n    def __init__(self, message='The MongoDB or its config has a problem.'):\n        super().__init__(f'MONGODB ERROR: {message}')\n\n\nclass ArgumentError(InputError):\n    \"\"\"Raised when the something is wrong with the parsed arguments\"\"\"\n\n    def __init__(self, message='The parsed arguments contain an error.'):\n        super().__init__(f'ARGUMENT ERROR: {message}')\n"
  },
  {
    "path": "src/seml/utils/io.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n\ndef tail_file(path: str | Path, n: int = 1):\n    \"\"\"\n    Returns the last n lines of a file.\n\n    Args:\n        path (str | Path): Path to the file.\n        n (int, optional): Number of lines to return. Defaults to 1.\n\n    Returns:\n        str: The last n lines of the file.\n    \"\"\"\n    if n == 0:\n        return ''\n    num_newlines = 0\n    with open(path, 'rb') as f:\n        try:\n            f.seek(-1, os.SEEK_END)\n            if f.read(1) == b'\\n':\n                num_newlines += 1\n            while num_newlines < n:\n                f.seek(-2, os.SEEK_CUR)\n                if f.read(1) in (b'\\n', b'\\r'):\n                    num_newlines += 1\n        except OSError:\n            f.seek(0)\n        last_line = f.read().decode(errors='replace')\n    return last_line\n"
  },
  {
    "path": "src/seml/utils/json.py",
    "content": "# type: ignore - we ignore typing here since this is copied from the CPython repo\n# We need the custom json encoding for vscode due to https://github.com/microsoft/vscode/issues/91578\n# Once this bug has been fixed we should only rely on `repr` and remove this file.\nimport json\n\n\nclass NumpyEncoder(json.JSONEncoder):\n    def default(self, o):\n        import numpy as np\n        from bson import json_util\n\n        if isinstance(o, np.integer):\n            return int(o)\n        elif isinstance(o, np.floating):\n            return float(o)\n        elif isinstance(o, np.ndarray):\n            return o.tolist()\n        # we default to json_util.default as it can deal with more data types.\n        return json_util.default(o)\n\n\nclass PythonEncoder(json.JSONEncoder):\n    def iterencode(self, o, _one_shot=False):\n        if self.check_circular:\n            markers = {}\n        else:\n            markers = None\n        if self.ensure_ascii:\n            _encoder = json.encoder.encode_basestring_ascii\n        else:\n            _encoder = json.encoder.encode_basestring\n\n        def floatstr(\n            o,\n            allow_nan=self.allow_nan,\n            _repr=float.__repr__,\n            _inf=json.encoder.INFINITY,\n            _neginf=-json.encoder.INFINITY,\n        ):\n            # Check for specials.  Note that this type of test is processor\n            # and/or platform-specific, so do tests which don't depend on the\n            # internals.\n\n            if o != o:\n                text = 'nan'\n            elif o == _inf:\n                text = 'inf'\n            elif o == _neginf:\n                text = '-inf'\n            else:\n                return _repr(o)\n\n            if not allow_nan:\n                raise ValueError(\n                    'Out of range float values are not JSON compliant: ' + repr(o)\n                )\n\n            return text\n\n        _iterencode = _make_iterencode(\n            markers,\n            self.default,\n            _encoder,\n            self.indent,\n            floatstr,\n            self.key_separator,\n            self.item_separator,\n            self.sort_keys,\n            self.skipkeys,\n            _one_shot,\n        )\n        return _iterencode(o, 0)\n\n\ndef _make_iterencode(\n    markers,\n    _default,\n    _encoder,\n    _indent,\n    _floatstr,\n    _key_separator,\n    _item_separator,\n    _sort_keys,\n    _skipkeys,\n    _one_shot,\n    ## HACK: hand-optimized bytecode; turn globals into locals\n    ValueError=ValueError,\n    dict=dict,\n    float=float,\n    id=id,\n    int=int,\n    isinstance=isinstance,\n    list=list,\n    str=str,\n    tuple=tuple,\n    _intstr=int.__repr__,\n):\n    if _indent is not None and not isinstance(_indent, str):\n        _indent = ' ' * _indent\n\n    def _iterencode_list(lst, _current_indent_level):\n        if not lst:\n            yield '[]'\n            return\n        if markers is not None:\n            markerid = id(lst)\n            if markerid in markers:\n                raise ValueError('Circular reference detected')\n            markers[markerid] = lst\n        buf = '['\n        if _indent is not None:\n            _current_indent_level += 1\n            newline_indent = '\\n' + _indent * _current_indent_level\n            separator = _item_separator + newline_indent\n            buf += newline_indent\n        else:\n            newline_indent = None\n            separator = _item_separator\n        first = True\n        for value in lst:\n            if first:\n                first = False\n            else:\n                buf = separator\n            if isinstance(value, str):\n                yield buf + _encoder(value)\n            elif value is None:\n                yield buf + 'None'\n            elif value is True:\n                yield buf + 'True'\n            elif value is False:\n                yield buf + 'False'\n            elif isinstance(value, int):\n                # Subclasses of int/float may override __repr__, but we still\n                # want to encode them as integers/floats in JSON. One example\n                # within the standard library is IntEnum.\n                yield buf + _intstr(value)\n            elif isinstance(value, float):\n                # see comment above for int\n                yield buf + _floatstr(value)\n            else:\n                yield buf\n                if isinstance(value, (list, tuple)):\n                    chunks = _iterencode_list(value, _current_indent_level)\n                elif isinstance(value, dict):\n                    chunks = _iterencode_dict(value, _current_indent_level)\n                else:\n                    chunks = _iterencode(value, _current_indent_level)\n                yield from chunks\n        if newline_indent is not None:\n            _current_indent_level -= 1\n            yield '\\n' + _indent * _current_indent_level\n        yield ']'\n        if markers is not None:\n            del markers[markerid]\n\n    def _iterencode_dict(dct, _current_indent_level):\n        if not dct:\n            yield '{}'\n            return\n        if markers is not None:\n            markerid = id(dct)\n            if markerid in markers:\n                raise ValueError('Circular reference detected')\n            markers[markerid] = dct\n        yield '{'\n        if _indent is not None:\n            _current_indent_level += 1\n            newline_indent = '\\n' + _indent * _current_indent_level\n            item_separator = _item_separator + newline_indent\n            yield newline_indent\n        else:\n            newline_indent = None\n            item_separator = _item_separator\n        first = True\n        if _sort_keys:\n            items = sorted(dct.items())\n        else:\n            items = dct.items()\n        for key, value in items:\n            if isinstance(key, str):\n                pass\n            # JavaScript is weakly typed for these, so it makes sense to\n            # also allow them.  Many encoders seem to do something like this.\n            elif isinstance(key, float):\n                # see comment for int/float in _make_iterencode\n                key = _floatstr(key)\n            elif key is True:\n                key = 'True'\n            elif key is False:\n                key = 'False'\n            elif key is None:\n                key = 'None'\n            elif isinstance(key, int):\n                # see comment for int/float in _make_iterencode\n                key = _intstr(key)\n            elif _skipkeys:\n                continue\n            else:\n                raise TypeError(\n                    f'keys must be str, int, float, bool or None, '\n                    f'not {key.__class__.__name__}'\n                )\n            if first:\n                first = False\n            else:\n                yield item_separator\n            yield _encoder(key)\n            yield _key_separator\n            if isinstance(value, str):\n                yield _encoder(value)\n            elif value is None:\n                yield 'None'\n            elif value is True:\n                yield 'True'\n            elif value is False:\n                yield 'False'\n            elif isinstance(value, int):\n                # see comment for int/float in _make_iterencode\n                yield _intstr(value)\n            elif isinstance(value, float):\n                # see comment for int/float in _make_iterencode\n                yield _floatstr(value)\n            else:\n                if isinstance(value, (list, tuple)):\n                    chunks = _iterencode_list(value, _current_indent_level)\n                elif isinstance(value, dict):\n                    chunks = _iterencode_dict(value, _current_indent_level)\n                else:\n                    chunks = _iterencode(value, _current_indent_level)\n                yield from chunks\n        if newline_indent is not None:\n            _current_indent_level -= 1\n            yield '\\n' + _indent * _current_indent_level\n        yield '}'\n        if markers is not None:\n            del markers[markerid]\n\n    def _iterencode(o, _current_indent_level):\n        if isinstance(o, str):\n            yield _encoder(o)\n        elif o is None:\n            yield 'None'\n        elif o is True:\n            yield 'True'\n        elif o is False:\n            yield 'False'\n        elif isinstance(o, int):\n            # see comment for int/float in _make_iterencode\n            yield _intstr(o)\n        elif isinstance(o, float):\n            # see comment for int/float in _make_iterencode\n            yield _floatstr(o)\n        elif isinstance(o, (list, tuple)):\n            yield from _iterencode_list(o, _current_indent_level)\n        elif isinstance(o, dict):\n            yield from _iterencode_dict(o, _current_indent_level)\n        else:\n            if markers is not None:\n                markerid = id(o)\n                if markerid in markers:\n                    raise ValueError('Circular reference detected')\n                markers[markerid] = o\n            o = _default(o)\n            yield from _iterencode(o, _current_indent_level)\n            if markers is not None:\n                del markers[markerid]\n\n    return _iterencode\n"
  },
  {
    "path": "src/seml/utils/multi_process.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport os\nimport sys\nfrom typing import Callable, TypeVar, overload\n\nfrom typing_extensions import ParamSpec\n\n_LOCAL_ID = 'SLURM_LOCALID'\n_PROCESS_ID = 'SLURM_PROCID'\n_PROCESS_COUNT = 'SLURM_NTASKS'\n\n\ndef process_id():\n    return int(os.environ.get(_PROCESS_ID, 0))\n\n\ndef local_id():\n    return int(os.environ.get(_LOCAL_ID, 0))\n\n\ndef process_count():\n    return int(os.environ.get(_PROCESS_COUNT, 1))\n\n\ndef is_main_process():\n    return process_id() == 0\n\n\ndef is_local_main_process():\n    return local_id() == 0\n\n\ndef is_running_in_multi_process():\n    return process_count() > 1\n\n\nclass ChildProcessSkip(Exception): ...\n\n\nclass MainProcessExecuteContext:\n    def __enter__(self):\n        if not is_main_process():\n            sys.settrace(lambda *args, **keys: None)\n            frame = sys._getframe(1)\n            frame.f_trace = self.trace\n\n    def trace(self, frame, event, arg):\n        raise ChildProcessSkip()\n\n    def __exit__(self, type, value, traceback):\n        if type is None:\n            return  # No exception\n        if issubclass(type, ChildProcessSkip):\n            return True  # Suppress special SkipWithBlock exception\n\n\nP = ParamSpec('P')\nR = TypeVar('R')\n\n\n@overload\ndef only_on_main_process(func: Callable[P, R]) -> Callable[P, R | None]: ...\n\n\n@overload\ndef only_on_main_process(func: None = None) -> MainProcessExecuteContext: ...\n\n\ndef only_on_main_process(\n    func: Callable[P, R] | None = None,\n) -> Callable[P, R | None] | MainProcessExecuteContext:\n    if callable(func):\n\n        @functools.wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs):\n            if is_main_process():\n                return func(*args, **kwargs)\n            return None\n\n        return wrapper\n    else:\n        return MainProcessExecuteContext()\n"
  },
  {
    "path": "src/seml/utils/network.py",
    "content": "from __future__ import annotations\n\nimport sys\n\n\ndef get_network_interfaces():\n    import array\n    import fcntl\n    import socket\n    import struct\n\n    # From https://code.activestate.com/recipes/439093/#c1\n    is_64bits = sys.maxsize > 2**32\n    struct_size = 40 if is_64bits else 32\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    max_possible = 8  # initial value\n    while True:\n        _bytes = max_possible * struct_size\n        names = array.array('B')\n        for i in range(0, _bytes):\n            names.append(0)\n        outbytes = struct.unpack(\n            'iL',\n            fcntl.ioctl(\n                s.fileno(),\n                0x8912,  # SIOCGIFCONF\n                struct.pack('iL', _bytes, names.buffer_info()[0]),\n            ),\n        )[0]\n        if outbytes == _bytes:\n            max_possible *= 2\n        else:\n            break\n    namestr = names.tobytes()\n    ifaces: dict[str, str] = {}\n    for i in range(0, outbytes, struct_size):\n        iface_name = bytes.decode(namestr[i : i + 16]).split('\\0', 1)[0]\n        iface_addr = socket.inet_ntoa(namestr[i + 20 : i + 24])\n        ifaces[iface_name] = iface_addr\n\n    return ifaces\n\n\ndef find_free_port():\n    import socket\n    from contextlib import closing\n\n    ifaces = get_network_interfaces()\n    if 'lo' in ifaces:\n        del ifaces['lo']\n    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:\n        s.bind((list(ifaces.values())[0], 0))\n        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n        ip_address, port = s.getsockname()[:2]\n        return ip_address, port\n"
  },
  {
    "path": "src/seml/utils/slurm.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport os\nimport subprocess\nimport time\n\nfrom seml.settings import SETTINGS\n\n\n@functools.lru_cache\ndef get_cluster_name():\n    \"\"\"\n    Retrieves the name of the cluster from the Slurm configuration.\n\n    Returns\n    -------\n    str\n        The name of the cluster\n    \"\"\"\n    try:\n        return (\n            subprocess.run(\n                \"scontrol show config | grep ClusterName | awk '{print $3}'\",\n                shell=True,\n                capture_output=True,\n            )\n            .stdout.decode()\n            .strip()\n        )\n    except subprocess.SubprocessError:\n        return 'unknown'\n\n\ndef get_slurm_jobs(*job_ids: str):\n    \"\"\"\n    Returns a list of dictionaries containing information about the Slurm jobs with the given job IDs.\n    If no job IDs are provided, information about all jobs is returned.\n\n    Parameters\n    ----------\n    job_ids : Optional[Sequence[str]]\n        The job IDs of the jobs to get information about\n\n    Returns\n    -------\n    List[Dict[str, str]]\n        A list of dictionaries containing information about the jobs\n    \"\"\"\n    if len(job_ids) == 0:\n        job_info_str = subprocess.run(\n            'scontrol show job',\n            shell=True,\n            check=True,\n            capture_output=True,\n        ).stdout.decode('utf-8')\n        job_info_strs = job_info_str.split('\\n\\n')\n    else:\n        job_info_strs = []\n        for job_id in job_ids:\n            job_info_str = subprocess.run(\n                f'scontrol show job {job_id}',\n                shell=True,\n                check=True,\n                capture_output=True,\n            ).stdout.decode('utf-8')\n            job_info_strs.append(job_info_str)\n    job_info_strs = list(filter(None, job_info_strs))\n    job_infos = list(map(parse_scontrol_job_info, job_info_strs))\n    return job_infos\n\n\ndef parse_scontrol_job_info(job_info: str):\n    \"\"\"\n    Converts the return value of `scontrol show job <jobid>` into a python dictionary.\n\n    Parameters\n    ----------\n    job_info : str\n        The output of `scontrol show job <jobid>`\n\n    Returns\n    -------\n    dict\n        The job information as a dictionary\n    \"\"\"\n    job_info_dict: dict[str, str] = {}\n    # we may split to many times, e.g., if a value contains a space\n    unfiltered_lines = job_info.split()\n    filtered_lines = []\n    for line in unfiltered_lines:\n        if line:\n            if '=' in line:\n                # new variable\n                filtered_lines.append(line)\n            else:\n                # just append to the previous variable\n                filtered_lines[-1] += ' ' + line\n\n    # Now every line must contain a '=' sign and we can simply split here\n    for line in filtered_lines:\n        key, value = line.split('=', 1)\n        job_info_dict[key] = value\n    return job_info_dict\n\n\ndef get_slurm_arrays_tasks(\n    filter_by_user: bool = False,\n) -> dict[int, tuple[list[range], list[int]]]:\n    \"\"\"Get a dictionary of running/pending Slurm job arrays (as keys) and tasks (as values)\n\n    Parameters:\n    -----------\n    filter_by_user : bool\n        Whether to only check jobs by the current user, by default False\n    \"\"\"\n    try:\n        squeue_cmd = f'SLURM_BITSTR_LEN=1024 squeue -a -t {\",\".join(SETTINGS.SLURM_STATES.ACTIVE)} -h -o %i'\n        if filter_by_user:\n            squeue_cmd += ' -u `whoami`'\n        squeue_out = subprocess.run(\n            squeue_cmd, shell=True, check=True, capture_output=True\n        ).stdout\n        jobs = [job_str for job_str in squeue_out.splitlines() if b'_' in job_str]\n        if len(jobs) > 0:\n            array_ids_str, task_ids = zip(*[job_str.split(b'_') for job_str in jobs])\n            # `job_dict`: This dictionary has the job array IDs as keys and the values are\n            # a list of 1) the pending job task range and 2) a list of running job task IDs.\n            job_dict: dict[int, tuple[list[range], list[int]]] = {}\n            for i, task_range_str in enumerate(task_ids):\n                array_id = int(array_ids_str[i])\n                if array_id not in job_dict:\n                    job_dict[array_id] = ([range(0)], [])\n\n                if b'[' in task_range_str:\n                    # Remove brackets and maximum number of simultaneous jobs\n                    task_range_str = task_range_str[1:-1].split(b'%')[0]\n                    # The overall pending tasks array can be split into multiple arrays by cancelling jobs\n                    job_id_ranges = task_range_str.split(b',')\n                    for r in job_id_ranges:\n                        if b'-' in r:\n                            lower, upper = r.split(b'-')\n                        else:\n                            lower = upper = r\n                        job_dict[array_id][0].append(range(int(lower), int(upper) + 1))\n                else:\n                    # Single task IDs belong to running jobs\n                    task_id = int(task_range_str)\n                    job_dict[array_id][1].append(task_id)\n\n            return job_dict\n        else:\n            return {}\n    except subprocess.CalledProcessError:\n        return {}\n\n\ndef get_current_slurm_array_id():\n    slurm_array_id = os.environ.get('SLURM_ARRAY_JOB_ID', None)\n    slurm_task_id = os.environ.get('SLURM_ARRAY_TASK_ID', None)\n    return slurm_array_id, slurm_task_id\n\n\ndef get_current_slurm_job_id():\n    return os.environ.get('SLURM_JOB_ID', None)\n\n\ndef cancel_slurm_jobs(*job_ids: str | int, state: str | None = None):\n    \"\"\"\n    Cancels the Slurm jobs with the given job IDs.\n\n    Parameters\n    ----------\n    job_ids : Sequence[str]\n        The job IDs of the jobs to cancel\n    \"\"\"\n    if len(job_ids) == 0:\n        return\n    job_str = ' '.join(map(str, job_ids))\n    if state is not None:\n        subprocess.run(f'scancel -t {state} {job_str}', shell=True, check=False)\n    else:\n        subprocess.run(f'scancel {job_str}', shell=True, check=False)\n\n\ndef are_slurm_jobs_running(*job_ids: str):\n    \"\"\"\n    Checks the Slurm queue to see if the jobs with the given job IDs are still running.\n\n    Parameters\n    ----------\n    job_ids : Sequence[str]\n        The job IDs of the jobs to check\n\n    Returns\n    -------\n    bool\n        True if the jobs are still running, False otherwise\n    \"\"\"\n    return (\n        len(\n            subprocess.run(\n                f\"squeue -h -o '%A' --jobs={','.join(job_ids)}\",\n                shell=True,\n                check=True,\n                capture_output=True,\n            ).stdout\n        )\n        > 0\n    )\n\n\ndef wait_until_slurm_jobs_finished(*job_ids: str, timeout: int | float):\n    \"\"\"\n    Waits until all jobs are finished or until the timeout is reached.\n\n    Parameters\n    ----------\n    job_ids: Sequence[str]\n        The job IDs of the jobs to wait for\n    timeout: Union[int, float]\n        The maximum time to wait in seconds\n\n    Returns\n    -------\n    bool\n        True if the jobs finished before the timeout, False otherwise\n    \"\"\"\n    end_time = time.time() + timeout\n    while are_slurm_jobs_running(*job_ids):\n        time.sleep(0.1)\n        if time.time() > end_time:\n            return False\n    return True\n"
  },
  {
    "path": "src/seml/utils/ssh_forward.py",
    "content": "from __future__ import annotations\n\nimport atexit\nimport logging\nimport time\nfrom typing import TYPE_CHECKING, Any\n\nfrom seml.document import ExperimentDoc\nfrom seml.settings import SETTINGS\nfrom seml.utils import Hashabledict, assert_package_installed\n\nif TYPE_CHECKING:\n    import multiprocessing.connection\n\nStates = SETTINGS.STATES\n\n\ndef retried_and_locked_ssh_port_forward(\n    retries_max: int = SETTINGS.SSH_FORWARD.RETRIES_MAX,\n    retries_delay: int = SETTINGS.SSH_FORWARD.RETRIES_DELAY,\n    lock_file: str = SETTINGS.SSH_FORWARD.LOCK_FILE,\n    lock_timeout: int = SETTINGS.SSH_FORWARD.LOCK_TIMEOUT,\n    **ssh_config,\n):\n    \"\"\"\n    Attempt to establish an SSH tunnel with retries and a lock file to avoid parallel tunnel establishment.\n\n    Parameters\n    ----------\n    retries_max: int\n        Maximum number of retries to establish the tunnel.\n    retries_delay: float\n        Initial delay for exponential backoff.\n    lock_file: str\n        Path to the lock file.\n    lock_timeout: int\n        Timeout for acquiring the lock.\n    ssh_config: dict\n        Configuration for the SSH tunnel.\n\n    Returns\n    -------\n    server: SSHTunnelForwarder\n        The SSH tunnel server.\n    \"\"\"\n    import random\n\n    from filelock import FileLock, Timeout\n    from sshtunnel import BaseSSHTunnelForwarderError, SSHTunnelForwarder\n\n    delay = retries_delay\n    error = None\n    # disable SSH forward messages\n    logging.getLogger('paramiko.transport').disabled = True\n    for _ in range(retries_max):\n        try:\n            lock = FileLock(lock_file, mode=0o666, timeout=lock_timeout)\n            with lock:\n                server = SSHTunnelForwarder(**ssh_config)\n                server.start()\n                if not server.tunnel_is_up[server.local_bind_address]:\n                    raise BaseSSHTunnelForwarderError()\n                return server\n        except Timeout as e:\n            error = e\n            logging.warn(f'Failed to aquire lock for ssh tunnel {lock_file}')\n        except BaseSSHTunnelForwarderError as e:\n            error = e\n            logging.warn(f'Retry establishing ssh tunnel in {delay} s')\n            # Jittered exponential retry\n            time.sleep(delay)\n            delay *= 2\n            delay += random.uniform(0, 1)\n\n    logging.error(f'Failed to establish ssh tunnel: {error}')\n    exit(1)\n\n\ndef _ssh_forward_process(\n    pipe: multiprocessing.connection.Connection, ssh_config: dict[str, Any]\n):\n    \"\"\"\n    Establish an SSH tunnel in a separate process. The process periodically checks if the tunnel is still up and\n    restarts it if it is not.\n\n    Parameters\n    ----------\n    pipe: multiprocessing.communication.Connection\n        Pipe to communicate with the main process.\n    ssh_config: dict\n        Configuration for the SSH tunnel.\n    \"\"\"\n    server = retried_and_locked_ssh_port_forward(**ssh_config)\n    # We need to bind to the same local addresses\n    server._local_binds = server.local_bind_addresses\n    pipe.send((server.local_bind_host, server.local_bind_port))\n    while True:\n        # check if we should end the process\n        try:\n            if pipe.poll(SETTINGS.SSH_FORWARD.HEALTH_CHECK_INTERVAL):\n                if pipe.closed or pipe.recv() == 'stop':\n                    server.stop()\n                    break\n\n            # Check for tunnel health\n            server.check_tunnels()\n            if not server.tunnel_is_up[server.local_bind_address]:\n                logging.warning('SSH tunnel was closed unexpectedly. Restarting.')\n                server.restart()\n        except KeyboardInterrupt:\n            server.stop()\n            break\n        except EOFError:\n            server.stop()\n            break\n        except Exception as e:\n            logging.error(f'Error in SSH tunnel health check:\\n{e}')\n            server.restart()\n    pipe.close()\n\n\n# We want to reuse the same multiprocessing context for all SSH tunnels\n_mp_context = None\n# To establish only a single connection to a remote\n_forwards: dict[Hashabledict, tuple[str, int]] = {}\n\n\ndef _get_ssh_forward(ssh_config: dict[str, Any]):\n    \"\"\"\n    Establishes an SSH tunnel in a separate process and returns the local address of the tunnel.\n    If a connection to the remote host already exists, it is reused.\n\n    Parameters\n    ----------\n    ssh_config: dict\n        Configuration for the SSH tunnel.\n\n    Returns\n    -------\n    local_address: tuple\n        Local address of the SSH tunnel.\n    try_close: Callable\n        Function to close the SSH tunnel.\n    \"\"\"\n    assert_package_installed(\n        'sshtunnel',\n        'Opening ssh tunnel requires `sshtunnel` (e.g. `pip install sshtunnel`)',\n    )\n    assert_package_installed(\n        'filelock',\n        'Opening ssh tunnel requires `filelock` (e.g. `pip install filelock`)',\n    )\n    import multiprocessing as mp\n\n    global _forwards, _mp_context\n\n    ssh_config = Hashabledict(ssh_config)\n    if ssh_config not in _forwards:\n        if _mp_context is None:\n            _mp_context = mp.get_context('forkserver')\n        main_pipe, forward_pipe = _mp_context.Pipe(True)\n        proc = _mp_context.Process(\n            target=_ssh_forward_process, args=(forward_pipe, ssh_config)\n        )\n        proc.start()\n\n        def try_close():\n            try:\n                if not main_pipe.closed:\n                    main_pipe.send('stop')\n                    main_pipe.close()\n            finally:\n                pass\n\n        # Send stop if we exit the program\n        atexit.register(try_close)\n\n        # Compute the maximum time we should wait\n        retries_max = ssh_config.get('retries_max', SETTINGS.SSH_FORWARD.RETRIES_MAX)\n        retries_delay = ssh_config.get(\n            'retries_delay', SETTINGS.SSH_FORWARD.RETRIES_DELAY\n        )\n        max_delay = 2 ** (retries_max + 1) * retries_delay\n\n        # check if the forward process has been established correctly\n        if main_pipe.poll(max_delay):\n            host, port = main_pipe.recv()\n            _forwards[ssh_config] = (str(host), int(port))\n        else:\n            logging.error('Failed to establish SSH tunnel.')\n            exit(1)\n    return _forwards[ssh_config]\n\n\ndef get_forwarded_mongo_client(\n    db_name: str, username: str, password: str, ssh_config: dict[str, Any], **kwargs\n):\n    \"\"\"\n    Establish an SSH tunnel and return a forwarded MongoDB client.\n    The SSH tunnel is established in a separate process to enable continuously checking for its health.\n\n    Parameters\n    ----------\n    db_name: str\n        Name of the database.\n    username: str\n        Username for the database.\n    password: str\n        Password for the database.\n    ssh_config: dict\n        Configuration for the SSH tunnel.\n    kwargs: dict\n        Additional arguments for the MongoDB client.\n\n    Returns\n    -------\n    client: pymongo.MongoClient\n        Forwarded MongoDB client.\n    \"\"\"\n    import pymongo\n\n    host, port = _get_ssh_forward(ssh_config)\n\n    client = pymongo.MongoClient[ExperimentDoc](\n        host,\n        int(port),\n        username=username,\n        password=password,\n        authSource=db_name,\n        **kwargs,\n    )\n    return client\n"
  },
  {
    "path": "src/seml/utils/yaml.py",
    "content": "import yaml\n\nfrom seml.utils.errors import ConfigError\n\n\nclass YamlUniqueLoader(yaml.FullLoader):\n    \"\"\"\n    Custom YAML loader that disallows duplicate keys\n\n    From https://github.com/encukou/naucse_render/commit/658197ed142fec2fe31574f1ff24d1ff6d268797\n    Workaround for PyYAML issue: https://github.com/yaml/pyyaml/issues/165\n    This disables some uses of YAML merge (`<<`)\n    \"\"\"\n\n\ndef construct_mapping(loader, node, deep=False):\n    \"\"\"Construct a YAML mapping node, avoiding duplicates\"\"\"\n    loader.flatten_mapping(node)\n    result = {}\n    for key_node, value_node in node.value:\n        key = loader.construct_object(key_node, deep=deep)\n        if key in result:\n            raise ConfigError(f\"Found duplicate keys: '{key}'\")\n        result[key] = loader.construct_object(value_node, deep=deep)\n    return result\n\n\nYamlUniqueLoader.add_constructor(\n    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,\n    construct_mapping,\n)\n\n\nclass YamlDumper(yaml.Dumper):\n    def represent_mapping(self, tag, mapping, flow_style=None):\n        if flow_style is None:\n            flow_style = False\n        return super().represent_mapping(tag, mapping, flow_style)\n"
  },
  {
    "path": "test/.ruff.toml",
    "content": "[lint]\nignore = [\n    \"F841\", # ignore errors due to unused variables\n]"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/resources/config/config_nested_parameter_collections.yaml",
    "content": "grid:\n\n  a.b.c:\n    type: choice\n    options:\n      - 999\n      - 1111\n\n  a:\n    type: parameter_collection\n    params:\n      b:\n        type: parameter_collection\n        params:\n          c:\n            type: choice\n            options:\n              - 11\n              - 22"
  },
  {
    "path": "test/resources/config/config_resolve_config.yaml",
    "content": "fixed:\n  foo: 33\n  bar: 44\n  +json.priority: 1\n  +yaml.priority: 2 # yaml can overwrite the json\n\ngrid:\n  # Load parameters from python code\n  +py.name:\n    type: choice\n    options:\n      - py_named_1\n      - py_named_2\n\n  # Load parameters from a json file\n  +json.name:\n    type: choice\n    options:\n      - resources/config/config_resolve_config_named_1.json\n      - resources/config/config_resolve_config_named_2.json\n\n  # Load parameters from a yaml file\n  +yaml.name:\n    type: choice\n    options:\n      - resources/config/config_resolve_config_named_1.yaml\n      - resources/config/config_resolve_config_named_2.yaml\n\n\n\n"
  },
  {
    "path": "test/resources/config/config_resolve_config_named_1.json",
    "content": "{\n    \"json\" : {\n        \"value\" : 11\n    }\n}"
  },
  {
    "path": "test/resources/config/config_resolve_config_named_1.yaml",
    "content": "yaml:\n    value: -1"
  },
  {
    "path": "test/resources/config/config_resolve_config_named_2.json",
    "content": "{\n    \"json\" : {\n        \"value\" : 22\n    }\n}"
  },
  {
    "path": "test/resources/config/config_resolve_config_named_2.yaml",
    "content": "yaml:\n    value: -2\n\n# overwrites the json value due to higher priority value\njson:\n    value: 10000"
  },
  {
    "path": "test/resources/config/config_resolve_with_interpolation.yaml",
    "content": "fixed:\n  foo:\n    bar: 3\n  param1: 'value'\n  interpolated_1: ${config.foo.bar}_${config.param1}\n  interpolated_2: ${config.foo.bar}\n"
  },
  {
    "path": "test/resources/config/config_slurm_default.yaml",
    "content": "seml:\n  executable: test_config.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: ../..\n\nslurm:\n"
  },
  {
    "path": "test/resources/config/config_slurm_default_empty_sbatch.yaml",
    "content": "seml:\n  executable: test_config.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: ../..\n\nslurm:\n  - sbatch_options:\n"
  },
  {
    "path": "test/resources/config/config_slurm_experiment.yaml",
    "content": "seml:\n  executable: test_config.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: ../..\n\nslurm:\n  - sbatch_options_template: GPU\n    sbatch_options:\n      cpus-per-task: 4\n"
  },
  {
    "path": "test/resources/config/config_slurm_experiments_and_tasks.yaml",
    "content": "seml:\n  executable: test_config.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: ../..\n\nslurm:\n  - experiments_per_job: 2\n    sbatch_options:\n      ntasks: 2\n"
  },
  {
    "path": "test/resources/config/config_slurm_template.yaml",
    "content": "seml:\n  executable: test_config.py\n  name: example_experiment\n  output_dir: logs\n  project_root_dir: ../..\n\nslurm:\n  - sbatch_options_template: GPU\n"
  },
  {
    "path": "test/resources/config/config_with_all_types.yaml",
    "content": "fixed:\n  a: 333\n  b: 444\n\ngrid:\n  c:\n    type: choice\n    options:\n      - 555\n      - 666\n\n\nrandom:\n  samples: 3\n  seed: 333\n  d:\n    type: uniform\n    min: 1\n    max: 1\n\nsub1:\n  fixed:\n    a: 9999\n    b: 7777\n\n  grid:\n    c:\n      type: choice\n      options:\n        - 1234\n        - 5678\n\n  random:\n    samples: 5\n    seed: 9999\n    e:\n      type: uniform\n      min: 2\n      max: 2\n\nsub2:\n  grid:\n    f:\n      type: choice\n      options:\n        - 9199\n        - 1099"
  },
  {
    "path": "test/resources/config/config_with_dict_choice.yaml",
    "content": "grid:\n  coll1:\n    type: parameter_collection\n    params:\n      a:\n        type: choice\n        options:\n          - sub1: 999\n            sub2: 1919\n          - sub1: 1111\n            sub2: 29292\n\nrandom:\n  samples: 3\n  seed: 821\n\n  coll1:\n    type: parameter_collection\n    params:\n      b:\n        type: uniform\n        min: 0.0\n        max: 0.7"
  },
  {
    "path": "test/resources/config/config_with_duplicate_parameters_1.yaml",
    "content": "fixed:\n  a: ccc\n\ngrid:\n  a:\n    type: choice\n    options:\n      - 1\n      - 2"
  },
  {
    "path": "test/resources/config/config_with_duplicate_parameters_2.yaml",
    "content": "fixed:\n  a:\n    b.c: 333\n\ngrid:\n  a:\n    type: parameter_collection\n    params:\n      b:\n        type: parameter_collection\n        params:\n          c:\n            type: choice\n            options:\n              - 11\n              - 22"
  },
  {
    "path": "test/resources/config/config_with_duplicate_parameters_3.yaml",
    "content": "fixed:\n  a: ccc\n  a: ddd\n"
  },
  {
    "path": "test/resources/config/config_with_duplicate_random_parameters_1.yaml",
    "content": "fixed:\n  seed: 33333\n  samples: 99\n\n\nrandom:\n  seed: 222\n  samples: 5\n\n  a:\n    type: uniform\n    min: 0\n    max: 1\n    seed: 333"
  },
  {
    "path": "test/resources/config/config_with_empty_dictionary.yaml",
    "content": "fixed:\n  attribute.test: \"{}\"\n"
  },
  {
    "path": "test/resources/config/config_with_grid.yaml",
    "content": "grid:\n  dataset:\n    type: choice\n    options:\n      - small\n      - big\n\n  lr:\n    type: choice\n    options:\n      - 0.1\n      - 0.01\n"
  },
  {
    "path": "test/resources/config/config_with_named_config.yaml",
    "content": "fixed:\n  +model:\n    name: cora_ml\n  +evaluation:\n    priority: 1\n\ngrid:\n  dataset:\n    type: choice\n    options:\n      - big\n      - medium\n      - average\n\n\n  +model.priority:\n    type: choice\n    options:\n      - 1\n      - 2\n\n  +evaluation.name:\n    type: choice\n    options:\n      - standard\n      - advanced\n"
  },
  {
    "path": "test/resources/config/config_with_parameter_collections.yaml",
    "content": "grid:\n  coll1:\n    type: parameter_collection\n    params:\n      a:\n        type: choice\n        options:\n          - 1\n          - 2\n\nrandom:\n  samples: 3\n  seed: 821\n\n  coll1:\n    type: parameter_collection\n    params:\n      b:\n        type: uniform\n        min: 0.0\n        max: 0.7\n        seed: 333"
  },
  {
    "path": "test/resources/config/config_with_parameter_collections_random.yaml",
    "content": "grid:\n  coll1:\n    type: parameter_collection\n    params:\n      a:\n        type: choice\n        options:\n          - 1\n          - 2\n\nrandom:\n  samples: 3\n  seed: 821\n\n  coll1:\n    type: parameter_collection\n    params:\n      b:\n        type: uniform\n        min: 0.0\n        max: 0.7\n\n      c:\n        type: uniform\n        min: 1\n        max: 2"
  },
  {
    "path": "test/resources/config/config_with_zipped_parameters.yaml",
    "content": "grid:\n  attribute.test:\n    type: choice\n    options:\n      - 1\n      - 2\n    zip_id: 2\n\n  learning_rate:\n    type: uniform\n    min: 0.0\n    max: 1\n    num: 2\n    zip_id: 2\n\n  other_attribute:\n    type: choice\n    options:\n      - True\n      - False\n"
  },
  {
    "path": "test/resources/scripts/experiment_resolve_config.py",
    "content": "from sacred import Experiment\n\nex = Experiment()\n\n\n@ex.config\ndef config():\n    foo = 33\n    bar = {\n        \"fizz\": None,\n    }\n\n\n@ex.named_config\ndef py_named_1():\n    py = {\"value\": 1}\n\n\n@ex.named_config\ndef py_named_2():\n    py = {\"value\": 2}\n\n\n@ex.automain\ndef main(foo, bar, py, json, yaml): ...\n"
  },
  {
    "path": "test/resources/scripts/experiment_resolve_config_interpolate.py",
    "content": "from sacred import Experiment\n\nex = Experiment()\n\n\n@ex.automain\ndef main(foo, bar, param1, interpolated_1, interpolated_2): ...\n"
  },
  {
    "path": "test/test_config.py",
    "content": "import copy\nimport os\nimport unittest\nfrom pathlib import Path\n\nimport yaml\nfrom seml import utils\nfrom seml.commands import add\nfrom seml.experiment import config\nfrom seml.experiment.config import assemble_slurm_config_dict, read_config\nfrom seml.settings import SETTINGS\nfrom seml.utils import flatten, merge_dicts\nfrom seml.utils.errors import ConfigError\n\n\nclass TestParseConfigDicts(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls._orig_cwd = os.getcwd()\n        os.chdir(Path(__file__).parent)\n\n    @classmethod\n    def tearDownClass(cls):\n        os.chdir(cls._orig_cwd)\n\n    SIMPLE_CONFIG_WITH_PARAMETER_COLLECTIONS = (\n        \"resources/config/config_with_parameter_collections.yaml\"\n    )\n    SIMPLE_CONFIG_WITH_PARAMETER_COLLECTIONS_RANDOM = (\n        \"resources/config/config_with_parameter_collections_random.yaml\"\n    )\n    CONFIG_WITH_DUPLICATE_PARAMETERS_1 = (\n        \"resources/config/config_with_duplicate_parameters_1.yaml\"\n    )\n    CONFIG_WITH_DUPLICATE_PARAMETERS_2 = (\n        \"resources/config/config_with_duplicate_parameters_2.yaml\"\n    )\n    CONFIG_WITH_DUPLICATE_PARAMETERS_3 = (\n        \"resources/config/config_with_duplicate_parameters_3.yaml\"\n    )\n    CONFIG_WITH_DUPLICATE_PARAMETERS_NESTED = (\n        \"resources/config/config_nested_parameter_collections.yaml\"\n    )\n    CONFIG_WITH_DUPLICATE_RDM_PARAMETERS_2 = (\n        \"resources/config/config_with_duplicate_random_parameters_1.yaml\"\n    )\n    CONFIG_WITH_ALL_TYPES = \"resources/config/config_with_all_types.yaml\"\n    CONFIG_WITH_EMPTY_DICT = \"resources/config/config_with_empty_dictionary.yaml\"\n    CONFIG_WITH_ZIPPED_PARAMETERS = (\n        \"resources/config/config_with_zipped_parameters.yaml\"\n    )\n    CONFIG_WITH_NAMED_CONFIGS = \"resources/config/config_with_named_config.yaml\"\n    CONFIG_WITH_GRID = \"resources/config/config_with_grid.yaml\"\n    CONFIG_SLURM_DEFAULT = \"resources/config/config_slurm_default.yaml\"\n    CONFIG_SLURM_DEFAULT_EMPTY_SBATCH = (\n        \"resources/config/config_slurm_default_empty_sbatch.yaml\"\n    )\n    CONFIG_SLURM_TEMPLATE = \"resources/config/config_slurm_template.yaml\"\n    CONFIG_SLURM_EXPERIMENT = \"resources/config/config_slurm_experiment.yaml\"\n    CONFIG_SLURM_EXPERIMENTS_AND_TASKS = (\n        \"resources/config/config_slurm_experiments_and_tasks.yaml\"\n    )\n    CONFIG_RESOLVE_CONFIG = \"resources/config/config_resolve_config.yaml\"\n    CONFIG_RESOLVE_INTERPOLATION = (\n        \"resources/config/config_resolve_with_interpolation.yaml\"\n    )\n\n    EXPERIMENT_RESOLVE_CONFIG = \"resources/scripts/experiment_resolve_config.py\"\n    EXPERIMENT_RESOLVE_INTERPOLATION = (\n        \"resources/scripts/experiment_resolve_config_interpolate.py\"\n    )\n\n    def load_config_dict(self, path):\n        with open(path) as conf:\n            config_dict = config.convert_values(yaml.load(conf, Loader=yaml.FullLoader))\n        return config_dict\n\n    def test_config_inheritance(self):\n        # Check default config\n        seml_config, slurm_configs, experiment_config = read_config(\n            self.CONFIG_SLURM_DEFAULT\n        )\n        slurm_config = slurm_configs[0]\n        slurm_config = assemble_slurm_config_dict(slurm_config)\n        self.assertEqual(slurm_config, SETTINGS.SLURM_DEFAULT)\n\n        # Check default config with empty sbatch options\n        seml_config, slurm_configs, experiment_config = read_config(\n            self.CONFIG_SLURM_DEFAULT_EMPTY_SBATCH\n        )\n        slurm_config = slurm_configs[0]\n        slurm_config = assemble_slurm_config_dict(slurm_config)\n        self.assertEqual(slurm_config, SETTINGS.SLURM_DEFAULT)\n\n        # Check default -> template inheritance\n        seml_config, slurm_configs, experiment_config = read_config(\n            self.CONFIG_SLURM_TEMPLATE\n        )\n        slurm_config = slurm_configs[0]\n        slurm_config = assemble_slurm_config_dict(slurm_config)\n        target_config = copy.deepcopy(SETTINGS.SLURM_DEFAULT)\n        target_config[\"sbatch_options\"] = merge_dicts(\n            target_config[\"sbatch_options\"], SETTINGS.SBATCH_OPTIONS_TEMPLATES.GPU\n        )\n        target_config[\"sbatch_options_template\"] = \"GPU\"\n        self.assertEqual(slurm_config, target_config)\n\n        # Check default -> template -> experiment inheritance\n        seml_config, slurm_configs, experiment_config = read_config(\n            self.CONFIG_SLURM_EXPERIMENT\n        )\n        slurm_config = slurm_configs[0]\n        slurm_config = assemble_slurm_config_dict(slurm_config)\n        target_config = copy.deepcopy(SETTINGS.SLURM_DEFAULT)\n        target_config[\"sbatch_options\"] = merge_dicts(\n            target_config[\"sbatch_options\"], SETTINGS.SBATCH_OPTIONS_TEMPLATES.GPU\n        )\n        target_config[\"sbatch_options_template\"] = \"GPU\"\n        target_config[\"sbatch_options\"][\"cpus-per-task\"] = 4\n        self.assertEqual(slurm_config, target_config)\n\n    def test_convert_parameter_collections(self):\n        config_dict = self.load_config_dict(\n            self.SIMPLE_CONFIG_WITH_PARAMETER_COLLECTIONS\n        )\n        converted = config.convert_parameter_collections(config_dict)\n        expected = {\n            \"grid\": {\"coll1\": {\"a\": {\"type\": \"choice\", \"options\": [1, 2]}}},\n            \"random\": {\n                \"samples\": 3,\n                \"seed\": 821,\n                \"coll1\": {\n                    \"b\": {\n                        \"type\": \"uniform\",\n                        \"min\": 0.0,\n                        \"max\": 0.7,\n                        \"seed\": 333,\n                    }\n                },\n            },\n        }\n        self.assertEqual(converted, expected)\n\n    def test_resolve_config(self):\n        config_dict = self.load_config_dict(self.CONFIG_RESOLVE_CONFIG)\n        configs_unresolved = config.generate_configs(config_dict)\n        configs, named_configs = config.generate_named_configs(configs_unresolved)\n\n        configs = {\n            frozenset((k, v) for k, v in flatten(config).items())\n            for config in config.resolve_configs(\n                self.EXPERIMENT_RESOLVE_CONFIG, None, configs, named_configs, \".\"\n            )\n        }\n        # Note that the yaml config overrides a parameter set by the json config due to its higher priority\n        expected_configs = {\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 1),\n                    (\"json.value\", 11),\n                    (\"yaml.value\", -1),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 1),\n                    (\"json.value\", 10000),\n                    (\"yaml.value\", -2),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 1),\n                    (\"json.value\", 22),\n                    (\"yaml.value\", -1),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 1),\n                    (\"json.value\", 10000),\n                    (\"yaml.value\", -2),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 2),\n                    (\"json.value\", 11),\n                    (\"yaml.value\", -1),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 2),\n                    (\"json.value\", 10000),\n                    (\"yaml.value\", -2),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 2),\n                    (\"json.value\", 22),\n                    (\"yaml.value\", -1),\n                )\n            ),\n            frozenset(\n                (\n                    (\"foo\", 33),\n                    (\"bar\", 44),\n                    (\"py.value\", 2),\n                    (\"json.value\", 10000),\n                    (\"yaml.value\", -2),\n                )\n            ),\n        }\n        self.assertSetEqual(configs, expected_configs)\n\n    def test_resolve_config_interpolation(self):\n        config_dict = self.load_config_dict(self.CONFIG_RESOLVE_INTERPOLATION)\n        configs_unresolved = config.generate_configs(config_dict)\n        configs, named_configs = config.generate_named_configs(configs_unresolved)\n        configs = [\n            config\n            for config in config.resolve_configs(\n                self.EXPERIMENT_RESOLVE_INTERPOLATION, None, configs, named_configs, \".\"\n            )\n        ]\n        documents = [\n            add.resolve_interpolations({\"config\": config}) for config in configs\n        ]\n\n        self.assertEqual(len(documents), 1)\n        expected_document = {\n            \"config\": {\n                \"foo\": {\"bar\": 3},\n                \"param1\": \"value\",\n                \"interpolated_1\": \"3_value\",\n                \"interpolated_2\": 3,\n            }\n        }\n        self.assertDictEqual(expected_document, documents[0])\n\n        # Only `config` should be resolved\n        self.assertEqual(\n            add.resolve_interpolations(\n                {\"config\": configs[0], \"foo\": \"${config.param1}\"},\n                allow_interpolation_keys=[\"config\"],\n            )[\"foo\"],\n            \"${config.param1}\",\n        )\n\n    def test_unpack_config_dict(self):\n        config_dict = self.load_config_dict(\n            self.SIMPLE_CONFIG_WITH_PARAMETER_COLLECTIONS\n        )\n        unpacked, next_level = config.unpack_config(config_dict)\n\n        self.assertEqual(next_level, {})\n\n        expected_random = {\n            \"samples\": 3,\n            \"seed\": 821,\n            \"coll1\": {\n                \"b\": {\n                    \"type\": \"uniform\",\n                    \"min\": 0.0,\n                    \"max\": 0.7,\n                    \"seed\": 333,\n                    # 'samples': 4,\n                }\n            },\n        }\n\n        self.assertEqual(unpacked[\"random\"], expected_random)\n\n    def test_empty_dictionary(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_EMPTY_DICT)\n        configs = config.generate_configs(config_dict)[0]\n        expected_config = {\"attribute\": {\"test\": {}}}\n        self.assertEqual(configs, expected_config)\n\n    def test_overwrite_parameters(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_GRID)\n        configs = config.generate_configs(config_dict, {\"dataset\": \"small\"})\n        expected_configs = [\n            {\"dataset\": \"small\", \"lr\": 0.1},\n            {\"dataset\": \"small\", \"lr\": 0.01},\n        ]\n        self.assertEqual(configs, expected_configs)\n\n    def test_zipped_parameters(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_ZIPPED_PARAMETERS)\n        configs = config.generate_configs(config_dict)\n        expected_configs = [\n            {\"attribute\": {\"test\": 1}, \"learning_rate\": 0.0, \"other_attribute\": True},\n            {\"attribute\": {\"test\": 1}, \"learning_rate\": 0.0, \"other_attribute\": False},\n            {\"attribute\": {\"test\": 2}, \"learning_rate\": 1.0, \"other_attribute\": True},\n            {\"attribute\": {\"test\": 2}, \"learning_rate\": 1.0, \"other_attribute\": False},\n        ]\n        self.assertEqual(configs, expected_configs)\n\n    def test_named_config_python(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_NAMED_CONFIGS)\n        configs_unresolved = config.generate_configs(config_dict)\n        configs, named_configs = config.generate_named_configs(configs_unresolved)\n        expected_configs = [\n            {\"dataset\": \"big\"},\n            {\"dataset\": \"big\"},\n            {\"dataset\": \"big\"},\n            {\"dataset\": \"big\"},\n            {\"dataset\": \"medium\"},\n            {\"dataset\": \"medium\"},\n            {\"dataset\": \"medium\"},\n            {\"dataset\": \"medium\"},\n            {\"dataset\": \"average\"},\n            {\"dataset\": \"average\"},\n            {\"dataset\": \"average\"},\n            {\"dataset\": \"average\"},\n        ]\n        self.assertListEqual(configs, expected_configs)\n        expected_named_configs = [\n            # explanation: the grid goes over the evaluation method (standard or advanced) and the priority of cora_ml\n            # (equal or higher (later in list))\n            # (standard, 1), (advanced, 1), (standard, 2), (advanced, 2)\n            # note that this also tests that the named configs are sorted lexicographically if the priorities match\n            [\"cora_ml\", \"standard\"],\n            [\"advanced\", \"cora_ml\"],\n            [\"standard\", \"cora_ml\"],\n            [\"advanced\", \"cora_ml\"],\n            [\"cora_ml\", \"standard\"],\n            [\"advanced\", \"cora_ml\"],\n            [\"standard\", \"cora_ml\"],\n            [\"advanced\", \"cora_ml\"],\n            [\"cora_ml\", \"standard\"],\n            [\"advanced\", \"cora_ml\"],\n            [\"standard\", \"cora_ml\"],\n            [\"advanced\", \"cora_ml\"],\n        ]\n        self.assertListEqual(named_configs, expected_named_configs)\n\n    def test_named_config_python_raises(self):\n        # make an unnamed named config\n        config_dict = self.load_config_dict(self.CONFIG_WITH_NAMED_CONFIGS)\n        del config_dict[\"fixed\"][\"+model\"][\"name\"]\n        with self.assertRaises(ConfigError):\n            configs_unresolved = config.generate_configs(config_dict)\n            configs, named_configs = config.generate_named_configs(configs_unresolved)\n\n        # assign invalid priorities\n        for priority in (None, \"foo\"):\n            config_dict = self.load_config_dict(self.CONFIG_WITH_NAMED_CONFIGS)\n            config_dict[\"fixed\"][\"+evaluation\"][\"priority\"] = priority\n            with self.assertRaises(ConfigError):\n                configs_unresolved = config.generate_configs(config_dict)\n                configs, named_configs = config.generate_named_configs(\n                    configs_unresolved\n                )\n\n    def test_duplicate_parameters(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_DUPLICATE_PARAMETERS_1)\n        with self.assertRaises(ConfigError):\n            configs = config.generate_configs(config_dict)\n\n        config_dict = self.load_config_dict(self.CONFIG_WITH_DUPLICATE_PARAMETERS_2)\n        with self.assertRaises(ConfigError):\n            configs = config.generate_configs(config_dict)\n\n        with self.assertRaises(ConfigError):\n            configs = config.read_config(self.CONFIG_WITH_DUPLICATE_PARAMETERS_3)\n\n        config_dict = self.load_config_dict(\n            self.CONFIG_WITH_DUPLICATE_PARAMETERS_NESTED\n        )\n        with self.assertRaises(ConfigError):\n            configs = config.generate_configs(config_dict)\n\n        config_dict = self.load_config_dict(self.CONFIG_WITH_DUPLICATE_RDM_PARAMETERS_2)\n        configs = config.generate_configs(config_dict)\n        assert len(configs) == config_dict[\"random\"][\"samples\"]\n\n    def test_generate_configs(self):\n        config_dict = self.load_config_dict(self.CONFIG_WITH_ALL_TYPES)\n        configs = config.generate_configs(config_dict)\n        assert len(configs) == 22\n        expected_configs = [\n            *(\n                5\n                * [\n                    {\"a\": 9999, \"b\": 7777, \"c\": 1234, \"d\": 1.0, \"e\": 2.0},\n                    {\"a\": 9999, \"b\": 7777, \"c\": 5678, \"d\": 1.0, \"e\": 2.0},\n                ]\n            ),\n            *(\n                3\n                * [\n                    {\"a\": 333, \"b\": 444, \"c\": 555, \"d\": 1.0, \"f\": 9199},\n                    {\"a\": 333, \"b\": 444, \"c\": 555, \"d\": 1.0, \"f\": 1099},\n                    {\"a\": 333, \"b\": 444, \"c\": 666, \"d\": 1.0, \"f\": 9199},\n                    {\"a\": 333, \"b\": 444, \"c\": 666, \"d\": 1.0, \"f\": 1099},\n                ]\n            ),\n        ]\n        expected_config_hashes = sorted([utils.make_hash(x) for x in expected_configs])\n        actual_config_hashes = sorted([utils.make_hash(x) for x in configs])\n        assert expected_config_hashes == actual_config_hashes\n\n    def test_config_experiments_error(self):\n        slurm_conf = config.read_config(self.CONFIG_SLURM_EXPERIMENTS_AND_TASKS)[1]\n        with self.assertRaises(ConfigError):\n            assemble_slurm_config_dict(slurm_conf[0])\n"
  },
  {
    "path": "test/test_start.py",
    "content": "import unittest\n\nfrom seml.experiment.command import value_to_string\n\n\nclass TestValueToString(unittest.TestCase):\n    def test_literal(self):\n        vals = [True, False, None]\n        for val in vals:\n            str_json = value_to_string(val, use_json=True)\n            str_repr = value_to_string(val, use_json=False)\n            self.assertEqual(str_json, str_repr)\n\n    def test_list(self):\n        vals = [True, False, None]\n        lists = [\n            [4, \"test\"],\n            [\"test\", {\"a\": 5}],\n            [[5, 3], {6.5: 2.3}],\n        ]\n        res_json = [\n            ['[{val}, 4, \"test\"]', '[4, {val}, \"test\"]', '[4, \"test\", {val}]'],\n            [\n                '[{val}, \"test\", {{\"a\": 5}}]',\n                '[\"test\", {val}, {{\"a\": 5}}]',\n                '[\"test\", {{\"a\": 5}}, {val}]',\n            ],\n            [\n                '[{val}, [5, 3], {{\"6.5\": 2.3}}]',\n                '[[5, 3], {val}, {{\"6.5\": 2.3}}]',\n                '[[5, 3], {{\"6.5\": 2.3}}, {val}]',\n            ],\n        ]\n        res_repr = [\n            [\"[{val}, 4, 'test']\", \"[4, {val}, 'test']\", \"[4, 'test', {val}]\"],\n            [\n                \"[{val}, 'test', {{'a': 5}}]\",\n                \"['test', {val}, {{'a': 5}}]\",\n                \"['test', {{'a': 5}}, {val}]\",\n            ],\n            [\n                \"[{val}, [5, 3], {{6.5: 2.3}}]\",\n                \"[[5, 3], {val}, {{6.5: 2.3}}]\",\n                \"[[5, 3], {{6.5: 2.3}}, {val}]\",\n            ],\n        ]\n        for ilist, raw_list in enumerate(lists):\n            for pos in range(3):\n                for val in vals:\n                    test_list = raw_list.copy()\n                    test_list.insert(pos, val)\n                    str_json = value_to_string(test_list, use_json=True)\n                    str_repr = value_to_string(test_list, use_json=False)\n                    self.assertEqual(str_json, res_json[ilist][pos].format(val=val))\n                    self.assertEqual(str_repr, res_repr[ilist][pos].format(val=val))\n\n    def test_dict(self):\n        vals = [True, False, None]\n        dicts = [\n            {1: \"test\"},\n            {\"test\": {\"a\": 5}},\n            {\"a\": [6.5, 2.3]},\n        ]\n        keys = [3, \"b\", \"nest\", 4.3]\n        res_json = [\n            [\n                '{{\"1\": \"test\", \"3\": {val}}}',\n                '{{\"1\": \"test\", \"b\": {val}}}',\n                '{{\"1\": \"test\", \"nest\": {val}}}',\n                '{{\"1\": \"test\", \"4.3\": {val}}}',\n            ],\n            [\n                '{{\"test\": {{\"a\": 5}}, \"3\": {val}}}',\n                '{{\"test\": {{\"a\": 5}}, \"b\": {val}}}',\n                '{{\"test\": {{\"a\": 5}}, \"nest\": {val}}}',\n                '{{\"test\": {{\"a\": 5}}, \"4.3\": {val}}}',\n            ],\n            [\n                '{{\"a\": [6.5, 2.3], \"3\": {val}}}',\n                '{{\"a\": [6.5, 2.3], \"b\": {val}}}',\n                '{{\"a\": [6.5, 2.3], \"nest\": {val}}}',\n                '{{\"a\": [6.5, 2.3], \"4.3\": {val}}}',\n            ],\n        ]\n        res_repr = [\n            [\n                \"{{1: 'test', 3: {val}}}\",\n                \"{{1: 'test', 'b': {val}}}\",\n                \"{{1: 'test', 'nest': {val}}}\",\n                \"{{1: 'test', 4.3: {val}}}\",\n            ],\n            [\n                \"{{'test': {{'a': 5}}, 3: {val}}}\",\n                \"{{'test': {{'a': 5}}, 'b': {val}}}\",\n                \"{{'test': {{'a': 5}}, 'nest': {val}}}\",\n                \"{{'test': {{'a': 5}}, 4.3: {val}}}\",\n            ],\n            [\n                \"{{'a': [6.5, 2.3], 3: {val}}}\",\n                \"{{'a': [6.5, 2.3], 'b': {val}}}\",\n                \"{{'a': [6.5, 2.3], 'nest': {val}}}\",\n                \"{{'a': [6.5, 2.3], 4.3: {val}}}\",\n            ],\n        ]\n        for idict, raw_dict in enumerate(dicts):\n            for ikey, key in enumerate(keys):\n                for val in vals:\n                    test_dict = raw_dict.copy()\n                    test_dict[key] = val\n                    str_json = value_to_string(test_dict, use_json=True)\n                    str_repr = value_to_string(test_dict, use_json=False)\n                    self.assertEqual(str_json, res_json[idict][ikey].format(val=val))\n                    self.assertEqual(str_repr, res_repr[idict][ikey].format(val=val))\n"
  },
  {
    "path": "test/test_utils.py",
    "content": "import unittest\n\nfrom seml import utils\n\n\nclass TestMergeDictionaries(unittest.TestCase):\n    def test_basic(self):\n        d1 = {\"a\": 3, \"b\": 5}\n        d2 = {\"b\": 99, \"c\": 7}\n        merged = utils.merge_dicts(d1, d2)\n        expected = {\"a\": 3, \"b\": 99, \"c\": 7}\n        self.assertEqual(merged, expected)\n\n    def test_nested(self):\n        d1 = {\"a\": 3, \"b\": {\"c\": 10, \"d\": 9}}\n        d2 = {\"e\": 7, \"b\": {\"c\": 99, \"f\": 11}}\n        merged = utils.merge_dicts(d1, d2)\n        expected = {\"a\": 3, \"b\": {\"c\": 99, \"d\": 9, \"f\": 11}, \"e\": 7}\n        self.assertEqual(merged, expected)\n\n    def test_empty(self):\n        d1 = {\"a\": 3}\n        d2 = {}\n        merged1 = utils.merge_dicts(d1, d2)\n        merged2 = utils.merge_dicts(d2, d1)\n        expected = {\"a\": 3}\n        self.assertEqual(merged1, expected)\n        self.assertEqual(merged2, expected)\n\n    def test_fails_not_dict(self):\n        d1 = {\"a\": 3}\n        d2 = [\"not_dict\"]\n        with self.assertRaises(ValueError):\n            merged = utils.merge_dicts(d1, d2)\n        with self.assertRaises(ValueError):\n            merged = utils.merge_dicts(d2, d1)\n\n    def test_nested_non_dict_override(self):\n        d1 = {\"a\": 3, \"b\": {\"c\": {\"d\": 4}, \"e\": 11}}\n        d2 = {\"b\": {\"c\": [\"not_dict\"]}}\n\n        merged1 = utils.merge_dicts(d1, d2)\n        expected1 = {\"a\": 3, \"b\": {\"c\": [\"not_dict\"], \"e\": 11}}\n        merged2 = utils.merge_dicts(d2, d1)\n        expected2 = {\"a\": 3, \"b\": {\"c\": {\"d\": 4}, \"e\": 11}}\n\n        self.assertEqual(merged1, expected1)\n        self.assertEqual(merged2, expected2)\n\n\nclass TestUnflattenDictionaries(unittest.TestCase):\n    def test_basic(self):\n        flattened = {\"a.b.c\": 111, \"a.d\": 22}\n        unflattened = utils.unflatten(flattened, sep=\".\", recursive=False)\n        unflattened2 = utils.unflatten(\n            flattened, sep=\".\", recursive=True\n        )  # should not make a difference here\n        expected = {\"a\": {\"b\": {\"c\": 111}, \"d\": 22}}\n\n        self.assertEqual(expected, unflattened)\n        self.assertEqual(expected, unflattened2)\n\n    def test_recursive(self):\n        flattened = {\"a.b.c\": 111, \"a.d\": {\"e\": {\"f.g\": 333}}}\n        unflattened_recursive = utils.unflatten(flattened, sep=\".\", recursive=True)\n        expected_recursive = {\"a\": {\"b\": {\"c\": 111}, \"d\": {\"e\": {\"f\": {\"g\": 333}}}}}\n        assert unflattened_recursive == expected_recursive\n        self.assertEqual(unflattened_recursive, expected_recursive)\n\n        unflattened_nonrecursive = utils.unflatten(flattened, sep=\".\", recursive=False)\n        expected_nonrecursive = {\"a\": {\"b\": {\"c\": 111}, \"d\": {\"e\": {\"f.g\": 333}}}}\n        self.assertEqual(unflattened_nonrecursive, expected_nonrecursive)\n\n    def test_merge_duplicate_keys(self):\n        flattened = {\"a.b.c\": 111, \"a\": {\"b\": {\"d\": 222}}}\n        unflattened = utils.unflatten(flattened, sep=\".\", recursive=True)\n        expected = {\"a\": {\"b\": {\"c\": 111, \"d\": 222}}}\n        self.assertEqual(unflattened, expected)\n\n    def test_conflicting_keys(self):\n        flattened = {\"a.b.c\": 111, \"a.b\": {\"c\": 222}}\n        unflattened = utils.unflatten(flattened, sep=\".\", recursive=True)\n        expected = {\"a\": {\"b\": {\"c\": 222}}}  # later entries overwrite former ones\n        self.assertEqual(unflattened, expected)\n\n        flattened2 = {\"a.b\": {\"c\": 222}, \"a.b.c\": 111}  # different order of keys\n        unflattened2 = utils.unflatten(flattened2, sep=\".\", recursive=True)\n        expected2 = {\"a\": {\"b\": {\"c\": 111}}}\n        self.assertEqual(unflattened2, expected2)\n\n        # this case is actually a bit tricky, but again we follow the paradigm that later entries overwrite former ones.\n        flattened3 = {\"a.b\": [\"not_dict\"], \"a.b.c\": 111}\n        unflattened3 = utils.unflatten(flattened3, sep=\".\", recursive=True)\n        expected3 = {\"a\": {\"b\": {\"c\": 111}}}\n        self.assertEqual(unflattened3, expected3)\n\n        # now the other way round\n        flattened4 = {\"a.b.c\": 111, \"a.b\": [\"not_dict\"]}\n        unflattened4 = utils.unflatten(flattened4, sep=\".\", recursive=True)\n        expected4 = {\"a\": {\"b\": [\"not_dict\"]}}\n        self.assertEqual(unflattened4, expected4)\n\n        flattened5 = {\"a\": {\"b\": [\"not_dict\"]}, \"a.b.c\": 111}\n        unflattened5 = utils.unflatten(flattened5, sep=\".\", recursive=True)\n        expected5 = {\"a\": {\"b\": {\"c\": 111}}}\n        self.assertEqual(unflattened5, expected5)\n\n        flattened6 = {\"a.b.c\": 111, \"a\": {\"b\": [\"not_dict\"]}}\n        unflattened6 = utils.unflatten(flattened6, sep=\".\", recursive=True)\n        expected6 = {\"a\": {\"b\": [\"not_dict\"]}}\n        self.assertEqual(unflattened6, expected6)\n\n    def test_unflatten_single_level(self):\n        flattened = {\"a.b.c\": 111, \"a.b\": {\"c\": 222}}\n        unflattened = utils.unflatten(flattened, sep=\".\", recursive=True, levels=[-1])\n        unflattened2 = utils.unflatten(flattened, sep=\".\", recursive=True, levels=-1)\n        expected = {\"a.b\": {\"c\": 111}, \"a\": {\"b\": {\"c\": 222}}}\n        self.assertEqual(unflattened, expected)\n        self.assertEqual(unflattened, unflattened2)\n\n        unflattened3 = utils.unflatten(flattened, sep=\".\", recursive=True, levels=[0])\n        expected2 = {\"a\": {\"b.c\": 111, \"b\": {\"c\": 222}}}\n        self.assertEqual(unflattened3, expected2)\n\n    def test_out_of_bounds(self):\n        flattened = {\"a.b.c.d.e\": 111, \"a.b.c.d.f\": 222, \"a.b.c.g.h\": 333}\n        with self.assertRaises(IndexError):\n            unflattened = utils.unflatten(\n                flattened, sep=\".\", recursive=False, levels=[5]\n            )\n\n        with self.assertRaises(IndexError):\n            unflattened = utils.unflatten(\n                flattened, sep=\".\", recursive=False, levels=[-5]\n            )\n\n    def test_errors(self):\n        with self.assertRaises(ValueError):\n            utils.unflatten({}, levels=[])\n\n        with self.assertRaises(TypeError):\n            utils.unflatten({}, levels=1.2)\n\n    def test_empty(self):\n        unflattened = utils.unflatten({})\n        self.assertEqual(unflattened, {})\n\n    def test_recursive_with_levels(self):\n        flattened_base = {\"a.b.c.d.e\": 111, \"a.b.c.d.f\": 222, \"a.b.c.g.h\": 333}\n\n        flattened2 = flattened_base.copy()\n        flattened2[\"a\"] = {\"b.c.d.e\": 777, \"b.c.d.i\": 999}\n\n        unflattened = utils.unflatten(flattened2, sep=\".\", recursive=True, levels=0)\n        expected = {\n            \"a\": {\n                \"b.c.d.e\": 111,\n                \"b.c.d.f\": 222,\n                \"b.c.g.h\": 333,\n                \"b\": {\n                    \"c.d.e\": 777,\n                    \"c.d.i\": 999,\n                },\n            }\n        }\n        self.assertEqual(unflattened, expected)\n\n        unflattened2 = utils.unflatten(flattened2, sep=\".\", recursive=False, levels=0)\n        expected2 = {\n            \"a\": {\n                \"b.c.d.e\": 777,\n                \"b.c.d.f\": 222,\n                \"b.c.g.h\": 333,\n                \"b.c.d.i\": 999,\n            }\n        }\n        self.assertEqual(unflattened2, expected2)\n\n        with self.assertRaises(IndexError):\n            utils.unflatten(flattened2, sep=\".\", recursive=True, levels=1)\n\n        with self.assertRaises(IndexError):\n            utils.unflatten(flattened2, sep=\".\", recursive=False, levels=1)\n\n    def test_unflatten_multiple_levels(self):\n        flattened = {\"a.b.c.d.e\": 111, \"a.b.c.d.f\": 222, \"a.b.c.g.h\": 333}\n        unflattened = utils.unflatten(\n            flattened, sep=\".\", recursive=False, levels=[0, -1]\n        )\n        expected = {\n            \"a\": {\n                \"b.c.d\": {\n                    \"e\": 111,\n                    \"f\": 222,\n                },\n                \"b.c.g\": {\n                    \"h\": 333,\n                },\n            }\n        }\n        self.assertEqual(unflattened, expected)\n\n        unflattened2 = utils.unflatten(\n            flattened, sep=\".\", recursive=False, levels=[0, 1, 3]\n        )\n        expected2 = {\"a\": {\"b\": {\"c.d\": {\"e\": 111, \"f\": 222}, \"c.g\": {\"h\": 333}}}}\n        self.assertEqual(unflattened2, expected2)\n\n        unflattened3 = utils.unflatten(\n            flattened, sep=\".\", recursive=False, levels=[0, 1, 2, 3]\n        )\n        expected3 = utils.unflatten(flattened, sep=\".\", recursive=False)\n        self.assertEqual(unflattened3, expected3)\n\n        unflattened4 = utils.unflatten(flattened, sep=\".\", recursive=False, levels=[4])\n        self.assertEqual(unflattened4, flattened)\n\n        unflattened5 = utils.unflatten(flattened, sep=\".\", recursive=False, levels=[-2])\n        expected5 = utils.unflatten(flattened, sep=\".\", recursive=False, levels=[2])\n        self.assertEqual(unflattened5, expected5)\n"
  }
]