diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..d0fadbd --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,95 @@ +name: Python CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + paths: + - "python/**" + - '.github/workflows/python.yml' + + pull_request: + branches: [ master ] + paths: + - "python/**" + - '.github/workflows/python.yml' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9, '3.10'] + include: + - os: macos-latest + - os: ubuntu-latest + INSTALL_DEPS: | + sudo apt-get install -y liblapack-dev + bash scripts/install_linux_libs.sh + defaults: + run: + working-directory: ./python + + steps: + - name: Checkout l0learn + uses: actions/checkout@v2 + with: + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: install-dependencies + run: ${{ matrix.INSTALL_DEPS }} + + - name: Install L0Learn + run: | + python -m pip install --upgrade pip + pip install . + + coverage: + needs: build + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./python + + steps: + - name: Checkout l0learn + uses: actions/checkout@v2 + with: + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: install-dependencies + run: | + sudo apt-get install -y liblapack-dev + bash scripts/install_linux_libs.sh + + - name: Install L0Learn + run: | + pip install . + + - name: Run Tests and Coverage + run: | + pip install pytest-cov coveralls hypothesis + pytest tests/ --cov=l0learn + + - name: Coveralls + run: coveralls --service=github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/python_docs.yml b/.github/workflows/python_docs.yml new file mode 100644 index 0000000..f9c4cab --- /dev/null +++ b/.github/workflows/python_docs.yml @@ -0,0 +1,34 @@ +# This is a basic workflow to help you get started with Actions + +name: Python Docs + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Pandoc + run: | + conda install pandoc + pip install nbsphinx + + - uses: ammaraskar/sphinx-action@master + with: + docs-folder: "python/doc/" diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml new file mode 100644 index 0000000..73c13ab --- /dev/null +++ b/.github/workflows/r.yml @@ -0,0 +1,61 @@ +name: R CI + +on: + push: + branches: [ master ] + paths: + - "R/**" + - '.github/workflows/r.yml' + + pull_request: + branches: [ master ] + paths: + - "R/**" + - '.github/workflows/r.yml' + + + workflow_dispatch: + +jobs: + build: + runs-on: macos-latest + strategy: + matrix: + r-version: ['3.6.3', '4.1.1', 'release'] + + defaults: + run: + working-directory: R + + steps: + - uses: actions/checkout@v2 + - name: Set up R ${{ matrix.r-version }} + uses: r-lib/actions/setup-r@v1 + with: + r-version: ${{ matrix.r-version }} + use-public-rspm: true + - uses: r-lib/actions/setup-pandoc@v1 + with: + pandoc-version: '2.7.3' + - name: Install dependencies + run: | + install.packages(c("remotes", "rcmdcheck", "devtools")) + remotes::install_deps(dependencies = TRUE) + shell: Rscript {0} + - name: Check + run: rcmdcheck::rcmdcheck("R", args = "--no-manual", error_on = "error", check_dir = "check") + shell: Rscript {0} + - uses: TNonet/actions/setup-r-dependencies@master + with: + cache-version: 1 + extra-packages: | + covr + xml2 + working-directory: R + + - name: Test coverage + run: | + covr::package_coverage( + type = "none", + code = "testthat::test_package('L0Learn', reporter = testthat::JunitReporter$new(file = 'test-results.xml'))") + shell: Rscript {0} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..16b327a --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,79 @@ +name: Build and Deploy + +on: + workflow_dispatch: + + push: + # Pattern matched against refs/tags + tags: + - '**' + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] # windows-2019, ] + + + steps: + - name: Checkout l0learn + uses: actions/checkout@v2 + with: + submodules: true + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Install cibuildwheel + run: python -m pip install cibuildwheel==2.5.0 + + - name: Build wheels + run: | + cd python + python -m cibuildwheel --output-dir l0learn-wheelhouse + env: + CIBW_SKIP: pp* *-win32 *-manylinux_i686 *musllinux* + CIBW_BEFORE_ALL_LINUX: "yum install -y lapack-devel || apt-get install -y liblapack-dev && bash scripts/install_linux_libs.sh" + CIBW_BEFORE_TEST: "pip install pytest numpy hypothesis" + CIBW_TEST_COMMAND: "pytest {package}/tests" + CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_BUILD_VERBOSITY: 3 + + - uses: actions/upload-artifact@v2 + with: + name: l0learn-wheelhouse + path: python/l0learn-wheelhouse/ + + upload-to-pypip: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + name: Publish a Python distribution to PyPI + runs-on: ubuntu-latest + needs: build_wheels + + steps: + - name: Checkout l0learn + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Download l0learn-wheelhouse artifact + uses: actions/download-artifact@v3 + with: + name: l0learn-wheelhouse + path: python/l0learn-wheelhouse/ + + - name: upload-artifact + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: python/l0learn-wheelhouse/ + diff --git a/.gitignore b/.gitignore index 858a36f..44f3d6a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,204 @@ L0Learn.Rproj config.status debug_log.ipynb tests/profile/ +.*idea + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# sphinx build folder +python/doc/_build + +# Compiled Object files +**/.DS_Store +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +**/cmake-build-debug +**/CMakeCache.txt +**/cmake_install.cmake +**/install_manifest.txt +**/CMakeFiles/ +**/CTestTestfile.cmake +**/Makefile +**/*.cbp +**/CMakeScripts +**/compile_commands.json + +include/divisible/* + + +## Local + +.idea/*.xml + +build/**/* + +include/* +lib/* +bin/* +test/test_runner + +Python/_skbuild/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dfd1040 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "python/external/carma"] + path = python/external/carma + url = https://github.com/TNonet/carma.git +[submodule "python/external/pybind11"] + path = python/external/pybind11 + url = https://github.com/pybind/pybind11.git +[submodule "python/external/armadillo-code"] + path = python/external/armadillo-code + url = https://gitlab.com/conradsnicta/armadillo-code.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0f6c9c7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +fail_fast: false +repos: + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + args: ['python/', '--extend-exclude', 'external/'] + - repo: local + hooks: + # - id: dirs_check + # name: dirs_check + # pass_filenames: false + # entry: python scripts/dirs_check.py r/src/src python/src/l0learn/src --ignore arma_includes.h .DS_Store + # language: python + - id: lintr + name: run_lintr + language: r + additional_dependencies: ['lintr'] + entry: Rscript -e "Sys.setenv(NOT_CRAN = 'true'); lintr::expect_lint_free(path='R/R/', linters = lintr::with_defaults(object_length_linter = NULL, object_name_linter=NULL, object_usage_linter=NULL, cyclocomp_linter=NULL), exclusions = c('R/R/RcppExports.R', list.files('R/tests', recursive=TRUE, full.names=TRUE)))" + - id: styler + name: run_styler + additional_dependencies: ['styler'] + entry: Rscript -e "styler::style_pkg('R/R/', dry='off')" + language: r + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: + - '--max-line-length=121' + - '--ignore=C901,W503' + - '--exclude=pypkg/tests/*.py R/vignettes/profile/*.py' + - 'python/src/l0learn/' + - 'python/setup.py' + - repo: https://github.com/pocc/pre-commit-hooks + rev: master + hooks: + - id: clang-format + # Ruh directly with find . -name \*.h -not -path "./pypkg/external/*" -print -o -name \*.cpp -not -path "./pypkg/external/*" -print -exec clang-format -style=file -i {} \; + args: ['--style=Google', "-i"] + exclude: '^R/src/RcppExports.cpp' +# - id: clang-tidy +# - id: oclint +# - id: uncrustify +# - id: cppcheck +# - id: cpplint +# - id: include-what-you-use + + + diff --git a/.travis.yml b/.travis.yml index 00e6771..fc4e38f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,34 @@ -language: R -r: - - release -sudo: false -cache: packages -warnings_are_errors: false -os: - - osx -after_failure: - - cat /home/travis/build/hazimehh/L0Learn/L0Learn.Rcheck/00check.log -after_success: - - cat /home/travis/build/hazimehh/L0Learn/L0Learn.Rcheck/00check.log - - travis_wait 300 Rscript -e "covr::codecov(quiet = FALSE, token='6f2e6703-ee4f-4479-b079-1d5d219786f4')" +jobs: + include: + language: R + r: + - release + before_script: + - cd R + sudo: false + cache: packages + warnings_are_errors: false + after_failure: + - cat /home/travis/build/hazimehh/L0Learn/L0Learn.Rcheck/00check.log + after_success: + - cat /home/travis/build/hazimehh/L0Learn/L0Learn.Rcheck/00check.log + - travis_wait 300 Rscript -e "covr::codecov(quiet = FALSE, token='6f2e6703-ee4f-4479-b079-1d5d219786f4')" + include: + language: python + before_script: + - cd python + python: + - "3.6" + - "3.7" + - "3.8" + - "3.9" + - "3.9-dev" + - "nightly" + install: + - pip install . + - pip install pytest-cov + - pip install coveralls + script: + - pytest --cov=happenings + after_success: + - coveralls \ No newline at end of file diff --git a/.Rbuildignore b/R/.Rbuildignore similarity index 100% rename from .Rbuildignore rename to R/.Rbuildignore diff --git a/.astylerc b/R/.astylerc similarity index 100% rename from .astylerc rename to R/.astylerc diff --git a/DESCRIPTION b/R/DESCRIPTION similarity index 98% rename from DESCRIPTION rename to R/DESCRIPTION index 0e4897b..b69596e 100644 --- a/DESCRIPTION +++ b/R/DESCRIPTION @@ -24,6 +24,4 @@ Suggests: rmarkdown, testthat, pracma, - raster, - covr VignetteBuilder: knitr diff --git a/R/LICENSE b/R/LICENSE new file mode 100644 index 0000000..25dd6a4 --- /dev/null +++ b/R/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2021 +COPYRIGHT HOLDER: Hussein Hazimeh \ No newline at end of file diff --git a/NAMESPACE b/R/NAMESPACE similarity index 100% rename from NAMESPACE rename to R/NAMESPACE diff --git a/R/L0Learn.R b/R/R/L0Learn.R similarity index 98% rename from R/L0Learn.R rename to R/R/L0Learn.R index 3af00ca..6485e95 100644 --- a/R/L0Learn.R +++ b/R/R/L0Learn.R @@ -1,3 +1,4 @@ +# nolint start #' @docType package #' @name L0Learn-package #' @title A package for L0-regularized learning @@ -24,3 +25,4 @@ #' @references Hazimeh and Mazumder. Fast Best Subset Selection: Coordinate Descent and Local Combinatorial #' Optimization Algorithms. Operations Research (2020). \url{https://pubsonline.informs.org/doi/10.1287/opre.2019.1919}. NULL +# nolint end diff --git a/R/RcppExports.R b/R/R/RcppExports.R similarity index 100% rename from R/RcppExports.R rename to R/R/RcppExports.R index 4c6db75..fd57c74 100644 --- a/R/RcppExports.R +++ b/R/R/RcppExports.R @@ -1,6 +1,10 @@ # Generated by using Rcpp::compileAttributes() -> do not edit by hand # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 +cor_matrix <- function(p, base_cor) { + .Call('_L0Learn_cor_matrix', PACKAGE = 'L0Learn', p, base_cor) +} + L0LearnFit_sparse <- function(X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs) { .Call('_L0Learn_L0LearnFit_sparse', PACKAGE = 'L0Learn', X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs) } @@ -17,10 +21,6 @@ L0LearnCV_dense <- function(X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, .Call('_L0Learn_L0LearnCV_dense', PACKAGE = 'L0Learn', X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, nfolds, seed, ExcludeFirstK, Intercept, withBounds, Lows, Highs) } -cor_matrix <- function(p, base_cor) { - .Call('_L0Learn_cor_matrix', PACKAGE = 'L0Learn', p, base_cor) -} - R_matrix_column_get_dense <- function(mat, col) { .Call('_L0Learn_R_matrix_column_get_dense', PACKAGE = 'L0Learn', mat, col) } diff --git a/R/R/coef.R b/R/R/coef.R new file mode 100644 index 0000000..9316a88 --- /dev/null +++ b/R/R/coef.R @@ -0,0 +1,71 @@ +#' @title Extract Solutions +#' +#' @description Extracts a specific solution in the regularization path. +#' @param object The output of L0Learn.fit or L0Learn.cvfit +#' @param ... ignore +#' @param lambda The value of lambda at which to extract the solution. +#' @param gamma The value of gamma at which to extract the solution. +#' @method coef L0Learn +#' @details +#' If both lambda and gamma are not supplied, then a matrix of coefficients +#' for all the solutions in the regularization path is returned. If lambda is +#' supplied but gamma is not, the smallest value of gamma is used. +#' @examples +#' # Generate synthetic data for this example +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' +#' # Fit an L0L2 Model with 10 values of Gamma ranging from 0.0001 to 10, +#' # using coordinate descent +#' fit <- L0Learn.fit(X, y, penalty="L0L2", maxSuppSize=50, +#' nGamma=10, gammaMin=0.0001, gammaMax = 10) +#' print(fit) +#' # Extract the coefficients of the solution at lambda = 0.0361829 +#' # and gamma = 0.0001 +#' coef(fit, lambda=0.0361829, gamma=0.0001) +#' # Extract the coefficients of all the solutions in the path +#' coef(fit) +#' +#' @export +coef.L0Learn <- function(object, lambda=NULL, gamma=NULL, ...) { + if (is.null(lambda) && is.null(gamma)) { + t <- do.call(cbind, object$beta) + if (object$settings$intercept) { + intercepts <- unlist(object$a0) + t <- rbind(intercepts, t) + } + } else { + if (is.null(gamma)) { + # if lambda is present but gamma is not, use smallest value of gamma + gamma <- object$gamma[1] + } + diffGamma <- abs(object$gamma - gamma) + gammaindex <- which(diffGamma == min(diffGamma)) + diffLambda <- abs(lambda - object$lambda[[gammaindex]]) + indices <- which(diffLambda == min(diffLambda)) + + if (object$settings$intercept) { + t <- rbind(object$a0[[gammaindex]][indices], + object$beta[[gammaindex]][, indices, drop = FALSE]) + rownames(t) <- c("Intercept", + paste(rep("V", object$p), + 1:object$p, + sep = "")) + } else { + t <- object$beta[[gammaindex]][, indices, drop = FALSE] + rownames(t) <- paste(rep("V", object$p), + 1:object$p, + sep = "") + } + } + t +} + + +#' @rdname coef.L0Learn +#' @method coef L0LearnCV +#' @export +coef.L0LearnCV <- function(object, lambda=NULL, gamma=NULL, ...) { + coef.L0Learn(object$fit, lambda, gamma, ...) +} diff --git a/R/R/cvfit.R b/R/R/cvfit.R new file mode 100644 index 0000000..6686696 --- /dev/null +++ b/R/R/cvfit.R @@ -0,0 +1,360 @@ +#' @title Cross Validation +#' +#' @inheritParams L0Learn.fit +#' @description Computes a regularization path and performs K-fold +#' cross-validation. +#' @param nFolds The number of folds for cross-validation. +#' @param seed The seed used in randomly shuffling the data for +#' cross-validation. +#' @return An S3 object of type "L0LearnCV" describing the regularization path. +#' The object has the following members. +#' \item{cvMeans}{This is a list, where the ith element is the sequence of +#' cross-validation errors corresponding to the ith gamma value, i.e., +#' the sequence cvMeans[[i]] corresponds to fit$gamma[i]} +#' \item{cvSDs}{This a list, where the ith element is a sequence of standard +#' deviations for the cross-validation errors: cvSDs[[i]] corresponds +#' to cvMeans[[i]].} +#' \item{fit}{The fitted model with type "L0Learn", i.e., this is the same +#' object returned by \code{\link{L0Learn.fit}}.} +#' +#' @examples +#' # Generate synthetic data for this example +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' +#' # Perform 5-fold cross-validation on an L0L2 regression model with 5 values +#' # of Gamma ranging from 0.0001 to 10 +#' fit <- L0Learn.cvfit(X, y, nFolds=5, seed=1, penalty="L0L2", maxSuppSize=20, +#' nGamma=5, gammaMin=0.0001, gammaMax = 10) +#' print(fit) +#' # Plot the graph of cross-validation error versus lambda for gamma = 0.0001 +#' plot(fit, gamma=0.0001) +#' # Extract the coefficients at lambda = 0.0361829 and gamma = 0.0001 +#' coef(fit, lambda=0.0361829, gamma=0.0001) +#' # Apply the fitted model on X to predict the response +#' predict(fit, newx = X, lambda=0.0361829, gamma=0.0001) +#' +#' @export +L0Learn.cvfit <- function(x, + y, + loss="SquaredError", + penalty="L0", + algorithm="CD", + maxSuppSize=100, + nLambda=100, + nGamma=10, + gammaMax=10, + gammaMin=0.0001, + partialSort = TRUE, + maxIters=200, + rtol=1e-6, + atol=1e-9, + activeSet=TRUE, + activeSetNum=3, + maxSwaps=100, + scaleDownFactor=0.8, + screenSize=1000, + autoLambda=NULL, + lambdaGrid = list(), + nFolds=10, + seed=1, + excludeFirstK=0, + intercept=TRUE, + lows=-Inf, + highs=Inf) { + set.seed(seed) + + if ((rtol < 0) || (rtol >= 1)) { + stop("The specified rtol parameter must exist in [0, 1)") + } + + if (atol < 0) { + stop("The specified atol parameter must exist in [0, INF)") + } + + # Some sanity checks for the inputs + if (!(loss %in% c("SquaredError", "Logistic", "SquaredHinge"))) { + stop("The specified loss function is not supported.") + } + if (!(penalty %in% c("L0", "L0L2", "L0L1"))) { + stop("The specified penalty is not supported.") + } + if (!(algorithm %in% c("CD", "CDPSI"))) { + stop("The specified algorithm is not supported.") + } + if (loss == "Logistic" | loss == "SquaredHinge") { + if (dim(table(y)) != 2) { + stop("Only binary classification is supported. + Make sure y has only 2 unique values.") + } + y <- factor(y, labels = c(-1, 1)) # returns a vector of strings + y <- as.numeric(levels(y))[y] + + if (penalty == "L0") { + if ((length(lambdaGrid) != 0) && (length(lambdaGrid) != 1)) { + # If this error checking was left to the lower section, + # it would confuse users as we are converting L0 to L0L2 with + # small L2 penalty. + # Here we must check if lambdaGrid is supplied + # (And thus use 'autolambda') + # If 'lambdaGrid' is supplied, we must only supply 1 list of + # lambda values + + stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. + Where lambdaGrid[[1]] is a list or vector of decreasing + positive values.") + } + penalty <- "L0L2" + nGamma <- 1 + gammaMax <- 1e-7 + gammaMin <- 1e-7 + } + } + + # Handle Lambda Grids: + if (length(lambdaGrid) != 0) { + if (!is.null(autoLambda) && !autoLambda) { + warning("In L0Learn V2+, autoLambda is ignored and inferred if + 'lambdaGrid' is supplied", call. = FALSE) + } + autoLambda <- FALSE + } else { + autoLambda <- TRUE + lambdaGrid <- list(0) + } + + if (penalty == "L0" && !autoLambda) { + bad_lambdaGrid <- FALSE + if (length(lambdaGrid) != 1) { + bad_lambdaGrid <- TRUE + } + current <- Inf + for (nxt in lambdaGrid[[1]]) { + if (nxt > current) { + # This must be > instead of >= to allow first iteration L0L1 + # lambdas of all 0s to be valid + bad_lambdaGrid <- TRUE + break + } + if (nxt < 0) { + bad_lambdaGrid <- TRUE + break + } + current <- nxt + + } + + if (bad_lambdaGrid) { + stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. + Where lambdaGrid[[1]] is a list or vector of decreasing + positive values.") + } + } + + if (penalty != "L0" && !autoLambda) { + # Covers L0L1, L0L2 cases + bad_lambdaGrid <- FALSE + if (length(lambdaGrid) != nGamma) { + warning("In L0Learn V2+, nGamma is ignored and replaced with + length(lambdaGrid)", call. = FALSE) + nGamma <- length(lambdaGrid) + } + + for (i in seq_len(lambdaGrid)) { + current <- Inf + for (nxt in lambdaGrid[[i]]) { + if (nxt > current) { + # This must be > instead of >= to allow first iteration + # L0L1 lambdas of all 0s to be valid + bad_lambdaGrid <- TRUE + break + } + if (nxt < 0) { + bad_lambdaGrid <- TRUE + break + } + current <- nxt + } + if (bad_lambdaGrid) { + break + } + } + + if (bad_lambdaGrid) { + stop("L0L1 or L0L2 Penalty requires 'lambdaGrid' to be a list of + length 'nGamma'. Where lambdaGrid[[i]] is a list or vector of + decreasing positive values.") + } + + } + + is.scalar <- function(x) { + return(is.atomic(x) + && length(x) == 1L + && !is.character(x) + && Im(x) == 0 + && !is.nan(x) + && !is.na(x)) + } + + p <- dim(x)[[2]] + + if ((excludeFirstK < 0) || (excludeFirstK >= p)) { + stop("The specified excludeFirstK parameter must be an integer + between 0 and p-1") + } + + withBounds <- FALSE + if ((!identical(lows, -Inf)) || (!identical(highs, Inf))) { + withBounds <- TRUE + + if (algorithm == "CDPSI") { + if (any(lows != -Inf) || any(highs != Inf)) { + stop("Bounds are not YET supported for CDPSI algorithm. + Please raise an issue at: + 'https://github.com/hazimehh/L0Learn' + to express interest in this functionality") + } + } + + if (is.scalar(lows)) { + lows <- lows * rep(1, p) + } else if (!all(sapply(lows, is.scalar)) || length(lows) != p) { + stop("Lows must be a vector of real values of length p") + } + + if (is.scalar(highs)) { + highs <- highs * rep(1, p) + } else if (!all(sapply(highs, is.scalar)) || length(highs) != p) { + stop("Highs must be a vector of real values of length p") + } + + if (any(lows >= highs) || any(lows > 0) || any(highs < 0)) { + stop("Bounds must conform to the following conditions: + Lows <= 0, Highs >= 0, Lows < Highs") + } + + } + + M <- list() + if (is(x, "sparseMatrix")) { + M <- .Call("_L0Learn_L0LearnCV_sparse", + PACKAGE = "L0Learn", + x, + y, + loss, + penalty, + algorithm, + maxSuppSize, + nLambda, + nGamma, + gammaMax, + gammaMin, + partialSort, + maxIters, + rtol, + atol, + activeSet, + activeSetNum, + maxSwaps, + scaleDownFactor, + screenSize, + !autoLambda, + lambdaGrid, + nFolds, + seed, + excludeFirstK, + intercept, + withBounds, + lows, + highs) + } else { + M <- .Call("_L0Learn_L0LearnCV_dense", + PACKAGE = "L0Learn", + x, + y, + loss, + penalty, + algorithm, + maxSuppSize, + nLambda, + nGamma, + gammaMax, + gammaMin, + partialSort, + maxIters, + rtol, + atol, + activeSet, + activeSetNum, + maxSwaps, + scaleDownFactor, + screenSize, + !autoLambda, + lambdaGrid, + nFolds, + seed, + excludeFirstK, + intercept, + withBounds, + lows, + highs) + } + + # The C++ function uses LambdaU = 1 for user-specified grid. + # In R, we use AutoLambda0 = 0 for user-specified grid (thus the negation + # when passing the parameter to the function below) + + settings <- list() + settings[[1]] <- intercept + # Settings only contains intercept for now. + # Might include additional elements later. + names(settings) <- c("intercept") + + # Find potential support sizes exceeding maxSuppSize and remove them + # (this is due to the C++ core whose last solution can exceed maxSuppSize + for (i in seq_len(M$SuppSize)) { + last <- length(M$SuppSize[[i]]) + if (M$SuppSize[[i]][last] > maxSuppSize) { + if (last == 1) { + warning("Warning! + Only 1 element in path with support size > maxSuppSize. + Try increasing maxSuppSize to resolve the issue.") + } + else{ + M$SuppSize[[i]] <- M$SuppSize[[i]][-last] + M$Converged[[i]] <- M$Converged[[i]][-last] + M$lambda[[i]] <- M$lambda[[i]][-last] + M$a0[[i]] <- M$a0[[i]][-last] + # conversion to sparseMatrix is necessary to handle the case + # of a single column + M$beta[[i]] <- as(M$beta[[i]][, -last], "sparseMatrix") + M$CVMeans[[i]] <- M$CVMeans[[i]][-last] + M$CVSDs[[i]] <- M$CVSDs[[i]][-last] + } + } + } + + fit <- list(beta = M$beta, + lambda = lapply(M$lambda, signif, digits = 6), + a0 = M$a0, + converged = M$Converged, + suppSize = M$SuppSize, + gamma = M$gamma, + penalty = penalty, + loss = loss, + settings = settings) + if (is.null(colnames(x))) { + varnames <- 1:dim(x)[2] + } else { + varnames <- colnames(x) + } + fit$varnames <- varnames + class(fit) <- "L0Learn" + fit$n <- dim(x)[1] + fit$p <- dim(x)[2] + G <- list(fit = fit, cvMeans = M$CVMeans, cvSDs = M$CVSDs) + class(G) <- "L0LearnCV" + G +} diff --git a/R/R/fit.R b/R/R/fit.R new file mode 100644 index 0000000..5a55c5c --- /dev/null +++ b/R/R/fit.R @@ -0,0 +1,457 @@ +# import C++ compiled code +#' @useDynLib L0Learn +#' @importFrom Rcpp evalCpp +#' @importFrom methods as +#' @importFrom methods is +#' @import Matrix + +#' @title Fit an L0-regularized model +#' +#' @description Computes the regularization path for the specified loss function +#' and penalty function (which can be a combination of the L0, L1, +#' and L2 norms). +#' @param x The data matrix. +#' @param y The response vector. For classification, we only support +#' binary vectors. +#' @param loss The loss function. Currently we support the choices +#' "SquaredError" (for regression), +#' "Logistic" (for logistic regression), +#' and "SquaredHinge" (for smooth SVM). +#' @param penalty The type of regularization. This can take either one of the +#' following choices: "L0", "L0L2", and "L0L1". +#' @param algorithm The type of algorithm used to minimize the objective +#' function. Currently "CD" and "CDPSI" are are supported. "CD" is a variant +#' of cyclic coordinate descent and runs very fast. "CDPSI" performs local +#' combinatorial search on top of CD and typically achieves higher quality +#' solutions (at the expense of increased running time). +#' @param maxSuppSize The maximum support size at which to terminate the +#' regularization path. We recommend setting this to a small fraction of +#' min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically +#' selects a small portion of non-zeros. +#' @param nLambda The number of Lambda values to select (recall that Lambda +#' is the regularization parameter corresponding to the L0 norm). +#' This value is ignored if 'lambdaGrid' is supplied. +#' @param nGamma The number of Gamma values to select (recall that Gamma is the +#' regularization parameter corresponding to L1 or L2, depending on the +#' chosen penalty). This value is ignored if 'lambdaGrid' is supplied and will +#' be set to length(lambdaGrid) +#' @param gammaMax The maximum value of Gamma when using the L0L2 penalty. +#' For the L0L1 penalty this is automatically selected. +#' @param gammaMin The minimum value of Gamma when using the L0L2 penalty. +#' For the L0L1 penalty, the minimum value of gamma in the grid is set to +#' gammaMin * gammaMax. Note that this should be a strictly positive quantity. +#' @param partialSort If TRUE partial sorting will be used for sorting the +#' coordinates to do greedy cycling (see our paper for for details). +#' Otherwise, full sorting is used. +#' @param maxIters The maximum number of iterations (full cycles) for CD per +#' grid point. +#' @param rtol The relative tolerance which decides when to terminate +#' optimization (based on the relative change in the objective between +#' iterations). +#' @param atol The absolute tolerance which decides when to terminate +#' optimization (based on the absolute L2 norm of the residuals). +#' @param activeSet If TRUE, performs active set updates. +#' @param activeSetNum The number of consecutive times a support should appear +#' before declaring support stabilization. +#' @param maxSwaps The maximum number of swaps used by CDPSI for +#' each grid point. +#' @param scaleDownFactor This parameter decides how close the selected +#' Lambda values are. The choice should be strictly between 0 and 1 (i.e., 0 +#' and 1 are not allowed). Larger values lead to closer lambdas and typically +#' to smaller gaps between the support sizes. For details, see our paper - +#' Section 5 on Adaptive Selection of Tuning Parameters). +#' @param screenSize The number of coordinates to cycle over when performing +#' initial correlation screening. +#' @param autoLambda Ignored parameter. Kept for backwards compatibility. +#' @param lambdaGrid A grid of Lambda values to use in computing the +#' regularization path. This is by default an empty list and is ignored. When +#' specified, LambdaGrid should be a list of length 'nGamma', where the ith +#' element (corresponding to the ith gamma) should be a decreasing sequence of +#' lambda values which are used by the algorithm when fitting for the ith value +#' of gamma (see the vignette for details). +#' @param excludeFirstK This parameter takes non-negative integers. The first +#' excludeFirstK features in x will be excluded from variable selection, i.e., +#' the first excludeFirstK variables will not be included in the L0-norm +#' penalty (they will still be included in the L1 or L2 norm penalties.). +#' @param intercept If FALSE, no intercept term is included in the model. +#' @param lows Lower bounds for coefficients. Either a scalar for all +#' coefficients to have the same bound or a vector of size p (number of +#' columns of X) where lows[i] is the lower bound for coefficient i. +#' @param highs Upper bounds for coefficients. Either a scalar for all +#' coefficients to have the same bound or a vector of size p (number of +#' columns of X) where highs[i] is the upper bound for coefficient i. +#' @return An S3 object of type "L0Learn" describing the regularization path. +#' The object has the following members. +#' \item{a0}{a0 is a list of intercept sequences. The ith element of the list +#' (i.e., a0[[i]]) is the sequence of intercepts corresponding to the ith gamma +#' value (i.e., gamma[i]).} +#' \item{beta}{This is a list of coefficient matrices. The ith element of the +#' list is a p x \code{length(lambda)} matrix which +#' corresponds to the ith gamma value. The jth column in each coefficient +#' matrix is the vector of coefficients for the jth lambda value.} +#' \item{lambda}{This is the list of lambda sequences used in fitting the +#' model. The ith element of lambda (i.e., lambda[[i]]) is the sequence +#' of Lambda values corresponding to the ith gamma value.} +#' \item{gamma}{This is the sequence of gamma values used in fitting the +#' model.} +#' \item{suppSize}{This is a list of support size sequences. The ith element +#' of the list is a sequence of support sizes (i.e., number of non-zero +#' coefficients) corresponding to the ith gamma value.} +#' \item{converged}{This is a list of sequences for checking whether the +#' algorithm has converged at every grid point. The ith element of the list +#' is a sequence corresponding to the ith value of gamma, where the jth element +#' in each sequence indicates whether the algorithm has converged at the jth +#' value of lambda.} +#' +#' @examples +#' # Generate synthetic data for this example +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' +#' # Fit an L0 regression model with a maximum of 50 non-zeros using coordinate +#' # descent (CD) +#' fit1 <- L0Learn.fit(X, y, penalty="L0", maxSuppSize=50) +#' print(fit1) +#' # Extract the coefficients at lambda = 0.0325142 +#' coef(fit1, lambda=0.0325142) +#' # Apply the fitted model on X to predict the response +#' predict(fit1, newx = X, lambda=0.0325142) +#' +#' # Fit an L0 regression model with a maximum of 50 non-zeros using CD +#' # and local search +#' fit2 <- L0Learn.fit(X, y, penalty="L0", algorithm="CDPSI", maxSuppSize=50) +#' print(fit2) +#' +#' # Fit an L0L2 regression model with 10 values of Gamma ranging from +#' # 0.0001 to 10, using CD +#' fit3 <- L0Learn.fit(X, y, penalty="L0L2", maxSuppSize=50, nGamma=10, +#' gammaMin=0.0001, gammaMax = 10) +#' print(fit3) +#' # Extract the coefficients at lambda = 0.0361829 and gamma = 0.0001 +#' coef(fit3, lambda=0.0361829, gamma=0.0001) +#' # Apply the fitted model on X to predict the response +#' predict(fit3, newx = X, lambda=0.0361829, gamma=0.0001) +#' +#' # Fit an L0 logistic regression model +#' # First, convert the response to binary +#' y = sign(y) +#' fit4 <- L0Learn.fit(X, y, loss="Logistic", maxSuppSize=20) +#' print(fit4) +#' +#' @export +L0Learn.fit <- function(x, + y, + loss="SquaredError", + penalty="L0", + algorithm="CD", + maxSuppSize=100, + nLambda=100, + nGamma=10, + gammaMax=10, + gammaMin=0.0001, + partialSort = TRUE, + maxIters=200, + rtol=1e-6, + atol=1e-9, + activeSet=TRUE, + activeSetNum=3, + maxSwaps=100, + scaleDownFactor=0.8, + screenSize=1000, + autoLambda = NULL, + lambdaGrid = list(), + excludeFirstK=0, + intercept = TRUE, + lows=-Inf, + highs=Inf) { + + set.seed(seed) + + if ((rtol < 0) || (rtol >= 1)) { + stop("The specified rtol parameter must exist in [0, 1)") + } + + if (atol < 0) { + stop("The specified atol parameter must exist in [0, INF)") + } + + # Some sanity checks for the inputs + if (!(loss %in% c("SquaredError", "Logistic", "SquaredHinge"))) { + stop("The specified loss function is not supported.") + } + if (!(penalty %in% c("L0", "L0L2", "L0L1"))) { + stop("The specified penalty is not supported.") + } + if (!(algorithm %in% c("CD", "CDPSI"))) { + stop("The specified algorithm is not supported.") + } + if (loss == "Logistic" | loss == "SquaredHinge") { + if (dim(table(y)) != 2) { + stop("Only binary classification is supported. + Make sure y has only 2 unique values.") + } + y <- factor(y, labels = c(-1, 1)) # returns a vector of strings + y <- as.numeric(levels(y))[y] + + if (penalty == "L0") { + if ((length(lambdaGrid) != 0) && (length(lambdaGrid) != 1)) { + # If this error checking was left to the lower section, + # it would confuse users as we are converting L0 to L0L2 with + # small L2 penalty. + # Here we must check if lambdaGrid is supplied + # (And thus use 'autolambda') + # If 'lambdaGrid' is supplied, we must only supply 1 list of + # lambda values + + stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. + Where lambdaGrid[[1]] is a list or vector of decreasing + positive values.") + } + penalty <- "L0L2" + nGamma <- 1 + gammaMax <- 1e-7 + gammaMin <- 1e-7 + } + } + + # Handle Lambda Grids: + if (length(lambdaGrid) != 0) { + if (!is.null(autoLambda) && !autoLambda) { + warning("In L0Learn V2+, autoLambda is ignored and inferred if + 'lambdaGrid' is supplied", call. = FALSE) + } + autoLambda <- FALSE + } else { + autoLambda <- TRUE + lambdaGrid <- list(0) + } + + if (penalty == "L0" && !autoLambda) { + bad_lambdaGrid <- FALSE + if (length(lambdaGrid) != 1) { + bad_lambdaGrid <- TRUE + } + current <- Inf + for (nxt in lambdaGrid[[1]]) { + if (nxt > current) { + # This must be > instead of >= to allow first iteration L0L1 + # lambdas of all 0s to be valid + bad_lambdaGrid <- TRUE + break + } + if (nxt < 0) { + bad_lambdaGrid <- TRUE + break + } + current <- nxt + + } + + if (bad_lambdaGrid) { + stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. + Where lambdaGrid[[1]] is a list or vector of decreasing + positive values.") + } + } + + if (penalty != "L0" && !autoLambda) { + # Covers L0L1, L0L2 cases + bad_lambdaGrid <- FALSE + if (length(lambdaGrid) != nGamma) { + warning("In L0Learn V2+, nGamma is ignored and replaced with + length(lambdaGrid)", call. = FALSE) + nGamma <- length(lambdaGrid) + } + + for (i in seq_len(lambdaGrid)) { + current <- Inf + for (nxt in lambdaGrid[[i]]) { + if (nxt > current) { + # This must be > instead of >= to allow first iteration + # L0L1 lambdas of all 0s to be valid + bad_lambdaGrid <- TRUE + break + } + if (nxt < 0) { + bad_lambdaGrid <- TRUE + break + } + current <- nxt + } + if (bad_lambdaGrid) { + break + } + } + + if (bad_lambdaGrid) { + stop("L0L1 or L0L2 Penalty requires 'lambdaGrid' to be a list of + length 'nGamma'. Where lambdaGrid[[i]] is a list or vector of + decreasing positive values.") + } + + } + + is.scalar <- function(x) { + return(is.atomic(x) + && length(x) == 1L + && !is.character(x) + && Im(x) == 0 + && !is.nan(x) + && !is.na(x)) + } + + p <- dim(x)[[2]] + + if ((excludeFirstK < 0) || (excludeFirstK >= p)) { + stop("The specified excludeFirstK parameter must be an integer + between 0 and p-1") + } + + withBounds <- FALSE + if ((!identical(lows, -Inf)) || (!identical(highs, Inf))) { + withBounds <- TRUE + + if (algorithm == "CDPSI") { + if (any(lows != -Inf) || any(highs != Inf)) { + stop("Bounds are not YET supported for CDPSI algorithm. + Please raise an issue at: + 'https://github.com/hazimehh/L0Learn' + to express interest in this functionality") + } + } + + if (is.scalar(lows)) { + lows <- lows * rep(1, p) + } else if (!all(sapply(lows, is.scalar)) || length(lows) != p) { + stop("Lows must be a vector of real values of length p") + } + + if (is.scalar(highs)) { + highs <- highs * rep(1, p) + } else if (!all(sapply(highs, is.scalar)) || length(highs) != p) { + stop("Highs must be a vector of real values of length p") + } + + if (any(lows >= highs) || any(lows > 0) || any(highs < 0)) { + stop("Bounds must conform to the following conditions: + Lows <= 0, Highs >= 0, Lows < Highs") + } + + } + + M <- list() + if (is(x, "sparseMatrix")) { + M <- .Call("_L0Learn_L0LearnFit_sparse", + PACKAGE = "L0Learn", + x, + y, + loss, + penalty, + algorithm, + maxSuppSize, + nLambda, + nGamma, + gammaMax, + gammaMin, + partialSort, + maxIters, + rtol, + atol, + activeSet, + activeSetNum, + maxSwaps, + scaleDownFactor, + screenSize, + !autoLambda, + lambdaGrid, + excludeFirstK, + intercept, + withBounds, + lows, + highs) + } else{ + M <- .Call("_L0Learn_L0LearnFit_dense", + PACKAGE = "L0Learn", + x, + y, + loss, + penalty, + algorithm, + maxSuppSize, + nLambda, + nGamma, + gammaMax, + gammaMin, + partialSort, + maxIters, + rtol, + atol, + activeSet, + activeSetNum, + maxSwaps, + scaleDownFactor, + screenSize, + !autoLambda, + lambdaGrid, + excludeFirstK, + intercept, + withBounds, + lows, + highs) + } + + # The C++ function uses LambdaU = 1 for user-specified grid. In R, we use + # autoLambda0 = 0 for user-specified grid (thus the negation when passing + # the parameter to the function below) + + settings <- list() + settings[[1]] <- intercept + # Settings only contains intercept for now. + # Might include additional elements later. + names(settings) <- c("intercept") + + # Find potential support sizes exceeding maxSuppSize and remove them + # (this is due to the C++ core whose last solution can exceed maxSuppSize) + for (i in seq_len(M$SuppSize)) { + last <- length(M$SuppSize[[i]]) + if (M$SuppSize[[i]][last] > maxSuppSize) { + if (last == 1) { + warning("Warning! + Only 1 element in path with support size > maxSuppSize. + Try increasing maxSuppSize to resolve the issue.") + } else{ + M$SuppSize[[i]] <- M$SuppSize[[i]][-last] + M$Converged[[i]] <- M$Converged[[i]][-last] + M$lambda[[i]] <- M$lambda[[i]][-last] + M$a0[[i]] <- M$a0[[i]][-last] + M$beta[[i]] <- as(M$beta[[i]][, -last], "sparseMatrix") + # conversion to sparseMatrix is necessary to handle the case + # of a single column + } + } + } + + G <- list(beta = M$beta, + lambda = lapply(M$lambda, signif, digits = 6), + a0 = M$a0, + converged = M$Converged, + suppSize = M$SuppSize, + gamma = M$gamma, + penalty = penalty, + loss = loss, + settings = settings) + + + if (is.null(colnames(x))) { + varnames <- 1:dim(x)[2] + } else { + varnames <- colnames(x) + } + G$varnames <- varnames + + class(G) <- "L0Learn" + G$n <- dim(x)[1] + G$p <- dim(x)[2] + G +} diff --git a/R/R/genhighcorr.R b/R/R/genhighcorr.R new file mode 100644 index 0000000..aebf8ea --- /dev/null +++ b/R/R/genhighcorr.R @@ -0,0 +1,73 @@ +#' @importFrom stats rnorm var +#' @importFrom MASS mvrnorm +#' @importFrom Rcpp cppFunction +#' @title Generate Expoentential Correlated Synthetic Data +#' +#' @description Generates a synthetic dataset as follows: +#' 1) Generate a correlation matrix, SIG, where item [i, j] = A^|i-j|. +#' 2) Draw from a Multivariate Normal Distribution using (mu and SIG) +#' to generate X. +#' 3) Generate a vector B with every ~p/k entry set to 1 and the rest are zeros. +#' 4) Sample every element in the noise vector e from N(0,1). 4) +#' Set y = XB + b0 + e. +#' @param n Number of samples +#' @param p Number of features +#' @param k Number of non-zeros in true vector of coefficients +#' @param seed The seed used for randomly generating the data +#' @param rho The threshold for setting values to 0. +#' If |X(i, j)| > rho => X(i, j) <- 0 +#' @param b0 intercept value to scale y by. +#' @param snr desired Signal-to-Noise ratio. +#' This sets the magnitude of the error term 'e'. +#' SNR is defined as SNR = Var(XB)/Var(e) +#' @param mu The mean for drawing from the Multivariate Normal Distribution. +#' A scalar of vector of length p. +#' @param base_cor The base correlation, A in [i, j] = A^|i-j|. +#' @return A list containing: +#' the data matrix X, +#' the response vector y, +#' the coefficients B, +#' the error vector e, +#' the intercept term b0. +#' @examples +#' data <- L0Learn:::GenSyntheticHighCorr(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +GenSyntheticHighCorr <- function(n, + p, + k, + seed, + rho=0, + b0=0, + snr=1, + mu=0, + base_cor=.9) { + set.seed(seed) # fix the seed to get a reproducible result + cor <- .Call("_L0Learn_cor_matrix", p, base_cor) + + if (length(mu) == 1) { + mu <- rep(mu, p) + } + + X <- mvrnorm(n, mu, forceSymmetric(cor)) + + X[abs(X) < rho] <- 0. + + B_indices <- seq(from = 1, to = p, by = as.integer(p / k)) + + B <- rep(0, p) + + for (i in B_indices) { + B[i] <- 1 + } + + sd_e <- NULL + if (snr == +Inf) { + sd_e <- 0 + } else { + sd_e <- sqrt(var(X %*% B) / snr) + } + e <- rnorm(n, sd = sd_e) + y <- X %*% B + e + b0 + list(X = X, y = y, B = B, e = e, b0 = b0) +} diff --git a/R/R/genlogistic.R b/R/R/genlogistic.R new file mode 100644 index 0000000..33980c7 --- /dev/null +++ b/R/R/genlogistic.R @@ -0,0 +1,68 @@ +#' @importFrom stats rnorm rbinom +#' @importFrom MASS mvrnorm +#' @title Generate Logistic Synthetic Data +#' +#' @description Generates a synthetic dataset as follows: +#' 1) Generate a data matrix, X, drawn from a multivariate Gaussian +#' distribution with mean = 0, sigma = Sigma +#' 2) Generate a vector B with k entries set to 1 and the rest are zeros. +#' 3) Every coordinate yi of the outcome vector y exists in {0, 1}^n is sampled +#' independently from a Bernoulli distribution with success probability: +#' P(yi = 1|xi) = 1/(1 + exp(-s)) +#' Source https://arxiv.org/pdf/2001.06471.pdf Section 5.1 Data Generation +#' @param n Number of samples +#' @param p Number of features +#' @param k Number of non-zeros in true vector of coefficients +#' @param seed The seed used for randomly generating the data +#' @param rho The threshold for setting values to 0. I +#' If |X(i, j)| > rho => X(i, j) <- 0 +#' @param s Signal-to-noise parameter. As s -> +Inf, the data generated becomes +#' linearly separable. +#' @param sigma Correlation matrix, defaults to I. +#' @param shuffle_B A boolean flag for whether or not to randomly shuffle the +#' Beta vector, B. +#' If FALSE, the first k entries in B are set to 1. +#' @return A list containing: +#' the data matrix X, +#' the response vector y, +#' the coefficients B, +#' @examples +#' data <- L0Learn:::GenSyntheticLogistic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +GenSyntheticLogistic <- function(n, + p, + k, + seed, + rho=0, + s=1, + sigma=NULL, + shuffle_B=FALSE) { + if (s < 0) { + stop("s must be fall in the interval [0, +Inf)") + } + + X <- NULL + set.seed(seed) + + if (is.null(sigma)) { + X <- matrix(rnorm(n * p), n, p) + } else { + if ((ncol(sigma) != p) || (nrow(sigma) != p)) { + stop("sigma must be a semi positive definite matrix of + side length p") + } + X <- mvrnorm(n, mu = rep(0, p), Sigma = sigma) + } + + X[abs(X) < rho] <- 0. + B <- c(rep(1, k), rep(0, p - k)) + + if (shuffle_B) { + B <- sample(B) + } + + y <- rbinom(n, 1, 1 / (1 + exp(-s * X %*% B))) + + return(list(X = X, B = B, y = y, s = s)) +} diff --git a/R/R/gensynthetic.R b/R/R/gensynthetic.R new file mode 100644 index 0000000..bbfe4bb --- /dev/null +++ b/R/R/gensynthetic.R @@ -0,0 +1,48 @@ +#' @importFrom stats rnorm var +#' @title Generate Synthetic Data +#' +#' @description Generates a synthetic dataset as follows: +#' 1) Sample every element in data matrix X from N(0,1). +#' 2) Generate a vector B with the first k entries set to 1 and the rest are +#' zeros. +#' 3) Sample every element in the noise vector e from N(0,A) where A is +#' selected so y, X, B have snr as specified. +#' 4) Set y = XB + b0 + e. +#' @param n Number of samples +#' @param p Number of features +#' @param k Number of non-zeros in true vector of coefficients +#' @param seed The seed used for randomly generating the data +#' @param rho The threshold for setting values to 0. +#' If |X(i, j)| > rho => X(i, j) <- 0 +#' @param b0 intercept value to translate y by. +#' @param snr desired Signal-to-Noise ratio. +#' This sets the magnitude of the error term 'e'. +#' SNR is defined as SNR = Var(XB)/Var(e) +#' @return A list containing: +#' the data matrix X, +#' the response vector y, +#' the coefficients B, +#' the error vector e, +#' the intercept term b0. +#' @examples +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' @export +GenSynthetic <- function(n, p, k, seed, rho=0, b0=0, snr=1) { + set.seed(seed) # fix the seed to get a reproducible result + X <- matrix(rnorm(n * p), nrow = n, ncol = p) + X[abs(X) < rho] <- 0. + B <- c(rep(1, k), rep(0, p - k)) + sd_e <- NULL + + if (snr == +Inf) { + sd_e <- 0 + } else { + sd_e <- sqrt(var(X %*% B) / snr) + } + + e <- rnorm(n, sd = sd_e) + y <- X %*% B + e + b0 + list(X = X, y = y, B = B, e = e, b0 = b0) +} diff --git a/R/R/plot.R b/R/R/plot.R new file mode 100644 index 0000000..92c7842 --- /dev/null +++ b/R/R/plot.R @@ -0,0 +1,94 @@ +#' @title Plot Regularization Path +#' +#' @description Plots the regularization path for a given gamma. +#' @param gamma The value of gamma at which to plot. +#' @param x The output of L0Learn.fit +#' @param showLines If TRUE, the lines connecting the points in +#' the plot are shown. +#' @param ... ignore +#' +#' @examples +#' # Generate synthetic data for this example +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' # Fit an L0 Model with a maximum of 50 non-zeros +#' fit <- L0Learn.fit(X, y, penalty="L0", maxSuppSize=50) +#' plot(fit, gamma=0) +#' +#' @import ggplot2 +#' @importFrom reshape2 melt +#' @method plot L0Learn +#' @export +plot.L0Learn <- function(x, gamma=0, showLines=FALSE, ...) { + j <- which(abs(x$gamma - gamma) == min(abs(x$gamma - gamma))) + p <- x$p + allin <- c() # contains all the non-zero variables in the path + for (i in seq_len(x$lambda[[j]])) { + BetaTemp <- x$beta[[j]][, i] + supp <- which(as.matrix(BetaTemp != 0)) + allin <- c(allin, supp) + } + allin <- unique(allin) + + #ggplot needs a dataframe + # length(lambda) x length(allin) matrix + yy <- t(as.matrix(x$beta[[j]][allin, ])) + data <- as.data.frame(yy) + + colnames(data) <- x$varnames[allin] + + #id variable for position in matrix + data$id <- x$suppSize[[j]] + + #reshape to long format + plot_data <- melt(data, id.var = "id") + + plotObject <- ggplot(plot_data, + aes_string(x = "id", + y = "value", + group = "variable", + colour = "variable")) + + geom_point(size = 2.5) + + labs(x = "Support Size", y = "Coefficient") + + theme(axis.title = element_text(size = 14)) + + if (showLines) { + plotObject <- plotObject + + geom_line(aes_string(lty = "variable"), alpha = 0.3) + } + plotObject +} + +#' @title Plot Cross-validation Errors +#' +#' @description Plots cross-validation errors for a given gamma. +#' @param x The output of L0Learn.cvfit +#' @inheritParams plot.L0Learn +#' @examples +#' # Generate synthetic data for this example +#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) +#' X = data$X +#' y = data$y +#' +#' # Perform 5-fold cross-validation on an L0L2 Model with 5 values of +#' # Gamma ranging from 0.0001 to 10 +#' fit <- L0Learn.cvfit(X, y, nFolds=5, seed=1, penalty="L0L2", +#' maxSuppSize=20, nGamma=5, gammaMin=0.0001, gammaMax = 10) +#' # Plot the graph of cross-validation error versus lambda for gamma = 0.0001 +#' plot(fit, gamma=0.0001) +#' +#' @method plot L0LearnCV +#' @export +plot.L0LearnCV <- function(x, gamma=0, ...) { + j <- which(abs(x$fit$gamma - gamma) == min(abs(x$fit$gamma - gamma))) + data <- data.frame(x = x$fit$suppSize[[j]], + y = x$cvMeans[[j]], + sd = x$cvSDs[[j]]) + ggplot(data, aes_string(x = "x", y = "y")) + + geom_point() + + geom_errorbar(aes_string(ymin = "y-sd", ymax = "y+sd")) + + labs(x = "Support Size", y = "Cross-validation Error") + + theme(axis.title = element_text(size = 14)) + + theme(axis.text = element_text(size = 12)) +} diff --git a/R/predict.R b/R/R/predict.R similarity index 53% rename from R/predict.R rename to R/R/predict.R index 701c6e1..8e15564 100644 --- a/R/predict.R +++ b/R/R/predict.R @@ -3,10 +3,13 @@ #' @description Predicts the response for a given sample. #' @param object The output of L0Learn.fit or L0Learn.cvfit #' @param ... ignore -#' @param newx A matrix on which predictions are made. The matrix should have p columns. -#' @param lambda The value of lambda to use for prediction. A summary of the lambdas in the regularization +#' @param newx A matrix on which predictions are made. +#' The matrix should have p columns. +#' @param lambda The value of lambda to use for prediction. +#' A summary of the lambdas in the regularization #' path can be obtained using \code{print(fit)}. -#' @param gamma The value of gamma to use for prediction. A summary of the gammas in the regularization +#' @param gamma The value of gamma to use for prediction. +#' A summary of the gammas in the regularization #' path can be obtained using \code{print(fit)}. #' @method predict L0Learn #' @details @@ -20,39 +23,39 @@ #' X = data$X #' y = data$y #' -#' # Fit an L0L2 Model with 10 values of Gamma ranging from 0.0001 to 10, using coordinate descent -#' fit <- L0Learn.fit(X,y, penalty="L0L2", maxSuppSize=50, nGamma=10, gammaMin=0.0001, gammaMax = 10) +#' # Fit an L0L2 Model with 10 values of Gamma ranging from 0.0001 to 10, +#' # using coordinate descent +#' fit <- L0Learn.fit(X,y, penalty="L0L2", maxSuppSize=50, nGamma=10, +#' gammaMin=0.0001, gammaMax = 10) #' print(fit) -#' # Apply the fitted model with lambda=0.0361829 and gamma=0.0001 on X to predict the response +#' # Apply the fitted model with lambda=0.0361829 and gamma=0.0001 on X to +#' # predict the response #' predict(fit, newx = X, lambda=0.0361829, gamma=0.0001) -#' # Apply the fitted model on X to predict the response for all the solutions in the path +#' # Apply the fitted model on X to predict the response for all +#' # the solutions in the path #' predict(fit, newx = X) #' #' @export -predict.L0Learn <- function(object,newx,lambda=NULL,gamma=NULL, ...) -{ - beta = coef.L0Learn(object, lambda, gamma) - if (object$settings$intercept){ - # add a column of ones for the intercept - x = cbind(1,newx) - } - else{ - x = newx - } - prediction = x%*%beta - #if (object$loss == "Logistic" || object$loss == "SquaredHinge"){ - # prediction = sign(prediction) - #} - if (object$loss == "Logistic"){ - prediction = 1/(1+exp(-prediction)) - } - prediction +predict.L0Learn <- function(object, newx, lambda=NULL, gamma=NULL, ...) { + beta <- coef.L0Learn(object, lambda, gamma) + if (object$settings$intercept) { + # add a column of ones for the intercept + x <- cbind(1, newx) + } + else{ + x <- newx + } + prediction <- x %*% beta + + if (object$loss == "Logistic") { + prediction <- 1 / (1 + exp(-prediction)) + } + prediction } #' @rdname predict.L0Learn #' @method predict L0LearnCV #' @export -predict.L0LearnCV <- function(object,newx,lambda=NULL,gamma=NULL, ...) -{ - predict.L0Learn(object$fit,newx,lambda,gamma, ...) +predict.L0LearnCV <- function(object, newx, lambda=NULL, gamma=NULL, ...) { + predict.L0Learn(object$fit, newx, lambda, gamma, ...) } diff --git a/R/R/print.R b/R/R/print.R new file mode 100644 index 0000000..03741c3 --- /dev/null +++ b/R/R/print.R @@ -0,0 +1,23 @@ +#' @title Print L0Learn.fit object +#' +#' @description Prints a summary of L0Learn.fit +#' @param x The output of L0Learn.fit or L0Learn.cvfit +#' @param ... ignore +#' @method print L0Learn +#' @export +print.L0Learn <- function(x, ...) { + gammas <- rep(x$gamma, times = lapply(x$lambda, length)) + data.frame( + lambda = unlist(x["lambda"]), + gamma = gammas, + suppSize = unlist(x["suppSize"]), + row.names = NULL + ) +} + +#' @rdname print.L0Learn +#' @method print L0LearnCV +#' @export +print.L0LearnCV <- function(x, ...) { + print.L0Learn(x$fit) +} diff --git a/R/README.md b/R/README.md new file mode 100644 index 0000000..1dc37c6 --- /dev/null +++ b/R/README.md @@ -0,0 +1,77 @@ +# L0Learn: Fast Best Subset Selection +![example workflow](https://github.com/TNonet/L0Learn/actions/workflows/r.yml/badge.svg) [![CRAN](https://www.r-pkg.org/badges/version/L0Learn)](https://cran.r-project.org/package=L0Learn) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/7fd68c533fd1493288e7986df3cc6f6d)](https://www.codacy.com/gh/hazimehh/L0Learn/dashboard?utm_source=github.com&utm_medium=referral&utm_content=hazimehh/L0Learn&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/hazimehh/L0Learn/branch/master/graph/badge.svg?token=QYDNA400OI)](https://codecov.io/gh/hazimehh/L0Learn) + +### Hussein Hazimeh, Rahul Mazumder, and Tim Nonet +### Massachusetts Institute of Technology + +Downloads from Rstudio: [![](https://cranlogs.r-pkg.org/badges/grand-total/L0Learn)](https://cran.rstudio.com/web/packages/L0Learn/index.html) + +## Introduction +L0Learn is a highly efficient framework for solving L0-regularized learning problems. It can (approximately) solve the following three problems, where the empirical loss is penalized by combinations of the L0, L1, and L2 norms: + + + +We support both regression (using squared error loss) and classification (using logistic or squared hinge loss). Optimization is done using coordinate descent and local combinatorial search over a grid of regularization parameter(s) values. Several computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter λ in the path. We describe the details of the algorithms in our paper: *Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms* ([link](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919)). + +The toolkit is implemented in C++11 and can often run faster than popular sparse learning toolkits (see our experiments in the paper above). We also provide an easy-to-use R interface; see the section below for installation and usage of the R package. + +**NEW: Version 2 (03/2021) adds support for sparse matrices and box constraints on the coefficients.** + +## R Package Installation +The latest version (v2.0.3) can be installed from CRAN as follows: +```{R} +install.packages("L0Learn", repos = "http://cran.rstudio.com") +``` +Alternatively, L0Learn can also be installed from Github as follows: +```{R} +library(devtools) +install_github("hazimehh/L0Learn", subdir="R") +``` +L0Learn's changelog can be accessed from [here](https://github.com/hazimehh/L0Learn/blob/master/ChangeLog). + +## Usage +For a tutorial, please refer to [L0Learn's Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html). For a detailed description of the API, check the [Reference Manual](https://cran.r-project.org/web/packages/L0Learn/L0Learn.pdf). + +## FAQ +#### Which penalty to use? +Pure L0 regularization can overfit when the signal strength in the data is relatively low. Adding L2 regularization can alleviate this problem and lead to competitive models (see the experiments in our paper). Thus, in practice, **we strongly recommend using the L0L2 penalty**. Ideally, the parameter gamma (for L2 regularization) should be tuned over a sufficiently large interval, and this can be performed using L0Learn's built-in [cross-validation method](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#cross-validation). + +#### Which algorithm to use? +By default, L0Learn uses a coordinate descent-based algorithm, which achieves competitive run times compared to popular sparse learning toolkits. This can work well for many applications. We also offer a local search algorithm which is guarantteed to return higher quality solutions, at the expense of an increase in the run time. We recommend using the local search algorithm if your problem has highly correlated features or the number of samples is much smaller than the number of features---see the [local search section of the Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#higher-quality_solutions_using_local_search) for how to use this algorithm. + +#### How to certify optimality? +While for many challenging statistical instances L0Learn leads to optimal solutions, it cannot provide certificates of optimality. Such certificates can be provided via Integer Programming. Our toolkit [L0BnB](https://github.com/alisaab/l0bnb) is a scalable integer programming framework for L0-regularized regression, which can provide such certificates and potentially improve upon the solutions of L0Learn (if they are sub-optimal). We recommend using L0Learn first to obtain a candidtate solution (or a pool of solutions) and then checking optimality using L0BnB. + + +## Citing L0Learn +If you find L0Learn useful in your research, please consider citing the following two papers. + +**Paper 1:** +``` +@article{doi:10.1287/opre.2019.1919, +author = {Hazimeh, Hussein and Mazumder, Rahul}, +title = {Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms}, +journal = {Operations Research}, +volume = {68}, +number = {5}, +pages = {1517-1537}, +year = {2020}, +doi = {10.1287/opre.2019.1919}, +URL = {https://doi.org/10.1287/opre.2019.1919}, +eprint = {https://doi.org/10.1287/opre.2019.1919} +} +``` + +**Paper 2:** +``` +@article{JMLR:v22:19-1049, + author = {Antoine Dedieu and Hussein Hazimeh and Rahul Mazumder}, + title = {Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives}, + journal = {Journal of Machine Learning Research}, + year = {2021}, + volume = {22}, + number = {135}, + pages = {1-47}, + url = {http://jmlr.org/papers/v22/19-1049.html} +} +``` diff --git a/R/cleanup b/R/cleanup new file mode 100755 index 0000000..9fc525a --- /dev/null +++ b/R/cleanup @@ -0,0 +1,2 @@ +#!/bin/sh +rm -f config.* src/Makevars src/*.o src/src/*.o diff --git a/R/coef.R b/R/coef.R deleted file mode 100644 index c157db2..0000000 --- a/R/coef.R +++ /dev/null @@ -1,62 +0,0 @@ -#' @title Extract Solutions -#' -#' @description Extracts a specific solution in the regularization path. -#' @param object The output of L0Learn.fit or L0Learn.cvfit -#' @param ... ignore -#' @param lambda The value of lambda at which to extract the solution. -#' @param gamma The value of gamma at which to extract the solution. -#' @method coef L0Learn -#' @details -#' If both lambda and gamma are not supplied, then a matrix of coefficients -#' for all the solutions in the regularization path is returned. If lambda is -#' supplied but gamma is not, the smallest value of gamma is used. -#' @examples -#' # Generate synthetic data for this example -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' -#' # Fit an L0L2 Model with 10 values of Gamma ranging from 0.0001 to 10, using coordinate descent -#' fit <- L0Learn.fit(X, y, penalty="L0L2", maxSuppSize=50, nGamma=10, gammaMin=0.0001, gammaMax = 10) -#' print(fit) -#' # Extract the coefficients of the solution at lambda = 0.0361829 and gamma = 0.0001 -#' coef(fit, lambda=0.0361829, gamma=0.0001) -#' # Extract the coefficients of all the solutions in the path -#' coef(fit) -#' -#' @export -coef.L0Learn <- function(object,lambda=NULL,gamma=NULL, ...){ - if (is.null(lambda) && is.null(gamma)){ - t = do.call(cbind,object$beta) - if (object$settings$intercept){ - intercepts = unlist(object$a0) - t = rbind(intercepts, t) - } - } - else{ - if (is.null(gamma)){ # if lambda is present but gamma is not, use smallest value of gamma - gamma = object$gamma[1] - } - diffGamma = abs(object$gamma-gamma) - gammaindex = which(diffGamma==min(diffGamma)) - diffLambda = abs(lambda - object$lambda[[gammaindex]]) - indices = which(diffLambda == min(diffLambda)) - #indices = match(lambda,object$lambda[[gammaindex]]) - if (object$settings$intercept){ - t = rbind(object$a0[[gammaindex]][indices],object$beta[[gammaindex]][,indices,drop=FALSE]) - rownames(t) = c("Intercept",paste(rep("V",object$p),1:object$p,sep="")) - } - else{ - t = object$beta[[gammaindex]][,indices,drop=FALSE] - rownames(t) = paste(rep("V",object$p),1:object$p,sep="") - } - } - t -} - -#' @rdname coef.L0Learn -#' @method coef L0LearnCV -#' @export -coef.L0LearnCV <- function(object,lambda=NULL,gamma=NULL, ...){ - coef.L0Learn(object$fit,lambda,gamma, ...) -} diff --git a/configure b/R/configure similarity index 100% rename from configure rename to R/configure diff --git a/configure.ac b/R/configure.ac similarity index 100% rename from configure.ac rename to R/configure.ac diff --git a/R/cvfit.R b/R/cvfit.R deleted file mode 100644 index 0b6b173..0000000 --- a/R/cvfit.R +++ /dev/null @@ -1,247 +0,0 @@ -#' @title Cross Validation -#' -#' @inheritParams L0Learn.fit -#' @description Computes a regularization path and performs K-fold cross-validation. -#' @param nFolds The number of folds for cross-validation. -#' @param seed The seed used in randomly shuffling the data for cross-validation. -#' @return An S3 object of type "L0LearnCV" describing the regularization path. The object has the following members. -#' \item{cvMeans}{This is a list, where the ith element is the sequence of cross-validation errors corresponding to the ith gamma value, i.e., the sequence -#' cvMeans[[i]] corresponds to fit$gamma[i]} -#' \item{cvSDs}{This a list, where the ith element is a sequence of standard deviations for the cross-validation errors: cvSDs[[i]] corresponds to cvMeans[[i]].} -#' \item{fit}{The fitted model with type "L0Learn", i.e., this is the same object returned by \code{\link{L0Learn.fit}}.} -#' -#' @examples -#' # Generate synthetic data for this example -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' -#' # Perform 5-fold cross-validation on an L0L2 regression model with 5 values of -#' # Gamma ranging from 0.0001 to 10 -#' fit <- L0Learn.cvfit(X, y, nFolds=5, seed=1, penalty="L0L2", maxSuppSize=20, nGamma=5, -#' gammaMin=0.0001, gammaMax = 10) -#' print(fit) -#' # Plot the graph of cross-validation error versus lambda for gamma = 0.0001 -#' plot(fit, gamma=0.0001) -#' # Extract the coefficients at lambda = 0.0361829 and gamma = 0.0001 -#' coef(fit, lambda=0.0361829, gamma=0.0001) -#' # Apply the fitted model on X to predict the response -#' predict(fit, newx = X, lambda=0.0361829, gamma=0.0001) -#' -#' @export -L0Learn.cvfit <- function(x,y, loss="SquaredError", penalty="L0", algorithm="CD", - maxSuppSize=100, nLambda=100, nGamma=10, gammaMax=10, - gammaMin=0.0001, partialSort = TRUE, maxIters=200, - rtol=1e-6, atol=1e-9, activeSet=TRUE, activeSetNum=3, maxSwaps=100, - scaleDownFactor=0.8, screenSize=1000, autoLambda=NULL, - lambdaGrid = list(), nFolds=10, seed=1, excludeFirstK=0, - intercept=TRUE, lows=-Inf, highs=Inf) -{ - set.seed(seed) - - if ((rtol < 0) || (rtol >= 1)){ - stop("The specified rtol parameter must exist in [0, 1)") - } - - if (atol < 0){ - stop("The specified atol parameter must exist in [0, INF)") - } - - # Some sanity checks for the inputs - if ( !(loss %in% c("SquaredError","Logistic","SquaredHinge")) ){ - stop("The specified loss function is not supported.") - } - if ( !(penalty %in% c("L0","L0L2","L0L1")) ){ - stop("The specified penalty is not supported.") - } - if ( !(algorithm %in% c("CD","CDPSI")) ){ - stop("The specified algorithm is not supported.") - } - if (loss=="Logistic" | loss=="SquaredHinge"){ - if (dim(table(y)) != 2){ - stop("Only binary classification is supported. Make sure y has only 2 unique values.") - } - y = factor(y,labels=c(-1,1)) # returns a vector of strings - y = as.numeric(levels(y))[y] - - if (penalty == "L0"){ - if ((length(lambdaGrid) != 0) && (length(lambdaGrid) != 1)){ - # If this error checking was left to the lower section, it would confuse users as - # we are converting L0 to L0L2 with small L2 penalty. - # Here we must check if lambdaGrid is supplied (And thus use 'autolambda') - # If 'lambdaGrid' is supplied, we must only supply 1 list of lambda values - - - stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. - Where lambdaGrid[[1]] is a list or vector of decreasing positive values.") - } - penalty = "L0L2" - nGamma = 1 - gammaMax = 1e-7 - gammaMin = 1e-7 - } - } - - # Handle Lambda Grids: - if (length(lambdaGrid) != 0){ - if (!is.null(autoLambda) && !autoLambda){ - warning("In L0Learn V2+, autoLambda is ignored and inferred if 'lambdaGrid' is supplied", call.=FALSE) - } - autoLambda = FALSE - } else { - autoLambda = TRUE - lambdaGrid = list(0) - } - - if (penalty == "L0" && !autoLambda){ - bad_lambdaGrid = FALSE - if (length(lambdaGrid) != 1){ - bad_lambdaGrid = TRUE - } - current = Inf - for (nxt in lambdaGrid[[1]]){ - if (nxt > current){ - # This must be > instead of >= to allow first iteration L0L1 lambdas of all 0s to be valid - bad_lambdaGrid = TRUE - break - } - if (nxt < 0){ - bad_lambdaGrid = TRUE - break - } - current = nxt - - } - - if (bad_lambdaGrid){ - stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. - Where lambdaGrid[[1]] is a list or vector of decreasing positive values.") - } - } - - if (penalty != "L0" && !autoLambda){ - # Covers L0L1, L0L2 cases - bad_lambdaGrid = FALSE - if (length(lambdaGrid) != nGamma){ - warning("In L0Learn V2+, nGamma is ignored and replaced with length(lambdaGrid)", call.=FALSE) - nGamma = length(lambdaGrid) - # bad_lambdaGrid = TRUE # Remove in V2.0,0 - } - - for (i in 1:length(lambdaGrid)){ - current = Inf - for (nxt in lambdaGrid[[i]]){ - if (nxt > current){ - # This must be > instead of >= to allow first iteration L0L1 lambdas of all 0s to be valid - bad_lambdaGrid = TRUE - break - } - if (nxt < 0){ - bad_lambdaGrid = TRUE - break - } - current = nxt - } - if (bad_lambdaGrid){ - break - } - } - - if (bad_lambdaGrid){ - stop("L0L1 or L0L2 Penalty requires 'lambdaGrid' to be a list of length 'nGamma'. - Where lambdaGrid[[i]] is a list or vector of decreasing positive values.") - } - - - } - - is.scalar <- function(x) is.atomic(x) && length(x) == 1L && !is.character(x) && Im(x)==0 && !is.nan(x) && !is.na(x) - - p = dim(x)[[2]] - - withBounds = FALSE - if ((!identical(lows, -Inf)) || (!identical(highs, Inf))){ - withBounds=TRUE - - if (algorithm == "CDPSI"){ - if (any(lows != -Inf) || any(highs != Inf)){ - stop("Bounds are not YET supported for CDPSI algorithm. Please raise - an issue at 'https://github.com/hazimehh/L0Learn' to express - interest in this functionality") - } - } - - if (is.scalar(lows)){ - lows = lows*rep(1, p) - } else if (!all(sapply(lows, is.scalar)) || length(lows) != p) { - stop('Lows must be a vector of real values of length p') - } - - if (is.scalar(highs)){ - highs = highs*rep(1, p) - } else if (!all(sapply(highs, is.scalar)) || length(highs) != p) { - stop('Highs must be a vector of real values of length p') - } - - if (any(lows >= highs) || any(lows > 0) || any(highs < 0)){ - stop("Bounds must conform to the following conditions: Lows <= 0, Highs >= 0, Lows < Highs") - } - - } - - M = list() - if (is(x, "sparseMatrix")){ - M <- .Call('_L0Learn_L0LearnCV_sparse', PACKAGE = 'L0Learn', x, y, loss, penalty, - algorithm, maxSuppSize, nLambda, nGamma, gammaMax, gammaMin, - partialSort, maxIters, rtol, atol, activeSet, activeSetNum, maxSwaps, - scaleDownFactor, screenSize, !autoLambda, lambdaGrid, nFolds, - seed, excludeFirstK, intercept, withBounds, lows, highs) - } else { - M <- .Call('_L0Learn_L0LearnCV_dense', PACKAGE = 'L0Learn', x, y, loss, penalty, - algorithm, maxSuppSize, nLambda, nGamma, gammaMax, gammaMin, - partialSort, maxIters, rtol, atol, activeSet, activeSetNum, maxSwaps, - scaleDownFactor, screenSize, !autoLambda, lambdaGrid, nFolds, - seed, excludeFirstK, intercept, withBounds, lows, highs) - } - - # The C++ function uses LambdaU = 1 for user-specified grid. In R, we use AutoLambda0 = 0 for user-specified grid (thus the negation when passing the parameter to the function below) - - - settings = list() - settings[[1]] = intercept # Settings only contains intercept for now. Might include additional elements later. - names(settings) <- c("intercept") - - # Find potential support sizes exceeding maxSuppSize and remove them (this is due to - # the C++ core whose last solution can exceed maxSuppSize - for (i in 1:length(M$SuppSize)){ - last = length(M$SuppSize[[i]]) - if (M$SuppSize[[i]][last] > maxSuppSize){ - if (last == 1){ - warning("Warning! Only 1 element in path with support size > maxSuppSize. \n Try increasing maxSuppSize to resolve the issue.") - } - else{ - M$SuppSize[[i]] = M$SuppSize[[i]][-last] - M$Converged[[i]] = M$Converged[[i]][-last] - M$lambda[[i]] = M$lambda[[i]][-last] - M$a0[[i]] = M$a0[[i]][-last] - M$beta[[i]] = as(M$beta[[i]][,-last], "sparseMatrix") # conversion to sparseMatrix is necessary to handle the case of a single column - M$CVMeans[[i]] = M$CVMeans[[i]][-last] - M$CVSDs[[i]] = M$CVSDs[[i]][-last] - } - } - } - - fit <- list(beta = M$beta, lambda=lapply(M$lambda,signif, digits=6), a0=M$a0, converged = M$Converged, suppSize= M$SuppSize, gamma=M$gamma, penalty=penalty, loss=loss, settings=settings) - if (is.null(colnames(x))){ - varnames <- 1:dim(x)[2] - } else { - varnames <- colnames(x) - } - fit$varnames <- varnames - class(fit) <- "L0Learn" - fit$n <- dim(x)[1] - fit$p <- dim(x)[2] - G <- list(fit=fit, cvMeans=M$CVMeans,cvSDs=M$CVSDs) - class(G) <- "L0LearnCV" - G -} diff --git a/R/fit.R b/R/fit.R deleted file mode 100644 index c728298..0000000 --- a/R/fit.R +++ /dev/null @@ -1,313 +0,0 @@ -# import C++ compiled code -#' @useDynLib L0Learn -#' @importFrom Rcpp evalCpp -#' @importFrom methods as -#' @importFrom methods is -#' @import Matrix - -#' @title Fit an L0-regularized model -#' -#' @description Computes the regularization path for the specified loss function and -#' penalty function (which can be a combination of the L0, L1, and L2 norms). -#' @param x The data matrix. -#' @param y The response vector. For classification, we only support binary vectors. -#' @param loss The loss function. Currently we support the choices "SquaredError" (for regression), "Logistic" (for logistic regression), and "SquaredHinge" (for smooth SVM). -#' @param penalty The type of regularization. This can take either one of the following choices: -#' "L0", "L0L2", and "L0L1". -#' @param algorithm The type of algorithm used to minimize the objective function. Currently "CD" and "CDPSI" are -#' are supported. "CD" is a variant of cyclic coordinate descent and runs very fast. "CDPSI" performs -#' local combinatorial search on top of CD and typically achieves higher quality solutions (at the expense -#' of increased running time). -#' @param maxSuppSize The maximum support size at which to terminate the regularization path. We recommend setting -#' this to a small fraction of min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically selects a small -#' portion of non-zeros. -#' @param nLambda The number of Lambda values to select (recall that Lambda is the regularization parameter -#' corresponding to the L0 norm). This value is ignored if 'lambdaGrid' is supplied. -#' @param nGamma The number of Gamma values to select (recall that Gamma is the regularization parameter -#' corresponding to L1 or L2, depending on the chosen penalty). This value is ignored if 'lambdaGrid' is supplied -#' and will be set to length(lambdaGrid) -#' @param gammaMax The maximum value of Gamma when using the L0L2 penalty. For the L0L1 penalty this is -#' automatically selected. -#' @param gammaMin The minimum value of Gamma when using the L0L2 penalty. For the L0L1 penalty, the minimum -#' value of gamma in the grid is set to gammaMin * gammaMax. Note that this should be a strictly positive quantity. -#' @param partialSort If TRUE partial sorting will be used for sorting the coordinates to do greedy cycling (see our paper for -#' for details). Otherwise, full sorting is used. -#' @param maxIters The maximum number of iterations (full cycles) for CD per grid point. -#' @param rtol The relative tolerance which decides when to terminate optimization (based on the relative change in the objective between iterations). -#' @param atol The absolute tolerance which decides when to terminate optimization (based on the absolute L2 norm of the residuals). -#' @param activeSet If TRUE, performs active set updates. -#' @param activeSetNum The number of consecutive times a support should appear before declaring support stabilization. -#' @param maxSwaps The maximum number of swaps used by CDPSI for each grid point. -#' @param scaleDownFactor This parameter decides how close the selected Lambda values are. The choice should be -#' strictly between 0 and 1 (i.e., 0 and 1 are not allowed). Larger values lead to closer lambdas and typically to smaller -#' gaps between the support sizes. For details, see our paper - Section 5 on Adaptive Selection of Tuning Parameters). -#' @param screenSize The number of coordinates to cycle over when performing initial correlation screening. -#' @param autoLambda Ignored parameter. Kept for backwards compatibility. -#' @param lambdaGrid A grid of Lambda values to use in computing the regularization path. This is by default an empty list and is ignored. -#' When specified, LambdaGrid should be a list of length 'nGamma', where the ith element (corresponding to the ith gamma) should be a decreasing sequence of lambda values -#' which are used by the algorithm when fitting for the ith value of gamma (see the vignette for details). -#' @param excludeFirstK This parameter takes non-negative integers. The first excludeFirstK features in x will be excluded from variable selection, -#' i.e., the first excludeFirstK variables will not be included in the L0-norm penalty (they will still be included in the L1 or L2 norm penalties.). -#' @param intercept If FALSE, no intercept term is included in the model. -#' @param lows Lower bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of size p (number of columns of X) where lows[i] is the lower bound for coefficient i. -#' @param highs Upper bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of size p (number of columns of X) where highs[i] is the upper bound for coefficient i. -#' @return An S3 object of type "L0Learn" describing the regularization path. The object has the following members. -#' \item{a0}{a0 is a list of intercept sequences. The ith element of the list (i.e., a0[[i]]) is the sequence of intercepts corresponding to the ith gamma value (i.e., gamma[i]).} -#' \item{beta}{This is a list of coefficient matrices. The ith element of the list is a p x \code{length(lambda)} matrix which -#' corresponds to the ith gamma value. The jth column in each coefficient matrix is the vector of coefficients for the jth lambda value.} -#' \item{lambda}{This is the list of lambda sequences used in fitting the model. The ith element of lambda (i.e., lambda[[i]]) is the sequence -#' of Lambda values corresponding to the ith gamma value.} -#' \item{gamma}{This is the sequence of gamma values used in fitting the model.} -#' \item{suppSize}{This is a list of support size sequences. The ith element of the list is a sequence of support sizes (i.e., number of non-zero coefficients) -#' corresponding to the ith gamma value.} -#' \item{converged}{This is a list of sequences for checking whether the algorithm has converged at every grid point. The ith element of the list is a sequence -#' corresponding to the ith value of gamma, where the jth element in each sequence indicates whether the algorithm has converged at the jth value of lambda.} -#' -#' @examples -#' # Generate synthetic data for this example -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' -#' # Fit an L0 regression model with a maximum of 50 non-zeros using coordinate descent (CD) -#' fit1 <- L0Learn.fit(X, y, penalty="L0", maxSuppSize=50) -#' print(fit1) -#' # Extract the coefficients at lambda = 0.0325142 -#' coef(fit1, lambda=0.0325142) -#' # Apply the fitted model on X to predict the response -#' predict(fit1, newx = X, lambda=0.0325142) -#' -#' # Fit an L0 regression model with a maximum of 50 non-zeros using CD and local search -#' fit2 <- L0Learn.fit(X, y, penalty="L0", algorithm="CDPSI", maxSuppSize=50) -#' print(fit2) -#' -#' # Fit an L0L2 regression model with 10 values of Gamma ranging from 0.0001 to 10, using CD -#' fit3 <- L0Learn.fit(X, y, penalty="L0L2", maxSuppSize=50, nGamma=10, gammaMin=0.0001, gammaMax = 10) -#' print(fit3) -#' # Extract the coefficients at lambda = 0.0361829 and gamma = 0.0001 -#' coef(fit3, lambda=0.0361829, gamma=0.0001) -#' # Apply the fitted model on X to predict the response -#' predict(fit3, newx = X, lambda=0.0361829, gamma=0.0001) -#' -#' # Fit an L0 logistic regression model -#' # First, convert the response to binary -#' y = sign(y) -#' fit4 <- L0Learn.fit(X, y, loss="Logistic", maxSuppSize=20) -#' print(fit4) -#' -#' @export -L0Learn.fit <- function(x, y, loss="SquaredError", penalty="L0", algorithm="CD", - maxSuppSize=100, nLambda=100, nGamma=10, gammaMax=10, - gammaMin=0.0001, partialSort = TRUE, maxIters=200, - rtol=1e-6, atol=1e-9, activeSet=TRUE, activeSetNum=3, maxSwaps=100, - scaleDownFactor=0.8, screenSize=1000, autoLambda = NULL, - lambdaGrid = list(), excludeFirstK=0, intercept = TRUE, - lows=-Inf, highs=Inf) { - - if ((rtol < 0) || (rtol >= 1)){ - stop("The specified rtol parameter must exist in [0, 1)") - } - - if (atol < 0){ - stop("The specified atol parameter must exist in [0, INF)") - } - - # Some sanity checks for the inputs - if ( !(loss %in% c("SquaredError","Logistic","SquaredHinge")) ){ - stop("The specified loss function is not supported.") - } - if ( !(penalty %in% c("L0","L0L2","L0L1")) ){ - stop("The specified penalty is not supported.") - } - if ( !(algorithm %in% c("CD","CDPSI")) ){ - stop("The specified algorithm is not supported.") - } - if (loss=="Logistic" | loss=="SquaredHinge"){ - if (dim(table(y)) != 2){ - stop("Only binary classification is supported. Make sure y has only 2 unique values.") - } - y = factor(y,labels=c(-1,1)) # returns a vector of strings - y = as.numeric(levels(y))[y] - - if (penalty == "L0"){ - # Pure L0 is not supported for classification - # Below we add a small L2 component. - - if ((length(lambdaGrid) != 0) && (length(lambdaGrid) != 1)){ - # If this error checking was left to the lower section, it would confuse users as - # we are converting L0 to L0L2 with small L2 penalty. - # Here we must check if lambdaGrid is supplied (And thus use 'autolambda') - # If 'lambdaGrid' is supplied, we must only supply 1 list of lambda values - - - stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. - Where lambdaGrid[[1]] is a list or vector of decreasing positive values.") - } - penalty = "L0L2" - nGamma = 1 - gammaMax = 1e-7 - gammaMin = 1e-7 - } - } - - # Handle Lambda Grids: - if (length(lambdaGrid) != 0){ - if (!is.null(autoLambda)){ - warning("In L0Learn V2+, autoLambda is ignored and inferred if 'lambdaGrid' is supplied", call.=FALSE) - } - autoLambda = FALSE - } else { - autoLambda = TRUE - lambdaGrid = list(0) - } - - if (penalty == "L0" && !autoLambda){ - bad_lambdaGrid = FALSE - if (length(lambdaGrid) != 1){ - bad_lambdaGrid = TRUE - } - current = Inf - for (nxt in lambdaGrid[[1]]){ - if (nxt > current){ - # This must be > instead of >= to allow first iteration L0L1 lambdas of all 0s to be valid - bad_lambdaGrid = TRUE - break - } - if (nxt < 0){ - bad_lambdaGrid = TRUE - break - } - current = nxt - - } - - if (bad_lambdaGrid){ - stop("L0 Penalty requires 'lambdaGrid' to be a list of length 1. - Where lambdaGrid[[1]] is a list or vector of decreasing positive values.") - } - } - - if (penalty != "L0" && !autoLambda){ - # Covers L0L1, L0L2 cases - bad_lambdaGrid = FALSE - if (length(lambdaGrid) != nGamma){ - warning("In L0Learn V2+, nGamma is ignored and replaced with length(lambdaGrid)", call.=FALSE) - nGamma = length(lambdaGrid) - # bad_lambdaGrid = TRUE # Remove in V2.0,0 - } - - for (i in 1:length(lambdaGrid)){ - current = Inf - for (nxt in lambdaGrid[[i]]){ - if (nxt > current){ - # This must be > instead of >= to allow first iteration L0L1 lambdas of all 0s to be valid - bad_lambdaGrid = TRUE - break - } - if (nxt < 0){ - bad_lambdaGrid = TRUE - break - } - current = nxt - } - if (bad_lambdaGrid){ - break - } - } - - if (bad_lambdaGrid){ - stop("L0L1 or L0L2 Penalty requires 'lambdaGrid' to be a list of length 'nGamma'. - Where lambdaGrid[[i]] is a list or vector of decreasing positive values.") - } - - - } - - is.scalar <- function(x) is.atomic(x) && length(x) == 1L && !is.character(x) && Im(x)==0 && !is.nan(x) && !is.na(x) - - p = dim(x)[[2]] - - withBounds = FALSE - - if ((!identical(lows, -Inf)) || (!identical(highs, Inf))){ - withBounds = TRUE - - if (algorithm == "CDPSI"){ - if (any(lows != -Inf) || any(highs != Inf)){ - stop("Bounds are not YET supported for CDPSI algorithm. Please raise - an issue at 'https://github.com/hazimehh/L0Learn' to express - interest in this functionality") - } - } - - if (is.scalar(lows)){ - lows = lows*rep(1, p) - } else if (!all(sapply(lows, is.scalar)) || length(lows) != p) { - stop('Lows must be a vector of real values of length p') - } - - if (is.scalar(highs)){ - highs = highs*rep(1, p) - } else if (!all(sapply(highs, is.scalar)) || length(highs) != p) { - stop('Highs must be a vector of real values of length p') - } - - if (any(lows >= highs) || any(lows > 0) || any(highs < 0)){ - stop("Bounds must conform to the following conditions: Lows <= 0, Highs >= 0, Lows < Highs") - } - - } - - M = list() - if (is(x, "sparseMatrix")){ - M <- .Call('_L0Learn_L0LearnFit_sparse', PACKAGE = 'L0Learn', x, y, loss, penalty, - algorithm, maxSuppSize, nLambda, nGamma, gammaMax, gammaMin, - partialSort, maxIters, rtol, atol, activeSet, activeSetNum, maxSwaps, - scaleDownFactor, screenSize, !autoLambda, lambdaGrid, - excludeFirstK, intercept, withBounds, lows, highs) - } else{ - M <- .Call('_L0Learn_L0LearnFit_dense', PACKAGE = 'L0Learn', x, y, loss, penalty, - algorithm, maxSuppSize, nLambda, nGamma, gammaMax, gammaMin, - partialSort, maxIters, rtol, atol, activeSet, activeSetNum, maxSwaps, - scaleDownFactor, screenSize, !autoLambda, lambdaGrid, - excludeFirstK, intercept, withBounds, lows, highs) - } - - # The C++ function uses LambdaU = 1 for user-specified grid. In R, we use autoLambda0 = 0 for user-specified grid (thus the negation when passing the parameter to the function below) - - settings = list() - settings[[1]] = intercept # Settings only contains intercept for now. Might include additional elements later. - names(settings) <- c("intercept") - - # Find potential support sizes exceeding maxSuppSize and remove them (this is due to - # the C++ core whose last solution can exceed maxSuppSize - for (i in 1:length(M$SuppSize)){ - last = length(M$SuppSize[[i]]) - if (M$SuppSize[[i]][last] > maxSuppSize){ - if (last == 1){ - warning("Warning! Only 1 element in path with support size > maxSuppSize. \n Try increasing maxSuppSize to resolve the issue.") - } else{ - M$SuppSize[[i]] = M$SuppSize[[i]][-last] - M$Converged[[i]] = M$Converged[[i]][-last] - M$lambda[[i]] = M$lambda[[i]][-last] - M$a0[[i]] = M$a0[[i]][-last] - M$beta[[i]] = as(M$beta[[i]][,-last], "sparseMatrix") # conversion to sparseMatrix is necessary to handle the case of a single column - } - } - } - - G <- list(beta = M$beta, lambda=lapply(M$lambda,signif, digits=6), a0=M$a0, converged = M$Converged, suppSize= M$SuppSize, gamma=M$gamma, penalty=penalty, loss=loss, settings = settings) - - - if (is.null(colnames(x))){ - varnames <- 1:dim(x)[2] - } else { - varnames <- colnames(x) - } - G$varnames <- varnames - - class(G) <- "L0Learn" - G$n <- dim(x)[1] - G$p <- dim(x)[2] - G -} diff --git a/R/genhighcorr.R b/R/genhighcorr.R deleted file mode 100644 index 74c2e4d..0000000 --- a/R/genhighcorr.R +++ /dev/null @@ -1,60 +0,0 @@ -#' @importFrom stats rnorm var -#' @importFrom MASS mvrnorm -#' @importFrom Rcpp cppFunction -#' @title Generate Expoentential Correlated Synthetic Data -#' -#' @description Generates a synthetic dataset as follows: 1) Generate a correlation matrix, SIG, where item [i, j] = A^|i-j|. -#' 2) Draw from a Multivariate Normal Distribution using (mu and SIG) to generate X. 3) Generate a vector B with every ~p/k entry set to 1 and the rest are zeros. -#' 4) Sample every element in the noise vector e from N(0,1). 4) Set y = XB + b0 + e. -#' @param n Number of samples -#' @param p Number of features -#' @param k Number of non-zeros in true vector of coefficients -#' @param seed The seed used for randomly generating the data -#' @param rho The threshold for setting values to 0. if |X(i, j)| > rho => X(i, j) <- 0 -#' @param b0 intercept value to scale y by. -#' @param snr desired Signal-to-Noise ratio. This sets the magnitude of the error term 'e'. -#' SNR is defined as SNR = Var(XB)/Var(e) -#' @param mu The mean for drawing from the Multivariate Normal Distribution. A scalar of vector of length p. -#' @param base_cor The base correlation, A in [i, j] = A^|i-j|. -#' @return A list containing: -#' the data matrix X, -#' the response vector y, -#' the coefficients B, -#' the error vector e, -#' the intercept term b0. -#' @examples -#' data <- L0Learn:::GenSyntheticHighCorr(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -GenSyntheticHighCorr <- function(n, p, k, seed, rho=0, b0=0, snr=1, mu=0, base_cor=.9) -{ - set.seed(seed) # fix the seed to get a reproducible result - cor <- .Call("_L0Learn_cor_matrix", p, base_cor) - - if (length(mu) == 1){ - mu = rep(mu, p) - } - - X <- mvrnorm(n, mu, forceSymmetric(cor)) - - X[abs(X) < rho] <- 0. - - B_indices = seq(from=1, to=p, by=as.integer(p/k)) - - B = rep(0, p) - - for (i in B_indices){ - B[i] = 1 - } - - sd_e = NULL - if (snr == +Inf){ - sd_e = 0 - } else { - sd_e = sqrt(var(X %*% B)/snr) - } - e = rnorm(n, sd = sd_e) - y = X%*%B + e + b0 - list(X=X, y = y, B=B, e=e, b0=b0) -} - diff --git a/R/genlogistic.R b/R/genlogistic.R deleted file mode 100644 index b8607b8..0000000 --- a/R/genlogistic.R +++ /dev/null @@ -1,57 +0,0 @@ -#' @importFrom stats rnorm rbinom -#' @importFrom MASS mvrnorm -#' @title Generate Logistic Synthetic Data -#' -#' @description Generates a synthetic dataset as follows: 1) Generate a data matrix, -#' X, drawn from a multivariate Gaussian distribution with mean = 0, sigma = Sigma -#' 2) Generate a vector B with k entries set to 1 and the rest are zeros. -#' 3) Every coordinate yi of the outcome vector y exists in {-1, 1}^n is sampled -#' independently from a Bernoulli distribution with success probability: -#' P(yi = 1|xi) = 1/(1 + exp(-s)) -#' Source https://arxiv.org/pdf/2001.06471.pdf Section 5.1 Data Generation -#' @param n Number of samples -#' @param p Number of features -#' @param k Number of non-zeros in true vector of coefficients -#' @param seed The seed used for randomly generating the data -#' @param rho The threshold for setting values to 0. if |X(i, j)| > rho => X(i, j) <- 0 -#' @param s Signal-to-noise parameter. As s -> +Inf, the data generated becomes linearly separable. -#' @param sigma Correlation matrix, defaults to I. -#' @param shuffle_B A boolean flag for whether or not to randomly shuffle the Beta vector, B. -#' If FALSE, the first k entries in B are set to 1. -#' @return A list containing: -#' the data matrix X, -#' the response vector y, -#' the coefficients B, -#' @examples -#' data <- L0Learn:::GenSyntheticLogistic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -GenSyntheticLogistic <- function(n, p, k, seed, rho=0, s=1, sigma=NULL, shuffle_B=FALSE) -{ - if (s < 0){ - stop("s must be fall in the interval [0, +Inf)") - } - - X = NULL - set.seed(seed) - - if (is.null(sigma)){ - X = matrix(rnorm(n*p), n, p) - } else { - if ((ncol(sigma) != p) || (nrow(sigma) != p)){ - stop("sigma must be a semi positive definite matrix of side length p") - } - X = mvrnorm(n, mu=rep(0, p), Sigma=sigma) - } - - X[abs(X) < rho] <- 0. - B = c(rep(1,k),rep(0,p-k)) - - if (shuffle_B){ - B = sample(B) - } - - y = rbinom(n, 1, 1/(1 + exp(-s*X%*%B))) - - return(list(X=X, B=B, y=y, s=s)) -} diff --git a/R/gensynthetic.R b/R/gensynthetic.R deleted file mode 100644 index f5ecd04..0000000 --- a/R/gensynthetic.R +++ /dev/null @@ -1,41 +0,0 @@ -#' @importFrom stats rnorm var -#' @title Generate Synthetic Data -#' -#' @description Generates a synthetic dataset as follows: 1) Sample every element in data matrix X from N(0,1). -#' 2) Generate a vector B with the first k entries set to 1 and the rest are zeros. 3) Sample every element in the noise -#' vector e from N(0,1). 4) Set y = XB + b0 + e. -#' @param n Number of samples -#' @param p Number of features -#' @param k Number of non-zeros in true vector of coefficients -#' @param seed The seed used for randomly generating the data -#' @param rho The threshold for setting values to 0. if |X(i, j)| > rho => X(i, j) <- 0 -#' @param b0 intercept value to translate y by. -#' @param snr desired Signal-to-Noise ratio. This sets the magnitude of the error term 'e'. -#' SNR is defined as SNR = Var(XB)/Var(e) -#' @return A list containing: -#' the data matrix X, -#' the response vector y, -#' the coefficients B, -#' the error vector e, -#' the intercept term b0. -#' @examples -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' @export -GenSynthetic <- function(n, p, k, seed, rho=0, b0=0, snr=1) -{ - set.seed(seed) # fix the seed to get a reproducible result - X = matrix(rnorm(n*p),nrow=n,ncol=p) - X[abs(X) < rho] <- 0. - B = c(rep(1,k),rep(0,p-k)) - sd_e = NULL - if (snr == +Inf){ - sd_e = sqrt(var(X %*% B)/snr) - } else { - sd_e = 0 - } - e = rnorm(n, sd = sd_e) - y = X%*%B + e + b0 - list(X=X, y=y, B=B, e=e, b0=b0) -} diff --git a/man/GenSynthetic.Rd b/R/man/GenSynthetic.Rd similarity index 79% rename from man/GenSynthetic.Rd rename to R/man/GenSynthetic.Rd index 1c6a2cb..20a31db 100644 --- a/man/GenSynthetic.Rd +++ b/R/man/GenSynthetic.Rd @@ -31,9 +31,11 @@ A list containing: the intercept term b0. } \description{ -Generates a synthetic dataset as follows: 1) Sample every element in data matrix X from N(0,1). -2) Generate a vector B with the first k entries set to 1 and the rest are zeros. 3) Sample every element in the noise -vector e from N(0,1). 4) Set y = XB + b0 + e. +Generates a synthetic dataset as follows: +1) Sample every element in data matrix X from N(0,1). +2) Generate a vector B with the first k entries set to 1 and the rest are zeros. +3) Sample every element in the noise vector e from N(0,A) where A is selected so y, X, B have snr as specified. +4) Set y = XB + b0 + e. } \examples{ data <- GenSynthetic(n=500,p=1000,k=10,seed=1) diff --git a/man/GenSyntheticHighCorr.Rd b/R/man/GenSyntheticHighCorr.Rd similarity index 100% rename from man/GenSyntheticHighCorr.Rd rename to R/man/GenSyntheticHighCorr.Rd diff --git a/man/GenSyntheticLogistic.Rd b/R/man/GenSyntheticLogistic.Rd similarity index 94% rename from man/GenSyntheticLogistic.Rd rename to R/man/GenSyntheticLogistic.Rd index 16767ae..c05e798 100644 --- a/man/GenSyntheticLogistic.Rd +++ b/R/man/GenSyntheticLogistic.Rd @@ -43,7 +43,7 @@ A list containing: Generates a synthetic dataset as follows: 1) Generate a data matrix, X, drawn from a multivariate Gaussian distribution with mean = 0, sigma = Sigma 2) Generate a vector B with k entries set to 1 and the rest are zeros. -3) Every coordinate yi of the outcome vector y exists in {-1, 1}^n is sampled +3) Every coordinate yi of the outcome vector y exists in {0, 1}^n is sampled independently from a Bernoulli distribution with success probability: P(yi = 1|xi) = 1/(1 + exp(-s)) Source https://arxiv.org/pdf/2001.06471.pdf Section 5.1 Data Generation diff --git a/man/L0Learn-package.Rd b/R/man/L0Learn-package.Rd similarity index 100% rename from man/L0Learn-package.Rd rename to R/man/L0Learn-package.Rd diff --git a/man/L0Learn.cvfit.Rd b/R/man/L0Learn.cvfit.Rd similarity index 100% rename from man/L0Learn.cvfit.Rd rename to R/man/L0Learn.cvfit.Rd diff --git a/man/L0Learn.fit.Rd b/R/man/L0Learn.fit.Rd similarity index 100% rename from man/L0Learn.fit.Rd rename to R/man/L0Learn.fit.Rd diff --git a/man/coef.L0Learn.Rd b/R/man/coef.L0Learn.Rd similarity index 100% rename from man/coef.L0Learn.Rd rename to R/man/coef.L0Learn.Rd diff --git a/man/plot.L0Learn.Rd b/R/man/plot.L0Learn.Rd similarity index 100% rename from man/plot.L0Learn.Rd rename to R/man/plot.L0Learn.Rd diff --git a/man/plot.L0LearnCV.Rd b/R/man/plot.L0LearnCV.Rd similarity index 100% rename from man/plot.L0LearnCV.Rd rename to R/man/plot.L0LearnCV.Rd diff --git a/man/predict.L0Learn.Rd b/R/man/predict.L0Learn.Rd similarity index 100% rename from man/predict.L0Learn.Rd rename to R/man/predict.L0Learn.Rd diff --git a/man/print.L0Learn.Rd b/R/man/print.L0Learn.Rd similarity index 100% rename from man/print.L0Learn.Rd rename to R/man/print.L0Learn.Rd diff --git a/R/plot.R b/R/plot.R deleted file mode 100644 index e5f1b27..0000000 --- a/R/plot.R +++ /dev/null @@ -1,84 +0,0 @@ -#' @title Plot Regularization Path -#' -#' @description Plots the regularization path for a given gamma. -#' @param gamma The value of gamma at which to plot. -#' @param x The output of L0Learn.fit -#' @param showLines If TRUE, the lines connecting the points in the plot are shown. -#' @param ... ignore -#' -#' @examples -#' # Generate synthetic data for this example -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' # Fit an L0 Model with a maximum of 50 non-zeros -#' fit <- L0Learn.fit(X, y, penalty="L0", maxSuppSize=50) -#' plot(fit, gamma=0) -#' -#' @import ggplot2 -#' @importFrom reshape2 melt -#' @method plot L0Learn -#' @export -plot.L0Learn <- function(x, gamma=0, showLines=FALSE, ...) -{ - j = which(abs(x$gamma-gamma)==min(abs(x$gamma-gamma))) - p = x$p - allin = c() # contains all the non-zero variables in the path - for (i in 1:length(x$lambda[[j]])){ - BetaTemp = x$beta[[j]][,i] - supp = which(as.matrix(BetaTemp != 0)) - allin = c(allin, supp) - } - allin = unique(allin) - - #ggplot needs a dataframe - yy = t(as.matrix(x$beta[[j]][allin,])) # length(lambda) x length(allin) matrix - data <- as.data.frame(yy) - - colnames(data) = x$varnames[allin] - - #id variable for position in matrix - data$id <- x$suppSize[[j]] - - #reshape to long format - plot_data <- melt(data,id.var="id") - - #breaks = x$suppSize[[j]] - - #plot - plotObject = ggplot(plot_data, aes_string(x="id",y="value",group="variable",colour="variable")) + geom_point(size=2.5) + - labs(x = "Support Size", y = "Coefficient") + theme(axis.title=element_text(size=14)) # + scale_x_continuous(breaks = breaks) + theme(axis.text = element_text(size = 12)) - - if (showLines == TRUE){ - plotObject = plotObject + geom_line(aes_string(lty="variable"),alpha=0.3) - } - plotObject -} - -#' @title Plot Cross-validation Errors -#' -#' @description Plots cross-validation errors for a given gamma. -#' @param x The output of L0Learn.cvfit -#' @inheritParams plot.L0Learn -#' @examples -#' # Generate synthetic data for this example -#' data <- GenSynthetic(n=500,p=1000,k=10,seed=1) -#' X = data$X -#' y = data$y -#' -#' # Perform 5-fold cross-validation on an L0L2 Model with 5 values of -#' # Gamma ranging from 0.0001 to 10 -#' fit <- L0Learn.cvfit(X, y, nFolds=5, seed=1, penalty="L0L2", -#' maxSuppSize=20, nGamma=5, gammaMin=0.0001, gammaMax = 10) -#' # Plot the graph of cross-validation error versus lambda for gamma = 0.0001 -#' plot(fit, gamma=0.0001) -#' -#' @method plot L0LearnCV -#' @export -plot.L0LearnCV <- function(x, gamma=0, ...) -{ - j = which(abs(x$fit$gamma-gamma)==min(abs(x$fit$gamma-gamma))) - data = data.frame(x=x$fit$suppSize[[j]], y=x$cvMeans[[j]], sd=x$cvSDs[[j]]) - ggplot(data, aes_string(x="x",y="y")) + geom_point() + geom_errorbar(aes_string(ymin="y-sd", ymax="y+sd"))+ - labs(x = "Support Size", y = "Cross-validation Error") + theme(axis.title=element_text(size=14)) + theme(axis.text = element_text(size = 12)) -} diff --git a/R/print.R b/R/print.R deleted file mode 100644 index 5b2af64..0000000 --- a/R/print.R +++ /dev/null @@ -1,20 +0,0 @@ -#' @title Print L0Learn.fit object -#' -#' @description Prints a summary of L0Learn.fit -#' @param x The output of L0Learn.fit or L0Learn.cvfit -#' @param ... ignore -#' @method print L0Learn -#' @export -print.L0Learn <- function(x, ...) -{ - gammas = rep(x$gamma, times=lapply(x$lambda, length) ) - data.frame(lambda = unlist(x["lambda"]), gamma = gammas, suppSize = unlist(x["suppSize"]), row.names = NULL) -} - -#' @rdname print.L0Learn -#' @method print L0LearnCV -#' @export -print.L0LearnCV <- function(x, ...) -{ - print.L0Learn(x$fit) -} diff --git a/R/src/Makevars b/R/src/Makevars new file mode 100644 index 0000000..351f3e0 --- /dev/null +++ b/R/src/Makevars @@ -0,0 +1,16 @@ +CXX_STD = CXX11 +PKG_CXXFLAGS = -Iinclude -Isrc/include +PKG_LIBS= $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) + +SOURCES = RcppExports.cpp \ + R_L0Learn_Interface.cpp \ + Test_Interface.cpp \ + src/CDL012LogisticSwaps.cpp \ + src/CDL012SquaredHingeSwaps.cpp \ + src/CDL012Swaps.cpp \ + src/Grid.cpp \ + src/Grid1D.cpp \ + src/Grid2D.cpp \ + src/Normalize.cpp + +OBJECTS = $(SOURCES:.cpp=.o) diff --git a/R/src/Makevars.in b/R/src/Makevars.in new file mode 100644 index 0000000..2a62abf --- /dev/null +++ b/R/src/Makevars.in @@ -0,0 +1,16 @@ +CXX_STD = CXX11 +PKG_CXXFLAGS = -Iinclude -Isrc/include @OPENMP_FLAG@ +PKG_LIBS= @OPENMP_FLAG@ $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) + +SOURCES = RcppExports.cpp \ + R_L0Learn_Interface.cpp \ + Test_Interface.cpp \ + src/CDL012LogisticSwaps.cpp \ + src/CDL012SquaredHingeSwaps.cpp \ + src/CDL012Swaps.cpp \ + src/Grid.cpp \ + src/Grid1D.cpp \ + src/Grid2D.cpp \ + src/Normalize.cpp + +OBJECTS = $(SOURCES:.cpp=.o) \ No newline at end of file diff --git a/R/src/Makevars.win b/R/src/Makevars.win new file mode 100644 index 0000000..ff893d0 --- /dev/null +++ b/R/src/Makevars.win @@ -0,0 +1,16 @@ +CXX_STD = CXX11 +PKG_CXXFLAGS = -Iinclude -Isrc/include $(SHLIB_OPENMP_CXXFLAGS) +PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS) $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) + +SOURCES = RcppExports.cpp \ + R_L0Learn_Interface.cpp \ + Test_Interface.cpp \ + src/CDL012LogisticSwaps.cpp \ + src/CDL012SquaredHingeSwaps.cpp \ + src/CDL012Swaps.cpp \ + src/Grid.cpp \ + src/Grid1D.cpp \ + src/Grid2D.cpp \ + src/Normalize.cpp + +OBJECTS = $(SOURCES:.cpp=.o) diff --git a/R/src/R_L0Learn_Interface.cpp b/R/src/R_L0Learn_Interface.cpp new file mode 100644 index 0000000..62b9178 --- /dev/null +++ b/R/src/R_L0Learn_Interface.cpp @@ -0,0 +1,114 @@ +#include "R_L0Learn_Interface.h" + +// [[Rcpp::export]] +Rcpp::NumericMatrix cor_matrix(const int p, const double base_cor) { + Rcpp::NumericMatrix cor(p, p); + for (int i = 0; i < p; i++) { + for (int j = 0; j < p; j++) { + cor(i, j) = std::pow(base_cor, std::abs(i - j)); + } + } + return cor; +} + +// [[Rcpp::export]] +Rcpp::List L0LearnFit_sparse( + const arma::sp_mat &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> Lambdas, + const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + fitmodel l = L0LearnFit( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, + Intercept, withBounds, Lows, Highs); + + return Rcpp::List::create( + Rcpp::Named("lambda") = l.Lambda0, Rcpp::Named("gamma") = l.Lambda12, + Rcpp::Named("SuppSize") = l.NnzCount, Rcpp::Named("beta") = l.Beta, + Rcpp::Named("a0") = l.Intercept, Rcpp::Named("Converged") = l.Converged); +} + +// [[Rcpp::export]] +Rcpp::List L0LearnFit_dense( + const arma::mat &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> Lambdas, + const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + fitmodel l = L0LearnFit( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, + Intercept, withBounds, Lows, Highs); + + return Rcpp::List::create( + Rcpp::Named("lambda") = l.Lambda0, Rcpp::Named("gamma") = l.Lambda12, + Rcpp::Named("SuppSize") = l.NnzCount, Rcpp::Named("beta") = l.Beta, + Rcpp::Named("a0") = l.Intercept, Rcpp::Named("Converged") = l.Converged); +} + +// [[Rcpp::export]] +Rcpp::List L0LearnCV_sparse( + const arma::sp_mat &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> Lambdas, const std::size_t nfolds, + const double seed, const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + cvfitmodel l = L0LearnCV( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, nfolds, seed, + ExcludeFirstK, Intercept, withBounds, Lows, Highs); + + return Rcpp::List::create( + Rcpp::Named("lambda") = l.Lambda0, Rcpp::Named("gamma") = l.Lambda12, + Rcpp::Named("SuppSize") = l.NnzCount, Rcpp::Named("beta") = l.Beta, + Rcpp::Named("a0") = l.Intercept, Rcpp::Named("Converged") = l.Converged, + Rcpp::Named("CVMeans") = l.CVMeans, Rcpp::Named("CVSDs") = l.CVSDs); +} + +// [[Rcpp::export]] +Rcpp::List L0LearnCV_dense( + const arma::mat &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> Lambdas, const std::size_t nfolds, + const double seed, const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + cvfitmodel l = L0LearnCV( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, nfolds, seed, + ExcludeFirstK, Intercept, withBounds, Lows, Highs); + + return Rcpp::List::create( + Rcpp::Named("lambda") = l.Lambda0, Rcpp::Named("gamma") = l.Lambda12, + Rcpp::Named("SuppSize") = l.NnzCount, Rcpp::Named("beta") = l.Beta, + Rcpp::Named("a0") = l.Intercept, Rcpp::Named("Converged") = l.Converged, + Rcpp::Named("CVMeans") = l.CVMeans, Rcpp::Named("CVSDs") = l.CVSDs); +} \ No newline at end of file diff --git a/src/RcppExports.cpp b/R/src/RcppExports.cpp similarity index 94% rename from src/RcppExports.cpp rename to R/src/RcppExports.cpp index d9470a7..fe65527 100644 --- a/src/RcppExports.cpp +++ b/R/src/RcppExports.cpp @@ -6,8 +6,25 @@ using namespace Rcpp; +#ifdef RCPP_USE_GLOBAL_ROSTREAM +Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); +Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); +#endif + +// cor_matrix +Rcpp::NumericMatrix cor_matrix(const int p, const double base_cor); +RcppExport SEXP _L0Learn_cor_matrix(SEXP pSEXP, SEXP base_corSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< const int >::type p(pSEXP); + Rcpp::traits::input_parameter< const double >::type base_cor(base_corSEXP); + rcpp_result_gen = Rcpp::wrap(cor_matrix(p, base_cor)); + return rcpp_result_gen; +END_RCPP +} // L0LearnFit_sparse -Rcpp::List L0LearnFit_sparse(const arma::sp_mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector< std::vector > Lambdas, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); +Rcpp::List L0LearnFit_sparse(const arma::sp_mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector> Lambdas, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); RcppExport SEXP _L0Learn_L0LearnFit_sparse(SEXP XSEXP, SEXP ySEXP, SEXP LossSEXP, SEXP PenaltySEXP, SEXP AlgorithmSEXP, SEXP NnzStopNumSEXP, SEXP G_ncolsSEXP, SEXP G_nrowsSEXP, SEXP Lambda2MaxSEXP, SEXP Lambda2MinSEXP, SEXP PartialSortSEXP, SEXP MaxItersSEXP, SEXP rtolSEXP, SEXP atolSEXP, SEXP ActiveSetSEXP, SEXP ActiveSetNumSEXP, SEXP MaxNumSwapsSEXP, SEXP ScaleDownFactorSEXP, SEXP ScreenSizeSEXP, SEXP LambdaUSEXP, SEXP LambdasSEXP, SEXP ExcludeFirstKSEXP, SEXP InterceptSEXP, SEXP withBoundsSEXP, SEXP LowsSEXP, SEXP HighsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -32,7 +49,7 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const double >::type ScaleDownFactor(ScaleDownFactorSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ScreenSize(ScreenSizeSEXP); Rcpp::traits::input_parameter< const bool >::type LambdaU(LambdaUSEXP); - Rcpp::traits::input_parameter< const std::vector< std::vector > >::type Lambdas(LambdasSEXP); + Rcpp::traits::input_parameter< const std::vector> >::type Lambdas(LambdasSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ExcludeFirstK(ExcludeFirstKSEXP); Rcpp::traits::input_parameter< const bool >::type Intercept(InterceptSEXP); Rcpp::traits::input_parameter< const bool >::type withBounds(withBoundsSEXP); @@ -43,7 +60,7 @@ BEGIN_RCPP END_RCPP } // L0LearnFit_dense -Rcpp::List L0LearnFit_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector< std::vector > Lambdas, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); +Rcpp::List L0LearnFit_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector> Lambdas, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); RcppExport SEXP _L0Learn_L0LearnFit_dense(SEXP XSEXP, SEXP ySEXP, SEXP LossSEXP, SEXP PenaltySEXP, SEXP AlgorithmSEXP, SEXP NnzStopNumSEXP, SEXP G_ncolsSEXP, SEXP G_nrowsSEXP, SEXP Lambda2MaxSEXP, SEXP Lambda2MinSEXP, SEXP PartialSortSEXP, SEXP MaxItersSEXP, SEXP rtolSEXP, SEXP atolSEXP, SEXP ActiveSetSEXP, SEXP ActiveSetNumSEXP, SEXP MaxNumSwapsSEXP, SEXP ScaleDownFactorSEXP, SEXP ScreenSizeSEXP, SEXP LambdaUSEXP, SEXP LambdasSEXP, SEXP ExcludeFirstKSEXP, SEXP InterceptSEXP, SEXP withBoundsSEXP, SEXP LowsSEXP, SEXP HighsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -68,7 +85,7 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const double >::type ScaleDownFactor(ScaleDownFactorSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ScreenSize(ScreenSizeSEXP); Rcpp::traits::input_parameter< const bool >::type LambdaU(LambdaUSEXP); - Rcpp::traits::input_parameter< const std::vector< std::vector > >::type Lambdas(LambdasSEXP); + Rcpp::traits::input_parameter< const std::vector> >::type Lambdas(LambdasSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ExcludeFirstK(ExcludeFirstKSEXP); Rcpp::traits::input_parameter< const bool >::type Intercept(InterceptSEXP); Rcpp::traits::input_parameter< const bool >::type withBounds(withBoundsSEXP); @@ -79,7 +96,7 @@ BEGIN_RCPP END_RCPP } // L0LearnCV_sparse -Rcpp::List L0LearnCV_sparse(const arma::sp_mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector< std::vector > Lambdas, const std::size_t nfolds, const double seed, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); +Rcpp::List L0LearnCV_sparse(const arma::sp_mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector> Lambdas, const std::size_t nfolds, const double seed, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); RcppExport SEXP _L0Learn_L0LearnCV_sparse(SEXP XSEXP, SEXP ySEXP, SEXP LossSEXP, SEXP PenaltySEXP, SEXP AlgorithmSEXP, SEXP NnzStopNumSEXP, SEXP G_ncolsSEXP, SEXP G_nrowsSEXP, SEXP Lambda2MaxSEXP, SEXP Lambda2MinSEXP, SEXP PartialSortSEXP, SEXP MaxItersSEXP, SEXP rtolSEXP, SEXP atolSEXP, SEXP ActiveSetSEXP, SEXP ActiveSetNumSEXP, SEXP MaxNumSwapsSEXP, SEXP ScaleDownFactorSEXP, SEXP ScreenSizeSEXP, SEXP LambdaUSEXP, SEXP LambdasSEXP, SEXP nfoldsSEXP, SEXP seedSEXP, SEXP ExcludeFirstKSEXP, SEXP InterceptSEXP, SEXP withBoundsSEXP, SEXP LowsSEXP, SEXP HighsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -104,7 +121,7 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const double >::type ScaleDownFactor(ScaleDownFactorSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ScreenSize(ScreenSizeSEXP); Rcpp::traits::input_parameter< const bool >::type LambdaU(LambdaUSEXP); - Rcpp::traits::input_parameter< const std::vector< std::vector > >::type Lambdas(LambdasSEXP); + Rcpp::traits::input_parameter< const std::vector> >::type Lambdas(LambdasSEXP); Rcpp::traits::input_parameter< const std::size_t >::type nfolds(nfoldsSEXP); Rcpp::traits::input_parameter< const double >::type seed(seedSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ExcludeFirstK(ExcludeFirstKSEXP); @@ -117,7 +134,7 @@ BEGIN_RCPP END_RCPP } // L0LearnCV_dense -Rcpp::List L0LearnCV_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector< std::vector > Lambdas, const std::size_t nfolds, const double seed, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); +Rcpp::List L0LearnCV_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, const std::vector> Lambdas, const std::size_t nfolds, const double seed, const std::size_t ExcludeFirstK, const bool Intercept, const bool withBounds, const arma::vec& Lows, const arma::vec& Highs); RcppExport SEXP _L0Learn_L0LearnCV_dense(SEXP XSEXP, SEXP ySEXP, SEXP LossSEXP, SEXP PenaltySEXP, SEXP AlgorithmSEXP, SEXP NnzStopNumSEXP, SEXP G_ncolsSEXP, SEXP G_nrowsSEXP, SEXP Lambda2MaxSEXP, SEXP Lambda2MinSEXP, SEXP PartialSortSEXP, SEXP MaxItersSEXP, SEXP rtolSEXP, SEXP atolSEXP, SEXP ActiveSetSEXP, SEXP ActiveSetNumSEXP, SEXP MaxNumSwapsSEXP, SEXP ScaleDownFactorSEXP, SEXP ScreenSizeSEXP, SEXP LambdaUSEXP, SEXP LambdasSEXP, SEXP nfoldsSEXP, SEXP seedSEXP, SEXP ExcludeFirstKSEXP, SEXP InterceptSEXP, SEXP withBoundsSEXP, SEXP LowsSEXP, SEXP HighsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -142,7 +159,7 @@ BEGIN_RCPP Rcpp::traits::input_parameter< const double >::type ScaleDownFactor(ScaleDownFactorSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ScreenSize(ScreenSizeSEXP); Rcpp::traits::input_parameter< const bool >::type LambdaU(LambdaUSEXP); - Rcpp::traits::input_parameter< const std::vector< std::vector > >::type Lambdas(LambdasSEXP); + Rcpp::traits::input_parameter< const std::vector> >::type Lambdas(LambdasSEXP); Rcpp::traits::input_parameter< const std::size_t >::type nfolds(nfoldsSEXP); Rcpp::traits::input_parameter< const double >::type seed(seedSEXP); Rcpp::traits::input_parameter< const std::size_t >::type ExcludeFirstK(ExcludeFirstKSEXP); @@ -154,18 +171,6 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } -// cor_matrix -Rcpp::NumericMatrix cor_matrix(const int p, const double base_cor); -RcppExport SEXP _L0Learn_cor_matrix(SEXP pSEXP, SEXP base_corSEXP) { -BEGIN_RCPP - Rcpp::RObject rcpp_result_gen; - Rcpp::RNGScope rcpp_rngScope_gen; - Rcpp::traits::input_parameter< const int >::type p(pSEXP); - Rcpp::traits::input_parameter< const double >::type base_cor(base_corSEXP); - rcpp_result_gen = Rcpp::wrap(cor_matrix(p, base_cor)); - return rcpp_result_gen; -END_RCPP -} // R_matrix_column_get_dense arma::vec R_matrix_column_get_dense(const arma::mat& mat, int col); RcppExport SEXP _L0Learn_R_matrix_column_get_dense(SEXP matSEXP, SEXP colSEXP) { @@ -386,11 +391,11 @@ END_RCPP } static const R_CallMethodDef CallEntries[] = { + {"_L0Learn_cor_matrix", (DL_FUNC) &_L0Learn_cor_matrix, 2}, {"_L0Learn_L0LearnFit_sparse", (DL_FUNC) &_L0Learn_L0LearnFit_sparse, 26}, {"_L0Learn_L0LearnFit_dense", (DL_FUNC) &_L0Learn_L0LearnFit_dense, 26}, {"_L0Learn_L0LearnCV_sparse", (DL_FUNC) &_L0Learn_L0LearnCV_sparse, 28}, {"_L0Learn_L0LearnCV_dense", (DL_FUNC) &_L0Learn_L0LearnCV_dense, 28}, - {"_L0Learn_cor_matrix", (DL_FUNC) &_L0Learn_cor_matrix, 2}, {"_L0Learn_R_matrix_column_get_dense", (DL_FUNC) &_L0Learn_R_matrix_column_get_dense, 2}, {"_L0Learn_R_matrix_column_get_sparse", (DL_FUNC) &_L0Learn_R_matrix_column_get_sparse, 2}, {"_L0Learn_R_matrix_rows_get_dense", (DL_FUNC) &_L0Learn_R_matrix_rows_get_dense, 2}, diff --git a/R/src/Test_Interface.cpp b/R/src/Test_Interface.cpp new file mode 100644 index 0000000..35789b5 --- /dev/null +++ b/R/src/Test_Interface.cpp @@ -0,0 +1,111 @@ +#include "Test_Interface.h" +// [[Rcpp::depends(RcppArmadillo)]] + +// [[Rcpp::export]] +arma::vec R_matrix_column_get_dense(const arma::mat &mat, int col) { + return matrix_column_get(mat, col); +} + +// [[Rcpp::export]] +arma::vec R_matrix_column_get_sparse(const arma::sp_mat &mat, int col) { + return matrix_column_get(mat, col); +} + +// [[Rcpp::export]] +arma::mat R_matrix_rows_get_dense(const arma::mat &mat, + const arma::ucolvec rows) { + return matrix_rows_get(mat, rows); +} + +// [[Rcpp::export]] +arma::sp_mat R_matrix_rows_get_sparse(const arma::sp_mat &mat, + const arma::ucolvec rows) { + return matrix_rows_get(mat, rows); +} + +// [[Rcpp::export]] +arma::mat R_matrix_vector_schur_product_dense(const arma::mat &mat, + const arma::vec &u) { + return matrix_vector_schur_product(mat, &u); +} + +// [[Rcpp::export]] +arma::sp_mat R_matrix_vector_schur_product_sparse(const arma::sp_mat &mat, + const arma::vec &u) { + return matrix_vector_schur_product(mat, &u); +} + +// [[Rcpp::export]] +arma::mat R_matrix_vector_divide_dense(const arma::mat &mat, + const arma::vec &u) { + return matrix_vector_divide(mat, u); +} + +// [[Rcpp::export]] +arma::sp_mat R_matrix_vector_divide_sparse(const arma::sp_mat &mat, + const arma::vec &u) { + return matrix_vector_divide(mat, u); +} + +// [[Rcpp::export]] +arma::rowvec R_matrix_column_sums_dense(const arma::mat &mat) { + return matrix_column_sums(mat); +} + +// [[Rcpp::export]] +arma::rowvec R_matrix_column_sums_sparse(const arma::sp_mat &mat) { + return matrix_column_sums(mat); +} + +// [[Rcpp::export]] +double R_matrix_column_dot_dense(const arma::mat &mat, int col, + const arma::vec u) { + return matrix_column_dot(mat, col, u); +} + +// [[Rcpp::export]] +double R_matrix_column_dot_sparse(const arma::sp_mat &mat, int col, + const arma::vec u) { + return matrix_column_dot(mat, col, u); +} + +// [[Rcpp::export]] +arma::vec R_matrix_column_mult_dense(const arma::mat &mat, int col, double u) { + return matrix_column_mult(mat, col, u); +} + +// [[Rcpp::export]] +arma::vec R_matrix_column_mult_sparse(const arma::sp_mat &mat, int col, + double u) { + return matrix_column_mult(mat, col, u); +} + +// [[Rcpp::export]] +Rcpp::List R_matrix_normalize_dense(arma::mat mat_norm) { + arma::rowvec ScaleX = matrix_normalize(mat_norm); + return Rcpp::List::create(Rcpp::Named("mat_norm") = mat_norm, + Rcpp::Named("ScaleX") = ScaleX); +}; + +// [[Rcpp::export]] +Rcpp::List R_matrix_normalize_sparse(arma::sp_mat mat_norm) { + arma::rowvec ScaleX = matrix_normalize(mat_norm); + return Rcpp::List::create(Rcpp::Named("mat_norm") = mat_norm, + Rcpp::Named("ScaleX") = ScaleX); +}; + +// [[Rcpp::export]] +Rcpp::List R_matrix_center_dense(const arma::mat mat, arma::mat X_normalized, + bool intercept) { + arma::rowvec meanX = matrix_center(mat, X_normalized, intercept); + return Rcpp::List::create(Rcpp::Named("mat_norm") = X_normalized, + Rcpp::Named("MeanX") = meanX); +}; + +// [[Rcpp::export]] +Rcpp::List R_matrix_center_sparse(const arma::sp_mat mat, + arma::sp_mat X_normalized, bool intercept) { + arma::rowvec meanX = matrix_center(mat, X_normalized, intercept); + return Rcpp::List::create(Rcpp::Named("mat_norm") = X_normalized, + Rcpp::Named("MeanX") = meanX); +}; diff --git a/R/src/include/R_L0Learn_Interface.h b/R/src/include/R_L0Learn_Interface.h new file mode 100644 index 0000000..fe7a785 --- /dev/null +++ b/R/src/include/R_L0Learn_Interface.h @@ -0,0 +1,13 @@ +// [[Rcpp::depends(RcppArmadillo)]] +#ifndef R_L0LEARN_INTERFACE_H +#define R_L0LEARN_INTERFACE_H + +#include +#include +#include +#include "RcppArmadillo.h" +#include "L0LearnCore.h" +#include +#include + +#endif // R_L0LEARN_INTERFACE_H diff --git a/src/include/Test_Interface.h b/R/src/include/Test_Interface.h similarity index 67% rename from src/include/Test_Interface.h rename to R/src/include/Test_Interface.h index 788aa7a..ae550de 100644 --- a/src/include/Test_Interface.h +++ b/R/src/include/Test_Interface.h @@ -2,7 +2,9 @@ #define R_TEST_INTERFACE_H #include +#include "RcppArmadillo.h" #include "utils.h" #include "BetaVector.h" -#endif //R_TEST_INTERFACE_H \ No newline at end of file + +#endif //R_TEST_INTERFACE_H \ No newline at end of file diff --git a/R/src/src/CDL012LogisticSwaps.cpp b/R/src/src/CDL012LogisticSwaps.cpp new file mode 100644 index 0000000..cef8c40 --- /dev/null +++ b/R/src/src/CDL012LogisticSwaps.cpp @@ -0,0 +1,182 @@ +#include "CDL012LogisticSwaps.h" + +template +CDL012LogisticSwaps::CDL012LogisticSwaps(const T &Xi, const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = + (LipschitzConst + twolambda2); // this is the univariate lipschitz const + // of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); + lambda1ol = this->lambda1 / qp2lamda2; + Xy = Pi.Xy; +} + +template FitResult CDL012LogisticSwaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template FitResult CDL012LogisticSwaps::_Fit() { + auto result = CDL012Logistic(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->b0 = result.b0; // Initialize from previous later....! + this->B = result.B; + ExpyXB = result.ExpyXB; // Maintained throughout the algorithm + + double objective = result.Objective; + double Fmin = objective; + std::size_t maxindex; + double Bmaxindex; + + this->P.Init = 'u'; + + bool foundbetter = false; + bool foundbetter_i = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + // TODO: Add shuffle of Order + // std::shuffle(std::begin(Order), std::end(Order), engine); + + foundbetter = false; + + // TODO: Check if this should be Templated Operation + arma::mat ExpyXBnojs = arma::zeros(this->n, NnzIndices.size()); + + int j_index = -1; + for (auto &j : NnzIndices) { + // Remove NnzIndices[j] + ++j_index; + ExpyXBnojs.col(j_index) = + ExpyXB % + arma::exp(-this->B.at(j) * matrix_column_get(*(this->Xy), j)); + } + arma::mat gradients = -1 / (1 + ExpyXBnojs).t() * *Xy; + arma::mat abs_gradients = arma::abs(gradients); + + j_index = -1; + for (auto &j : NnzIndices) { + // Set B[j] = 0 + ++j_index; + arma::vec ExpyXBnoj = ExpyXBnojs.col(j_index); + arma::rowvec gradient = gradients.row(j_index); + arma::rowvec abs_gradient = abs_gradients.row(j_index); + + arma::uvec indices = arma::sort_index(arma::abs(gradient), "descend"); + foundbetter_i = false; + + // TODO: make sure this scans at least 100 coordinates from outside supp + // (now it does not) + for (std::size_t ll = 0; ll < std::min(50, (int)this->p); ++ll) { + std::size_t i = indices(ll); + + if (this->B[i] == 0 && i >= this->NoSelectK) { + // Do not swap B[i] if i between 0 and NoSelectK; + + arma::vec ExpyXBnoji = ExpyXBnoj; + + double Biold = 0; + double partial_i = gradient[i]; + bool converged = false; + + beta_vector Btemp = this->B; + Btemp[j] = 0; + double ObjTemp = Objective(ExpyXBnoji, Btemp); + std::size_t innerindex = 0; + + double x = Biold - partial_i / qp2lamda2; + double z = std::abs(x) - lambda1ol; + double Binew = std::copysign(z, x); + // double Binew = clamp(std::copysign(z, x), this->Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + + while (!converged && innerindex < 10 && + ObjTemp >= Fmin) { // ObjTemp >= Fmin + ExpyXBnoji %= + arma::exp((Binew - Biold) * matrix_column_get(*Xy, i)); + // partial_i = - arma::sum( matrix_column_get(*Xy, i) / (1 + + // ExpyXBnoji) ) + twolambda2 * Binew; + partial_i = + -arma::dot(matrix_column_get(*Xy, i), 1 / (1 + ExpyXBnoji)) + + twolambda2 * Binew; + + if (std::abs((Binew - Biold) / Biold) < 0.0001) { + converged = true; + // std::cout<<"swaps converged!!!"<Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + innerindex += 1; + } + + Btemp[i] = Binew; + ObjTemp = Objective(ExpyXBnoji, Btemp); + + if (ObjTemp >= Fmin) { + ExpyXBnoji %= arma::exp( (Binew - Biold) * matrix_column_get(*Xy, i)); + Btemp[i] = Binew; + ObjTemp = Objective(ExpyXBnoji, Btemp); + } else { + Binew = 0; + Fmin = ObjTemp; + maxindex = i; + Bmaxindex = Binew; + foundbetter_i = true; + } + + // Can be made much faster (later) + Btemp[i] = Binew; + } + + if (foundbetter_i) { + this->B[j] = 0; + this->B[maxindex] = Bmaxindex; + this->P.InitialSol = &(this->B); + + // TODO: Check if this line is necessary. P should already have b0. + this->P.b0 = this->b0; + + result = CDL012Logistic(*(this->X), this->y, this->P).Fit(); + + ExpyXB = result.ExpyXB; + this->B = result.B; + this->b0 = result.b0; + objective = result.Objective; + Fmin = objective; + foundbetter = true; + break; + } + } + + // auto end2 = std::chrono::high_resolution_clock::now(); + // std::cout<<"restricted: + // "<(end2-start2).count() + // << " ms " << std::endl; + + if (foundbetter) { + break; + } + } + + if (!foundbetter) { + // Early exit to prevent looping + return result; + } + } + + // result.Model = this; + return result; +} + +template class CDL012LogisticSwaps; +template class CDL012LogisticSwaps; diff --git a/R/src/src/CDL012SquaredHingeSwaps.cpp b/R/src/src/CDL012SquaredHingeSwaps.cpp new file mode 100644 index 0000000..5011461 --- /dev/null +++ b/R/src/src/CDL012SquaredHingeSwaps.cpp @@ -0,0 +1,145 @@ +#include "CDL012SquaredHingeSwaps.h" + +template +CDL012SquaredHingeSwaps::CDL012SquaredHingeSwaps(const T &Xi, + const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = + (LipschitzConst + twolambda2); // this is the univariate lipschitz + // constant of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); + lambda1ol = this->lambda1 / qp2lamda2; +} + +template FitResult CDL012SquaredHingeSwaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template FitResult CDL012SquaredHingeSwaps::_Fit() { + auto result = CDL012SquaredHinge(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->b0 = result.b0; // Initialize from previous later....! + this->B = result.B; + + arma::vec onemyxb = result.onemyxb; + + this->objective = result.Objective; + double Fmin = this->objective; + std::size_t maxindex; + double Bmaxindex; + + this->P.Init = 'u'; + + bool foundbetter = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + // Rcpp::Rcout << "Swap Number: " << t << "|mean(onemyxb): " << + // arma::mean(onemyxb) << "\n"; + + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + // TODO: Implement shuffle of NnzIndices Indicies + + foundbetter = false; + + for (auto &j : NnzIndices) { + arma::vec onemyxbnoj = + onemyxb + this->B[j] * this->y % matrix_column_get(*(this->X), j); + arma::uvec indices = arma::find(onemyxbnoj > 0); + + for (std::size_t i = 0; i < this->p; ++i) { + if (this->B[i] == 0 && i >= this->NoSelectK) { + double Biold = 0; + double Binew; + + double partial_i = + arma::sum(2 * onemyxbnoj.elem(indices) % + (-(this->y.elem(indices) % + matrix_column_get(*(this->X), i).elem(indices)))); + + bool converged = false; + if (std::abs(partial_i) >= this->lambda1 + stl0Lc) { + // std::cout<<"Adding: "<B; + Btemp[j] = 0; + // double ObjTemp = Objective(onemyxbnoj,Btemp); + // double Biolddescent = 0; + while (!converged) { + double x = Biold - partial_i / qp2lamda2; + double z = std::abs(x) - lambda1ol; + Binew = std::copysign(z, x); + + // Binew = clamp(std::copysign(z, x), this->Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + onemyxbnoji += + (Biold - Binew) * this->y % matrix_column_get(*(this->X), i); + + arma::uvec indicesi = arma::find(onemyxbnoji > 0); + partial_i = + arma::sum(2 * onemyxbnoji.elem(indicesi) % + (-this->y.elem(indicesi) % + matrix_column_get(*(this->X), i).elem(indicesi))); + + if (std::abs((Binew - Biold) / Biold) < 0.0001) { + converged = true; + } + + Biold = Binew; + l += 1; + } + + Btemp[i] = Binew; + double Fnew = Objective(onemyxbnoji, Btemp); + + if (Fnew < Fmin) { + Fmin = Fnew; + maxindex = i; + Bmaxindex = Binew; + } + } + } + } + + if (Fmin < this->objective) { + this->B[j] = 0; + this->B[maxindex] = Bmaxindex; + + this->P.InitialSol = &(this->B); + + // TODO: Check if this line is needed. P should already have b0. + this->P.b0 = this->b0; + + result = CDL012SquaredHinge(*(this->X), this->y, this->P).Fit(); + + this->B = result.B; + this->b0 = result.b0; + + onemyxb = result.onemyxb; + this->objective = result.Objective; + Fmin = this->objective; + foundbetter = true; + break; + } + if (foundbetter) { + break; + } + } + + if (!foundbetter) { + return result; + } + } + + return result; +} + +template class CDL012SquaredHingeSwaps; +template class CDL012SquaredHingeSwaps; diff --git a/R/src/src/CDL012Swaps.cpp b/R/src/src/CDL012Swaps.cpp new file mode 100644 index 0000000..b482208 --- /dev/null +++ b/R/src/src/CDL012Swaps.cpp @@ -0,0 +1,108 @@ +#include "CDL012Swaps.h" + +template +CDL012Swaps::CDL012Swaps(const T &Xi, const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) {} + +template FitResult CDL012Swaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template FitResult CDL012Swaps::_Fit() { + auto result = CDL012(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->B = result.B; + this->b0 = result.b0; + double objective = result.Objective; + this->P.Init = 'u'; + + bool foundbetter = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + foundbetter = false; + + // TODO: shuffle NNz Indices to prevent bias. + // std::shuffle(std::begin(Order), std::end(Order), engine); + + // TODO: This calculation is already preformed in a previous step + // Can be pulled/stored + arma::vec r = this->y - *(this->X) * this->B - this->b0; + + for (auto &i : NnzIndices) { + arma::rowvec riX = + (r + this->B[i] * matrix_column_get(*(this->X), i)).t() * *(this->X); + + double maxcorr = -1; + std::size_t maxindex = -1; + + for (std::size_t j = this->NoSelectK; j < this->p; ++j) { + // TODO: Account for bounds when determining best swap + // Loops through each column and finds the column with the highest + // correlation to residuals In non-constrained cases, the highest + // correlation will always be the best option However, if bounds + // restrict the value of B[j], it is possible that swapping column 'i' + // and column 'j' might be rejected as B[j], when constrained, is not + // able to take a value with sufficient magnitude to utilize the + // correlation. Therefore, we must ensure that 'j' was not already + // rejected. + if (std::fabs(riX[j]) > maxcorr && this->B[j] == 0) { + maxcorr = std::fabs(riX[j]); + maxindex = j; + } + } + + // Check if the correlation is sufficiently large to make up for + // regularization + if (maxcorr > (1 + 2 * this->ModelParams[2]) * std::fabs(this->B[i]) + + this->ModelParams[1]) { + // Rcpp::Rcout << t << ": Proposing Swap " << i << " => NNZ and " << + // maxindex << " => 0 \n"; Proposed new Swap Value (without considering + // bounds are solvable in closed form) Must be clamped to bounds + + this->B[i] = 0; + + // Bi with No Bounds (nb); + double Bi_nb = (riX[maxindex] - + std::copysign(this->ModelParams[1], riX[maxindex])) / + (1 + 2 * this->ModelParams[2]); + // double Bi_wb = clamp(Bi_nb, this->Lows[maxindex], + // this->Highs[maxindex]); // Bi With Bounds (wb) + this->B[maxindex] = Bi_nb; + + // Change initial solution to Swapped value to seed standard CD + // algorithm. + this->P.InitialSol = &(this->B); + *this->P.r = this->y - *(this->X) * (this->B) - this->b0; + // this->P already has access to b0. + + // proposed_result object. + // Keep tack of previous_best result object + // Only override previous_best if proposed_result has a better + // objective. + result = CDL012(*(this->X), this->y, this->P).Fit(); + + // Rcpp::Rcout << "Swap Objective " << result.Objective << " \n"; + // Rcpp::Rcout << "Old Objective " << objective << " \n"; + this->B = result.B; + objective = result.Objective; + foundbetter = true; + break; + } + } + + if (!foundbetter) { + // Early exit to prevent looping + return result; + } + } + + return result; +} + +template class CDL012Swaps; +template class CDL012Swaps; diff --git a/R/src/src/Grid.cpp b/R/src/src/Grid.cpp new file mode 100644 index 0000000..58e805f --- /dev/null +++ b/R/src/src/Grid.cpp @@ -0,0 +1,67 @@ +#include "Grid.h" + +// Assumes PG.P.Specs have been already set +template +Grid::Grid(const T &X, const arma::vec &y, const GridParams &PGi) { + PG = PGi; + + std::tie(BetaMultiplier, meanX, meany, scaley) = Normalize( + X, y, Xscaled, yscaled, !PG.P.Specs.Classification, PG.intercept); + + // Must rescale bounds by BetaMultiplier in order for final result to conform + // to bounds + if (PG.P.withBounds) { + PG.P.Lows /= BetaMultiplier; + PG.P.Highs /= BetaMultiplier; + } +} + +template void Grid::Fit() { + + std::vector>>> G; + + if (PG.P.Specs.L0) { + G.push_back(std::move(Grid1D(Xscaled, yscaled, PG).Fit())); + Lambda12.push_back(0); + } else { + G = std::move(Grid2D(Xscaled, yscaled, PG).Fit()); + } + + Lambda0 = std::vector>(G.size()); + NnzCount = std::vector>(G.size()); + Solutions = std::vector>(G.size()); + Intercepts = std::vector>(G.size()); + Converged = std::vector>(G.size()); + + for (std::size_t i = 0; i < G.size(); ++i) { + if (PG.P.Specs.L0L1) { + Lambda12.push_back(G[i][0]->ModelParams[1]); + } else if (PG.P.Specs.L0L2) { + Lambda12.push_back(G[i][0]->ModelParams[2]); + } + + for (auto &g : G[i]) { + Lambda0[i].push_back(g->ModelParams[0]); + + NnzCount[i].push_back(n_nonzero(g->B)); + + Converged[i].push_back(g->IterNum != PG.P.MaxIters); + + beta_vector B_unscaled; + double b0; + + std::tie(B_unscaled, b0) = + DeNormalize(g->B, BetaMultiplier, meanX, meany); + Solutions[i].push_back(arma::sp_mat(B_unscaled)); + /* scaley is 1 for classification problems. + * g->intercept is 0 unless specifically optimized for in: + * classification + * sparse regression and intercept = true + */ + Intercepts[i].push_back(scaley * g->b0 + b0); + } + } +} + +template class Grid; +template class Grid; diff --git a/R/src/src/Grid1D.cpp b/R/src/src/Grid1D.cpp new file mode 100644 index 0000000..ca04e44 --- /dev/null +++ b/R/src/src/Grid1D.cpp @@ -0,0 +1,248 @@ +#include "Grid1D.h" + +template +Grid1D::Grid1D(const T &Xi, const arma::vec &yi, const GridParams &PG) { + // automatically selects lambda_0 (but assumes other lambdas are given in + // PG.P.ModelParams) + + X = Ξ + y = &yi; + p = Xi.n_cols; + LambdaMinFactor = PG.LambdaMinFactor; + ScaleDownFactor = PG.ScaleDownFactor; + P = PG.P; + P.Xtr = new std::vector(X->n_cols); // needed! careful + P.ytX = new arma::rowvec(X->n_cols); + P.D = new std::map(); + P.r = new arma::vec(Xi.n_rows); + Xtr = P.Xtr; + ytX = P.ytX; + NoSelectK = P.NoSelectK; + + LambdaU = PG.LambdaU; + + if (!LambdaU) { + G_ncols = PG.G_ncols; + } else { + G_ncols = PG.Lambdas.n_rows; // override the user's ncols if LambdaU = 1 + } + + G.reserve(G_ncols); + if (LambdaU) { + Lambdas = PG.Lambdas; + } // user-defined lambda0 grid + /* + else { + Lambdas.reserve(G_ncols); + Lambdas.push_back((0.5*arma::square(y->t() * *X)).max()); + } + */ + NnzStopNum = PG.NnzStopNum; + PartialSort = PG.PartialSort; + XtrAvailable = PG.XtrAvailable; + if (XtrAvailable) { + ytXmax2d = PG.ytXmax; + Xtr = PG.Xtr; + } +} + +template Grid1D::~Grid1D() { + // delete all dynamically allocated memory + delete P.Xtr; + delete P.ytX; + delete P.D; + delete P.r; +} + +template std::vector>> Grid1D::Fit() { + + if (P.Specs.L0 || P.Specs.L0L2 || P.Specs.L0L1) { + bool scaledown = false; + + double Lipconst; + arma::vec Xtrarma; + if (P.Specs.Logistic) { + if (!XtrAvailable) { + Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); + } // = gradient of logistic loss at zero} + Lipconst = 0.25 + 2 * P.ModelParams[2]; + } else if (P.Specs.SquaredHinge) { + if (!XtrAvailable) { + // gradient of loss function at zero + Xtrarma = 2 * arma::abs(y->t() * *X).t(); + } + Lipconst = 2 + 2 * P.ModelParams[2]; + } else { + if (!XtrAvailable) { + *ytX = y->t() * *X; + Xtrarma = arma::abs(*ytX).t(); // Least squares + } + Lipconst = 1 + 2 * P.ModelParams[2]; + *P.r = *y - P.b0; // B = 0 initially + } + + double ytXmax; + if (!XtrAvailable) { + *Xtr = arma::conv_to>::from(Xtrarma); + ytXmax = arma::max(Xtrarma); + } else { + ytXmax = ytXmax2d; + } + + double lambdamax = + ((ytXmax - P.ModelParams[1]) * (ytXmax - P.ModelParams[1])) / + (2 * (Lipconst)); + + // Rcpp::Rcout << "lambdamax: " << lambdamax << "\n"; + + if (!LambdaU) { + P.ModelParams[0] = lambdamax; + } else { + P.ModelParams[0] = Lambdas[0]; + } + + // Rcpp::Rcout << "P ModelParams: {" << P.ModelParams[0] << ", " << + // P.ModelParams[1] << ", " << P.ModelParams[2] << ", " << P.ModelParams[3] + // << "}\n"; + + P.Init = 'z'; + + // std::cout<< "Lambda max: "<< lambdamax << std::endl; + // double lambdamin = lambdamax*LambdaMinFactor; + // Lambdas = arma::logspace(std::log10(lambdamin), std::log10(lambdamax), + // G_ncols); Lambdas = arma::flipud(Lambdas); + + // std::size_t StopNum = (X->n_rows < NnzStopNum) ? X->n_rows : NnzStopNum; + std::size_t StopNum = NnzStopNum; + // std::vector* Xtr = P.Xtr; + std::vector idx(p); + double Xrmax; + bool prevskip = false; // previous grid point was skipped + bool currentskip = false; // current grid point should be skipped + + for (std::size_t i = 0; i < G_ncols; ++i) { + UserInterrupt(); + // Rcpp::Rcout << "Grid1D: " << i << "\n"; + FitResult *prevresult = + new FitResult; // prevresult is ptr to the prev result object + // std::unique_ptr prevresult; + if (i > 0) { + // prevresult = std::move(G.back()); + *prevresult = *(G.back()); + } + + currentskip = false; + + if (!prevskip) { + + std::iota(idx.begin(), idx.end(), 0); // make global class var later + // Exclude the first NoSelectK features from sorting. + if (PartialSort && p > 5000 + NoSelectK) + std::partial_sort(idx.begin() + NoSelectK, + idx.begin() + 5000 + NoSelectK, idx.end(), + [this](std::size_t i1, std::size_t i2) { + return (*Xtr)[i1] > (*Xtr)[i2]; + }); + else + std::sort(idx.begin() + NoSelectK, idx.end(), + [this](std::size_t i1, std::size_t i2) { + return (*Xtr)[i1] > (*Xtr)[i2]; + }); + P.CyclingOrder = 'u'; + P.Uorder = idx; // can be made faster + + // + Xrmax = (*Xtr)[idx[NoSelectK]]; + + if (i > 0) { + std::vector Sp = nnzIndicies(prevresult->B); + + for (std::size_t l = NoSelectK; l < p; ++l) { + if (std::binary_search(Sp.begin(), Sp.end(), idx[l]) == false) { + Xrmax = (*Xtr)[idx[l]]; + // std::cout<<"Grid Iteration: "<> result(new FitResult); + *result = Model->Fit(); + + delete Model; + + scaledown = false; + if (i >= 1) { + std::vector Spold = nnzIndicies(prevresult->B); + + std::vector Spnew = nnzIndicies(result->B); + + bool samesupp = false; + + if (Spold == Spnew) { + samesupp = true; + scaledown = true; + } + + // // + // + // if (samesupp) { + // scaledown = true; + // } // got same solution + } + + // else {scaledown = false;} + G.push_back(std::move(result)); + + if (n_nonzero(G.back()->B) >= StopNum) { + break; + } + // result->B.t().print(); + P.InitialSol = &(G.back()->B); + P.b0 = G.back()->b0; + // Udate: After 1.1.0, P.r is automatically updated by the previous call + // to CD + //*P.r = G.back()->r; + } + + delete prevresult; + + P.Init = 'u'; + P.Iter += 1; + prevskip = currentskip; + } + } + + return std::move(G); +} + +template class Grid1D; +template class Grid1D; diff --git a/R/src/src/Grid2D.cpp b/R/src/src/Grid2D.cpp new file mode 100644 index 0000000..463f9aa --- /dev/null +++ b/R/src/src/Grid2D.cpp @@ -0,0 +1,131 @@ +#include "Grid2D.h" + +template +Grid2D::Grid2D(const T &Xi, const arma::vec &yi, const GridParams &PGi) { + // automatically selects lambda_0 (but assumes other lambdas are given in + // PG.P.ModelParams) + X = Ξ + y = &yi; + p = Xi.n_cols; + PG = PGi; + G_nrows = PG.G_nrows; + G_ncols = PG.G_ncols; + G.reserve(G_nrows); + Lambda2Max = PG.Lambda2Max; + Lambda2Min = PG.Lambda2Min; + LambdaMinFactor = PG.LambdaMinFactor; + + P = PG.P; +} + +template Grid2D::~Grid2D() { + delete Xtr; + if (PG.P.Specs.Logistic) + delete PG.P.Xy; + if (PG.P.Specs.SquaredHinge) + delete PG.P.Xy; +} + +template +std::vector>>> Grid2D::Fit() { + arma::vec Xtrarma; + + if (PG.P.Specs.Logistic) { + auto n = X->n_rows; + double b0 = 0; + arma::vec ExpyXB = arma::ones(n); + if (PG.intercept) { + for (std::size_t t = 0; t < 50; ++t) { + double partial_b0 = -arma::sum(*y / (1 + ExpyXB)); + b0 -= partial_b0 / (n * 0.25); // intercept is not regularized + ExpyXB = arma::exp(b0 * *y); + } + } + PG.P.b0 = b0; + Xtrarma = arma::abs(-arma::trans(*y / (1 + ExpyXB)) * *X) + .t(); // = gradient of logistic loss at zero + // Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); // = gradient of logistic + // loss at zero + + T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; + + PG.P.Xy = new T; + *PG.P.Xy = Xy; + } + + else if (PG.P.Specs.SquaredHinge) { + auto n = X->n_rows; + double b0 = 0; + arma::vec onemyxb = arma::ones(n); + arma::uvec indices = arma::find(onemyxb > 0); + if (PG.intercept) { + for (std::size_t t = 0; t < 50; ++t) { + double partial_b0 = + arma::sum(2 * onemyxb.elem(indices) % (-y->elem(indices))); + b0 -= partial_b0 / (n * 2); // intercept is not regularized + onemyxb = 1 - (*y * b0); + indices = arma::find(onemyxb > 0); + } + } + PG.P.b0 = b0; + T indices_rows = matrix_rows_get(*X, indices); + Xtrarma = + 2 * arma::abs(arma::trans(y->elem(indices) % onemyxb.elem(indices)) * + indices_rows) + .t(); // = gradient of loss function at zero + // Xtrarma = 2 * arma::abs(y->t() * *X).t(); // = gradient of loss function + // at zero + T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; + PG.P.Xy = new T; + *PG.P.Xy = Xy; + } else { + Xtrarma = arma::abs(y->t() * *X).t(); + } + + double ytXmax = arma::max(Xtrarma); + + std::size_t index; + if (PG.P.Specs.L0L1) { + index = 1; + if (G_nrows != 1) { + Lambda2Max = ytXmax; + Lambda2Min = Lambda2Max * LambdaMinFactor; + } + } else if (PG.P.Specs.L0L2) { + index = 2; + } + + arma::vec Lambdas2 = + arma::logspace(std::log10(Lambda2Min), std::log10(Lambda2Max), G_nrows); + Lambdas2 = arma::flipud(Lambdas2); + + std::vector Xtrvec = + arma::conv_to>::from(Xtrarma); + + Xtr = new std::vector(X->n_cols); // needed! careful + + PG.XtrAvailable = true; + // Rcpp::Rcout << "Grid2D Start\n"; + for (std::size_t i = 0; i < Lambdas2.size(); ++i) { // auto &l : Lambdas2 + // Rcpp::Rcout << "Grid1D Start: " << i << "\n"; + *Xtr = Xtrvec; + + PG.Xtr = Xtr; + PG.ytXmax = ytXmax; + + PG.P.ModelParams[index] = Lambdas2[i]; + + if (PG.LambdaU == true) + PG.Lambdas = PG.LambdasGrid[i]; + + // std::vector> Gl(); + // auto Gl = Grid1D(*X, *y, PG).Fit(); + // Rcpp::Rcout << "Grid1D Start: " << i << "\n"; + G.push_back(std::move(Grid1D(*X, *y, PG).Fit())); + } + + return std::move(G); +} + +template class Grid2D; +template class Grid2D; diff --git a/R/src/src/Normalize.cpp b/R/src/src/Normalize.cpp new file mode 100644 index 0000000..e6f313f --- /dev/null +++ b/R/src/src/Normalize.cpp @@ -0,0 +1,14 @@ +#include "Normalize.h" + +std::tuple DeNormalize(beta_vector &B_scaled, + arma::vec &BetaMultiplier, + arma::vec &meanX, double meany) { + beta_vector B_unscaled = B_scaled % BetaMultiplier; + double intercept = meany - arma::dot(B_unscaled, meanX); + // Matrix Type, Intercept + // Dense, True -> meanX = colMeans(X) + // Dense, False -> meanX = 0 Vector (meany = 0) + // Sparse, True -> meanX = 0 Vector + // Sparse, False -> meanX = 0 Vector + return std::make_tuple(B_unscaled, intercept); +} diff --git a/src/BetaVector.cpp b/R/src/src/include/BetaVector.h similarity index 81% rename from src/BetaVector.cpp rename to R/src/src/include/BetaVector.h index c6f5d1b..0d5b86b 100644 --- a/src/BetaVector.cpp +++ b/R/src/src/include/BetaVector.h @@ -1,13 +1,20 @@ -#include "BetaVector.h" +#ifndef BETA_VECTOR_H +#define BETA_VECTOR_H +#include +#include "arma_includes.h" /* * arma::vec implementation */ -std::vector nnzIndicies(const arma::vec& B){ + +using beta_vector = arma::vec; +//using beta_vector = arma::sp_mat; + +inline std::vector nnzIndicies(const arma::vec& B){ // Returns a vector of the Non Zero Indicies of B const arma::ucolvec nnzs_indicies = arma::find(B); - return arma::conv_to>::from(nnzs_indicies); + return arma::conv_to>::from(nnzs_indicies); } // std::vector nnzIndicies(const arma::sp_mat& B){ @@ -19,53 +26,53 @@ std::vector nnzIndicies(const arma::vec& B){ // { // S.push_back(it.row()); // } -// return S; +// return S; // } -std::vector nnzIndicies(const arma::vec& B, const std::size_t low){ +inline std::vector nnzIndicies(const arma::vec& B, const std::size_t low){ // Returns a vector of the Non Zero Indicies of a slice of B starting at low // This is for NoSelectK situations const arma::vec B_slice = B.subvec(low, B.n_rows-1); const arma::ucolvec nnzs_indicies = arma::find(B_slice); - return arma::conv_to>::from(nnzs_indicies); + return arma::conv_to>::from(nnzs_indicies); } // std::vector nnzIndicies(const arma::sp_mat& B, const std::size_t low){ // // Returns a vector of the Non Zero Indicies of B // std::vector S; -// -// +// +// // arma::sp_mat::const_iterator it; // const arma::sp_mat::const_iterator it_end = B.end(); -// -// +// +// // for(it = B.begin(); it != it_end; ++it) // { // if (it.row() >= low){ // S.push_back(it.row()); // } // } -// return S; +// return S; // } -std::size_t n_nonzero(const arma::vec& B){ +inline std::size_t n_nonzero(const arma::vec& B){ const arma::vec nnzs = arma::nonzeros(B); return nnzs.n_rows; - + } // std::size_t n_nonzero(const arma::sp_mat& B){ // return B.n_nonzero; -// +// // } -bool has_same_support(const arma::vec& B1, const arma::vec& B2){ +inline bool has_same_support(const arma::vec& B1, const arma::vec& B2){ if (B1.size() != B2.size()){ return false; } std::size_t n = B1.n_rows; - + bool same_support = true; for (std::size_t i = 0; i < n; i++){ same_support = same_support && ((B1.at(i) != 0) == (B2.at(i) != 0)); @@ -74,14 +81,14 @@ bool has_same_support(const arma::vec& B1, const arma::vec& B2){ } // bool has_same_support(const arma::sp_mat& B1, const arma::sp_mat& B2){ -// +// // if (B1.n_nonzero != B2.n_nonzero) { // return false; // } else { // same number of nnz and Supp is sorted // arma::sp_mat::const_iterator i1, i2; // const arma::sp_mat::const_iterator i1_end = B1.end(); -// -// +// +// // for(i1 = B1.begin(), i2 = B2.begin(); i1 != i1_end; ++i1, ++i2) // { // if(i1.row() != i2.row()) @@ -93,3 +100,4 @@ bool has_same_support(const arma::vec& B1, const arma::vec& B2){ // } // } +#endif // BETA_VECTOR_H diff --git a/src/include/CD.h b/R/src/src/include/CD.h similarity index 98% rename from src/include/CD.h rename to R/src/src/include/CD.h index db0af06..2e2e06f 100644 --- a/src/include/CD.h +++ b/R/src/src/include/CD.h @@ -1,7 +1,8 @@ #ifndef CD_H #define CD_H #include -#include "RcppArmadillo.h" +#include +#include "arma_includes.h" #include "BetaVector.h" #include "FitResult.h" #include "Params.h" @@ -489,13 +490,9 @@ bool CD::CWMinCheck() { bool Cwmin = true; for (auto& i : Sc) { - // Rcpp::Rcout << "CW Iteration: " << i << "\n"; - // std::this_thread::sleep_for(std::chrono::milliseconds(100)); Cwmin = this->UpdateBiCWMinCheck(i, Cwmin); } - // Rcpp::Rcout << "CWMinCheckL " << Cwmin << "\n"; - return Cwmin; } diff --git a/src/include/CDL0.h b/R/src/src/include/CDL0.h similarity index 99% rename from src/include/CDL0.h rename to R/src/src/include/CDL0.h index 7eda481..90a3cad 100644 --- a/src/include/CDL0.h +++ b/R/src/src/include/CDL0.h @@ -1,7 +1,7 @@ #ifndef CDL0_H #define CDL0_H #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "Params.h" #include "FitResult.h" diff --git a/src/include/CDL012.h b/R/src/src/include/CDL012.h similarity index 99% rename from src/include/CDL012.h rename to R/src/src/include/CDL012.h index 1a8f3cf..2c08905 100644 --- a/src/include/CDL012.h +++ b/R/src/src/include/CDL012.h @@ -1,6 +1,6 @@ #ifndef CDL012_H #define CDL012_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "FitResult.h" #include "utils.h" diff --git a/src/include/CDL012Logistic.h b/R/src/src/include/CDL012Logistic.h similarity index 99% rename from src/include/CDL012Logistic.h rename to R/src/src/include/CDL012Logistic.h index a9a6356..b661590 100644 --- a/src/include/CDL012Logistic.h +++ b/R/src/src/include/CDL012Logistic.h @@ -1,6 +1,6 @@ #ifndef CDL012Logistic_H #define CDL012Logistic_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "FitResult.h" #include "Params.h" diff --git a/src/include/CDL012LogisticSwaps.h b/R/src/src/include/CDL012LogisticSwaps.h similarity index 84% rename from src/include/CDL012LogisticSwaps.h rename to R/src/src/include/CDL012LogisticSwaps.h index cd098f3..7c25148 100644 --- a/src/include/CDL012LogisticSwaps.h +++ b/R/src/src/include/CDL012LogisticSwaps.h @@ -1,6 +1,6 @@ #ifndef CDL012LogisticSwaps_H #define CDL012LogisticSwaps_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "CDSwaps.h" #include "CDL012Logistic.h" @@ -43,8 +43,7 @@ inline double CDL012LogisticSwaps::Objective(const arma::vec & r, const beta_ template inline double CDL012LogisticSwaps::Objective() { - auto l2norm = arma::norm(this->B, 2); - return arma::sum(arma::log(1 + 1 / ExpyXB)) + this->lambda0 * n_nonzero(this->B) + this->lambda1 * arma::norm(this->B, 1) + this->lambda2 * l2norm * l2norm; + return this->Objective(ExpyXB, this->B); } #endif diff --git a/src/include/CDL012SquaredHinge.h b/R/src/src/include/CDL012SquaredHinge.h similarity index 99% rename from src/include/CDL012SquaredHinge.h rename to R/src/src/include/CDL012SquaredHinge.h index da318ca..78438b4 100644 --- a/src/include/CDL012SquaredHinge.h +++ b/R/src/src/include/CDL012SquaredHinge.h @@ -1,6 +1,6 @@ #ifndef CDL012SquaredHinge_H #define CDL012SquaredHinge_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "FitResult.h" #include "Params.h" diff --git a/src/include/CDL012SquaredHingeSwaps.h b/R/src/src/include/CDL012SquaredHingeSwaps.h similarity index 98% rename from src/include/CDL012SquaredHingeSwaps.h rename to R/src/src/include/CDL012SquaredHingeSwaps.h index fb3ccff..f9d177d 100644 --- a/src/include/CDL012SquaredHingeSwaps.h +++ b/R/src/src/include/CDL012SquaredHingeSwaps.h @@ -1,6 +1,6 @@ #ifndef CDL012SquredHingeSwaps_H #define CDL012SquredHingeSwaps_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "CDSwaps.h" #include "CDL012SquaredHinge.h" diff --git a/src/include/CDL012Swaps.h b/R/src/src/include/CDL012Swaps.h similarity index 97% rename from src/include/CDL012Swaps.h rename to R/src/src/include/CDL012Swaps.h index ac322fb..309629f 100644 --- a/src/include/CDL012Swaps.h +++ b/R/src/src/include/CDL012Swaps.h @@ -1,7 +1,7 @@ #ifndef CDL012SWAPS_H #define CDL012SWAPS_H #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "CD.h" #include "CDSwaps.h" #include "CDL012.h" diff --git a/src/include/CDSwaps.h b/R/src/src/include/CDSwaps.h similarity index 88% rename from src/include/CDSwaps.h rename to R/src/src/include/CDSwaps.h index b13632c..752d0a1 100644 --- a/src/include/CDSwaps.h +++ b/R/src/src/include/CDSwaps.h @@ -2,4 +2,4 @@ #define CDSWAPS_H #include "CD.h" -#endif \ No newline at end of file +#endif diff --git a/src/include/FitResult.h b/R/src/src/include/FitResult.h similarity index 95% rename from src/include/FitResult.h rename to R/src/src/include/FitResult.h index b95e4f2..7b5d28c 100644 --- a/src/include/FitResult.h +++ b/R/src/src/include/FitResult.h @@ -1,6 +1,6 @@ #ifndef FITRESULT_H #define FITRESULT_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "BetaVector.h" template // Forward Reference to prevent circular dependencies diff --git a/src/include/Grid.h b/R/src/src/include/Grid.h similarity index 96% rename from src/include/Grid.h rename to R/src/src/include/Grid.h index 6eacfbc..e6a832f 100644 --- a/src/include/Grid.h +++ b/R/src/src/include/Grid.h @@ -3,7 +3,7 @@ #include #include #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "GridParams.h" #include "FitResult.h" #include "Grid1D.h" diff --git a/src/include/Grid1D.h b/R/src/src/include/Grid1D.h similarity index 97% rename from src/include/Grid1D.h rename to R/src/src/include/Grid1D.h index 35fc2db..4b03aa9 100644 --- a/src/include/Grid1D.h +++ b/R/src/src/include/Grid1D.h @@ -3,7 +3,7 @@ #include #include #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "Params.h" #include "GridParams.h" #include "FitResult.h" diff --git a/src/include/Grid2D.h b/R/src/src/include/Grid2D.h similarity index 96% rename from src/include/Grid2D.h rename to R/src/src/include/Grid2D.h index d7be15d..0d33079 100644 --- a/src/include/Grid2D.h +++ b/R/src/src/include/Grid2D.h @@ -1,7 +1,7 @@ #ifndef GRID2D_H #define GRID2D_H #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "GridParams.h" #include "Params.h" #include "FitResult.h" diff --git a/src/include/GridParams.h b/R/src/src/include/GridParams.h similarity index 95% rename from src/include/GridParams.h rename to R/src/src/include/GridParams.h index 1d2206d..b1795ab 100644 --- a/src/include/GridParams.h +++ b/R/src/src/include/GridParams.h @@ -1,6 +1,6 @@ #ifndef GRIDPARAMS_H #define GRIDPARAMS_H -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "Params.h" template diff --git a/R/src/src/include/L0LearnCore.h b/R/src/src/include/L0LearnCore.h new file mode 100644 index 0000000..84c2bb8 --- /dev/null +++ b/R/src/src/include/L0LearnCore.h @@ -0,0 +1,324 @@ +#ifndef INTERFACE_H +#define INTERFACE_H + +#include +#include +#include +#include +#include "arma_includes.h" +#include "FitResult.h" +#include "Grid.h" +#include "GridParams.h" + +// Make an external struct + +struct fitmodel +{ + const std::vector> Lambda0; + const std::vector Lambda12; + const std::vector> NnzCount; + const arma::field Beta; + const std::vector> Intercept; + const std::vector> Converged; + + fitmodel(std::vector> lambda0, + std::vector lambda12, + std::vector> nnzCount, + arma::field beta, + std::vector> intercept, + std::vector> converged): + Lambda0(std::move(lambda0)), + Lambda12(std::move(lambda12)), + NnzCount(std::move(nnzCount)), + Beta(std::move(beta)), + Intercept(std::move(intercept)), + Converged(std::move(converged)){} +}; + +struct cvfitmodel : fitmodel +{ + const arma::field CVMeans; + const arma::field CVSDs; + + cvfitmodel(std::vector> lambda0, + std::vector lambda12, + std::vector> nnzCount, + arma::field beta, + std::vector> intercept, + std::vector> converged, + arma::field cVMeans, + arma::field cVSDs) : + fitmodel(std::move(lambda0), + std::move(lambda12), + std::move(nnzCount), + std::move(beta), + std::move(intercept), + std::move(converged)), + CVMeans(std::move(cVMeans)), + CVSDs(std::move(cVSDs)){} +}; + + + +template +GridParams makeGridParams(const std::string Loss, const std::string Penalty, + const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector< std::vector > Lambdas, + const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs){ + GridParams PG; + PG.NnzStopNum = NnzStopNum; + PG.G_ncols = G_ncols; + PG.G_nrows = G_nrows; + PG.Lambda2Max = Lambda2Max; + PG.Lambda2Min = Lambda2Min; + PG.LambdaMinFactor = Lambda2Min; // + PG.PartialSort = PartialSort; + PG.ScaleDownFactor = ScaleDownFactor; + PG.LambdaU = LambdaU; + PG.LambdasGrid = Lambdas; + PG.Lambdas = Lambdas[0]; // to handle the case of L0 (i.e., Grid1D) + PG.intercept = Intercept; + + Params P; + PG.P = P; + PG.P.MaxIters = MaxIters; + PG.P.rtol = rtol; + PG.P.atol = atol; + PG.P.ActiveSet = ActiveSet; + PG.P.ActiveSetNum = ActiveSetNum; + PG.P.MaxNumSwaps = MaxNumSwaps; + PG.P.ScreenSize = ScreenSize; + PG.P.NoSelectK = ExcludeFirstK; + PG.P.intercept = Intercept; + PG.P.withBounds = withBounds; + PG.P.Lows = Lows; + PG.P.Highs = Highs; + + if (Loss == "SquaredError") { + PG.P.Specs.SquaredError = true; + } else if (Loss == "Logistic") { + PG.P.Specs.Logistic = true; + PG.P.Specs.Classification = true; + } else if (Loss == "SquaredHinge") { + PG.P.Specs.SquaredHinge = true; + PG.P.Specs.Classification = true; + } + + if (Algorithm == "CD") { + PG.P.Specs.CD = true; + } else if (Algorithm == "CDPSI") { + PG.P.Specs.PSI = true; + } + + if (Penalty == "L0") { + PG.P.Specs.L0 = true; + } else if (Penalty == "L0L2") { + PG.P.Specs.L0L2 = true; + } else if (Penalty == "L0L1") { + PG.P.Specs.L0L1 = true; + } + return PG; +} + + +template +fitmodel L0LearnFit(const T& X, const arma::vec& y, const std::string Loss, const std::string Penalty, + const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, + const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, + const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, + const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, + const std::vector< std::vector >& Lambdas, const std::size_t ExcludeFirstK, + const bool Intercept, const bool withBounds, const arma::vec &Lows, + const arma::vec &Highs){ + + GridParams PG = makeGridParams(Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, + Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, + ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, + LambdaU, Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs); + + Grid G(X, y, PG); + G.Fit(); + + // Next Construct the list of Sparse Beta Matrices. + + auto p = X.n_cols; + arma::field Bs(G.Lambda12.size()); + + for (std::size_t i=0; i +cvfitmodel L0LearnCV(const T& X, const arma::vec& y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, + const double atol, const bool ActiveSet, + const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector< std::vector > Lambdas, + const unsigned int nfolds, const double seed, + const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, + const arma::vec &Highs){ + + GridParams PG = makeGridParams(Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, + Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, + ActiveSet,ActiveSetNum, MaxNumSwaps, ScaleDownFactor, + ScreenSize,LambdaU, Lambdas, ExcludeFirstK, Intercept, + withBounds, Lows, Highs); + + Grid G(X, y, PG); + G.Fit(); + + // Next Construct the list of Sparse Beta Matrices. + + auto p = X.n_cols; + auto n = X.n_rows; + arma::field Bs(G.Lambda12.size()); + + for (std::size_t i=0; i >(G.size()); + //Intercepts = std::vector< std::vector >(G.size()); + + std::size_t Ngamma = G.Lambda12.size(); + + //std::vector< arma::mat > CVError (G.Solutions.size()); + arma::field< arma::mat > CVError (G.Solutions.size()); + + for (std::size_t i=0; i(0, X.n_rows-1, X.n_rows); + + arma::uvec indices = arma::shuffle(a); + + int samplesperfold = std::ceil(n/double(nfolds)); + int samplesinlastfold = samplesperfold - (samplesperfold*nfolds - n); + + std::vector fullindices(X.n_rows); + std::iota(fullindices.begin(), fullindices.end(), 0); + + + for (std::size_t j=0; j validationindices; + if (j < nfolds-1) + validationindices.resize(samplesperfold); + else + validationindices.resize(samplesinlastfold); + + std::iota(validationindices.begin(), validationindices.end(), samplesperfold*j); + + std::vector trainingindices; + + std::set_difference(fullindices.begin(), fullindices.end(), validationindices.begin(), validationindices.end(), + std::inserter(trainingindices, trainingindices.begin())); + + + // validationindicesarma contains the randomly permuted validation indices as a uvec + arma::uvec validationindicesarma; + arma::uvec validationindicestemp = arma::conv_to< arma::uvec >::from(validationindices); + validationindicesarma = indices.elem(validationindicestemp); + + // trainingindicesarma is similar to validationindicesarma but for training + arma::uvec trainingindicesarma; + + arma::uvec trainingindicestemp = arma::conv_to< arma::uvec >::from(trainingindices); + + + trainingindicesarma = indices.elem(trainingindicestemp); + + + T Xtraining = matrix_rows_get(X, trainingindicesarma); + + arma::mat ytraining = y.elem(trainingindicesarma); + + T Xvalidation = matrix_rows_get(X, validationindicesarma); + + arma::mat yvalidation = y.elem(validationindicesarma); + + PG.LambdaU = true; + PG.XtrAvailable = false; // reset XtrAvailable since its changed upon every call + PG.LambdasGrid = G.Lambda0; + PG.NnzStopNum = p+1; // remove any constraints on the supp size when fitting over the cv folds // +1 is imp to avoid =p edge case + if (PG.P.Specs.L0 == true){ + PG.Lambdas = PG.LambdasGrid[0]; + } + Grid Gtraining(Xtraining, ytraining, PG); + Gtraining.Fit(); + + for (std::size_t i=0; i 0); + CVError[i](k,j) = arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) / yvalidation.n_rows; + } + } + } + } + + arma::field CVMeans(Ngamma); + arma::field CVSDs(Ngamma); + + for (std::size_t i=0; i * make_CD(const T& Xi, const arma::vec& yi, const Params& P) { } -#endif \ No newline at end of file +#endif diff --git a/src/include/Model.h b/R/src/src/include/Model.h similarity index 100% rename from src/include/Model.h rename to R/src/src/include/Model.h diff --git a/src/include/Normalize.h b/R/src/src/include/Normalize.h similarity index 98% rename from src/include/Normalize.h rename to R/src/src/include/Normalize.h index 241b89e..7c962d0 100644 --- a/src/include/Normalize.h +++ b/R/src/src/include/Normalize.h @@ -2,7 +2,7 @@ #define NORMALIZE_H #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "utils.h" #include "BetaVector.h" diff --git a/src/include/Params.h b/R/src/src/include/Params.h similarity index 97% rename from src/include/Params.h rename to R/src/src/include/Params.h index ec583c7..00bce14 100644 --- a/src/include/Params.h +++ b/R/src/src/include/Params.h @@ -1,7 +1,7 @@ #ifndef PARAMS_H #define PARAMS_H #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "Model.h" #include "BetaVector.h" diff --git a/R/src/src/include/arma_includes.h b/R/src/src/include/arma_includes.h new file mode 100644 index 0000000..e166b06 --- /dev/null +++ b/R/src/src/include/arma_includes.h @@ -0,0 +1,12 @@ +#ifndef INCLUDES_H +#define INCLUDES_H + +#include +// [[Rcpp::depends(RcppArmadillo)]] + +#define COUT Rcpp::Rcout +#define STOP Rcpp::stop + +void inline UserInterrupt() { Rcpp::checkUserInterrupt(); } + +#endif // INCLUDES_H \ No newline at end of file diff --git a/src/include/utils.h b/R/src/src/include/utils.h similarity index 52% rename from src/include/utils.h rename to R/src/src/include/utils.h index 167a27d..dbf79f6 100644 --- a/src/include/utils.h +++ b/R/src/src/include/utils.h @@ -1,7 +1,7 @@ -#ifndef L0LEARN_UTILS_H -#define L0LEARN_UTILS_H +#ifndef L0LEARN_UTILS_HPP +#define L0LEARN_UTILS_HPP #include -#include "RcppArmadillo.h" +#include "arma_includes.h" #include "BetaVector.h" @@ -15,11 +15,6 @@ inline T clamp(T x, T low, T high) { return x; } -void clamp_by_vector(arma::vec &B, const arma::vec& lows, const arma::vec& highs); - -void clamp_by_vector(arma::sp_mat &B, const arma::vec& lows, const arma::vec& highs); - - template arma::vec inline matrix_column_get(const arma::mat &mat, T1 col){ return mat.unsafe_col(col); @@ -120,12 +115,102 @@ arma::vec inline matrix_column_mult(const arma::sp_mat &mat, T1 col, const T2 &u return matrix_column_get(mat, col)*u; } -arma::rowvec matrix_normalize(arma::sp_mat &mat_norm); +void inline clamp_by_vector(arma::vec &B, const arma::vec& lows, const arma::vec& highs){ + const std::size_t n = B.n_rows; + for (std::size_t i = 0; i < n; i++){ + B.at(i) = clamp(B.at(i), lows.at(i), highs.at(i)); + } +} + +// void clamp_by_vector(arma::sp_mat &B, const arma::vec& lows, const arma::vec& highs){ +// // See above implementation without filter for error. +// auto begin = B.begin(); +// auto end = B.end(); +// +// std::vector inds; +// for (; begin != end; ++begin) +// inds.push_back(begin.row()); +// +// auto n = B.size(); +// inds.erase(std::remove_if(inds.begin(), +// inds.end(), +// [n](size_t x){return (x > n) && (x < 0);}), +// inds.end()); +// for (auto& it : inds) { +// double B_item = B(it, 0); +// const double low = lows(it); +// const double high = highs(it); +// B(it, 0) = clamp(B_item, low, high); +// } +// } + +arma::rowvec inline matrix_normalize(arma::sp_mat &mat_norm){ + auto p = mat_norm.n_cols; + arma::rowvec scaleX = arma::zeros(p); // will contain the l2norm of every col + + for (auto col = 0; col < p; col++){ + double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); + scaleX(col) = l2norm; + } + + scaleX.replace(0, -1); + + for (auto col = 0; col < p; col++){ + arma::sp_mat::col_iterator begin = mat_norm.begin_col(col); + arma::sp_mat::col_iterator end = mat_norm.end_col(col); + for (; begin != end; ++begin) + (*begin) = (*begin)/scaleX(col); + } + + if (mat_norm.has_nan()) + mat_norm.replace(arma::datum::nan, 0); // can handle numerical instabilities. + + return scaleX; +} + +arma::rowvec inline matrix_normalize(arma::mat& mat_norm){ + + auto p = mat_norm.n_cols; + arma::rowvec scaleX = arma::zeros(p); // will contain the l2norm of every col + + for (auto col = 0; col < p; col++) { + double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); + scaleX(col) = l2norm; + } -arma::rowvec matrix_normalize(arma::mat &mat_norm); + scaleX.replace(0, -1); + mat_norm.each_row() /= scaleX; -arma::rowvec matrix_center(const arma::mat& X, arma::mat& X_normalized, bool intercept); + if (mat_norm.has_nan()){ + mat_norm.replace(arma::datum::nan, 0); // can handle numerical instabilities. + } + + return scaleX; +} + +arma::rowvec inline matrix_center(const arma::mat& X, arma::mat& X_normalized, + bool intercept){ + auto p = X.n_cols; + arma::rowvec meanX; + + if (intercept){ + meanX = arma::mean(X, 0); + X_normalized = X.each_row() - meanX; + } else { + meanX = arma::zeros(p); + X_normalized = arma::mat(X); + } + + return meanX; +} + +arma::rowvec inline matrix_center(const arma::sp_mat& X, arma::sp_mat& X_normalized, + bool intercept){ + auto p = X.n_cols; + arma::rowvec meanX = arma::zeros(p); + X_normalized = arma::sp_mat(X); + return meanX; +} -arma::rowvec matrix_center(const arma::sp_mat& X, arma::sp_mat& X_normalized, bool intercept); -#endif //L0LEARN_UTILS_H \ No newline at end of file +#endif //L0LEARN_UTILS_HPP diff --git a/tests/testthat.R b/R/tests/testthat.R similarity index 100% rename from tests/testthat.R rename to R/tests/testthat.R diff --git a/R/tests/testthat/test-L0Learn_accuracy.R b/R/tests/testthat/test-L0Learn_accuracy.R new file mode 100644 index 0000000..e6355dc --- /dev/null +++ b/R/tests/testthat/test-L0Learn_accuracy.R @@ -0,0 +1,105 @@ +library("Matrix") +library("testthat") +library("L0Learn") +library("pracma") + +K <- 10 + +tmp <- L0Learn::GenSynthetic(n = 100, p = 1000, k = K, seed = 1, rho = .5, snr = +Inf) +X <- tmp[[1]] +y <- tmp[[2]] +tol <- 1e-4 + +if (norm(X %*% tmp$B + tmp$b0 - y) >= 1e-9) { + stop() +} + +if (0 %in% y) { + stop() +} + + +norm_vec <- function(x) { + Norm(as.matrix(x), p = Inf) +} + +test_that("L0Learn recovers coefficients with no error for L0", { + skip_on_cran() + fit <- L0Learn.fit(X, y, loss = "SquaredError", penalty = "L0") + + for (j in 1:length(fit$suppSize[[1]])) { + # With only L0 penalty, therefore, once the support size is 10, all coefficients should be 1. + if (fit$suppSize[[1]][[j]] >= 10) { + expect_equal(norm_vec(fit$beta[[1]][, j] - tmp$B), 0, tolerance = 1e-3, info = j) + } + } +}) + +test_that("L0Learn seperates data with no error for L0", { + skip_on_cran() + for (l in c("Logisitic", "SquaredHinge")) { + fit <- L0Learn.fit(X, sign(y), loss = "Logistic", penalty = "L0") + + predict_ <- function(index) { + sign(X %*% fit$beta[[1]][, index] + fit$a0[[1]][index]) + } + + for (j in 1:length(fit$suppSize[[1]])) { + if (fit$suppSize[[1]][[j]] >= 10) { + expect_equal(predict_(j), sign(y)) + } + } + } +}) + + +test_that("L0Learn recovers coefficients with no error for L0L1/L0L2", { + skip_on_cran() + for (p in c("L0L1", "L0L2")) { + fit <- L0Learn.fit(X, y, loss = "SquaredError", penalty = p) + + for (i in 1:length(fit$suppSize)) { + past_K_support_error <- Inf + for (j in 1:length(fit$suppSize[[i]])) { + # With L0 and L1/L2 penalty, once the support size is 10 (dictated by L0 and L1 together), the coefficients + # will most likely not be 1 due the L1/L2 penalty. Therefore, as the L1/L2 penalty decreases, the coefficients + # should approach 1. + # Each iteration, the norm should decrease + if (fit$suppSize[[i]][[j]] >= K) { + new_K_support_error <- norm_vec(fit$beta[[i]][, j] - tmp$B) + expect_lte(new_K_support_error, past_K_support_error) + new_K_support_error <- past_K_support_error + } + } + } + } +}) + + +test_that("L0Learn seperates data with no error for L0L1/L0L2", { + skip_on_cran() + for (l in c("Logistic", "SquaredHinge")) { + for (p in c("L0L1", "L0L2")) { + fit <- L0Learn.fit(X, sign(y), loss = l, penalty = p) + + predict_ <- function(index1, index2) { + sign(X %*% fit$beta[[index1]][, index2] + fit$a0[[index1]][index2]) + } + + for (i in 1:length(fit$suppSize)) { + past_K_support_error <- Inf + for (j in 1:length(fit$suppSize[[i]])) { + # With L0 and L1/L2 penalty, once the support size is 10 (dictated by L0 and L1 together), the coefficients + # will most likely not be 1 due the L1/L2 penalty. Therefore, as the L1/L2 penalty decreases, the coefficients + # should approach 1. + # Each iteration, the norm should decrease + if (fit$suppSize[[i]][[j]] >= K) { + new_K_support_error <- Norm(predict_(i, j) - sign(y)) + expect_lte(new_K_support_error, past_K_support_error) + new_K_support_error <- past_K_support_error + } + } + } + } + } +}) diff --git a/R/tests/testthat/test-L0Learn_gen.R b/R/tests/testthat/test-L0Learn_gen.R new file mode 100644 index 0000000..2af9897 --- /dev/null +++ b/R/tests/testthat/test-L0Learn_gen.R @@ -0,0 +1,29 @@ +library("testthat") +library("L0Learn") + + +test_that("L0Learn GenSyntheticLogistic fails for improper s", { + expect_error(L0Learn:::GenSyntheticLogistic(n = 1000, p = 1000, k = 10, seed = 1, s = -1)) +}) + +test_that("L0Learn GenSyntheticLogistic accepts Null and Diagonal Sigma", { + L0Learn:::GenSyntheticLogistic(n = 1000, p = 1000, k = 10, seed = 1, sigma = NULL) + L0Learn:::GenSyntheticLogistic( + n = 1000, p = 1000, k = 10, seed = 1, + sigma = diag(1:1000) + ) + + expect_error(L0Learn:::GenSyntheticLogistic( + n = 1000, p = 1000, k = 10, seed = 1, + sigma = diag(1:999) + )) + + succeed() +}) + +test_that("L0Learn GenSyntheticLogistic shuffles B", { + L0Learn:::GenSyntheticLogistic(n = 1000, p = 1000, k = 10, seed = 1, shuffle_B = TRUE) + L0Learn:::GenSyntheticLogistic(n = 1000, p = 1000, k = 10, seed = 1, shuffle_B = FALSE) + + succeed() +}) diff --git a/R/tests/testthat/test_L0Learn.R b/R/tests/testthat/test_L0Learn.R new file mode 100644 index 0000000..44e45ed --- /dev/null +++ b/R/tests/testthat/test_L0Learn.R @@ -0,0 +1,581 @@ +library("Matrix") +library("testthat") +library("L0Learn") + +tmp <- L0Learn::GenSynthetic(n = 100, p = 1000, k = 20, seed = 1, snr = 10, rho = .5) +X <- tmp[[1]] +y <- tmp[[2]] +tol <- 1e-4 + +if (sum(apply(X, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") +} + +X_sparse <- as(X, "dgCMatrix") + +test_that("L0Learn Accepts Proper Matricies", { + skip_on_cran() + ignore <- L0Learn.fit(X, y) + ignore <- L0Learn.cvfit(X, y) + ignore <- L0Learn.fit(X_sparse, y, intercept = FALSE) + ignore <- L0Learn.cvfit(X_sparse, y, intercept = FALSE) + succeed() +}) + +test_that("L0Learn V2+ raises warning on autolambda usage", { + fit_user_grid <- list() + fit_user_grid[[1]] <- c(10:1) + expect_warning(L0Learn.fit(X, y, lambdaGrid = fit_user_grid, autoLambda = FALSE)) + + expect_warning(L0Learn.cvfit(X, y, lambdaGrid = fit_user_grid, autoLambda = FALSE)) + + expect_silent(L0Learn.fit(X, y, lambdaGrid = fit_user_grid, penalty = "L0L2", nGamma = 1)) + expect_silent(L0Learn.cvfit(X, y, lambdaGrid = fit_user_grid, penalty = "L0L2", nGamma = 1)) +}) + +test_that("L0Learn V2+ raises error on negative user_grid values", { + fit_user_grid <- list() + fit_user_grid[[1]] <- c(-2:-10) + + for (p in c("L0", "L0L1", "L0L2")) { + expect_error(L0Learn.fit(X, y, lambdaGrid = fit_user_grid, penalty = p)) + expect_error(L0Learn.cvfit(X, y, lambdaGrid = fit_user_grid, penalty = p)) + } +}) + +test_that("L0Learn respect colnames on X data matrix", { + X_with_names <- matrix(X, nrow = nrow(X), ncol = ncol(X)) + names <- c() + for (i in 1:1000) { + names[i] <- paste("F", i) + } + colnames(X_with_names) <- names + fit <- L0Learn.fit(X_with_names, y) + + # TODO: Add colnames to beta + expect_equal(colnames(X_with_names), fit$varnames) + fit <- L0Learn.cvfit(X_with_names, y) + expect_equal(colnames(X_with_names), fit$fit$varnames) +}) + +test_that("L0Learn raises error when classification has 3 or more values in y.", { + y_bin_bad <- sign(y) + y_bin_bad[[1]] <- 2 + + for (loss in c("Logistic", "SquaredHinge")) { + expect_error(L0Learn.fit(X, y_bin_bad, loss = loss)) + expect_error(L0Learn.cvfit(X, y_bin_bad, loss = loss)) + } +}) + +test_that("L0Learn raises error when L0 classification has too large of a lambda grid", { + # This is tricky. See implementation of L0 classification penalty in fit.R and cvfit.R + + lambda_grid <- list() + lambda_grid[[1]] <- c(10e-8, 10e-9) + lambda_grid[[2]] <- c(10e-8, 10e-9) + + for (loss in c("Logistic", "SquaredHinge")) { + expect_error(L0Learn.fit(X, sign(y), loss = loss, lambdaGrid = lambda_grid)) + expect_error(L0Learn.cvfit(X, sign(y), loss = loss, lambdaGrid = lambda_grid)) + } +}) + +test_that("L0Learn raises warning on degenerate solution path", { + lambda_grid <- list() + lambda_grid[[1]] <- c(10e-8, 10e-9) + + expect_warning(L0Learn.fit(X, y, lambdaGrid = lambda_grid)) + expect_warning(L0Learn.cvfit(X, y, lambdaGrid = lambda_grid)) +}) + +test_that("L0Learn respects excludeFirstK for large L0", { + skip_on_cran() + BIGuserLambda <- list() + BIGuserLambda[[1]] <- c(10) + for (k in c(0, 1, 10)) { + x1 <- L0Learn.fit(X, y, + penalty = "L0", autoLambda = FALSE, + lambdaGrid = BIGuserLambda, excludeFirstK = k + ) + + expect_equal(x1$suppSize[[1]][1], k) + } +}) + +test_that("L0Learn excludeFirstK is still subject to L1 norms", { + skip_on_cran() + K <- p <- 10 + n <- 100 + + tmp <- L0Learn::GenSynthetic(n = n, p = p, k = 5, seed = 1) + X_real <- tmp[[1]] + + tmp <- L0Learn::GenSynthetic(n = n, p = p, k = 5, seed = 2) + y_fake <- tmp[[2]] + + # X_real has little to do with generation of y_fake. + # Therefore, as L1 grows we can expect that the columns go to 0. + + + x1 <- L0Learn.fit(X_real, y_fake, penalty = "L0", excludeFirstK = K - 1, maxSuppSize = p) + + expect_equal(length(x1$suppSize[[1]]), 2) + expect_equal(x1$suppSize[[1]][2], 10) + + # TODO: Fix Crash when excludeFirstK >= p + # x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K, maxSuppSize = 10) + + # TODO: Fix issue when support is not maximized in first iteration for + # x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K-1, maxSuppSize = 10) + # All coefficients should only be regularized by L1, the x2$suppSize is strange. + + + x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K - 1, maxSuppSize = p) + for (s in x2$suppSize[[1]]) { + expect_lt(s, p) + } +}) + + +test_that("L0Learn fit are deterministic for Dense fit", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.fit(X, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.fit(X, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + + +test_that("L0Learn cvfit are deterministic for Dense cvfit", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.cvfit(X, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.cvfit(X, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + +test_that("L0Learn fit and cvfit are deterministic for Dense fit", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.fit(X_sparse, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.fit(X_sparse, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + +test_that("L0Learn fit and cvfit are deterministic for Dense cvfit", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.cvfit(X_sparse, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.cvfit(X_sparse, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + + +test_that("L0Learn fit find same solution for different matrix representations", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.fit(X_sparse, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.fit(X, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + +test_that("L0Learn fit find same solution for different matrix representations", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.fit(X_sparse, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.fit(X, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = paste(p, lows)) + } +}) + +# test_that("L0Learn fit find similar solution for different matrix representations with bounds", { +# skip_on_cran() +# for (p in c("L0", "L0L2", "L0L1")){ +# for (lows in (c(0, -10000, -.1))){ +# set.seed(1) +# x1 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE, lows=lows) +# set.seed(1) +# x2 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE, lows=lows) +# # TODO: Investigate why X_sparse is missing a solution +# for (i in 1:length(x1$beta)){ +# expect_equal(x1$beta[[i]], x2$beta[[i]][, 2:ncol(x2$beta[[i]])], info=paste(p, lows, i)) +# } +# +# } +# } +# }) + +test_that("L0Learn cvfit find same solution for different matrix representations", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + set.seed(1) + x1 <- L0Learn.cvfit(X_sparse, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.cvfit(X, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2, info = p) + } +}) + + +test_that("L0Learn fit and cvfit run with sparse X and intercepts", { + skip_on_cran() + L0Learn.fit(X_sparse, y, intercept = TRUE) + L0Learn.cvfit(X_sparse, y, intercept = TRUE) + succeed() +}) + +test_that("L0Learn fit and cvfit run with sparse X and intercepts and CDPSI", { + skip_on_cran() + L0Learn.fit(X_sparse, y, intercept = TRUE, algorithm = "CDPSI", maxSwaps = 2) + L0Learn.cvfit(X_sparse, y, intercept = TRUE, algorithm = "CDPSI", maxSwaps = 2) + succeed() +}) + + +test_that("L0Learn matches for all penalty for Sparse and Dense Matrices", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + for (f in c(L0Learn.cvfit, L0Learn.fit)) { + set.seed(1) + x1 <- f(X, y, penalty = p, intercept = FALSE) + set.seed(1) + x2 <- f(X_sparse, y, penalty = p, intercept = FALSE) + expect_equal(x1, x2) + } + } +}) + + + +test_that("L0Learn.Fit runs for all Loss for Sparse and Dense Matrices", { + skip_on_cran() + y_bin <- matrix(rbinom(dim(y)[1], 1, 0.5)) + for (l in c("Logistic", "SquaredHinge")) { + set.seed(1) + x1 <- L0Learn.fit(X, y_bin, loss = l, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.fit(X_sparse, y_bin, loss = l, intercept = FALSE) + expect_equal(x1, x2, info = paste("fit", l)) + + set.seed(1) + x1 <- L0Learn.cvfit(X, y_bin, loss = l, intercept = FALSE) + set.seed(1) + x2 <- L0Learn.cvfit(X_sparse, y_bin, loss = l, intercept = FALSE) + expect_equal(x1, x2, info = paste("fit", l)) + } +}) + + +test_that("L0Learn.Fit runs for all algorithm for Sparse and Dense Matrices", { + skip_on_cran() + for (p in c("L0", "L0L2", "L0L1")) { + for (intercept in c(TRUE, FALSE)) { + set.seed(1) + x1 <- L0Learn.fit(X, y, penalty = p, algorithm = "CDPSI", intercept = intercept) + set.seed(1) + x2 <- L0Learn.fit(X, y, penalty = p, algorithm = "CDPSI", intercept = intercept) + expect_equal(x1, x2, info = paste(p, intercept)) + } + } +}) + + + +test_that("Utilities for processing regression fit and cv objects run", { + skip_on_cran() + # Test utils for L0Learn.fit + fit <- L0Learn.fit(X, y) + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + + # Test utils for L0Learn.cvfit + fit <- L0Learn.cvfit(X, y) + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + succeed() +}) + +test_that("Utilities for processing logistic fit and cv objects run", { + skip_on_cran() + # Test utils for L0Learn.fit + fit <- L0Learn.fit(X, sign(y), loss = "Logistic") + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + + # Test utils for L0Learn.cvfit + fit <- L0Learn.cvfit(X, sign(y), loss = "Logistic") + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + succeed() +}) + +test_that("Utilities for processing non-intercept fit and cv objects run", { + skip_on_cran() + # Test utils for L0Learn.fit + fit <- L0Learn.fit(X, y, intercept = FALSE) + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + + # Test utils for L0Learn.cvfit + fit <- L0Learn.cvfit(X, y, intercept = FALSE) + print(fit) + coef(fit, lambda = 0.01) + coef(fit, lambda = 0.01, gamma = 0) + coef(fit) + plot(fit) + plot(fit, showlines = FALSE) + predict(fit, newx = X, lambda = 0.01) + predict(fit, newx = X, lambda = 0.01, gamma = 0) + succeed() +}) + + +test_that("The CDPSI algorithm runs for different losses.", { + skip_on_cran() + # Test utils for L0Learn.fit + L0Learn.fit(X, y, algorithm = "CDPSI", loss = "SquaredError", maxSuppSize = 5) + L0Learn.fit(X, sign(y), algorithm = "CDPSI", loss = "Logistic", maxSuppSize = 5) + L0Learn.fit(X, sign(y), algorithm = "CDPSI", loss = "SquaredHinge", maxSuppSize = 5) + succeed() +}) + +test_that("The fit and cvfit gracefully error on bad rtol.", { + skip_on_cran() + + f1 <- function() { + L0Learn.fit(X, y, rtol = 1.1) + } + f2 <- function() { + L0Learn.fit(X, y, rtol = -.1) + } + f3 <- function() { + L0Learn.fit(X, y, atol = -.1) + } + f4 <- function() { + L0Learn.cvfit(X, y, rtol = 1.1) + } + f5 <- function() { + L0Learn.cvfit(X, y, rtol = -.1) + } + f6 <- function() { + L0Learn.cvfit(X, y, atol = -.1) + } + + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) + expect_error(f4()) + expect_error(f5()) + expect_error(f6()) +}) + +test_that("The fit and cvfit gracefully error on bad loss specifications", { + skip_on_cran() + + f1 <- function() { + L0Learn.fit(X, y, loss = "NOT A LOSS") + } + f2 <- function() { + L0Learn.cvfit(X, y, loss = "NOT A LOSS") + } + + expect_error(f1()) + expect_error(f2()) +}) + +test_that("The fit and cvfit gracefully error on bad penalty specifications", { + skip_on_cran() + + f1 <- function() { + L0Learn.fit(X, y, penalty = "NOT A PENALTY") + } + f2 <- function() { + L0Learn.cvfit(X, y, penalty = "NOT A PENALTY") + } + + expect_error(f1()) + expect_error(f2()) +}) + +test_that("The fit and cvfit gracefully error on bad algorithim specifications", { + skip_on_cran() + + f1 <- function() { + L0Learn.fit(X, y, algorithm = "NOT A ALGO") + } + f2 <- function() { + L0Learn.cvfit(X, y, algorithm = "NOT A ALGO") + } + + expect_error(f1()) + expect_error(f2()) +}) + +test_that("The fit and cvfit gracefully error on non classifcation y when for classicaiton", { + skip_on_cran() + + f1 <- function() { + L0Learn.fit(X, y, loss = "Logistic") + } + f2 <- function() { + L0Learn.fit(X, y, loss = "SquaredHinge") + } + f1 <- function() { + L0Learn.cvfit(X, y, loss = "Logistic") + } + f2 <- function() { + L0Learn.cvfit(X, y, loss = "SquaredHinge") + } + + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) + expect_error(f4()) +}) + + +test_that("The fit and cvfit gracefully error on L0 classifcation when lambdagrid is the wrong size", { + skip_on_cran() + + lambda_grid <- list() + lambda_grid[[1]] <- c(10:1) + lambda_grid[[2]] <- c(10:1) + f1 <- function() { + L0Learn.fit(X, sign(y), loss = "Logistic", penalty = "L0", lambdaGrid = lambda_grid) + } + f2 <- function() { + L0Learn.fit(X, sign(y), loss = "SquaredHinge", penalty = "L0", lambdaGrid = lambda_grid) + } + f1 <- function() { + L0Learn.cvfit(X, sign(y), loss = "Logistic", penalty = "L0", lambdaGrid = lambda_grid) + } + f2 <- function() { + L0Learn.cvfit(X, sign(y), loss = "SquaredHinge", penalty = "L0", lambdaGrid = lambda_grid) + } + + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) + expect_error(f4()) +}) + +test_that("The fit and cvfit gracefully error on L0 when lambdagrid is the wrong size", { + skip_on_cran() + + lambda_grid <- list() + lambda_grid[[1]] <- c(10:1) + lambda_grid[[2]] <- c(10:1) + f1 <- function() { + L0Learn.fit(X, y, penalty = "L0", lambdaGrid = lambda_grid) + } + f2 <- function() { + L0Learn.cvfit(X, y, penalty = "L0", lambdaGrid = lambda_grid) + } + + expect_error(f1()) + expect_error(f2()) +}) + +test_that("The fit and cvfit gracefully error on L0 when lambdagrid has not decreasing values", { + skip_on_cran() + + lambda_grid <- list() + lambda_grid[[1]] <- c(1:10) + f1 <- function() { + L0Learn.fit(X, y, penalty = "L0", lambdaGrid = lambda_grid) + } + f2 <- function() { + L0Learn.cvfit(X, y, penalty = "L0", lambdaGrid = lambda_grid) + } + + expect_error(f1()) + expect_error(f2()) +}) + +test_that("The fit and cvfit gracefully error on L0LX when lambdagrid has not decreasing values", { + skip_on_cran() + + lambda_grid <- list() + lambda_grid[[1]] <- c(10:1) + lambda_grid[[1]] <- c(1:10) + f1 <- function() { + L0Learn.fit(X, y, penalty = "L0L1", lambdaGrid = lambda_grid) + } + f2 <- function() { + L0Learn.cvfit(X, y, penalty = "L0L1", lambdaGrid = lambda_grid) + } + f3 <- function() { + L0Learn.fit(X, y, penalty = "L0L2", lambdaGrid = lambda_grid) + } + f4 <- function() { + L0Learn.cvfit(X, y, penalty = "L0L2", lambdaGrid = lambda_grid) + } + + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) + expect_error(f4()) +}) + +test_that("The fit and cvfit gracefully error on CDPSI when bounds are supplied", { + skip_on_cran() + + + f1 <- function() { + L0Learn.fit(X, y, algorithm = "CDPSI", lows = 0) + } + f2 <- function() { + L0Learn.cvfit(X, y, algorithm = "CDPSI", lows = 0) + } + + expect_error(f1()) + expect_error(f2()) +}) diff --git a/R/tests/testthat/test_L0Learn_bounds.R b/R/tests/testthat/test_L0Learn_bounds.R new file mode 100644 index 0000000..b04c84c --- /dev/null +++ b/R/tests/testthat/test_L0Learn_bounds.R @@ -0,0 +1,230 @@ +library("Matrix") +library("testthat") +library("L0Learn") +library("pracma") + +tmp <- L0Learn::GenSynthetic(n = 100, p = 5000, k = 10, seed = 1, rho = 1.5) +X <- tmp[[1]] +y <- tmp[[2]] +y_bin <- sign(y + rnorm(100)) +tol <- 1e-4 +epsilon <- 1e-12 + +if (sum(apply(X, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") +} + +X_sparse <- as(X, "dgCMatrix") + +# test_that("L0Learn finds the same solution with LOOSE bounds", { +# x1 <- L0Learn.fit(X, y, lows=-10000, highs=10000) +# x2 <- L0Learn.fit(X, y) +# expect_equal(x1, x2) +# }) + + +test_that("L0Learn Fails on in-proper Bounds", { + skip_on_cran() + for (f in c(L0Learn.fit, L0Learn.cvfit)) { + for (m in list(X, X_sparse)) { + f1 <- function() { + f(m, y, intercept = FALSE, lows = NaN) + } + f2 <- function() { + f(m, y, intercept = FALSE, highs = NaN) + } + f3 <- function() { + f(m, y, intercept = FALSE, lows = 1, highs = 0) + } + f4 <- function() { + f(m, y, intercept = FALSE, lows = 0, highs = 0) + } + f5 <- function() { + f(m, y, intercept = FALSE, lows = rep(1, dim(m)[[2]]), highs = 0) + } + f6 <- function() { + f(m, y, intercept = FALSE, lows = rep(0, dim(m)[[2]]), highs = 0) + } + f7 <- function() { + f(m, y, intercept = FALSE, lows = 1, highs = rep(1, dim(m)[[2]])) + } + f8 <- function() { + f(m, y, intercept = FALSE, lows = 1, highs = rep(0, dim(m)[[2]])) + } + f9 <- function() { + f(m, y, intercept = FALSE, lows = 1, highs = 2) + } + f10 <- function() { + f(m, y, intercept = FALSE, lows = -2, highs = -1) + } + f11 <- function() { + f(m, y, intercept = FALSE, lows = c(1, rep(0, dim(m)[[2]] - 1)), highs = rep(1, dim(m)[[2]])) + } + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) + expect_error(f4()) + expect_error(f5()) + expect_error(f6()) + expect_error(f7()) + expect_error(f8()) + expect_error(f9()) + expect_error(f10()) + expect_error(f11()) + } + } +}) + +test_that("L0Learn fit fails on CDPSI with bound", { + skip_on_cran() + f1 <- function() { + L0Learn.fit(X, y, algorithm = "CDPSI", lows = 0) + } + f2 <- function() { + L0Learn.fit(X, y, algorithm = "CDPSI", highs = 0) + } + f3 <- function() { + L0Learn.fit(X, y, algorithm = "CDPSI", lows = rep(0, 5000), highs = rep(1, 5000)) + } + expect_error(f1()) + expect_error(f2()) + expect_error(f3()) +}) + +test_that("L0Learn fit respect bounds", { + skip_on_cran() + low <- -.04 + high <- .05 + for (m in list(X, X_sparse)) { + for (p in c("L0", "L0L1", "L0L2")) { + fit <- L0Learn.fit(m, y, intercept = FALSE, penalty = p, lows = low, highs = high) + for (i in 1:length(fit$beta)) { + expect_gte(min(fit$beta[[i]]), low - epsilon) + expect_lte(max(fit$beta[[i]]), high + epsilon) + } + } + } +}) + +test_that("L0Learn cvfit respect bounds", { + skip_on_cran() + low <- -.04 + high <- .05 + for (m in list(X, X_sparse)) { + for (p in c("L0", "L0L1", "L0L2")) { + fit <- L0Learn.cvfit(m, y, intercept = FALSE, penalty = p, lows = low, highs = high) + for (i in 1:length(fit$fit$beta)) { + expect_gte(min(fit$fit$beta[[i]]), low - epsilon) + expect_lte(max(fit$fit$beta[[i]]), high + epsilon) + } + } + } +}) + +test_that("L0Learn respects bounds for all Losses", { + skip_on_cran() + low <- -.04 + high <- .05 + maxIters <- 2 + maxSwaps <- 2 + for (a in c("CD")) { # for (a in c("CD", "CDPSI")){ + for (m in list(X, X_sparse)) { + for (p in c("L0", "L0L1", "L0L2")) { + for (l in c("Logistic", "SquaredHinge")) { + fit <- L0Learn.fit(m, y_bin, + loss = l, intercept = FALSE, + penalty = p, algorithm = a, lows = low, + highs = high, maxIters = maxIters, maxSwaps = maxSwaps + ) + for (i in 1:length(fit$beta)) { + expect_gte(min(fit$beta[[i]]), low - epsilon) + expect_lte(max(fit$beta[[i]]), high + epsilon) + } + } + + fit <- L0Learn.fit(m, y, + loss = "SquaredError", intercept = FALSE, + penalty = p, algorithm = a, lows = low, + highs = high, maxIters = maxIters, maxSwaps = maxSwaps + ) + for (i in 1:length(fit$beta)) { + expect_gte(min(fit$beta[[i]]), low - epsilon) + expect_lte(max(fit$beta[[i]]), high + epsilon) + } + } + } + } +}) + + +test_that("L0Learn respects vector bounds", { + p <- dim(X)[[2]] + bounds <- rnorm(p, 0, .5) + lows <- -(bounds^2) - .01 + highs <- (bounds^2) + .01 + for (m in list(X, X_sparse)) { + fit <- L0Learn.fit(m, y, intercept = FALSE, lows = lows, highs = highs) + for (i in 1:ncol(fit$beta[[1]])) { + expect_true(all(lows - 1e-9 <= fit$beta[[1]][, i])) + expect_true(all(fit$beta[[1]][, i] <= highs + 1e-9)) + } + } +}) + +find <- function(x, inside) { + which(sapply(inside, FUN = function(X) x %in% X), arr.ind = TRUE) +} + +clamp <- function(x, lower = -Inf, upper = Inf, ...) { + x[x < lower] <- lower + x[x > upper] <- upper + return(x) +} + +test_that("L0Learn with bounds is better than no-bounds", { + skip_on_cran() + lows <- -.02 + highs <- .02 + fit_wb <- L0Learn.fit(X, y, intercept = FALSE, lows = lows, highs = highs) + fit_nb <- L0Learn.fit(X, y, intercept = FALSE, scaleDownFactor = .8, nLambda = 300) + + for (i in 1:length(fit_wb$suppSize[[1]])) { + nnz_wb <- fit_wb$suppSize[[1]][i] + if (nnz_wb > 10) { # Don't look at NoSelectK + solution_with_same_nnz <- find(nnz_wb, fit_nb$suppSize[[1]])[1] + if (is.finite(solution_with_same_nnz)) { + # If there is a solution in fit_nb that has the same number of nnz. + beta_wb <- fit_wb$beta[[1]][, i] + beta_nb <- clamp(fit_nb$beta[[1]][, solution_with_same_nnz], lows, highs) + + beta_wb <- fit_wb$beta[[1]][, i] + beta_nb <- clamp(fit_nb$beta[[1]][, i], lows, highs) + + r_wb <- y - X %*% beta_wb + r_nb <- y - X %*% beta_nb + + expect_gte(Norm(r_nb), Norm(r_wb)) + } + } + } +}) + +# test_that("L0Learn and glmnet find similar solutions", { +# lows = -0.02 +# highs = 0.02 +# for (i in 1:2){ +# if (i == 1){ +# p = "L0L1" +# alpha = 1 +# } else{ +# p = "L0L2" +# alpha = 0 +# } +# fit_L0 = L0Learn.fit(X, y, penalty = p, autoLambda= FALSE, lambdaGrid = list(c(1e-6)), lows=lows,highs=highs) +# fit_glmnet = glmnet(X, y, alpha = alpha, lower=lows,upper=highs) +# } +# +# } +# fit_L0 = L0Learn.fit(X, y, penalty = "L0L1", lows=-.02,highs=.02) +# fit_glmnet = glmnet(X,y,lower=-.02,upper=.02) +# }) diff --git a/tests/testthat/test_L0Learn_highcorr.R b/R/tests/testthat/test_L0Learn_highcorr.R similarity index 50% rename from tests/testthat/test_L0Learn_highcorr.R rename to R/tests/testthat/test_L0Learn_highcorr.R index ab59d74..bf3b841 100644 --- a/tests/testthat/test_L0Learn_highcorr.R +++ b/R/tests/testthat/test_L0Learn_highcorr.R @@ -3,101 +3,104 @@ library("testthat") library("L0Learn") test_that("CDPSI recovers true support when the correlation is high.", { - skip_on_cran() - n = 200 - p = 1000 - k = 25 + n <- 200 + p <- 1000 + k <- 25 + + tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed = 1, snr = +Inf, base_cor = .95) - tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed=1, snr = +Inf, base_cor=.95) - X <- tmp$X y <- tmp$y B <- tmp$B - + fitCD <- L0Learn.fit(X, y, penalty = "L0") fitSWAPS <- L0Learn.fit(X, y, penalty = "L0", algorithm = "CDPSI") - - k_support_index <- function(l, k){ - # The closest support size to k - sort(abs(l$suppSize[[1]] - k), index.return=TRUE)$ix[1] + + k_support_index <- function(l, k) { + # The closest support size to k + sort(abs(l$suppSize[[1]] - k), index.return = TRUE)$ix[1] } - - + + fitCD_k <- k_support_index(fitCD, k) # Expected to fail expect_false(all(which(B != 0, arr.ind = TRUE) == which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE))) - + fitSWAPS_k <- k_support_index(fitSWAPS, k) - expect_equal(which(B != 0, arr.ind = TRUE), - which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE)) + expect_equal( + which(B != 0, arr.ind = TRUE), + which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE) + ) }) test_that("CDPSI Logistic recovers true support when the correlation is high.", { - skip_on_cran() - n = 1000 - p = 500 - k = 10 - - tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed=1, snr = +Inf, base_cor=.95) - + n <- 1000 + p <- 500 + k <- 10 + + tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed = 1, snr = +Inf, base_cor = .95) + X <- tmp$X y <- sign(tmp$y) B <- tmp$B - - fitCD <- L0Learn.fit(X, y, penalty = "L0", loss="Logistic") - fitSWAPS <- L0Learn.fit(X, y, penalty = "L0", algorithm = "CDPSI", loss="Logistic") - - - k_support_index <- function(l, k){ + + fitCD <- L0Learn.fit(X, y, penalty = "L0", loss = "Logistic") + fitSWAPS <- L0Learn.fit(X, y, penalty = "L0", algorithm = "CDPSI", loss = "Logistic") + + + k_support_index <- function(l, k) { # The closest support size to k - sort(abs(l$suppSize[[1]] - k), index.return=TRUE)$ix[1] + sort(abs(l$suppSize[[1]] - k), index.return = TRUE)$ix[1] } - - + + fitCD_k <- k_support_index(fitCD, k) # Expected to fail - if (length(which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE)) != k){ + if (length(which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE)) != k) { expect_false(FALSE) - } else{ - expect_false(all(which(B != 0, arr.ind = TRUE) == which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE))) + } else { + expect_false(all(which(B != 0, arr.ind = TRUE) == which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE))) } - + fitSWAPS_k <- k_support_index(fitSWAPS, k) - expect_equal(which(B != 0, arr.ind = TRUE), - which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE)) + expect_equal( + which(B != 0, arr.ind = TRUE), + which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE) + ) }) test_that("CDPSI SquaredHinge recovers true support when the correlation is high.", { - skip_on_cran() - n = 1000 - p = 500 - k = 10 - - tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed=1, snr = +Inf, base_cor=.95) - + n <- 1000 + p <- 500 + k <- 10 + + tmp <- L0Learn:::GenSyntheticHighCorr(n, p, k, seed = 1, snr = +Inf, base_cor = .95) + X <- tmp$X y <- sign(tmp$y) B <- tmp$B - - fitCD <- L0Learn.fit(X, y, penalty = "L0", loss="SquaredHinge") - fitSWAPS <- L0Learn.fit(X, y, penalty = "L0", algorithm = "CDPSI", loss="SquaredHinge") - - - k_support_index <- function(l, k){ + + fitCD <- L0Learn.fit(X, y, penalty = "L0", loss = "SquaredHinge") + fitSWAPS <- L0Learn.fit(X, y, penalty = "L0", algorithm = "CDPSI", loss = "SquaredHinge") + + + k_support_index <- function(l, k) { # The closest support size to k - sort(abs(l$suppSize[[1]] - k), index.return=TRUE)$ix[1] + sort(abs(l$suppSize[[1]] - k), index.return = TRUE)$ix[1] } - - + + fitCD_k <- k_support_index(fitCD, k) # Expected to fail - if (length(which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE)) != k){ + if (length(which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE)) != k) { expect_false(FALSE) - } else{ - expect_false(all(which(B != 0, arr.ind = TRUE) == which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE))) + } else { + expect_false(all(which(B != 0, arr.ind = TRUE) == which(fitCD$beta[[1]][, fitCD_k] != 0, arr.ind = TRUE))) } - + fitSWAPS_k <- k_support_index(fitSWAPS, k) - expect_equal(which(B != 0, arr.ind = TRUE), - which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE)) -}) \ No newline at end of file + expect_equal( + which(B != 0, arr.ind = TRUE), + which(fitSWAPS$beta[[1]][, fitSWAPS_k] != 0, arr.ind = TRUE) + ) +}) diff --git a/R/tests/testthat/test_L0Learn_intercept.R b/R/tests/testthat/test_L0Learn_intercept.R new file mode 100644 index 0000000..2a56a47 --- /dev/null +++ b/R/tests/testthat/test_L0Learn_intercept.R @@ -0,0 +1,227 @@ +library("Matrix") +library("testthat") +library("L0Learn") +library("pracma") + +# quad <- function(n, p, k=10, thr=.9){ +# means = runif(p) +# X = matrix(runif(n*p),nrow=n,ncol=p) +# m = matrix(runif(n*p),nrow=n,ncol=p) <= thr +# X[m] <- 0.0 +# B = c(rep(1,k),rep(0,p-k)) +# e = rnorm(n)/100 +# y = ((X - means)**2)%*%B + e +# list(X=X, y = y) +# } + +tmp <- L0Learn::GenSynthetic(n = 50, p = 200, k = 10, seed = 1, rho = 0.5, b0 = 0, snr = +Inf) +Xsmall <- tmp[[1]] +ysmall <- tmp[[2]] +tol <- 1e-4 +if (sum(apply(Xsmall, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") +} + +Xsmall_sparse <- as(Xsmall, "dgCMatrix") + +userLambda <- list() +userLambda[[1]] <- c(logspace(-1, -10, 100)) + +test_that("Intercepts are supported for all losses, algorithims, penalites, and matrix types", { + skip_on_cran() + # Try all losses + for (p in c("L0", "L0L1", "L0L2")) { + L0Learn.fit(Xsmall_sparse, ysmall, penalty = p, intercept = TRUE) + L0Learn.cvfit(Xsmall_sparse, ysmall, penalty = p, nFolds = 2, intercept = TRUE) + } + + for (a in c("CD", "CDPSI")) { + L0Learn.fit(Xsmall_sparse, ysmall, algorithm = a, intercept = TRUE) + L0Learn.cvfit(Xsmall_sparse, ysmall, algorithm = a, nFolds = 2, intercept = TRUE) + } + + for (l in c("Logistic", "SquaredHinge")) { + L0Learn.fit(Xsmall_sparse, sign(ysmall), loss = l, intercept = TRUE) + L0Learn.cvfit(Xsmall_sparse, ysmall, algorithm = a, nFolds = 2, intercept = TRUE) + } + succeed() +}) + +test_that("Intercepts for Sparse Matricies are deterministic", { + skip_on_cran() + # Try all losses + for (p in c("L0", "L0L1", "L0L2")) { + set.seed(1) + x1 <- L0Learn.fit(Xsmall_sparse, ysmall, penalty = p) + set.seed(1) + x2 <- L0Learn.fit(Xsmall_sparse, ysmall, penalty = p) + expect_equal(x1$a0, x2$a0, info = p) + } + + for (a in c("CD", "CDPSI")) { + set.seed(1) + x1 <- L0Learn.fit(Xsmall_sparse, ysmall, algorithm = a) + set.seed(1) + x2 <- L0Learn.fit(Xsmall_sparse, ysmall, algorithm = a) + expect_equal(x1$a0, x2$a0, info = a) + } + + for (l in c("Logistic", "SquaredHinge")) { + set.seed(1) + x1 <- L0Learn.fit(Xsmall_sparse, sign(ysmall), loss = l) + set.seed(1) + x2 <- L0Learn.fit(Xsmall_sparse, sign(ysmall), loss = l) + expect_equal(x1$a0, x2$a0, info = l) + } +}) + +test_that("Intercepts are passed between Swap iterations", { + skip_on_cran() + # TODO : Implement test case +}) + +tmp <- L0Learn::GenSynthetic(n = 100, p = 1000, k = 10, seed = 1, rho = 1.5, b0 = 0) +X <- tmp[[1]] +y <- tmp[[2]] +tol <- 1e-4 + +if (sum(apply(X, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") +} + +X_sparse <- as(X, "dgCMatrix") + +test_that("When lambda0 is large, intecepts should be found similar for both sparse and dense methods", { + skip_on_cran() + BIGuserLambda <- list() + BIGuserLambda[[1]] <- c(logspace(2, -2, 10)) + + # TODO: Prevent crash if lambdaGrid is not "acceptable. + for (a in c("CD", "CDPSI")) { + set.seed(1) + x1 <- L0Learn.fit(X_sparse, y, + penalty = "L0", intercept = TRUE, algorithm = a, + autoLambda = FALSE, lambdaGrid = BIGuserLambda, maxSuppSize = 100 + ) + set.seed(1) + x2 <- L0Learn.fit(X, y, + penalty = "L0", intercept = TRUE, algorithm = a, + autoLambda = FALSE, lambdaGrid = BIGuserLambda, maxSuppSize = 100 + ) + + for (i in 1:length(x1$a0)) { + if ((x1$suppSize[[1]][i] == 0) && (x2$suppSize[[1]][i] == 0)) { + expect_equal(x1$a0[[1]][i], x2$a0[[1]][i]) + } else if (x1$suppSize[[1]][i] == x2$suppSize[[1]][i]) { + expect_equal(x1$a0[[1]][i], x2$a0[[1]][i], tolerance = 1e-6, scale = x1$a0[[1]][i]) + } + } + } +}) + +# test_that("Intercepts achieve a lower insample-error", { +# skip_on_cran() +# +# for (a in c("CD", "CDPSI")){ +# y_scaled = y*2 + 10 +# set.seed(1) +# x1 <- L0Learn.fit(X_sparse, y_scaled, penalty="L0", intercept = TRUE, +# algorithm = a, +# autoLambda=FALSE, lambdaGrid=userLambda, maxSuppSize=100) +# set.seed(1) +# x2 <- L0Learn.fit(X_sparse, y_scaled, penalty="L0", intercept = FALSE, +# algorithm = a, +# autoLambda=FALSE, lambdaGrid=userLambda, maxSuppSize=100) +# +# min_length = min(length(x1$a0[[1]]), length(x1$a0[[1]])) +# for (i in 1:min_length){ +# if (TRUE){ # x1$suppSize[[1]][i] >= x2$suppSize[[1]][i] +# x1_loss = norm(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i] - y_scaled, '2') +# x2_loss = norm(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i] - y_scaled, '2') +# expect_lte(x1_loss, x2_loss) +# } +# } +# +# logistic <- function(x){1/(1+exp(-x))}; +# logit <- sum(log(logistic)) +# +# x1 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = TRUE, +# algorithm = a, +# loss = "Logistic", autoLambda=FALSE, lambdaGrid=userLambda, +# maxSuppSize=1000) +# x2 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = FALSE, +# algorithm = a, +# loss = "Logistic", autoLambda=FALSE, lambdaGrid=userLambda, +# maxSuppSize=1000) +# +# for (i in 1:min_length){ +# +# x1_loss = sum(sign(y)*logistic(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i])) # more 1s +# x2_loss = sum(sign(y)*logistic(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i])) # more -1s +# print(paste(i, x1_loss - x2_loss)) +# #expect_lt(x1_loss, x2_loss) +# } +# +# squaredHinge <- function(y, yhat){max(0, 1-y*yhat)**2} +# +# x1 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = TRUE, +# algorithm = a, +# loss = "SquaredHinge", autoLambda=FALSE, lambdaGrid=userLambda, +# maxSuppSize=1000) +# x2 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = FALSE, +# algorithm = a, +# loss = "SquaredHinge", autoLambda=FALSE, lambdaGrid=userLambda, +# maxSuppSize=1000) +# +# for (i in 1:min_length){ +# x1_loss = sum(squaredHinge(sign(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i]), sign(y))) +# x2_loss = sum(squaredHinge(sign(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i]), sign(y))) +# #print(paste(i, x1_loss - x2_loss)) +# expect_lt(x1_loss, x2_loss) +# } +# } +# }) + +test_that("Intercepts are learned close to real values", { + skip_on_cran() + fineuserLambda <- list() + fineuserLambda[[1]] <- c(logspace(-1, -10, 100)) + + k <- 10 + for (a in c("CD", "CDPSI")) { + for (b0 in c(-100, -10, -2, 2, 10, 100)) { + tmp <- L0Learn::GenSynthetic(n = 5000, p = 200, k = k, seed = 1, rho = 0, b0 = b0) + X2 <- tmp[[1]] + y2 <- tmp[[2]] + + tol <- 1e-4 + if (sum(apply(X2, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") + } + X2_sparse <- as(X2, "dgCMatrix") + + x1 <- L0Learn.fit(X2_sparse, y2, penalty = "L0", intercept = TRUE, algorithm = a) + # autoLambda=FALSE, lambdaGrid=fineuserLambda, maxSuppSize=1000) + + x2 <- L0Learn.fit(X2, y2, penalty = "L0", intercept = TRUE, algorithm = a) + # autoLambda=FALSE, lambdaGrid=fineuserLambda, maxSuppSize=1000) + y2_mean <- mean(y2) + for (j in 1:length(x1$suppSize)) { + for (i in 1:length(x1$suppSize[[1]])) { + if (x1$suppSize[[j]][i] == k) { + expect_lt(abs(x1$a0[[j]][i] - b0), abs(2 * (abs(b0) - abs(y2_mean)))) + # print(paste(abs(x1$a0[[j]][i] - b0), abs(2*(abs(b0) - abs(y2_mean))))) + } + } + } + + for (j in 1:length(x1$suppSize)) { + for (i in 1:length(x2$suppSize[[j]])) { + if (x2$suppSize[[j]][i] == k) { + expect_lt(abs(x2$a0[[j]][i] - b0), abs(2 * (abs(b0) - abs(y2_mean)))) + } + } + } + } + } +}) diff --git a/R/tests/testthat/test_L0Learn_usergrids.R b/R/tests/testthat/test_L0Learn_usergrids.R new file mode 100644 index 0000000..fb93a9a --- /dev/null +++ b/R/tests/testthat/test_L0Learn_usergrids.R @@ -0,0 +1,199 @@ +library("testthat") +library("L0Learn") + +tmp <- L0Learn::GenSynthetic(n = 100, p = 1000, k = 10, seed = 1) +X <- tmp[[1]] +y <- tmp[[2]] + +test_that("L0Learn L0 grid works", { + skip_on_cran() + userLambda <- list() + userLambda[[1]] <- c(10, 1, 0.1, 0.01) + x1 <- L0Learn.fit(X, y, + penalty = "L0", + lambdaGrid = userLambda + ) + + expect_equal(length(x1$lambda[[1]]), 4) + for (l in c("SquaredError", "SquaredHinge")) { + x1 <- L0Learn.fit(X, sign(y), + penalty = "L0", loss = l, + lambdaGrid = userLambda + ) + expect_equal(length(x1$lambda[[1]]), 4) + + x1 <- L0Learn.fit(X, sign(y), + penalty = "L0", loss = l, + lambdaGrid = userLambda + ) + expect_equal(length(x1$lambda[[1]]), 4) + } +}) + +test_that("L0Learn L0 fails on bad userLambda", { + skip_on_cran() + userLambda <- list() + userLambda[[1]] <- c(10, 11, 0.1, 0.01) + f1 <- function() { + L0Learn.fit(X, y, + penalty = "L0", + lambdaGrid = userLambda + ) + } + expect_error(f1()) + + for (l in c("SquaredError", "SquaredHinge")) { + f2 <- function() { + L0Learn.fit(X, sign(y), + penalty = "L0", loss = l, + lambdaGrid = userLambda + ) + } + expect_error(f2()) + + f3 <- function() { + L0Learn.fit(X, sign(y), + penalty = "L0", loss = l, + lambdaGrid = userLambda + ) + } + expect_error(f3()) + } +}) + +test_that("L0Learn L0 grid ignores nGamma ", { + skip_on_cran() + userLambda <- list() + userLambda[[1]] <- c(10, 1, 0.1, 0.01) + x1 <- L0Learn.fit(X, y, + penalty = "L0", nGamma = 1, + lambdaGrid = userLambda + ) + + expect_equal(length(x1$lambda), 1) + expect_equal(length(x1$lambda[[1]]), 4) +}) + + +test_that("L0Learn L0L1/2 grid works", { + skip_on_cran() + userLambda <- list() + userLambda[[1]] <- c(10, 1, 0.1, 0.01) + userLambda[[2]] <- c(11, 1.1, 0.11, 0.011, 0.0011) + userLambda[[3]] <- c(12, 1.2, 0.12) + x1 <- L0Learn.fit(X, y, + penalty = "L0L1", + lambdaGrid = userLambda, nGamma = 3 + ) + + expect_equal(length(x1$lambda), 3) + expect_equal(length(x1$lambda[[1]]), 4) + expect_equal(length(x1$lambda[[2]]), 5) + expect_equal(length(x1$lambda[[3]]), 3) + + x1 <- L0Learn.fit(X, y, + penalty = "L0L2", + lambdaGrid = userLambda, nGamma = 3 + ) + + expect_equal(length(x1$lambda), 3) + expect_equal(length(x1$lambda[[1]]), 4) + expect_equal(length(x1$lambda[[2]]), 5) + expect_equal(length(x1$lambda[[3]]), 3) + + for (l in c("SquaredError", "SquaredHinge")) { + x1 <- L0Learn.fit(X, sign(y), + penalty = "L0L1", loss = l, + lambdaGrid = userLambda, nGamma = 3 + ) + expect_equal(length(x1$lambda), 3) + expect_equal(length(x1$lambda[[1]]), 4) + expect_equal(length(x1$lambda[[2]]), 5) + expect_equal(length(x1$lambda[[3]]), 3) + + x1 <- L0Learn.fit(X, sign(y), + penalty = "L0L2", loss = l, + lambdaGrid = userLambda, nGamma = 3, maxSuppSize = 1000 + ) + expect_equal(length(x1$lambda), 3) + expect_equal(length(x1$lambda[[1]]), 4) + expect_equal(length(x1$lambda[[2]]), 5) + expect_equal(length(x1$lambda[[3]]), 3) + } + + + succeed() +}) + +test_that("L0Learn L0L1/2 ignores with wrong nGamma in v2.0.0", { + skip_on_cran() + # This changed between v1.2.0 and v2.0.0 + userLambda <- list() + userLambda[[1]] <- c(10, 1, 0.1, 0.01) + userLambda[[2]] <- c(11, 1.1, 0.11, 0.011, 0.0011) + userLambda[[3]] <- c(12, 1.2, 0.12) + + f1 <- function() { + L0Learn.fit(X, y, penalty = "L0L1", lambdaGrid = userLambda, nGamma = 4) + } + f2 <- function() { + L0Learn.fit(X, y, penalty = "L0L2", lambdaGrid = userLambda, nGamma = 4) + } + + if (packageVersion("L0Learn") >= "2.0.0") { + f1() + f2() + succeed() + } else { + expect_error(f1()) + expect_error(f2()) + } + + for (l in c("SquaredError", "SquaredHinge")) { + f1 <- function() { + L0Learn.fit(X, sign(y), penalty = "L0L1", loss = l, lambdaGrid = userLambda, nGamma = 4) + } + f2 <- function() { + L0Learn.fit(X, sign(y), penalty = "L0L2", loss = l, lambdaGrid = userLambda, nGamma = 4) + } + + if (packageVersion("L0Learn") >= "2.0.0") { + f1() + f2() + succeed() + } else { + expect_error(f1()) + expect_error(f2()) + } + } +}) + +test_that("L0Learn L0L1/2 grid fails with bad userLambda", { + skip_on_cran() + userLambda <- list() + userLambda[[1]] <- c(10, 1, 0.1, 0.01) + userLambda[[2]] <- c(11, 12, 0.11, 0.011, 0.0011) + userLambda[[3]] <- c(12, 1.2, 0.12) + + f1 <- function() { + L0Learn.fit(X, y, penalty = "L0L1", lambdaGrid = userLambda, nGamma = 3) + } + expect_error(f1()) + + f2 <- function() { + L0Learn.fit(X, y, penalty = "L0L2", lambdaGrid = userLambda, nGamma = 3) + } + expect_error(f2()) + + for (l in c("SquaredError", "SquaredHinge")) { + f1 <- function() { + L0Learn.fit(X, sign(y), penalty = "L0L1", loss = l, lambdaGrid = userLambda, nGamma = 3) + } + expect_error(f1()) + + f2 <- function() { + L0Learn.fit(X, sign(y), penalty = "L0L2", loss = l, lambdaGrid = userLambda, nGamma = 3) + } + expect_error(f2()) + } +}) diff --git a/R/tests/testthat/test_L0Learn_utils.R b/R/tests/testthat/test_L0Learn_utils.R new file mode 100644 index 0000000..da71fe9 --- /dev/null +++ b/R/tests/testthat/test_L0Learn_utils.R @@ -0,0 +1,190 @@ +library("Matrix") +library("testthat") +library("L0Learn") + +tmp <- L0Learn::GenSynthetic(n = 1000, p = 500, k = 10, seed = 1, rho = 1) +X <- tmp[[1]] +y <- tmp[[2]] + 1 +tol <- 1e-4 + +if (sum(apply(X, 2, sd) == 0)) { + stop("X needs to have non-zero std for each column") +} + +X_sparse <- as(X, "dgCMatrix") + +test_that("matrix_column_get dense", { + skip_on_cran() + x1 <- as.matrix(X[, 1]) + x2 <- .Call("_L0Learn_R_matrix_column_get_dense", X, 0) # C++ and R use different indexes + expect_equal(x1, x2) +}) + +test_that("matrix_column_get sparse", { + skip_on_cran() + x1 <- as.matrix(X_sparse[, 1]) + x2 <- .Call("_L0Learn_R_matrix_column_get_sparse", X_sparse, 0) # C++ and R use different indexes + expect_equal(x1, x2) +}) + +test_that("matrix_rows_get dense", { + skip_on_cran() + x1 <- X[1:4, ] + x2 <- .Call("_L0Learn_R_matrix_rows_get_dense", X, 0:3) + expect_equal(x1, x2) +}) + +test_that("matrix_rows_get sparse", { + skip_on_cran() + x1 <- X_sparse[1:4, ] + x2 <- .Call("_L0Learn_R_matrix_rows_get_sparse", X_sparse, 0:3) + expect_equal(x1, x2) +}) + +test_that("matrix_vector_schur_produce dense", { + skip_on_cran() + x1 <- X * as.vector(y) + x2 <- .Call("_L0Learn_R_matrix_vector_schur_product_dense", X, y) + expect_equal(x1, x2) +}) + +test_that("matrix_vector_schur_produce sparse", { + skip_on_cran() + x1 <- X_sparse * as.vector(y) + x2 <- .Call("_L0Learn_R_matrix_vector_schur_product_sparse", X_sparse, y) + expect_equal(x1, x2) +}) + + +test_that("matrix_vector_divide dense", { + skip_on_cran() + x1 <- X / as.vector(y) + x2 <- .Call("_L0Learn_R_matrix_vector_divide_dense", X, y) + expect_equal(x1, x2) +}) + +test_that("matrix_vector_divide sparse", { + skip_on_cran() + x1 <- X_sparse / as.vector(y) + x2 <- .Call("_L0Learn_R_matrix_vector_divide_sparse", X_sparse, y) + expect_equal(x1, x2) +}) + +test_that("matrix_column_sums dense", { + skip_on_cran() + x1 <- colSums(X) + x2 <- as.vector(.Call("_L0Learn_R_matrix_column_sums_dense", X)) + expect_equal(x1, x2) +}) + +test_that("matrix_column_sums sparse", { + skip_on_cran() + x1 <- colSums(X_sparse) + x2 <- as.vector(.Call("_L0Learn_R_matrix_column_sums_sparse", X_sparse)) + expect_equal(x1, x2) +}) + +test_that("matrix_column_dot dense", { + skip_on_cran() + x1 <- X[, 1] %*% y + x2 <- .Call("_L0Learn_R_matrix_column_dot_dense", X, 0, y) + expect_equal(as.double(x1), as.double(x2)) +}) + +test_that("matrix_column_dot sparse", { + skip_on_cran() + x1 <- X_sparse[, 1] %*% y + x2 <- .Call("_L0Learn_R_matrix_column_dot_sparse", X_sparse, 0, y) + expect_equal(as.double(x1), as.double(x2)) +}) + +test_that("matrix_column_mult dense", { + skip_on_cran() + c <- 3.14 + x1 <- X[, 1] * c + x2 <- .Call("_L0Learn_R_matrix_column_mult_dense", X, 0, c) + expect_equal(as.double(x1), as.double(x2)) +}) + +test_that("matrix_column_mult sparse", { + skip_on_cran() + c <- 3.14 + x1 <- X_sparse[, 1] * c + x2 <- .Call("_L0Learn_R_matrix_column_mult_sparse", X_sparse, 0, c) + expect_equal(as.double(x1), as.double(x2)) +}) + +center_colmeans <- function(x) { + skip_on_cran() + xcenter <- colMeans(x) + x - rep(xcenter, rep.int(nrow(x), ncol(x))) +} + +colNorms <- function(x) { + apply(x, 2, function(x) { + sqrt(sum(x^2)) + }) +} + +test_that("matrix_normalize dense", { + skip_on_cran() + for (norm in c(TRUE, FALSE)) { + if (norm) { + X_norm <- center_colmeans(X) + expect_equal(colMeans(X_norm), 0 * colMeans(X_norm)) + } else { + X_norm <- as.matrix(X) + } + X_norm_copy <- as.matrix(X_norm) + expect_equal(X_norm, X_norm_copy) + + x1 <- .Call("_L0Learn_R_matrix_normalize_dense", X_norm) + + expect_equal(X_norm, X_norm_copy) # R should not modify X_norm + + expect_equal(colNorms(X_norm), as.vector(x1$ScaleX)) + expect_equal(X_norm %*% diag(1 / colNorms(X_norm)), x1$mat_norm) + } +}) + +test_that("matrix_normalize sparse", { + skip_on_cran() + X_norm <- as(X, "dgCMatrix") + X_norm_copy <- as(X, "dgCMatrix") + + expect_equal(X_norm, X_norm_copy) + + x1 <- .Call("_L0Learn_R_matrix_normalize_sparse", X_norm) + + expect_equal(X_norm, X_norm_copy) # R should not modify X_norm + + expect_equal(colNorms(X_sparse), as.vector(x1$ScaleX)) + expect_equal(as.matrix(X_norm %*% diag(1 / colNorms(X_sparse))), as.matrix(x1$mat_norm)) +}) + +test_that("matrix_center dense", { + skip_on_cran() + for (intercept in c(TRUE, FALSE)) { + x_norm <- 0 * X + x1 <- .Call("_L0Learn_R_matrix_center_dense", X, x_norm, intercept) + + if (intercept) { + expect_equal(as.vector(x1$MeanX), colMeans(X)) + expect_equal(x1$mat_norm, center_colmeans(X)) + } else { + expect_equal(as.vector(x1$MeanX), 0 * colMeans(X)) + expect_equal(x1$mat_norm, X) + } + } +}) + + +test_that("matrix_center sparse", { + skip_on_cran() + for (intercept in c(TRUE, FALSE)) { + x_norm <- 0 * X_sparse + x1 <- .Call("_L0Learn_R_matrix_center_sparse", X_sparse, x_norm, intercept) + expect_equal(as.vector(x1$MeanX), 0 * colMeans(X)) + expect_equal(x1$mat_norm, X_sparse) + } +}) diff --git a/vignettes/L0Learn-vignette.Rmd b/R/vignettes/L0Learn-vignette.Rmd similarity index 100% rename from vignettes/L0Learn-vignette.Rmd rename to R/vignettes/L0Learn-vignette.Rmd diff --git a/vignettes/profile/L0Learn_Profile.R b/R/vignettes/profile/L0Learn_Profile.R similarity index 100% rename from vignettes/profile/L0Learn_Profile.R rename to R/vignettes/profile/L0Learn_Profile.R diff --git a/vignettes/profile/L0Learn_Profile_Install.R b/R/vignettes/profile/L0Learn_Profile_Install.R similarity index 100% rename from vignettes/profile/L0Learn_Profile_Install.R rename to R/vignettes/profile/L0Learn_Profile_Install.R diff --git a/vignettes/profile/L0Learn_Profile_Run.R b/R/vignettes/profile/L0Learn_Profile_Run.R similarity index 100% rename from vignettes/profile/L0Learn_Profile_Run.R rename to R/vignettes/profile/L0Learn_Profile_Run.R diff --git a/vignettes/profile/L0Learn_Profile_Run.py b/R/vignettes/profile/L0Learn_Profile_Run.py similarity index 61% rename from vignettes/profile/L0Learn_Profile_Run.py rename to R/vignettes/profile/L0Learn_Profile_Run.py index 2e14ceb..e7a9122 100644 --- a/vignettes/profile/L0Learn_Profile_Run.py +++ b/R/vignettes/profile/L0Learn_Profile_Run.py @@ -5,15 +5,26 @@ CMD_BASE = "mprof run -o {o}.dat Rscript L0Learn_Profile.R --n {n} --p {p} --k {k} --s {s} --t {t} --w {w} --m {m} --f {f}" -file_name = 'test_run3' +file_name = "test_run3" -run = {"n":1000, "p":10000, "k":10, "s":1, "t":2.1, "w":4, "m":1, "f":file_name, "o":file_name} +run = { + "n": 1000, + "p": 10000, + "k": 10, + "s": 1, + "t": 2.1, + "w": 4, + "m": 1, + "f": file_name, + "o": file_name, +} cmd = CMD_BASE.format(**run) -os.system(cmd) # Creates .dat and .csv files in same directory as file. +os.system( + cmd +) # Creates .dat and .csv files in same directory as file. # This cmd will often error out for no reason. # https://github.com/pythonprofilers/memory_profiler/issues/240 memory_usage = read_mprofile_file(file_name + ".dat") timing = pd.read_csv(file_name + ".csv") - diff --git a/vignettes/profile/ProfilePlotting.ipynb b/R/vignettes/profile/ProfilePlotting.ipynb similarity index 100% rename from vignettes/profile/ProfilePlotting.ipynb rename to R/vignettes/profile/ProfilePlotting.ipynb diff --git a/README.md b/README.md index 9973f31..f372ad4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ L0Learn is a highly efficient framework for solving L0-regularized learning prob We support both regression (using squared error loss) and classification (using logistic or squared hinge loss). Optimization is done using coordinate descent and local combinatorial search over a grid of regularization parameter(s) values. Several computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter λ in the path. We describe the details of the algorithms in our paper: *Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms* ([link](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919)). -The toolkit is implemented in C++11 and can often run faster than popular sparse learning toolkits (see our experiments in the paper above). We also provide an easy-to-use R interface; see the section below for installation and usage of the R package. +The toolkit is implemented in C++11 and can often run faster than popular sparse learning toolkits (see our experiments in the paper above). We also provide an easy-to-use R and Pythoninterface; see the section below for installation and usage of the package. **NEW: Version 2 (03/2021) adds support for sparse matrices and box constraints on the coefficients.** @@ -29,15 +29,31 @@ install_github("hazimehh/L0Learn") ``` L0Learn's changelog can be accessed from [here](https://github.com/hazimehh/L0Learn/blob/master/ChangeLog). +## Python Package Installation +The latest version (v0.1.0) can be installed from PIP as follows: +```{python} +pip install l0learn +``` +Alternative, L0learn can also be built from source as follows: +```{bash} +git clone https://github.com/hazimehh/L0Learn.git +cd L0Learn +pip install . +``` + + ## Usage +### R Usage For a tutorial, please refer to [L0Learn's Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html). For a detailed description of the API, check the [Reference Manual](https://cran.r-project.org/web/packages/L0Learn/L0Learn.pdf). +### Python Usage +For a tutorial and detailed documentation, please refer to [l0learn's documetnation](https://tnonet.github.io/L0Learn/). ## FAQ #### Which penalty to use? -Pure L0 regularization can overfit when the signal strength in the data is relatively low. Adding L2 regularization can alleviate this problem and lead to competitive models (see the experiments in our paper). Thus, in practice, **we strongly recommend using the L0L2 penalty**. Ideally, the parameter gamma (for L2 regularization) should be tuned over a sufficiently large interval, and this can be performed using L0Learn's built-in [cross-validation method](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#cross-validation). +Pure L0 regularization can overfit when the signal strength in the data is relatively low. Adding L2 regularization can alleviate this problem and lead to competitive models (see the experiments in our paper). Thus, in practice, **we strongly recommend using the L0L2 penalty**. Ideally, the parameter gamma (for L2 regularization) should be tuned over a sufficiently large interval, and this can be performed using L0Learn's built-in [R cross-validation method)](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#cross-validation) or [Python cross-validation method](MISSING). #### Which algorithm to use? -By default, L0Learn uses a coordinate descent-based algorithm, which achieves competitive run times compared to popular sparse learning toolkits. This can work well for many applications. We also offer a local search algorithm which is guarantteed to return higher quality solutions, at the expense of an increase in the run time. We recommend using the local search algorithm if the problem has highly correlated features or the number of samples is much smaller than the number of features---see the [local search section of the Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#higher-quality_solutions_using_local_search) for how to use this algorithm. +By default, L0Learn uses a coordinate descent-based algorithm, which achieves competitive run times compared to popular sparse learning toolkits. This can work well for many applications. We also offer a local search algorithm which is guarantteed to return higher quality solutions, at the expense of an increase in the run time. We recommend using the local search algorithm if your problem has highly correlated features or the number of samples is much smaller than the number of features---see the [R local search section of the Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#higher-quality_solutions_using_local_search) or [Python local search section of the Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#higher-quality_solutions_using_local_search)for how to use this algorithm. #### How to certify optimality? While for many challenging statistical instances L0Learn leads to optimal solutions, it cannot provide certificates of optimality. Such certificates can be provided via Integer Programming. Our toolkit [L0BnB](https://github.com/alisaab/l0bnb) is a scalable integer programming framework for L0-regularized regression, which can provide such certificates and potentially improve upon the solutions of L0Learn (if they are sub-optimal). We recommend using L0Learn first to obtain a candidtate solution (or a pool of solutions) and then checking optimality using L0BnB. diff --git a/cleanup b/cleanup deleted file mode 100755 index f3de94e..0000000 --- a/cleanup +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -rm -f config.* src/Makevars src/*.o diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/_images/tutorial_18_0.png b/docs/_images/tutorial_18_0.png new file mode 100644 index 0000000..69d2175 Binary files /dev/null and b/docs/_images/tutorial_18_0.png differ diff --git a/docs/_images/tutorial_20_1.png b/docs/_images/tutorial_20_1.png new file mode 100644 index 0000000..9e9aead Binary files /dev/null and b/docs/_images/tutorial_20_1.png differ diff --git a/docs/_images/tutorial_30_2.png b/docs/_images/tutorial_30_2.png new file mode 100644 index 0000000..93e1854 Binary files /dev/null and b/docs/_images/tutorial_30_2.png differ diff --git a/docs/_images/tutorial_36_1.png b/docs/_images/tutorial_36_1.png new file mode 100644 index 0000000..0974195 Binary files /dev/null and b/docs/_images/tutorial_36_1.png differ diff --git a/docs/_images/tutorial_57_1.png b/docs/_images/tutorial_57_1.png new file mode 100644 index 0000000..2513307 Binary files /dev/null and b/docs/_images/tutorial_57_1.png differ diff --git a/docs/_images/tutorial_61_2.png b/docs/_images/tutorial_61_2.png new file mode 100644 index 0000000..09e6089 Binary files /dev/null and b/docs/_images/tutorial_61_2.png differ diff --git a/docs/_sources/code.rst.txt b/docs/_sources/code.rst.txt new file mode 100644 index 0000000..fe9f1e0 --- /dev/null +++ b/docs/_sources/code.rst.txt @@ -0,0 +1,39 @@ +`fit` function +---------------- + +.. autofunction:: l0learn.fit + + +`cvfit` function +---------------- + +.. autofunction:: l0learn.cvfit + + +FitModels +--------- +.. autoclass:: l0learn.models.FitModel + + +CVFitModels +----------- +.. autoclass:: l0learn.models.CVFitModel + + +Generating Functions +-------------------- +.. autofunction:: l0learn.models.gen_synthetic +.. autofunction:: l0learn.models.gen_synthetic_high_corr +.. autofunction:: l0learn.models.gen_synthetic_logistic + +Scoring Functions +-------------------- +These functions are called by :py:meth:`l0learn.models.FitModel.score` and :py:meth:`l0learn.models.CVFitModel.score`. + +.. autofunction:: l0learn.models.regularization_loss +.. autofunction:: l0learn.models.squared_error +.. autofunction:: l0learn.models.logistic_loss +.. autofunction:: l0learn.models.squared_hinge_loss + + + diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt new file mode 100644 index 0000000..18e9975 --- /dev/null +++ b/docs/_sources/index.rst.txt @@ -0,0 +1,21 @@ +.. l0learn documentation master file, created by + sphinx-quickstart on Wed Nov 17 07:46:32 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to l0learn's documentation! +=================================== + +.. toctree:: + :maxdepth: 2 + + tutorial.ipynb + code + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/_sources/tutorial.ipynb.txt b/docs/_sources/tutorial.ipynb.txt new file mode 100644 index 0000000..fce1fac --- /dev/null +++ b/docs/_sources/tutorial.ipynb.txt @@ -0,0 +1,1338 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Introduction\n", + "`l0learn` is a fast toolkit for L0-regularized learning. L0 regularization selects the best subset of features and can outperform commonly used feature selection methods (e.g., L1 and MCP) under many sparse learning regimes. The toolkit can (approximately) solve the following three problems\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 \\quad \\quad (L0) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_1 \\quad (L0L1) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_2^2 \\quad (L0L2)\n", + "\\end{equation}\n", + "\n", + "where $\\ell$ is the loss function, $\\beta_0$ is the intercept, $\\beta$ is the vector of coefficients, and $||\\beta||_0$ denotes the L0 norm of $\\beta$, i.e., the number of non-zeros in $\\beta$. We support both regression and classification using either one of the following loss functions:\n", + "\n", + "* Squared error loss\n", + "* Logistic loss (logistic regression)\n", + "* Squared hinge loss (smoothed version of SVM).\n", + "\n", + "The parameter $\\lambda$ controls the strength of the L0 regularization (larger $\\lambda$ leads to less non-zeros). The parameter $\\gamma$ controls the strength of the shrinkage component (which is the L1 norm in case of L0L1 or squared L2 norm in case of L0L2); adding a shrinkage term to L0 can be very effective in avoiding overfitting and typically leads to better predictive models. The fitting is done over a grid of $\\lambda$ and $\\gamma$ values to generate a regularization path. \n", + "\n", + "The algorithms provided in l0learn` are based on cyclic coordinate descent and local combinatorial search. Many computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter $\\lambda$ in the path. For more details on the algorithms used, please refer to our paper [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919).\n", + "\n", + "The toolkit is implemented in C++ along with an easy-to-use Python interface. In this vignette, we provide a tutorial on using the Python interface. Particularly, we will demonstrate how use L0Learn's main functions for fitting models, cross-validation, and visualization.\n", + "\n", + "# Installation\n", + "L0Learn can be installed directly from pip by executing:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "```console\n", + "pip install l0learn\n", + "```" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "If you face installation issues, please refer to the [Installation Troubleshooting Wiki](https://github.com/hazimehh/L0Learn/wiki/Installation-Troubleshooting). If the issue is not resolved, you can submit an issue on [L0Learn's Github Repo](https://github.com/hazimehh/L0Learn).\n", + "\n", + "# Tutorial\n", + "To demonstrate how `l0learn` works, we will first generate a synthetic dataset and then proceed to fitting L0-regularized models. The synthetic dataset (y,X) will be generated from a sparse linear model as follows:\n", + "\n", + "* X is a 500x1000 design matrix with iid standard normal entries\n", + "* B is a 1000x1 vector with the first 10 entries set to 1 and the rest are zeros.\n", + "* e is a 500x1 vector with iid standard normal entries\n", + "* y is a 500x1 response vector such that y = XB + e\n", + "\n", + "This dataset can be generated in python as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 1, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = X@B + e" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "We will use `l0learn` to estimate B from the data (y,X). First we load L0Learn:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [], + "source": [ + "import l0learn" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We will start by fitting a simple L0 model and then proceed to the case of L0L2 and L0L1.\n", + "\n", + "## Fitting L0 Regression Models\n", + "To fit a path of solutions for the L0-regularized model with at most 20 non-zeros using coordinate descent (CD), we use the [l0learn.fit](code.rst#l0learn.models.gen_synthetic) function as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [], + "source": [ + "fit_model = l0learn.fit(X, y, penalty=\"L0\", max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "This will generate solutions for a sequence of $\\lambda$ values (chosen automatically by the algorithm). To view the sequence of $\\lambda$ along with the associated support sizes (i.e., the number of non-zeros), we use the built-in rich display from [ipython Rich Display](https://ipython.readthedocs.io/en/stable/config/integrating.html+) for iPython Notebooks. When running this tutorial in a more standard python environment, use the function [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) to display the sequence of solutions." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.0795460-0.156704True
10.0787501-0.147182True
20.0658622-0.161024True
30.0504643-0.002500True
40.0445175-0.041058True
50.0416727-0.058013True
60.0397058-0.061685True
70.032715100.002157True
80.00021211-0.000857True
90.00018712-0.002161True
100.00017813-0.001199True
110.00015915-0.007959True
120.00014116-0.009603True
130.00013318-0.015697True
140.00013221-0.012732True
\n
" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model\n", + "# fit_model.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To extract the estimated B for particular values of $\\lambda$ and $\\gamma$, we use the function [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff). For example, the solution at $\\lambda = 0.032715$ (which corresponds to a support size of 10 + plus an intercept term) can be extracted using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 6, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model.coeff(lambda_0=0.032715, gamma=0)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The output is a sparse matrix of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). Depending on the `include_intercept` parameter of [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff), The first element in the vector is the intercept and the rest are the B coefficients. Aside from the intercept, the only non-zeros in the above solution are coordinates 0, 1, 2, 3, ..., 9, which are the non-zero coordinates in the true support (used to generated the data). Thus, this solution successfully recovers the true support. Note that on some BLAS implementations, the `lambda` value we used above (i.e., `0.032715`) might be slightly different due to the limitations of numerical precision. Moreover, all the solutions in the regularization path can be extracted at once by calling [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) without specifying a `lambda_0` or `gamma` value.\n", + "\n", + "The sequence of $\\lambda$ generated by `L0Learn` is stored in the object `fit_model`. Specifically, `fit_model.lambda_0` is a list, where each element of the list is a sequence of $\\lambda$ values corresponding to a single value of $\\gamma$. When using an L0 penalty , which has only one value of $\\gamma$ (i.e., 0), we can access the sequence of $\\lambda$ values using `fit.lambda_0[0]`. Thus, $\\lambda=0.032715$ we used previously can be accessed using `fit_model.lambda_0[0][7]` (since it is the 8th value in the output of :code:`fit.characteristics()`). The previous solution can also be extracted using `fit_model.coeff(lambda_0=0.032, gamma=0)`." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 8, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fit_model.lambda_0[0][7] = 0.03271533058913737\n" + ] + }, + { + "data": { + "text/plain": "array([[0.00215713],\n [1.02014176],\n [0.97338278],\n ...,\n [0. ],\n [0. ],\n [0. ]])" + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(f\"fit_model.lambda_0[0][7] = {fit_model.lambda_0[0][7]}\")\n", + "fit_model.coeff(lambda_0=0.032715, gamma=0).toarray()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can make predictions using a specific solution in the grid using the function `fit_model.predict(newx, lambda, gamma)` where `newx` is a testing sample (vector or matrix). For example, to predict the response for the samples in the data matrix X using the solution with $\\lambda=0.0058037$, we call the prediction function as follows:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 10, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-2.68272239]\n", + " [-3.667317 ]\n", + " [-1.77309853]\n", + " ...\n", + " [ 2.25545111]\n", + " [-0.77364234]\n", + " [-2.15002055]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model.predict(x=X, lambda_0=0.032715, gamma=0))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can also visualize the regularization path by plotting the coefficients of the estimated B versus the support size (i.e., the number of non-zeros) using the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) method as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 9, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "ax = fit_model.plot(include_legend=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The legend of the plot presents the variables in the order they entered the regularization path. For example, variable 7 is the first variable to enter the path, and variable 6 is the second to enter. Thus, roughly speaking, we can view the first $k$ variables in the legend as the best subset of size $k$. To show the lines connecting the points in the plot, we can set the parameter :code:`show_lines=True` in the `plot` function, i.e., call :code:`fit.plot(fit, gamma=0, show_lines=True)`. Moreover, we note that the plot function returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) object, which can be further customized using the `matplotlib` package. In addition, both the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) and [l0learn.models.CVFitModel.cv_plot()](code.rst#l0learn.models.CVFitModel.cv_plot) accept :code:`**kwargs` parameter to allow for customization of the plotting behavior.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 11, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model.plot(show_lines=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Fitting L0L2 and L0L1 Regression Models\n", + "We have demonstrated the simple case of using an L0 penalty. We can also fit more elaborate models that combine L0 regularization with shrinkage-inducing penalties like the L1 norm or squared L2 norm. Adding shrinkage helps in avoiding overfitting and typically improves the predictive performance of the models. Next, we will discuss how to fit a model using the L0L2 penalty for a two-dimensional grid of :math:`\\lambda` and :math:`\\gamma` values. Recall that by default, `l0learn` automatically selects the :math:`\\lambda` sequence, so we only need to specify the :math:`\\gamma` sequence. Suppose we want to fit an L0L2 model with a maximum of 20 non-zeros and a sequence of 5 :math:`\\gamma` values ranging between 0.0001 and 10. We can do so by calling [l0learn.fit](code.rst#l0learn.fit) with :code:`penalty=\"L0L2\"`, :code:`num_gamma=5`, :code:`gamma_min=0.0001`, and :code:`gamma_max=10` as follows:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 12, + "outputs": [], + "source": [ + "fit_model_2 = l0learn.fit(X, y, penalty=\"L0L2\", num_gamma = 5, gamma_min = 0.0001, gamma_max = 10, max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "`l0learn` will generate a grid of 5 :math:`\\gamma` values equi-spaced on the logarithmic scale between 0.0001 and 10. Similar to the case for L0, we can display a summary of the regularization path using the [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) function as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 14, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
00.0037880-0.156704True10.0000
10.0037501-0.156250True10.0000
20.0029112-0.148003True10.0000
30.0026543-0.148650True10.0000
40.0025973-0.148650True10.0000
..................
1280.000216100.002130True0.0001
1290.00017313-0.003684True0.0001
1300.00016713-0.003685True0.0001
1310.00013418-0.015724True0.0001
1320.00013021-0.012762True0.0001
\n

133 rows × 5 columns

\n
" + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2 # Using ipython Rich Display\n", + "# fit_model_2.characteristics() # For non Rich Display" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The sequence of $\\gamma$ values can be accessed using `fit_model_2.gamma`. To extract a solution we use the [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) method. For example, extracting the solution at $\\lambda=0.0016$ and $\\gamma=10$ can be done using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 40, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2.coeff(lambda_0=0.0016, gamma=10)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Similarly, we can predict the response at this pair of $\\lambda$ and $\\gamma$ for the matrix X using" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 41, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-0.31499242]\n", + " [-0.3474209 ]\n", + " [-0.23997924]\n", + " ...\n", + " [-0.06707991]\n", + " [-0.18562493]\n", + " [-0.25608131]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_2.predict(x=X, lambda_0=0.0016, gamma=10))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The regularization path can also be plotted at a specific $\\gamma$ using `fit_model_2.plot(gamma=10)`. Here we can see the influence of ratio of $\\gamma$ and $\\lambda$. Since $\\gamma$ is so large ($10$) maintaining sparisity, then $\\lambda$ can be quite small $0.0016$ which this results in a very stable estimate of the magnitude of the coeffs." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 44, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 3. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 10. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 11. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 12. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "data": { + "text/plain": "" + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_2.plot(gamma=10, show_lines=True)\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we note that fitting an L0L1 model can be done by just changing the `penalty` to \"L0L1\" in the above (in this case `gamma_max` will be ignored since it is automatically selected by the toolkit; see the reference manual for more details.)\n", + "\n", + "## Higher-quality Solutions using Local Search\n", + "By default, `l0learn` uses coordinate descent (CD) to fit models. Since the objective function is non-convex, the choice of the optimization algorithm can have a significant effect on the solution quality (different algorithms can lead to solutions with very different objective values). A more elaborate algorithm based on combinatorial search can be used by setting the parameter `algorithm=\"CDPSI\"` in the call to `l0learn.fit`. `CDPSI` typically leads to higher-quality solutions compared to CD, especially when the features are highly correlated. CDPSI is slower than CD, however, for typical applications it terminates in the order of seconds.\n", + "\n", + "## Cross-validation\n", + "We will demonstrate how to use K-fold cross-validation (CV) to select the optimal values of the tuning parameters $\\lambda$ and $\\gamma$. To perform CV, we use the [l0learn.cvfit](code.rst#l0learn.cvfit) function, which takes the same parameters as [l0learn.fit](code.rst#l0learn.fit), in addition to the number of folds using the `num_folds` parameter and a seed value using the `seed` parameter (this is used when randomly shuffling the data before performing CV).\n", + "\n", + "For example, to perform 5-fold CV using the `L0L2` penalty (over a range of 5 `gamma` values between 0.0001 and 0.1) with a maximum of 50 non-zeros, we run:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 45, + "outputs": [], + "source": [ + "cv_fit_result = l0learn.cvfit(X, y, num_folds=5, seed=1, penalty=\"L0L2\", num_gamma=5, gamma_min=0.0001, gamma_max=0.1, max_support_size=50)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Note that the object returned during cross validation is [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel) which subclasses [l0learn.models.FitModel](code.rst#l0learn.models.FitModel) and thus has the same methods and underlinying structure. The cross-validation errors can be accessed using the `cv_means` attribute of a `CVFitModel`: `cv_fit_result.cv_means` is a list where the ith element, `cv_fit_result.cv_means[i]`, stores the cross-validation errors for the ith value of gamma `cv_fit_result.gamma[i]`). To find the minimum cross-validation error for every `gamma`, we apply the :code:`np.argmin` function for every element in the list :`cv_fit_result.cv_means`, as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 52, + "outputs": [ + { + "data": { + "text/plain": "[(0, 8, 0.5313128699361661),\n (1, 8, 0.2669789604993652),\n (2, 8, 0.2558807301729078),\n (3, 20, 0.25555788170828786),\n (4, 19, 0.2555564968851251)]" + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gamma_mins = [(i, np.argmin(cv_mean), np.min(cv_mean)) for i, cv_mean in enumerate(cv_fit_result.cv_means)]\n", + "gamma_mins" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above output indicates that the 5th value of gamma achieves the lowest CV error (`=0.255`). We can plot the CV errors against the support size for the 5th value of gamma, i.e., `gamma = cv_fit_result.gamma[4]`, using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 50, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "cv_fit_result.cv_plot(gamma = cv_fit_result.gamma[4])\n", + "cv_fit_result.cv_sds" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above plot is produced using the [matplotlib](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) package and returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) which can be further customized by the user. We can also note that we have error bars in the cross validation error which is stored in `cv_sds` attribute and can be accessed with `cv_fit_result.cv_sds`. To extract the optimal $\\lambda$ (i.e., the one with minimum CV error) in this plot, we execute the following:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 57, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 19 0.2555564968851251\n", + "Optimal lambda = 0.0016080760437896327\n" + ] + } + ], + "source": [ + "optimal_gamma_index, optimal_lambda_index, min_error = min(gamma_mins, key = lambda t: t[2])\n", + "print(optimal_gamma_index, optimal_lambda_index, min_error)\n", + "print(\"Optimal lambda = \", fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index])" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To print the solution corresponding to the optimal gamma/lambda pair:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 58, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index])" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The optimal solution (above) selected by cross-validation correctly recovers the support of the true vector of coefficients used to generate the model." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 70, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.01994648 1. ]\n", + " [0.97317979 1. ]\n", + " [0.99813347 1. ]\n", + " [0.99669481 1. ]\n", + " [1.01128182 1. ]\n", + " [1.00190748 1. ]\n", + " [1.01272103 1. ]\n", + " [0.99204841 1. ]\n", + " [0.99607406 1. ]\n", + " [1.0266543 1. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " ...\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]]\n" + ] + } + ], + "source": [ + "beta_vector = cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index],\n", + " include_intercept=False).toarray()\n", + "\n", + "with np.printoptions(threshold=30, edgeitems=15):\n", + " print(np.hstack([beta_vector, B.reshape(-1, 1)]))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Fitting Classification Models\n", + "All the commands and plots we have seen in the case of regression extend to classification. We currently support logistic regression (using the parameter `loss=\"Logistic\"`) and a smoothed version of SVM (using the parameter `loss=\"SquaredHinge\"`). To give some examples, we first generate a synthetic classification dataset (similar to the one we generated in the case of regression):" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 72, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = np.sign(X@B + e)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)\n", + "\n", + "An L0-regularized logistic regression model can be fit by specificying `loss = \"Logistic\"` as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 117, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'Logistic', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
025.2253361-0.036989True1.000000e-07
119.4320532-0.043199True1.000000e-07
218.9286032-0.043230True1.000000e-07
315.1428822-0.043245True1.000000e-07
412.11430690.004044True1.000000e-07
57.411211100.188422False1.000000e-07
67.188875100.219990False1.000000e-07
75.751100100.234899False1.000000e-07
84.600880100.242631False1.000000e-07
93.680704100.243655True1.000000e-07
102.944563100.243993True1.000000e-07
112.355651100.244295True1.000000e-07
121.884520100.244520True1.000000e-07
131.507616100.244716True1.000000e-07
141.206093100.244886True1.000000e-07
150.964874100.245011True1.000000e-07
160.771900100.245133True1.000000e-07
170.617520120.178144False1.000000e-07
180.598994120.196406False1.000000e-07
190.479195120.208883False1.000000e-07
200.383356150.192033False1.000000e-07
210.371856150.221470False1.000000e-07
220.297484150.246182False1.000000e-07
230.23798816-0.110626False1.000000e-07
240.23084816-0.118558False1.000000e-07
250.18467816-0.124533False1.000000e-07
260.14774321-0.211002False1.000000e-07
\n
" + }, + "execution_count": 117, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_3 = l0learn.fit(X,y,loss=\"Logistic\", max_support_size=20)\n", + "fit_model_3" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The output above indicates that $\\gamma=10^{-7}$ by default we use a small ridge regularization (with $\\gamma=10^{-7}$) to ensure the existence of a unique solution. To extract the coefficients of the solution with $\\lambda = 8.69435$ we use the following code. Notice that we can ignore the specification of gamma as there is only one gamma used in L0 Logistic regression:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 83, + "outputs": [], + "source": [ + "np.where(fit_model_3.coeff(lambda_0=7.411211).toarray() > 0)[0]" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above indicates that the 10 non-zeros in the estimated model match those we used in generating the data (i.e, L0 regularization correctly recovered the true support). We can also make predictions at the latter $\\lambda$ using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 84, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.69583037e-04]\n", + " [4.92440655e-06]\n", + " [3.92195535e-03]\n", + " ...\n", + " [9.99161941e-01]\n", + " [1.69035746e-01]\n", + " [9.99171256e-04]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_3.predict(X, lambda_0=7.411211))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Each row in the above is the probability that the corresponding sample belongs to class $1$. Other models (i.e., L0L2 and L0L1) can be similarly fit by specifying `loss = \"Logistic\"`.\n", + "\n", + "Finally, we note that L0Learn also supports a smoothed version of SVM by using squared hinge loss `loss = \"SquaredHinge\"`. The only difference from logistic regression is that the `predict` function returns $\\beta_0 + \\langle x, \\beta \\rangle$ (where $x$ is the testing sample), instead of returning probabilities. The latter predictions can be assigned to the appropriate classes by using a thresholding function (e.g., the sign function)." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Advanced Options\n", + "\n", + "### Sparse Matrix Support\n", + "Starting in version 2.0.0, L0Learn supports sparse matrices of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). If your sparse matrix uses a different storage format, please convert it to `csc_matrix` before using it in `l0learn`. `l0learn` keeps the matrix sparse internally and thus is highly efficient if the matrix is sufficiently sparse. The API for sparse matrices is the same as that of dense matrices, so all the demonstrations in this vignette also apply for sparse matrices. For example, we can fit an L0-regularized model on a sparse matrix as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 90, + "outputs": [ + { + "data": { + "text/plain": " l0 support_size intercept converged\n0 0.031892 0 0.009325 True\n1 0.031573 1 0.004882 True\n2 0.020606 2 -0.002187 True\n3 0.014442 3 -0.004111 True\n4 0.013932 4 -0.002556 True\n5 0.010854 5 0.002987 True\n6 0.009286 6 0.002048 True\n7 0.009191 7 -0.001371 True\n8 0.008771 8 -0.000533 True\n9 0.008151 9 0.000064 True\n10 0.006480 11 0.001587 True\n11 0.006364 12 -0.003636 True\n12 0.005760 13 -0.003866 True\n13 0.005560 15 -0.004211 True\n14 0.005264 17 0.001497 True\n15 0.004637 18 -0.000797 True\n16 0.004515 19 -0.002634 True\n17 0.004113 22 0.001419 True", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.03189200.009325True
10.03157310.004882True
20.0206062-0.002187True
30.0144423-0.004111True
40.0139324-0.002556True
50.01085450.002987True
60.00928660.002048True
70.0091917-0.001371True
80.0087718-0.000533True
90.00815190.000064True
100.006480110.001587True
110.00636412-0.003636True
120.00576013-0.003866True
130.00556015-0.004211True
140.005264170.001497True
150.00463718-0.000797True
160.00451519-0.002634True
170.004113220.001419True
\n
" + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from scipy.sparse import random\n", + "from scipy.stats import norm\n", + "\n", + "\n", + "X_sparse = random(n, p, density=0.01, format='csc', data_rvs=norm().rvs)\n", + "y_sparse = (X_sparse@B + e)\n", + "\n", + "fit_model_sparse = l0learn.fit(X_sparse, y_sparse, penalty=\"L0\", max_support_size=20)\n", + "\n", + "fit_model_sparse.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Selection on Subset of Variables\n", + "In certain applications, it is desirable to always include some of the variables in the model and perform variable selection on others. `l0learn` supports this option through the `exclude_first_k` parameter. Specifically, setting `exclude_first_k = K` (where K is a non-negative integer) instructs `l0learn` to exclude the first K variables in the data matrix `X` from the L0-norm penalty (those K variables will still be penalized using the L2 or L1 norm penalties.). For example, below we fit an `L0` model and exclude the first 3 variables from selection by setting `excludeFirstK = 3`:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 94, + "outputs": [ + { + "data": { + "text/plain": " l0 support_size intercept converged\n0 0.050464 3 -0.017599 True\n1 0.044333 4 -0.021333 True\n2 0.032770 5 -0.027624 True\n3 0.029367 7 -0.029115 True\n4 0.024710 8 -0.021199 True\n5 0.021393 9 0.010249 True\n6 0.014785 10 0.016812 True", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.0504643-0.017599True
10.0443334-0.021333True
20.0327705-0.027624True
30.0293677-0.029115True
40.0247108-0.021199True
50.02139390.010249True
60.014785100.016812True
\n
" + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_k = l0learn.fit(X, y, penalty=\"L0\", max_support_size=10, exclude_first_k=3)\n", + "fit_model_k.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Plotting the regularization path:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 93, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_k.plot(show_lines=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can see in the plot above that first 3 variables, (0, 1, 2) are included in all the solutions of the path\n", + "\n", + "### Coefficient Bounds\n", + "Starting in version 2.0.0, `l0learn` supports bounds for CD algorithms for all losses and penalties. (We plan to support bound constraints for the CDPSI algorithm in the future). By default, `l0learn` does not apply bounds, i.e., it assumes $-\\infty <= \\beta_i <= \\infty$ for all i. Users can supply the same bounds for all coefficients by setting the parameters `lows` and `highs` to scalar values (these should satisfy: `lows <= 0`, `lows != highs`, and `highs >= 0`). To use different bounds for the coefficients, `lows` and `highs` can be both set to vectors of length `p` (where the i-th entry corresponds to the bound on coefficient i).\n", + "\n", + "All of the following examples are valid." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 107, + "outputs": [], + "source": [ + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", highs=0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5, highs=0.5)\n", + "\n", + "max_value = 0.25\n", + "highs_array = max_value*np.ones(p)\n", + "highs_array[0] = 0.1\n", + "fit_model_bounds = l0learn.fit(X, y, penalty=\"L0\", lows=-0.1, highs=highs_array, max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can see the coefficients are subject to the bounds." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 110, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 0. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 6. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 8. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "maximum value of coefficients = 0.25 <= 0.25\n", + "maximum value of first coefficient = 0.1 <= 0.1\n", + "minimum value of coefficient = -0.1 >= -0.1\n" + ] + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_bounds.plot(show_lines=True)\n", + "\n", + "print(f\"maximum value of coefficients = {np.max(fit_model_bounds.coeffs[0])} <= {max_value}\")\n", + "print(f\"maximum value of first coefficient = {np.max(fit_model_bounds.coeffs[0][0, :])} <= 0.1\")\n", + "print(f\"minimum value of coefficient = {np.min(fit_model_bounds.coeffs[0])} >= -0.1\")\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### User-specified Lambda Grids\n", + "By default, `l0learn` selects the sequence of lambda values in an efficient manner to avoid wasted computation (since close $\\lambda$ values can typically lead to the same solution). Advanced users of the toolkit can change this default behavior and supply their own sequence of $\\lambda$ values. This can be done supplying the $\\lambda$ values through the parameter `lambda_grid`. When `lambda_grid` is supplied, we require `num_gamma` and `num_lambda` to be `None` to ensure the is no ambiguity in the solution path requested.\n", + "\n", + "Specifically, the value assigned to `lambda_grid` should be a list of lists/arrays of decreasing positive values (floats). The length of `lambda_grid` (the number of lists stored) specifies the number of gamma parameters that will fill between `gamma_min`, and `gamma_max`. In the case of L0 penalty, `lambda_grid` must be a list of length 1. In case of L0L2/L0L1 `lambda_grid` can have any number of sub-lists stored. The ith element in `lambda_grid` should be a **strictly decreasing** sequence of positive lambda values which are used by the algorithm for the ith value of gamma. For example, to fit an L0 model with the sequence of user-specified lambda values: 1, 1e-1, 1e-2, 1e-3, 1e-4, we run the following:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 113, + "outputs": [], + "source": [ + "user_lambda_grid = [[1, 1e-1, 1e-2, 1e-3, 1e-4]]\n", + "fit_grid = l0learn.fit(X, y, penalty=\"L0\", lambda_grid=user_lambda_grid, max_support_size=1000, num_lambda=None, num_gamma=None)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To verify the results we print the fit object:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 114, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
01.00000-0.016000True
10.10000-0.016000True
20.0100100.016811True
30.0010620.018729True
40.00012670.051675True
\n
" + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid\n", + "# Use fit_grid.characteristics() for those without rich dispalys" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Note that the $\\lambda$ values above are the desired values. For L0L2 and L0L1 penalties, the same can be done where the `lambda_grid` parameter." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 115, + "outputs": [], + "source": [ + "user_lambda_grid_L2 = [[1, 1e-1, 1e-2, 1e-3, 1e-4],\n", + " [10, 2, 1, 0.01, 0.002, 0.001, 1e-5],\n", + " [1e-4, 1e-5]]\n", + "\n", + "# user_lambda_grid_L2[[i]] must be a sequence of positive decreasing reals.\n", + "fit_grid_L2 = l0learn.fit(X, y, penalty=\"L0L2\", lambda_grid=user_lambda_grid_L2, max_support_size=1000, num_lambda=None, num_gamma=None)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 116, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
01.000000-0.016000True10.000000
10.100000-0.016000True10.000000
20.010000-0.016000True10.000000
30.001009-0.014394True10.000000
40.00010134-0.012180True10.000000
510.000000-0.016000True0.031623
62.000000-0.016000True0.031623
71.000000-0.016000True0.031623
80.01000100.015045True0.031623
90.00200280.001483True0.031623
100.00100580.002821True0.031623
110.000015820.021913True0.031623
120.000103110.048700True0.000100
130.000014110.047991False0.000100
\n
" + }, + "execution_count": 116, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid_L2\n", + "# Use fit_grid_L2.characteristics() for those without rich dispalys" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "# More Details\n", + "For more details please inspect the doc strings of:\n", + "\n", + "* [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel)\n", + "* [l0learn.models.FitModel](code.rst#l0learn.models.FitModel)\n", + "* [l0learn.fit](code.rst#l0learn.fit)\n", + "* [l0learn.cvfit](code.rst#l0learn.cvfit)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "# References\n", + "Hussein Hazimeh and Rahul Mazumder. [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919). Operations Research (2020).\n", + "\n", + "Antoine Dedieu, Hussein Hazimeh, and Rahul Mazumder. [Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives](https://arxiv.org/abs/2001.06471). JMLR (to appear)." + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 0000000..603f6a8 --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,905 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dt:after { + content: ":"; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css new file mode 100644 index 0000000..e380325 --- /dev/null +++ b/docs/_static/css/badge_only.css @@ -0,0 +1 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/docs/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/docs/_static/css/fonts/lato-bold-italic.woff differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold.woff differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold.woff2 differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/docs/_static/css/fonts/lato-normal-italic.woff differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/docs/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/docs/_static/css/fonts/lato-normal.woff differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/docs/_static/css/fonts/lato-normal.woff2 differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css new file mode 100644 index 0000000..0d9ae7e --- /dev/null +++ b/docs/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js new file mode 100644 index 0000000..8cbf1b1 --- /dev/null +++ b/docs/_static/doctools.js @@ -0,0 +1,323 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated === 'undefined') + return string; + return (typeof translated === 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated === 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); + } + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keydown(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box, textarea, dropdown or button + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' + && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey + && !event.shiftKey) { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; + } + break; + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + break; + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js new file mode 100644 index 0000000..2fa8c97 --- /dev/null +++ b/docs/_static/documentation_options.js @@ -0,0 +1,12 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + LANGUAGE: 'None', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false +}; \ No newline at end of file diff --git a/docs/_static/file.png b/docs/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/_static/file.png differ diff --git a/docs/_static/jquery-3.5.1.js b/docs/_static/jquery-3.5.1.js new file mode 100644 index 0000000..5093733 --- /dev/null +++ b/docs/_static/jquery-3.5.1.js @@ -0,0 +1,10872 @@ +/*! + * jQuery JavaScript Library v3.5.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2020-05-04T22:49Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.5.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.5 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2020-03-14 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem.namespaceURI, + docElem = ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +}; +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the master Deferred + master = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + master.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( master.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return master.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + } + + return master.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( + dataPriv.get( cur, "events" ) || Object.create( null ) + )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

fit function

+
+
+l0learn.fit(X: Union[np.ndarray, csc_matrix], y: np.ndarray, unicode loss: str = u'SquaredError', unicode penalty: str = u'L0', unicode algorithm: str = u'CD', max_support_size: int = 100, num_lambda: Optional[int] = 100, num_gamma: Optional[int] = 1, double gamma_max: float = 10., double gamma_min: float = .0001, partial_sort: bool = True, max_iter: int = 200, double rtol: float = 1e-6, double atol: float = 1e-9, active_set: bool = True, active_set_num: int = 3, max_swaps: int = 100, double scale_down_factor: float = 0.8, screen_size: int = 1000, lambda_grid: Optional[List[Sequence[float]]] = None, exclude_first_k: int = 0, intercept: bool = True, lows: Union[np.ndarray, float] = -float(u'inf'), highs: Union[np.ndarray, float] = +float(u'inf')) l0learn.models.FitModel
+

Computes the regularization path for the specified loss function and penalty function.

+
+
Parameters
+
    +
  • X (np.ndarray or csc_matrix of shape (N, P)) – Data Matrix where rows of X are observations and columns of X are features

  • +
  • y (np.ndarray of shape (P)) – The response vector where y[i] corresponds to X[i, :] +For classification, a binary vector (-1, 1) is requried .

  • +
  • loss (str) –

    +
    The loss function. Currently supports the choices:

    ”SquaredError” (for regression), +“Logistic” (for logistic regression), and +“SquaredHinge” (for smooth SVM).

    +
    +
    +

  • +
  • penalty (str) –

    The type of regularization. +This can take either one of the following choices:

    +
    +

    ”L0”, +“L0L2”, and +“L0L1”

    +
    +

  • +
  • algorithm (str) – The type of algorithm used to minimize the objective function. Currently “CD” and “CDPSI” are are supported. +“CD” is a variant of cyclic coordinate descent and runs very fast. “CDPSI” performs local combinatorial search +on top of CD and typically achieves higher quality solutions (at the expense of increased running time).

  • +
  • max_support_size (int) – Must be greater than 0. +The maximum support size at which to terminate the regularization path. We recommend setting this to a small +fraction of min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically selects a small portion of non-zeros.

  • +
  • num_lambda (int, optional) – The number of lambda values to select in the regularization path. +This value must be None if lambda_grid is supplied.When supplied, must be greater than 0. +Note: lambda is the regularization parameter corresponding to the L0 norm.

  • +
  • num_gamma (int, optional) – The number of gamma values to select in the regularization path. +This value must be None if lambda_grid is supplied. When supplied, must be greater than 0. +Note: gamma is the regularization parameter corresponding to L1 or L2, depending on the chosen penalty).

  • +
  • gamma_max (float) –

    The maximum value of gamma when using the L0L2 penalty. +This value must be greater than 0.

    +

    Note: For the L0L1 penalty this is automatically selected.

    +

  • +
  • gamma_min (float) – The minimum value of Gamma when using the L0L2 penalty. +This value must be greater than 0 but less than gamma_max. +Note: For the L0L1 penalty, the minimum value of gamma in the grid is set to gammaMin * gammaMax.

  • +
  • partial_sort (bool) – If TRUE partial sorting will be used for sorting the coordinates to do greedy cycling (see our paper for +for details). Otherwise, full sorting is used. #TODO: Add link for paper

  • +
  • max_iter (int) – The maximum number of iterations (full cycles) for CD per grid point. The algorithm may not use the full number +of iteration per grid point if convergence is found (defined by rtol and atol parameter) +Must be greater than 0

  • +
  • rtol (float) – The relative tolerance which decides when to terminate optimization as based on the relative change in the +objective between iterations. +Must be greater than 0 and less than 1.

  • +
  • atol (float) – The absolute tolerance which decides when to terminate optimization as based on the absolute L2 norm of the +residuals +Must be greater than 0

  • +
  • active_set (bool) – If TRUE, performs active set updates. (see our paper for for details). #TODO: Add link for paper

  • +
  • active_set_num (int) –

    The number of consecutive times a support should appear before declaring support stabilization. +(see our paper for for details). #TODO: Add link for paper

    +

    Must be greater than 0.

    +

  • +
  • max_swaps (int) – The maximum number of swaps used by CDPSI for each grid point. +Must be greater than 0. Ignored by CD algorithims.

  • +
  • scale_down_factor (float) –

    Roughly amount each lambda value is scaled by between grid points. Larger values lead to closer lambdas and +typically to smaller gaps between the support sizes.

    +

    For details, see our paper - Section 5 on Adaptive Selection of Tuning Parameters). #TODO: Add link for paper

    +

    Must be greater than 0 and less than 1 (strictly for both.)

    +

  • +
  • screen_size (int) –

    The number of coordinates to cycle over when performing initial correlation screening. #TODO: Add link for paper

    +

    Must be greater than 0 and less than number of columns of X.

    +

  • +
  • lambda_grid (list of list of floats) –

    A grid of lambda values to use in computing the regularization path. This is by default an empty list +and is ignored. When specified, lambda_grid should be a list of list of floats, where the ith element

    +
    +

    (corresponding to the ith gamma) should be a decreasing sequence of lambda values. The length of this sequence +is directly the number of lambdas to be tried for that gamma.

    +
    +

    In the the “L0” penalty case, lambda_grid should be a list of 1. +In the “L0LX” penalty cases, lambda_grid can be a list of any length. The length of lambda_grid will be the +number of gamma values tried.

    +

    See the example notebook for more details.

    +

    Note: When lambda_grid is supplied, num_gamma and num_lambda must be None.

    +

  • +
  • exclude_first_k (int) –

    The first exclude_first_k features in X will be excluded from variable selection. In other words, the first +exclude_first_k variables will not be included in the L0-norm penalty however they will be included in the +L1 or L2 norm penalties, if they are specified.

    +

    Must be a positive integer less than the columns of X.

    +

  • +
  • intercept (bool) – If False, no intercept term is included or fit in the regularization path +Intercept terms are not regularized by L0 or L1/L2.

  • +
  • lows (np array or float) –

    Lower bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of +size p (number of columns of X) where lows[i] is the lower bound for coefficient i.

    +

    Lower bounds can not be above 0 (i.e. we can not specify that all coefficients must be larger than a > 0). +Lower bounds can be set to 0 iff the corresponding upper bound for that coefficient is also not 0.

    +

  • +
  • highs (np array or float) –

    Upper bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of +size p (number of columns of X) where highs[i] is the upper bound for coefficient i.

    +

    Upper bounds can not be below 0 (i.e. we can not specify that all coefficients must be smaller than a < 0). +Upper bounds can be set to 0 iff the corresponding lower bound for that coefficient is also not 0.

    +

  • +
+
+
Returns
+

fit_model – FitModel instance containing all relevant information from the solution path.

+
+
Return type
+

l0learn.models.FitModel

+
+
+ +

Examples

+

>>>fit_model = l0learn.fit(X, y, penalty=”L0”, max_support_size=20)

+
+ +
+
+

cvfit function

+
+
+l0learn.cvfit(X: Union[np.ndarray, csc_matrix], y: np.ndarray, unicode loss: str = u'SquaredError', unicode penalty: str = u'L0', unicode algorithm: str = u'CD', num_folds: int = 10, seed: int = 1, max_support_size: int = 100, num_lambda: Optional[int] = 100, num_gamma: Optional[int] = 1, double gamma_max: float = 10., double gamma_min: float = .0001, partial_sort: bool = True, max_iter: int = 200, double rtol: float = 1e-6, double atol: float = 1e-9, active_set: bool = True, active_set_num: int = 3, max_swaps: int = 100, double scale_down_factor: float = 0.8, screen_size: int = 1000, lambda_grid: Optional[List[Sequence[float]]] = None, exclude_first_k: int = 0, intercept: bool = True, lows: Union[np.ndarray, float] = -float(u'inf'), highs: Union[np.ndarray, float] = +float(u'inf')) l0learn.models.CVFitModel
+

Computes the regularization path for the specified loss function and penalty function and performs K-fold +cross-validation.

+
+
Parameters
+
    +
  • X (np.ndarray or csc_matrix of shape (N, P)) – Data Matrix where rows of X are observations and columns of X are features

  • +
  • y (np.ndarray of shape (P)) – The response vector where y[i] corresponds to X[i, :] +For classification, a binary vector (-1, 1) is requried .

  • +
  • loss (str) –

    +
    The loss function. Currently supports the choices:

    ”SquaredError” (for regression), +“Logistic” (for logistic regression), and +“SquaredHinge” (for smooth SVM).

    +
    +
    +

  • +
  • penalty (str) –

    The type of regularization. +This can take either one of the following choices:

    +
    +

    ”L0”, +“L0L2”, and +“L0L1”

    +
    +

  • +
  • algorithm (str) – The type of algorithm used to minimize the objective function. Currently “CD” and “CDPSI” are are supported. +“CD” is a variant of cyclic coordinate descent and runs very fast. “CDPSI” performs local combinatorial search +on top of CD and typically achieves higher quality solutions (at the expense of increased running time).

  • +
  • num_folds (int) – Must be greater than 1 and less than N (number of . +The number of folds for cross-validation.

  • +
  • max_support_size (int) – Must be greater than 0. +The maximum support size at which to terminate the regularization path. We recommend setting this to a small +fraction of min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically selects a small portion of non-zeros.

  • +
  • num_lambda (int, optional) – The number of lambda values to select in the regularization path. +This value must be None if lambda_grid is supplied.When supplied, must be greater than 0. +Note: lambda is the regularization parameter corresponding to the L0 norm.

  • +
  • num_gamma (int, optional) – The number of gamma values to select in the regularization path. +This value must be None if lambda_grid is supplied. When supplied, must be greater than 0. +Note: gamma is the regularization parameter corresponding to L1 or L2, depending on the chosen penalty).

  • +
  • gamma_max (float) –

    The maximum value of gamma when using the L0L2 penalty. +This value must be greater than 0.

    +

    Note: For the L0L1 penalty this is automatically selected.

    +

  • +
  • gamma_min (float) – The minimum value of Gamma when using the L0L2 penalty. +This value must be greater than 0 but less than gamma_max. +Note: For the L0L1 penalty, the minimum value of gamma in the grid is set to gammaMin * gammaMax.

  • +
  • partial_sort (bool) – If TRUE partial sorting will be used for sorting the coordinates to do greedy cycling (see our paper for +for details). Otherwise, full sorting is used. #TODO: Add link for paper

  • +
  • max_iter (int) – The maximum number of iterations (full cycles) for CD per grid point. The algorithm may not use the full number +of iteration per grid point if convergence is found (defined by rtol and atol parameter) +Must be greater than 0

  • +
  • rtol (float) – The relative tolerance which decides when to terminate optimization as based on the relative change in the +objective between iterations. +Must be greater than 0 and less than 1.

  • +
  • atol (float) – The absolute tolerance which decides when to terminate optimization as based on the absolute L2 norm of the +residuals +Must be greater than 0

  • +
  • active_set (bool) – If TRUE, performs active set updates. (see our paper for for details). #TODO: Add link for paper

  • +
  • active_set_num (int) –

    The number of consecutive times a support should appear before declaring support stabilization. +(see our paper for for details). #TODO: Add link for paper

    +

    Must be greater than 0.

    +

  • +
  • max_swaps (int) – The maximum number of swaps used by CDPSI for each grid point. +Must be greater than 0. Ignored by CD algorithims.

  • +
  • scale_down_factor (float) –

    Roughly amount each lambda value is scaled by between grid points. Larger values lead to closer lambdas and +typically to smaller gaps between the support sizes.

    +

    For details, see our paper - Section 5 on Adaptive Selection of Tuning Parameters). #TODO: Add link for paper

    +

    Must be greater than 0 and less than 1 (strictly for both.)

    +

  • +
  • screen_size (int) –

    The number of coordinates to cycle over when performing initial correlation screening. #TODO: Add link for paper

    +

    Must be greater than 0 and less than number of columns of X.

    +

  • +
  • lambda_grid (list of list of floats) –

    A grid of lambda values to use in computing the regularization path. This is by default an empty list +and is ignored. When specified, lambda_grid should be a list of list of floats, where the ith element

    +
    +

    (corresponding to the ith gamma) should be a decreasing sequence of lambda values. The length of this sequence +is directly the number of lambdas to be tried for that gamma.

    +
    +

    In the the “L0” penalty case, lambda_grid should be a list of 1. +In the “L0LX” penalty cases, lambda_grid can be a list of any length. The length of lambda_grid will be the +number of gamma values tried.

    +

    See the example notebook for more details.

    +

    Note: When lambda_grid is supplied, num_gamma and num_lambda must be None.

    +

  • +
  • exclude_first_k (int) –

    The first exclude_first_k features in X will be excluded from variable selection. In other words, the first +exclude_first_k variables will not be included in the L0-norm penalty however they will be included in the +L1 or L2 norm penalties, if they are specified.

    +

    Must be a positive integer less than the columns of X.

    +

  • +
  • intercept (bool) – If False, no intercept term is included or fit in the regularization path +Intercept terms are not regularized by L0 or L1/L2.

  • +
  • lows (np array or float) –

    Lower bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of +size p (number of columns of X) where lows[i] is the lower bound for coefficient i.

    +

    Lower bounds can not be above 0 (i.e. we can not specify that all coefficients must be larger than a > 0). +Lower bounds can be set to 0 iff the corresponding upper bound for that coefficient is also not 0.

    +

  • +
  • highs (np array or float) –

    Upper bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of +size p (number of columns of X) where highs[i] is the upper bound for coefficient i.

    +

    Upper bounds can not be below 0 (i.e. we can not specify that all coefficients must be smaller than a < 0). +Upper bounds can be set to 0 iff the corresponding lower bound for that coefficient is also not 0.

    +

  • +
+
+
Returns
+

fit_model – FitModel instance containing all relevant information from the solution path.

+
+
Return type
+

l0learn.models.FitModel

+
+
+ +

Examples

+

>>>fit_model = l0learn.fit(X, y, penalty=”L0”, max_support_size=20)

+
+ +
+
+

FitModels

+
+
+class l0learn.models.FitModel(settings: Dict[str, Any], lambda_0: List[List[float]], gamma: List[float], support_size: List[List[int]], coeffs: List[scipy.sparse.csc.csc_matrix], intercepts: List[List[float]], converged: List[List[bool]])
+

FitModel returned by calling l0learn.fit(…)

+
+ +
+
+

CVFitModels

+
+
+class l0learn.models.CVFitModel(settings: Dict[str, Any], lambda_0: List[List[float]], gamma: List[float], support_size: List[List[int]], coeffs: List[scipy.sparse.csc.csc_matrix], intercepts: List[List[float]], converged: List[List[bool]], cv_means: List[numpy.ndarray], cv_sds: List[numpy.ndarray])
+
+ +
+
+

Generating Functions

+
+
+l0learn.models.gen_synthetic(n: int, p: int, k: int, seed: Optional[int] = None, rho: float = 0, b0: float = 0, snr: float = 1) Dict[str, Union[float, numpy.ndarray]]
+
+
Generates a synthetic dataset as follows:
    +
  1. Sample every element in data matrix X from N(0,1).

  2. +
  3. Generate a vector B with the first k entries set to 1 and the rest are zeros.

  4. +
  5. Sample every element in the noise vector e from N(0,A) where A is selected so y, X, B have snr as specified.

  6. +
  7. Set y = XB + b0 + e.

  8. +
+
+
+
+
Parameters
+
    +
  • n (int) – Number of samples

  • +
  • p (int) – Number of features

  • +
  • k (int) – Number of non-zeros in true vector of coefficients

  • +
  • seed (int, optional) – The seed used for randomly generating the data +If None, numbers will be random

  • +
  • rho (float, default 0.) – The threshold for setting X values to 0. if |X[i, j]| < rho => X[i, j] <- 0

  • +
  • b0 (float, default 0) – The intercept value to translate y by.

  • +
  • snr (float, default 1) –

    Desired Signal-to-Noise ratio. This sets the magnitude of the error term ‘e’.

    +

    Note: SNR is defined as SNR = Var(XB)/Var(e)

    +

  • +
+
+
Returns
+

Data

+
+
A dict containing:

the data matrix X, +the response vector y, +the coefficients B, +the error vector e, +the intercept term b0.

+
+
+

+
+
Return type
+

Dict

+
+
+

Examples

+

>>>data = gen_synthetic(n=500,p=1000,k=10,seed=1)

+
+ +
+
+l0learn.models.gen_synthetic_high_corr(n: int, p: int, k: int, seed: Optional[int] = None, rho: float = 0, b0: float = 0, snr: float = 1, mu: float = 0, base_cor: float = 0.8) Dict[str, Union[float, numpy.ndarray]]
+
+
Generates a synthetic dataset as follows:
    +
  1. Generate a correlation matrix, SIG, where item [i, j] = base_core^|i-j|.

  2. +
  3. Draw from a Multivariate Normal Distribution using (mu and SIG) to generate X.

  4. +
  5. Generate a vector B with every ~p/k entry set to 1 and the rest are zeros.

  6. +
  7. Sample every element in the noise vector e from N(0,A) where A is selected so y, X, B have snr as specified.

  8. +
  9. Set y = XB + b0 + e.

  10. +
+
+
+
+
Parameters
+
    +
  • n (int) – Number of samples

  • +
  • p (int) – Number of features

  • +
  • k (int) – Number of non-zeros in true vector of coefficients

  • +
  • seed (int) – The seed used for randomly generating the data

  • +
  • rho (float) – The threshold for setting values to 0. if |X[i, j]| < rho => X[i, j] <- 0

  • +
  • b0 (float) – intercept value to scale y by.

  • +
  • snr (float) – desired Signal-to-Noise ratio. This sets the magnitude of the error term ‘e’. +SNR is defined as SNR = Var(XB)/Var(e)

  • +
  • mu (float) – The mean for drawing from the Multivariate Normal Distribution. A scalar of vector of length p.

  • +
  • base_cor (float) – The base correlation, A in [i, j] = A^|i-j|.

  • +
+
+
Returns
+

data

+
+
A dict containing:

the data matrix X, +the response vector y, +the coefficients B, +the error vector e, +the intercept term b0.

+
+
+

+
+
Return type
+

dict

+
+
+

Examples

+

>>>gen_synthetic_high_corr(n=500,p=1000,k=10,seed=1)

+
+ +
+
+l0learn.models.gen_synthetic_logistic(n: int, p: int, k: int, seed: Optional[int] = None, rho: float = 0, b0: float = 0, s: float = 1, mu: Optional[float] = None, base_cor: Optional[float] = None) Dict[str, Union[float, numpy.ndarray]]
+
+
Generates a synthetic dataset as follows:
    +
  1. +
    Generate a data matrix, X, drawn from either N(0, 1) (see gen_synthetic) or a multivariate_normal(mu, sigma)

    (See gen_synthetic_high_corr) +gen_synthetic_logistic delegates these caluclations to the respective functions.

    +
    +
    +
  2. +
  3. Generate a vector B with k entries set to 1 and the rest are zeros.

  4. +
  5. +
    Every coordinate yi of the outcome vector y exists in {0, 1}^n is sampled independently from a Bernoulli
    +
    distribution with success probability:

    P(yi = 1|xi) = 1/(1 + exp(-s<xi, B>))

    +
    +
    +

    Source https://arxiv.org/pdf/2001.06471.pdf Section 5.1 Data Generation

    +
    +
    +
  6. +
+
+
+
+
Parameters
+
    +
  • n (int) – Number of samples

  • +
  • p (int) – Number of features

  • +
  • k (int) – Number of non-zeros in true vector of coefficients

  • +
  • seed (int) – The seed used for randomly generating the data

  • +
  • rho (float) – The threshold for setting values to 0. if |X[i, j]| > rho => X[i, j] <- 0

  • +
  • b0 (float) – The intercept value to scale the log odds of y by. +As b0 -> +inf, y will contain more 1s +As b0 -> -inf, y will contain more 0s

  • +
  • s (float) – Signal-to-noise parameter. As s -> +Inf, the data generated becomes linearly separable.

  • +
  • mu (float, optional) – The mean for drawing from the Multivariate Normal Distribution. A scalar of vector of length p. +If mu and base_cor are not specified, will be drawn from N(0, 1) using gen_synthetic. +If mu and base_cor are specified will be drawn from multivariate_normal(mu, sigma) see gen_synthetic_high_corr

  • +
  • base_cor (float) – The base correlation, A in [i, j] = A^|i-j|. +If mu and base_cor are not specified, will be drawn from N(0, 1) +If mu and base_cor are specified will be drawn from multivariate_normal(mu, sigma) see gen_synthetic_high_corr

  • +
+
+
Returns
+

data

+
+
A dict containing:

the data matrix X +the response vector y, +the coefficients B, +the intercept term b0.

+
+
+

+
+
Return type
+

dict

+
+
+
+ +
+
+

Scoring Functions

+

These functions are called by l0learn.models.FitModel.score() and l0learn.models.CVFitModel.score().

+
+
+l0learn.models.regularization_loss(coeffs: scipy.sparse.csc.csc_matrix, l0: Union[float, Sequence[float]] = 0, l1: Union[float, Sequence[float]] = 0, l2: Union[float, Sequence[float]] = 0) Union[float, numpy.ndarray]
+

Calculates the regularization loss for a path of (or individual) solution(s).

+
+
Parameters
+
    +
  • coeffs

  • +
  • l0

  • +
  • l1

  • +
  • l2

  • +
+
+
Returns
+

loss

+
+
Let the shape of coeffs be (P, K) and l0, l1, and l2 by either a float or a sequence of length K
+
Then loss will be of shape: (K, ) with the following layout:

loss[i] = l0[i]*||coeffs[:, i||_0 + l1[i]*||coeffs[:, i||_1 + l2[i]*||coeffs[:, i||_2

+
+
+
+
+

+
+
Return type
+

float or np.ndarray of shape (K,) where K is the number of solutions in coeffs

+
+
+

Notes

+
+ +
+
+l0learn.models.squared_error(y_true: numpy.ndarray, y_pred: numpy.ndarray, coeffs: Optional[scipy.sparse.csc.csc_matrix] = None, l0: Union[float, Sequence[float]] = 0, l1: Union[float, Sequence[float]] = 0, l2: Union[float, Sequence[float]] = 0) numpy.ndarray
+

Calculates Squared Error loss of solution with optional regularization

+
+
Parameters
+
    +
  • y_true (np.ndarray of shape (m, )) –

  • +
  • y_pred (np.ndarray of shape (m, ) or (m, k)) –

  • +
  • coeffs (np.ndarray of shape (p, k), optional) –

  • +
  • l0 (float or sequence of floats of shape (l)) –

  • +
  • l1 (float or sequence of floats of shape (l)) –

  • +
  • l2 (float or sequence of floats of shape (l)) –

  • +
+
+
Returns
+

squared_error – Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D

+
+
Return type
+

np.ndarray

+
+
+
+ +
+
+l0learn.models.logistic_loss(y_true: numpy.ndarray, y_pred: numpy.ndarray, coeffs: Optional[scipy.sparse.csc.csc_matrix] = None, l0: float = 0, l1: float = 0, l2: float = 0, eps: float = 1e-15) numpy.ndarray
+

Calculates Logistic Loss of solution with optional regularization

+
+
Parameters
+
    +
  • y_true (np.ndarray of shape (m, )) –

  • +
  • y_pred (np.ndarray of shape (m, ) or (m, k)) –

  • +
  • coeffs (np.ndarray of shape (p, k), optional) –

  • +
  • l0 (float or sequence of floats of shape (l)) –

  • +
  • l1 (float or sequence of floats of shape (l)) –

  • +
  • l2 (float or sequence of floats of shape (l)) –

  • +
  • eps (float, default=1e-15) – Logistic loss is undefined for p=0 or p=1, so probabilities are clipped to max(eps, min(1 - eps, p)).

  • +
+
+
Returns
+

logistic_loss – Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D

+
+
Return type
+

np.ndarray

+
+
+
+ +
+
+l0learn.models.squared_hinge_loss(y_true: numpy.ndarray, y_pred: numpy.ndarray, coeffs: Optional[scipy.sparse.csc.csc_matrix] = None, l0: float = 0, l1: float = 0, l2: float = 0) numpy.ndarray
+

Calculates Logistic Loss of solution with optional regularization

+
+
Parameters
+
    +
  • y_true (np.ndarray of shape (m, )) –

  • +
  • y_pred (np.ndarray of shape (m, ) or (m, k)) –

  • +
  • coeffs (np.ndarray of shape (p, k), optional) –

  • +
  • l0 (float or sequence of floats of shape (l)) –

  • +
  • l1 (float or sequence of floats of shape (l)) –

  • +
  • l2 (float or sequence of floats of shape (l)) –

  • +
+
+
Returns
+

squared_hinge_loss – Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D

+
+
Return type
+

np.ndarray

+
+
+
+ +
+ + +
+
+
+ +
+ +
+

© Copyright 2021, Hussein Hazimeh, Rahul Mazumder, and Tim Nonet.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html new file mode 100644 index 0000000..1957eb9 --- /dev/null +++ b/docs/genindex.html @@ -0,0 +1,182 @@ + + + + + + Index — l0learn documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Index
  • +
  • +
  • +
+
+
+ +
+ +
+ +
+

© Copyright 2021, Hussein Hazimeh, Rahul Mazumder, and Tim Nonet.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..78661c6 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,167 @@ + + + + + + + Welcome to l0learn’s documentation! — l0learn documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Welcome to l0learn’s documentation!

+ +
+
+

Indices and tables

+ +
+ + +
+
+
+ +
+ +
+

© Copyright 2021, Hussein Hazimeh, Rahul Mazumder, and Tim Nonet.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv new file mode 100644 index 0000000..2244ecb Binary files /dev/null and b/docs/objects.inv differ diff --git a/docs/search.html b/docs/search.html new file mode 100644 index 0000000..3f03201 --- /dev/null +++ b/docs/search.html @@ -0,0 +1,125 @@ + + + + + + Search — l0learn documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Search
  • +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2021, Hussein Hazimeh, Rahul Mazumder, and Tim Nonet.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js new file mode 100644 index 0000000..1d1a55b --- /dev/null +++ b/docs/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["code","index","tutorial"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,nbsphinx:3,sphinx:56},filenames:["code.rst","index.rst","tutorial.ipynb"],objects:{"l0learn.models":[[0,1,1,"","CVFitModel"],[0,1,1,"","FitModel"],[0,0,1,"","gen_synthetic"],[0,0,1,"","gen_synthetic_high_corr"],[0,0,1,"","gen_synthetic_logistic"],[0,0,1,"","logistic_loss"],[0,0,1,"","regularization_loss"],[0,0,1,"","squared_error"],[0,0,1,"","squared_hinge_loss"]],l0learn:[[0,0,1,"","cvfit"],[0,0,1,"","fit"]]},objnames:{"0":["py","function","Python function"],"1":["py","class","Python class"]},objtypes:{"0":"py:function","1":"py:class"},terms:{"0":[0,2],"0000":2,"00000":2,"000000":2,"000000e":2,"00001":2,"000064":2,"0001":[0,2],"00010":2,"000100":2,"000130":2,"000132":2,"000133":2,"000134":2,"000141":2,"000159":2,"000167":2,"000173":2,"000178":2,"000187":2,"000212":2,"000216":2,"000533":2,"000797":2,"000857":2,"001":2,"0010":2,"00100":2,"001199":2,"001371":2,"001419":2,"001483":2,"001497":2,"001587":2,"0016":2,"0016080760437896327":2,"00190748":2,"002":2,"00200":2,"002048":2,"002130":2,"002157":2,"00215713":2,"002161":2,"002187":2,"002500":2,"002556":2,"002597":2,"002634":2,"002654":2,"002821":2,"002911":2,"002987":2,"003636":2,"003684":2,"003685":2,"003750":2,"003788":2,"003866":2,"004044":2,"004111":2,"004113":2,"004211":2,"004515":2,"004637":2,"004882":2,"005264":2,"005560":2,"005760":2,"0058037":2,"006364":2,"006480":2,"007959":2,"008151":2,"008771":2,"009191":2,"009286":2,"009325":2,"009603":2,"01":2,"0100":2,"01000":2,"010249":2,"010854":2,"01128182":2,"012180":2,"01272103":2,"012732":2,"012762":2,"013932":2,"014394":2,"014442":2,"014785":2,"015045":2,"015697":2,"015724":2,"016000":2,"016811":2,"016812":2,"017599":2,"018729":2,"01994648":2,"02014176":2,"020606":2,"021199":2,"021333":2,"021393":2,"021913":2,"024710":2,"0266543":2,"027624":2,"029115":2,"029367":2,"03":2,"031573":2,"031623":2,"031892":2,"032":2,"032715":2,"03271533058913737":2,"032770":2,"036989":2,"039705":2,"04":2,"041058":2,"041672":2,"043199":2,"043230":2,"043245":2,"044333":2,"044517":2,"047991":2,"048700":2,"05":0,"050464":2,"051675":2,"058013":2,"06":2,"061685":2,"06471":0,"065862":2,"06707991":2,"07":2,"078750":2,"079546":2,"0s":0,"1":[0,2],"10":[0,2],"100":0,"1000":[0,2],"10000":2,"1000x1":2,"1001x1":2,"107":2,"11":2,"110":2,"110626":2,"113":2,"114":2,"114306":2,"115":2,"116":2,"117":2,"118558":2,"12":2,"124533":2,"128":2,"129":2,"13":2,"130":2,"131":2,"132":2,"133":2,"134":2,"14":2,"142882":2,"147182":2,"147743":2,"148003":2,"148650":2,"15":[0,2],"15002055":2,"156250":2,"156704":2,"16":2,"161024":2,"17":2,"178144":2,"18":2,"184678":2,"18562493":2,"188422":2,"188875":2,"19":2,"192033":2,"196406":2,"1d":0,"1e":[0,2],"1s":0,"2":2,"20":[0,2],"200":0,"2001":0,"2020":2,"206093":2,"208883":2,"21":2,"211002":2,"219990":2,"22":2,"221470":2,"225336":2,"23":2,"230848":2,"234899":2,"237988":2,"23997924":2,"24":2,"242631":2,"243655":2,"243993":2,"244295":2,"244520":2,"244716":2,"244886":2,"245011":2,"245133":2,"246182":2,"25":2,"255":2,"25545111":2,"2555564968851251":2,"25555788170828786":2,"2558807301729078":2,"25608131":2,"26":2,"2669789604993652":2,"267":2,"28":2,"297484":2,"2d":0,"3":[0,2],"30":2,"311":2,"31499242":2,"34":2,"3474209":2,"355651":2,"371856":2,"383356":2,"39":2,"4":2,"40":2,"41":2,"411":2,"411211":2,"432053":2,"438":2,"44":2,"45":2,"479195":2,"5":[0,2],"50":2,"500":[0,2],"500x1":2,"500x1000":2,"507616":2,"52":2,"5313128699361661":2,"57":2,"58":2,"582":2,"598994":2,"5th":2,"6":[0,2],"600880":2,"617520":2,"62":2,"667317":2,"680704":2,"68272239":2,"69035746e":2,"69435":2,"69583037e":2,"7":2,"70":2,"72":2,"751100":2,"771900":2,"77309853":2,"77364234":2,"8":[0,2],"83":2,"84":2,"884520":2,"8th":2,"9":[0,2],"90":2,"92195535e":2,"92440655e":2,"928603":2,"93":2,"94":2,"944563":2,"964874":2,"97317979":2,"97338278":2,"99161941e":2,"99171256e":2,"99204841":2,"99607406":2,"99669481":2,"99813347":2,"case":[0,2],"class":[0,2],"default":[0,2],"do":[0,2],"final":2,"float":[0,2],"function":[1,2],"import":2,"int":0,"new":2,"return":[0,2],"true":[0,2],"var":0,A:[0,2],As:0,By:2,For:[0,2],If:[0,2],In:[0,2],The:[0,2],Then:0,These:[0,2],To:2,_0:[0,2],_1:[0,2],_2:[0,2],_subplot:2,abov:[0,2],absolut:0,accept:2,access:2,achiev:[0,2],activ:[0,2],active_set:0,active_set_num:0,ad:2,adapt:0,add:0,addit:2,advanc:1,against:2,algorithim:0,algorithm:[0,2],all:[0,2],allow:2,along:2,also:[0,2],alwai:2,ambigu:2,amount:0,an:[0,2],ani:[0,2],antoin:2,api:2,appear:[0,2],appli:2,applic:2,appropri:2,approxim:2,ar:[0,2],argmin:2,arrai:[0,2],arxiv:0,asid:2,assign:2,associ:2,assum:2,atol:0,attribut:2,automat:[0,2],avail:2,avoid:2,ax:2,axessubplot:2,b0:0,b:[0,2],bar:2,base:[0,2],base_cor:0,becom:0,befor:[0,2],begin:2,behavior:2,belong:2,below:[0,2],bernoulli:0,best:2,beta:2,beta_0:2,beta_i:2,beta_vector:2,better:2,between:[0,2],binari:0,bla:2,bool:0,both:[0,2],bound:0,built:2,c:2,calcul:0,call:[0,2],calucl:0,can:[0,2],cd:[0,2],cdpsi:[0,2],certain:2,chang:[0,2],characterist:2,choic:[0,2],chosen:[0,2],classif:[0,1],classifi:2,clip:0,close:2,closer:0,code:2,coeff:[0,2],coeffici:0,column:[0,2],combin:2,combinatori:[0,2],command:2,commonli:2,compar:2,complet:2,compon:2,compress:2,comput:[0,2],computation:2,connect:2,consecut:0,constraint:2,contain:0,continu:2,control:2,converg:[0,2],convert:2,convex:2,coordin:[0,2],correctli:2,correl:[0,2],correspond:[0,2],cross:[0,1],csc:[0,2],csc_matrix:[0,2],current:[0,2],custom:2,cv:2,cv_fit_result:2,cv_mean:[0,2],cv_plot:2,cv_sd:[0,2],cvfit:[1,2],cvfitmodel:[1,2],cycl:[0,2],cyclic:[0,2],data:[0,2],data_rv:2,dataset:[0,2],decid:0,declar:0,decreas:[0,2],dedieu:2,defin:0,deleg:0,demonstr:2,denot:2,dens:2,densiti:2,depend:[0,2],descent:[0,2],design:2,desir:[0,2],detail:[0,1],dict:0,differ:2,dimens:2,dimension:2,directli:[0,2],discuss:2,dispali:2,displai:2,distribut:0,doc:2,document:2,doe:2,done:2,doubl:0,draw:0,drawn:0,due:2,duplic:2,dure:2,dynam:2,e:[0,2],each:[0,2],easi:2,edgeitem:2,effect:2,effici:2,either:[0,2],elabor:2,element:[0,2],ell:2,emploi:2,empti:0,end:2,ensur:2,enter:2,entri:[0,2],enumer:2,environ:2,ep:0,equat:2,equi:2,error:[0,2],especi:2,estim:2,everi:[0,2],exampl:[0,2],exclud:[0,2],exclude_first_k:[0,2],excludefirstk:2,execut:2,exist:[0,2],exp:0,expens:0,exploit:2,express:2,extend:2,extract:2,f:2,face:2,fals:[0,2],fast:[0,2],featur:[0,2],fill:2,find:2,first:[0,2],fit:1,fit_grid:2,fit_grid_l2:2,fit_model:[0,2],fit_model_2:2,fit_model_3:2,fit_model_bound:2,fit_model_k:2,fit_model_spars:2,fitmodel:[1,2],fix:2,float64:2,fold:[0,2],follow:[0,2],format:2,found:[0,2],fraction:0,from:[0,2],full:0,further:2,futur:2,g:[0,2],gamma:[0,2],gamma_max:[0,2],gamma_min:[0,2],gammamax:0,gammamin:0,gap:0,gen_synthet:[0,2],gen_synthetic_high_corr:[0,2],gen_synthetic_logist:[0,2],gener:[1,2],get:2,github:2,give:2,greater:0,greedi:[0,2],grid:0,gt:2,ha:2,have:[0,2],hazimeh:2,help:2,here:2,heurist:2,high:[0,2],higher:[0,1],highli:2,highs_arrai:2,hing:2,how:2,howev:[0,2],hstack:2,http:0,hussein:2,i:[0,2],iff:0,ignor:[0,2],iid:2,implement:2,improv:2,includ:[0,2],include_intercept:2,include_legend:2,increas:0,independ:0,index:1,indic:2,individu:0,induc:2,inf:0,influenc:2,inform:0,infti:2,initi:0,inspect:2,instal:1,instanc:0,instead:2,instruct:2,integ:[0,2],intercept:[0,2],interfac:2,intern:2,introduct:1,ipython:2,issu:2,item:0,iter:0,ith:[0,2],j:0,jmlr:2,just:2,k:[0,2],keep:2,kei:2,kwarg:2,l0:[0,1],l0l1:[0,1],l0l2:[0,1],l0learn:[0,2],l0lx:0,l1:[0,2],l2:[0,2],l:0,lambda:0,lambda_0:[0,2],lambda_grid:[0,2],langl:2,larg:2,larger:[0,2],latter:2,layout:0,lead:[0,2],learn:2,legend:2,length:[0,2],less:[0,2],let:0,like:2,limit:2,line:2,linear:2,linearli:0,link:0,list:[0,2],load:2,local:[0,1],log:0,logarithm:2,logist:[0,2],logistic_loss:0,loss:[0,2],low:[0,2],lower:0,lowest:2,lt:2,m:0,magnitud:[0,2],mai:0,main:2,maintain:2,make:2,mani:2,manner:2,manual:2,match:2,math:2,matplotlib:2,matric:2,matrix:0,max:[0,2],max_it:0,max_support_s:[0,2],max_swap:0,max_valu:2,maximum:[0,2],mazumd:2,mcp:2,mean:0,method:2,might:2,min:[0,2],min_:2,min_error:2,minim:0,minimum:[0,2],mix:2,model:[0,1],modul:1,more:[0,1],moreov:2,most:2,mu:0,multivari:0,multivariate_norm:0,must:[0,2],n:[0,2],ndarrai:0,need:2,neg:2,newx:2,next:2,nois:0,non:[0,2],none:[0,2],norm:[0,2],normal:[0,2],note:[0,2],notebook:[0,2],notic:2,np:[0,2],num_fold:[0,2],num_gamma:[0,2],num_lambda:[0,2],number:[0,2],numer:2,numpi:[0,2],object:[0,2],observ:0,odd:0,onc:2,one:[0,2],ones:2,onli:2,oper:2,optim:[0,2],optimal_gamma_index:2,optimal_lambda_index:2,option:[0,1],order:2,org:0,other:[0,2],otherwis:0,our:[0,2],outcom:0,outperform:2,output:2,over:[0,2],overfit:2,own:2,p:[0,2],packag:2,page:1,pair:2,paper:[0,2],paramet:[0,2],partial:0,partial_sort:0,particular:2,particularli:2,path:[0,2],pdf:0,penal:2,penalti:[0,2],per:0,perform:[0,2],perspect:2,pip:2,plan:2,pleas:2,plot:2,plu:2,point:[0,2],portion:0,posit:[0,2],precis:2,predict:2,present:2,previou:2,previous:2,print:2,printopt:2,probabl:[0,2],problem:2,proce:2,produc:2,provid:2,py:2,python:2,quad:2,qualiti:[0,1],quit:2,rahul:2,random:[0,2],randomli:[0,2],rang:2,rangl:2,ratio:[0,2],real:2,recal:2,recommend:0,recov:2,refer:1,regim:2,regress:[0,1],regular:[0,2],regularization_loss:0,rel:0,relev:0,repo:2,reproduc:2,request:2,requir:2,requri:0,research:2,reshap:2,residu:[0,2],resolv:2,respect:0,respons:[0,2],rest:[0,2],result:2,rho:0,rich:2,ridg:2,roughli:[0,2],row:[0,2],rtol:0,run:[0,2],rv:2,s:[0,2],same:[0,2],sampl:[0,2],satisfi:2,scalar:[0,2],scale:[0,2],scale_down_factor:0,scipi:[0,2],score:1,screen:[0,2],screen_siz:0,search:[0,1],second:2,section:0,see:[0,2],seed:[0,2],seen:2,select:0,separ:0,sequenc:[0,2],set:[0,2],shape:0,should:[0,2],show:2,show_lin:2,shrinkag:2,shuffl:2,sig:0,sigma:0,sign:2,signal:0,signific:2,similar:2,similarli:2,simpl:2,sinc:2,singl:2,size:[0,2],slightli:2,slower:2,small:[0,2],smaller:0,smooth:[0,2],snr:0,so:[0,2],solut:[0,1],solv:2,some:2,sort:0,sourc:0,space:2,sparis:2,spars:0,sparsiti:2,speak:2,specif:2,specifi:0,specifici:2,speed:2,squar:[0,2],squared_error:0,squared_hinge_loss:0,squarederror:0,squaredhing:[0,2],stabil:0,stabl:2,standard:2,start:2,stat:2,still:2,storag:2,store:2,str:0,strength:2,strictli:[0,2],string:2,structur:2,sub:2,subclass:2,subject:2,submit:2,success:0,successfulli:2,suffici:2,sum_:2,summari:2,suppli:[0,2],support:0,support_s:[0,2],suppos:2,svm:[0,2],swap:0,synthet:[0,2],t:2,take:[0,2],term:[0,2],termin:[0,2],test:2,th:2,than:[0,2],thei:[0,2],thi:[0,2],those:2,three:2,threshold:[0,2],through:2,thu:2,time:0,tnonet:2,toarrai:2,todo:0,toler:0,toolkit:2,top:0,translat:0,tri:0,trick:2,troubleshoot:2,tune:[0,2],tutori:1,two:2,type:[0,2],typic:[0,2],u:0,undefin:0,under:2,underlini:2,unicod:0,union:0,uniqu:2,up:2,updat:[0,2],upper:0,us:[0,1],user_lambda_grid:2,user_lambda_grid_l2:2,userwarn:2,valid:[0,1],valu:[0,2],variabl:0,variant:0,vector:[0,2],veri:[0,2],verifi:2,version:2,versu:2,view:2,vignett:2,visual:2,want:2,warm:2,warn:2,wast:2,we:[0,2],when:[0,2],where:[0,2],which:[0,2],wiki:2,without:2,word:0,work:2,x:[0,2],x_i:2,x_spars:2,xb:[0,2],xi:0,xlabel:2,y:[0,2],y_i:2,y_pred:0,y_spars:2,y_true:0,yi:0,ylabel:2,you:2,your:2,zero:[0,2]},titles:["fit function","Welcome to l0learn\u2019s documentation!","Introduction"],titleterms:{"function":0,advanc:2,bound:2,classif:2,coeffici:2,cross:2,cvfit:0,cvfitmodel:0,detail:2,document:1,fit:[0,2],fitmodel:0,gener:0,grid:2,higher:2,indic:1,instal:2,introduct:2,l0:2,l0l1:2,l0l2:2,l0learn:1,lambda:2,local:2,matrix:2,model:2,more:2,option:2,qualiti:2,refer:2,regress:2,s:1,score:0,search:2,select:2,solut:2,spars:2,specifi:2,subset:2,support:2,tabl:1,tutori:2,us:2,user:2,valid:2,variabl:2,welcom:1}}) \ No newline at end of file diff --git a/docs/tutorial.html b/docs/tutorial.html new file mode 100644 index 0000000..b56f9a8 --- /dev/null +++ b/docs/tutorial.html @@ -0,0 +1,2155 @@ + + + + + + + Introduction — l0learn documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Introduction

+

l0learn is a fast toolkit for L0-regularized learning. L0 regularization selects the best subset of features and can outperform commonly used feature selection methods (e.g., L1 and MCP) under many sparse learning regimes. The toolkit can (approximately) solve the following three problems

+

\begin{equation} +\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 \quad \quad (L0) +\end{equation}

+

\begin{equation} +\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 + \gamma||\beta||_1 \quad (L0L1) +\end{equation}

+

\begin{equation} +\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 + \gamma||\beta||_2^2 \quad (L0L2) +\end{equation}

+

where \(\ell\) is the loss function, \(\beta_0\) is the intercept, \(\beta\) is the vector of coefficients, and \(||\beta||_0\) denotes the L0 norm of \(\beta\), i.e., the number of non-zeros in \(\beta\). We support both regression and classification using either one of the following loss functions:

+
    +
  • Squared error loss

  • +
  • Logistic loss (logistic regression)

  • +
  • Squared hinge loss (smoothed version of SVM).

  • +
+

The parameter \(\lambda\) controls the strength of the L0 regularization (larger \(\lambda\) leads to less non-zeros). The parameter \(\gamma\) controls the strength of the shrinkage component (which is the L1 norm in case of L0L1 or squared L2 norm in case of L0L2); adding a shrinkage term to L0 can be very effective in avoiding overfitting and typically leads to better predictive models. The fitting is done over a grid of \(\lambda\) and \(\gamma\) values to generate a +regularization path.

+

The algorithms provided in l0learn` are based on cyclic coordinate descent and local combinatorial search. Many computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for +dynamically selecting the regularization parameter \(\lambda\) in the path. For more details on the algorithms used, please refer to our paper Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms.

+

The toolkit is implemented in C++ along with an easy-to-use Python interface. In this vignette, we provide a tutorial on using the Python interface. Particularly, we will demonstrate how use L0Learn’s main functions for fitting models, cross-validation, and visualization.

+
+
+

Installation

+

L0Learn can be installed directly from pip by executing:

+
pip install l0learn
+
+
+

If you face installation issues, please refer to the Installation Troubleshooting Wiki. If the issue is not resolved, you can submit an issue on L0Learn’s Github Repo.

+
+
+

Tutorial

+

To demonstrate how l0learn works, we will first generate a synthetic dataset and then proceed to fitting L0-regularized models. The synthetic dataset (y,X) will be generated from a sparse linear model as follows:

+
    +
  • X is a 500x1000 design matrix with iid standard normal entries

  • +
  • B is a 1000x1 vector with the first 10 entries set to 1 and the rest are zeros.

  • +
  • e is a 500x1 vector with iid standard normal entries

  • +
  • y is a 500x1 response vector such that y = XB + e

  • +
+

This dataset can be generated in python as follows:

+
+
[1]:
+
+
+
+import numpy as np
+np.random.seed(4) # fix the seed to get a reproducible result
+n, p, k = 500, 1000, 10
+X = np.random.normal(size=(n, p))
+B = np.zeros(p)
+B[:k] = 1
+e = np.random.normal(size=(n,))/2
+y = X@B + e
+
+
+
+

More expressive and complete functions for generating datasets can be found are available in l0learn.models. The available functions are:

+ +

We will use l0learn to estimate B from the data (y,X). First we load L0Learn:

+
+
[2]:
+
+
+
+import l0learn
+
+
+
+

We will start by fitting a simple L0 model and then proceed to the case of L0L2 and L0L1.

+
+

Fitting L0 Regression Models

+

To fit a path of solutions for the L0-regularized model with at most 20 non-zeros using coordinate descent (CD), we use the l0learn.fit function as follows:

+
+
[3]:
+
+
+
+fit_model = l0learn.fit(X, y, penalty="L0", max_support_size=20)
+
+
+
+

This will generate solutions for a sequence of \(\lambda\) values (chosen automatically by the algorithm). To view the sequence of \(\lambda\) along with the associated support sizes (i.e., the number of non-zeros), we use the built-in rich display from ipython Rich Display for iPython Notebooks. When running this tutorial in a more standard python environment, use the function +l0learn.models.FitModel.characteristics to display the sequence of solutions.

+
+
[4]:
+
+
+
+fit_model
+# fit_model.characteristics()
+
+
+
+
+
[4]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconverged
00.0795460-0.156704True
10.0787501-0.147182True
20.0658622-0.161024True
30.0504643-0.002500True
40.0445175-0.041058True
50.0416727-0.058013True
60.0397058-0.061685True
70.032715100.002157True
80.00021211-0.000857True
90.00018712-0.002161True
100.00017813-0.001199True
110.00015915-0.007959True
120.00014116-0.009603True
130.00013318-0.015697True
140.00013221-0.012732True
+
+
+

To extract the estimated B for particular values of \(\lambda\) and \(\gamma\), we use the function l0learn.models.FitModel.coeff. For example, the solution at \(\lambda = 0.032715\) (which corresponds to a support size of 10 + plus an intercept term) can be extracted using:

+
+
[6]:
+
+
+
+fit_model.coeff(lambda_0=0.032715, gamma=0)
+
+
+
+
+
[6]:
+
+
+
+
+<1001x1 sparse matrix of type '<class 'numpy.float64'>'
+        with 11 stored elements in Compressed Sparse Column format>
+
+
+

The output is a sparse matrix of type scipy.sparse.csc_matrix. Depending on the include_intercept parameter of l0learn.models.FitModel.coeff, The first element in the vector is the intercept and the rest are the B coefficients. Aside from the intercept, the only non-zeros in the above solution are coordinates 0, 1, 2, 3, …, 9, which are the non-zero coordinates +in the true support (used to generated the data). Thus, this solution successfully recovers the true support. Note that on some BLAS implementations, the lambda value we used above (i.e., 0.032715) might be slightly different due to the limitations of numerical precision. Moreover, all the solutions in the regularization path can be extracted at once by calling l0learn.models.FitModel.coeff without specifying a lambda_0 or gamma value.

+

The sequence of \(\lambda\) generated by L0Learn is stored in the object fit_model. Specifically, fit_model.lambda_0 is a list, where each element of the list is a sequence of \(\lambda\) values corresponding to a single value of \(\gamma\). When using an L0 penalty , which has only one value of \(\gamma\) (i.e., 0), we can access the sequence of \(\lambda\) values using fit.lambda_0[0]. Thus, \(\lambda=0.032715\) we used previously can be accessed using +fit_model.lambda_0[0][7] (since it is the 8th value in the output of :code:fit.characteristics()). The previous solution can also be extracted using fit_model.coeff(lambda_0=0.032, gamma=0).

+
+
[8]:
+
+
+
+print(f"fit_model.lambda_0[0][7] = {fit_model.lambda_0[0][7]}")
+fit_model.coeff(lambda_0=0.032715, gamma=0).toarray()
+
+
+
+
+
+
+
+
+fit_model.lambda_0[0][7] = 0.03271533058913737
+
+
+
+
[8]:
+
+
+
+
+array([[0.00215713],
+       [1.02014176],
+       [0.97338278],
+       ...,
+       [0.        ],
+       [0.        ],
+       [0.        ]])
+
+
+

We can make predictions using a specific solution in the grid using the function fit_model.predict(newx, lambda, gamma) where newx is a testing sample (vector or matrix). For example, to predict the response for the samples in the data matrix X using the solution with \(\lambda=0.0058037\), we call the prediction function as follows:

+
+
[10]:
+
+
+
+with np.printoptions(threshold=10):
+    print(fit_model.predict(x=X, lambda_0=0.032715, gamma=0))
+
+
+
+
+
+
+
+
+[[-2.68272239]
+ [-3.667317  ]
+ [-1.77309853]
+ ...
+ [ 2.25545111]
+ [-0.77364234]
+ [-2.15002055]]
+
+
+

We can also visualize the regularization path by plotting the coefficients of the estimated B versus the support size (i.e., the number of non-zeros) using the l0learn.models.FitModel.plot() method as follows:

+
+
[9]:
+
+
+
+ax = fit_model.plot(include_legend=True)
+
+
+
+
+
+
+
+_images/tutorial_18_0.png +
+
+

The legend of the plot presents the variables in the order they entered the regularization path. For example, variable 7 is the first variable to enter the path, and variable 6 is the second to enter. Thus, roughly speaking, we can view the first \(k\) variables in the legend as the best subset of size \(k\). To show the lines connecting the points in the plot, we can set the parameter :code:show_lines=True in the plot function, i.e., call +:code:fit.plot(fit, gamma=0, show_lines=True). Moreover, we note that the plot function returns a matplotlib.axes._subplots.AxesSubplot object, which can be further customized using the matplotlib package. In addition, both the l0learn.models.FitModel.plot() and l0learn.models.CVFitModel.cv_plot() accept +:code:**kwargs parameter to allow for customization of the plotting behavior.

+
+
[11]:
+
+
+
+fit_model.plot(show_lines=True)
+
+
+
+
+
[11]:
+
+
+
+
+<AxesSubplot:xlabel='Support Size', ylabel='Coefficient Value'>
+
+
+
+
+
+
+_images/tutorial_20_1.png +
+
+
+
+

Fitting L0L2 and L0L1 Regression Models

+

We have demonstrated the simple case of using an L0 penalty. We can also fit more elaborate models that combine L0 regularization with shrinkage-inducing penalties like the L1 norm or squared L2 norm. Adding shrinkage helps in avoiding overfitting and typically improves the predictive performance of the models. Next, we will discuss how to fit a model using the L0L2 penalty for a two-dimensional grid of :math:\lambda and :math:\gamma values. Recall that by default, l0learn +automatically selects the :math:\lambda sequence, so we only need to specify the :math:\gamma sequence. Suppose we want to fit an L0L2 model with a maximum of 20 non-zeros and a sequence of 5 :math:\gamma values ranging between 0.0001 and 10. We can do so by calling l0learn.fit with :code:penalty="L0L2", :code:num_gamma=5, :code:gamma_min=0.0001, and :code:gamma_max=10 as follows:

+
+
[12]:
+
+
+
+fit_model_2 = l0learn.fit(X, y, penalty="L0L2", num_gamma = 5, gamma_min = 0.0001, gamma_max = 10, max_support_size=20)
+
+
+
+

l0learn will generate a grid of 5 :math:\gamma values equi-spaced on the logarithmic scale between 0.0001 and 10. Similar to the case for L0, we can display a summary of the regularization path using the l0learn.models.FitModel.characteristics function as follows:

+
+
[14]:
+
+
+
+fit_model_2 # Using ipython Rich Display
+# fit_model_2.characteristics()  # For non Rich Display
+
+
+
+
+
[14]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconvergedl2
00.0037880-0.156704True10.0000
10.0037501-0.156250True10.0000
20.0029112-0.148003True10.0000
30.0026543-0.148650True10.0000
40.0025973-0.148650True10.0000
..................
1280.000216100.002130True0.0001
1290.00017313-0.003684True0.0001
1300.00016713-0.003685True0.0001
1310.00013418-0.015724True0.0001
1320.00013021-0.012762True0.0001
+

133 rows × 5 columns

+
+
+

The sequence of \(\gamma\) values can be accessed using fit_model_2.gamma. To extract a solution we use the l0learn.models.FitModel.coeff method. For example, extracting the solution at \(\lambda=0.0016\) and \(\gamma=10\) can be done using:

+
+
[40]:
+
+
+
+fit_model_2.coeff(lambda_0=0.0016, gamma=10)
+
+
+
+
+
[40]:
+
+
+
+
+<1001x1 sparse matrix of type '<class 'numpy.float64'>'
+        with 11 stored elements in Compressed Sparse Column format>
+
+
+

Similarly, we can predict the response at this pair of \(\lambda\) and \(\gamma\) for the matrix X using

+
+
[41]:
+
+
+
+with np.printoptions(threshold=10):
+    print(fit_model_2.predict(x=X, lambda_0=0.0016, gamma=10))
+
+
+
+
+
+
+
+
+[[-0.31499242]
+ [-0.3474209 ]
+ [-0.23997924]
+ ...
+ [-0.06707991]
+ [-0.18562493]
+ [-0.25608131]]
+
+
+

The regularization path can also be plotted at a specific \(\gamma\) using fit_model_2.plot(gamma=10). Here we can see the influence of ratio of \(\gamma\) and \(\lambda\). Since \(\gamma\) is so large (\(10\)) maintaining sparisity, then \(\lambda\) can be quite small \(0.0016\) which this results in a very stable estimate of the magnitude of the coeffs.

+
+
[44]:
+
+
+
+fit_model_2.plot(gamma=10, show_lines=True)
+
+
+
+
+
+
+
+
+
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 3. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 10. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 11. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 12. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+
+
+
+
[44]:
+
+
+
+
+<AxesSubplot:xlabel='Support Size', ylabel='Coefficient Value'>
+
+
+
+
+
+
+_images/tutorial_30_2.png +
+
+

Finally, we note that fitting an L0L1 model can be done by just changing the penalty to “L0L1” in the above (in this case gamma_max will be ignored since it is automatically selected by the toolkit; see the reference manual for more details.)

+
+ +
+

Cross-validation

+

We will demonstrate how to use K-fold cross-validation (CV) to select the optimal values of the tuning parameters \(\lambda\) and \(\gamma\). To perform CV, we use the l0learn.cvfit function, which takes the same parameters as l0learn.fit, in addition to the number of folds using the num_folds parameter and a seed value using the seed parameter (this is used when randomly shuffling the data before performing CV).

+

For example, to perform 5-fold CV using the L0L2 penalty (over a range of 5 gamma values between 0.0001 and 0.1) with a maximum of 50 non-zeros, we run:

+
+
[45]:
+
+
+
+cv_fit_result = l0learn.cvfit(X, y, num_folds=5, seed=1, penalty="L0L2", num_gamma=5, gamma_min=0.0001, gamma_max=0.1, max_support_size=50)
+
+
+
+

Note that the object returned during cross validation is l0learn.models.CVFitModel which subclasses l0learn.models.FitModel and thus has the same methods and underlinying structure. The cross-validation errors can be accessed using the cv_means attribute of a CVFitModel: cv_fit_result.cv_means is a list where the ith element, cv_fit_result.cv_means[i], stores the cross-validation errors for the ith +value of gamma cv_fit_result.gamma[i]). To find the minimum cross-validation error for every gamma, we apply the :code:np.argmin function for every element in the list :cv_fit_result.cv_means, as follows:

+
+
[52]:
+
+
+
+gamma_mins = [(i, np.argmin(cv_mean), np.min(cv_mean)) for i, cv_mean in enumerate(cv_fit_result.cv_means)]
+gamma_mins
+
+
+
+
+
[52]:
+
+
+
+
+[(0, 8, 0.5313128699361661),
+ (1, 8, 0.2669789604993652),
+ (2, 8, 0.2558807301729078),
+ (3, 20, 0.25555788170828786),
+ (4, 19, 0.2555564968851251)]
+
+
+

The above output indicates that the 5th value of gamma achieves the lowest CV error (=0.255). We can plot the CV errors against the support size for the 5th value of gamma, i.e., gamma = cv_fit_result.gamma[4], using:

+
+
[50]:
+
+
+
+cv_fit_result.cv_plot(gamma = cv_fit_result.gamma[4])
+cv_fit_result.cv_sds
+
+
+
+
+
[50]:
+
+
+
+
+<AxesSubplot:xlabel='Support Size', ylabel='Cross-Validation Error'>
+
+
+
+
+
+
+_images/tutorial_36_1.png +
+
+

The above plot is produced using the matplotlib package and returns a matplotlib.axes._subplots.AxesSubplot which can be further customized by the user. We can also note that we have error bars in the cross validation error which is stored in cv_sds attribute and can be accessed with cv_fit_result.cv_sds. To extract the optimal +\(\lambda\) (i.e., the one with minimum CV error) in this plot, we execute the following:

+
+
[57]:
+
+
+
+optimal_gamma_index, optimal_lambda_index, min_error = min(gamma_mins, key = lambda t: t[2])
+print(optimal_gamma_index, optimal_lambda_index, min_error)
+print("Optimal lambda = ", fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index])
+
+
+
+
+
+
+
+
+4 19 0.2555564968851251
+Optimal lambda =  0.0016080760437896327
+
+
+

To print the solution corresponding to the optimal gamma/lambda pair:

+
+
[58]:
+
+
+
+cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],
+                    gamma=fit_model_2.gamma[optimal_gamma_index])
+
+
+
+
+
[58]:
+
+
+
+
+<1001x1 sparse matrix of type '<class 'numpy.float64'>'
+        with 11 stored elements in Compressed Sparse Column format>
+
+
+

The optimal solution (above) selected by cross-validation correctly recovers the support of the true vector of coefficients used to generate the model.

+
+
[70]:
+
+
+
+beta_vector = cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],
+                    gamma=fit_model_2.gamma[optimal_gamma_index],
+                    include_intercept=False).toarray()
+
+with np.printoptions(threshold=30, edgeitems=15):
+    print(np.hstack([beta_vector, B.reshape(-1, 1)]))
+
+
+
+
+
+
+
+
+[[1.01994648 1.        ]
+ [0.97317979 1.        ]
+ [0.99813347 1.        ]
+ [0.99669481 1.        ]
+ [1.01128182 1.        ]
+ [1.00190748 1.        ]
+ [1.01272103 1.        ]
+ [0.99204841 1.        ]
+ [0.99607406 1.        ]
+ [1.0266543  1.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ ...
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]
+ [0.         0.        ]]
+
+
+
+
+

Fitting Classification Models

+

All the commands and plots we have seen in the case of regression extend to classification. We currently support logistic regression (using the parameter loss="Logistic") and a smoothed version of SVM (using the parameter loss="SquaredHinge"). To give some examples, we first generate a synthetic classification dataset (similar to the one we generated in the case of regression):

+
+
[72]:
+
+
+
+import numpy as np
+np.random.seed(4) # fix the seed to get a reproducible result
+n, p, k = 500, 1000, 10
+X = np.random.normal(size=(n, p))
+B = np.zeros(p)
+B[:k] = 1
+e = np.random.normal(size=(n,))/2
+y = np.sign(X@B + e)
+
+
+
+

More expressive and complete functions for generating datasets can be found are available in l0learn.models. The available functions are:

+ +

An L0-regularized logistic regression model can be fit by specificying loss = "Logistic" as follows:

+
+
[117]:
+
+
+
+fit_model_3 = l0learn.fit(X,y,loss="Logistic", max_support_size=20)
+fit_model_3
+
+
+
+
+
[117]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconvergedl2
025.2253361-0.036989True1.000000e-07
119.4320532-0.043199True1.000000e-07
218.9286032-0.043230True1.000000e-07
315.1428822-0.043245True1.000000e-07
412.11430690.004044True1.000000e-07
57.411211100.188422False1.000000e-07
67.188875100.219990False1.000000e-07
75.751100100.234899False1.000000e-07
84.600880100.242631False1.000000e-07
93.680704100.243655True1.000000e-07
102.944563100.243993True1.000000e-07
112.355651100.244295True1.000000e-07
121.884520100.244520True1.000000e-07
131.507616100.244716True1.000000e-07
141.206093100.244886True1.000000e-07
150.964874100.245011True1.000000e-07
160.771900100.245133True1.000000e-07
170.617520120.178144False1.000000e-07
180.598994120.196406False1.000000e-07
190.479195120.208883False1.000000e-07
200.383356150.192033False1.000000e-07
210.371856150.221470False1.000000e-07
220.297484150.246182False1.000000e-07
230.23798816-0.110626False1.000000e-07
240.23084816-0.118558False1.000000e-07
250.18467816-0.124533False1.000000e-07
260.14774321-0.211002False1.000000e-07
+
+
+

The output above indicates that \(\gamma=10^{-7}\) by default we use a small ridge regularization (with \(\gamma=10^{-7}\)) to ensure the existence of a unique solution. To extract the coefficients of the solution with \(\lambda = 8.69435\) we use the following code. Notice that we can ignore the specification of gamma as there is only one gamma used in L0 Logistic regression:

+
+
[83]:
+
+
+
+np.where(fit_model_3.coeff(lambda_0=7.411211).toarray() > 0)[0]
+
+
+
+

The above indicates that the 10 non-zeros in the estimated model match those we used in generating the data (i.e, L0 regularization correctly recovered the true support). We can also make predictions at the latter \(\lambda\) using:

+
+
[84]:
+
+
+
+with np.printoptions(threshold=10):
+   print(fit_model_3.predict(X, lambda_0=7.411211))
+
+
+
+
+
+
+
+
+[[1.69583037e-04]
+ [4.92440655e-06]
+ [3.92195535e-03]
+ ...
+ [9.99161941e-01]
+ [1.69035746e-01]
+ [9.99171256e-04]]
+
+
+

Each row in the above is the probability that the corresponding sample belongs to class \(1\). Other models (i.e., L0L2 and L0L1) can be similarly fit by specifying loss = "Logistic".

+

Finally, we note that L0Learn also supports a smoothed version of SVM by using squared hinge loss loss = "SquaredHinge". The only difference from logistic regression is that the predict function returns \(\beta_0 + \langle x, \beta \rangle\) (where \(x\) is the testing sample), instead of returning probabilities. The latter predictions can be assigned to the appropriate classes by using a thresholding function (e.g., the sign function).

+
+
+

Advanced Options

+
+

Sparse Matrix Support

+

Starting in version 2.0.0, L0Learn supports sparse matrices of type scipy.sparse.csc_matrix. If your sparse matrix uses a different storage format, please convert it to csc_matrix before using it in l0learn. l0learn keeps the matrix sparse internally and thus is highly efficient if the matrix is sufficiently sparse. The API for sparse matrices is the same as that of dense matrices, so all the +demonstrations in this vignette also apply for sparse matrices. For example, we can fit an L0-regularized model on a sparse matrix as follows:

+
+
[90]:
+
+
+
+from scipy.sparse import random
+from scipy.stats import norm
+
+
+X_sparse = random(n, p, density=0.01, format='csc', data_rvs=norm().rvs)
+y_sparse = (X_sparse@B + e)
+
+fit_model_sparse = l0learn.fit(X_sparse, y_sparse, penalty="L0", max_support_size=20)
+
+fit_model_sparse.characteristics()
+
+
+
+
+
[90]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconverged
00.03189200.009325True
10.03157310.004882True
20.0206062-0.002187True
30.0144423-0.004111True
40.0139324-0.002556True
50.01085450.002987True
60.00928660.002048True
70.0091917-0.001371True
80.0087718-0.000533True
90.00815190.000064True
100.006480110.001587True
110.00636412-0.003636True
120.00576013-0.003866True
130.00556015-0.004211True
140.005264170.001497True
150.00463718-0.000797True
160.00451519-0.002634True
170.004113220.001419True
+
+
+
+
+

Selection on Subset of Variables

+

In certain applications, it is desirable to always include some of the variables in the model and perform variable selection on others. l0learn supports this option through the exclude_first_k parameter. Specifically, setting exclude_first_k = K (where K is a non-negative integer) instructs l0learn to exclude the first K variables in the data matrix X from the L0-norm penalty (those K variables will still be penalized using the L2 or L1 norm penalties.). For example, below we +fit an L0 model and exclude the first 3 variables from selection by setting excludeFirstK = 3:

+
+
[94]:
+
+
+
+fit_model_k = l0learn.fit(X, y, penalty="L0", max_support_size=10, exclude_first_k=3)
+fit_model_k.characteristics()
+
+
+
+
+
[94]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconverged
00.0504643-0.017599True
10.0443334-0.021333True
20.0327705-0.027624True
30.0293677-0.029115True
40.0247108-0.021199True
50.02139390.010249True
60.014785100.016812True
+
+
+

Plotting the regularization path:

+
+
[93]:
+
+
+
+fit_model_k.plot(show_lines=True)
+
+
+
+
+
[93]:
+
+
+
+
+<AxesSubplot:xlabel='Support Size', ylabel='Coefficient Value'>
+
+
+
+
+
+
+_images/tutorial_57_1.png +
+
+

We can see in the plot above that first 3 variables, (0, 1, 2) are included in all the solutions of the path

+
+
+

Coefficient Bounds

+

Starting in version 2.0.0, l0learn supports bounds for CD algorithms for all losses and penalties. (We plan to support bound constraints for the CDPSI algorithm in the future). By default, l0learn does not apply bounds, i.e., it assumes \(-\infty <= \beta_i <= \infty\) for all i. Users can supply the same bounds for all coefficients by setting the parameters lows and highs to scalar values (these should satisfy: lows <= 0, lows != highs, and highs >= 0). To use +different bounds for the coefficients, lows and highs can be both set to vectors of length p (where the i-th entry corresponds to the bound on coefficient i).

+

All of the following examples are valid.

+
+
[107]:
+
+
+
+l0learn.fit(X, y, penalty="L0", lows=-0.5)
+l0learn.fit(X, y, penalty="L0", highs=0.5)
+l0learn.fit(X, y, penalty="L0", lows=-0.5, highs=0.5)
+
+max_value = 0.25
+highs_array = max_value*np.ones(p)
+highs_array[0] = 0.1
+fit_model_bounds = l0learn.fit(X, y, penalty="L0", lows=-0.1, highs=highs_array, max_support_size=20)
+
+
+
+

We can see the coefficients are subject to the bounds.

+
+
[110]:
+
+
+
+fit_model_bounds.plot(show_lines=True)
+
+print(f"maximum value of coefficients = {np.max(fit_model_bounds.coeffs[0])} <= {max_value}")
+print(f"maximum value of first coefficient = {np.max(fit_model_bounds.coeffs[0][0, :])} <= 0.1")
+print(f"minimum value of coefficient = {np.min(fit_model_bounds.coeffs[0])} >= -0.1")
+
+
+
+
+
+
+
+
+
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 0. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 6. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 8. Plotting only first solution
+  warnings.warn(f"Duplicate solution seen at support size {support_size}. Plotting only first solution")
+
+
+
+
+
+
+
+maximum value of coefficients = 0.25 <= 0.25
+maximum value of first coefficient = 0.1 <= 0.1
+minimum value of coefficient = -0.1 >= -0.1
+
+
+
+
+
+
+_images/tutorial_61_2.png +
+
+
+
+

User-specified Lambda Grids

+

By default, l0learn selects the sequence of lambda values in an efficient manner to avoid wasted computation (since close \(\lambda\) values can typically lead to the same solution). Advanced users of the toolkit can change this default behavior and supply their own sequence of \(\lambda\) values. This can be done supplying the \(\lambda\) values through the parameter lambda_grid. When lambda_grid is supplied, we require num_gamma and num_lambda to be None to +ensure the is no ambiguity in the solution path requested.

+

Specifically, the value assigned to lambda_grid should be a list of lists/arrays of decreasing positive values (floats). The length of lambda_grid (the number of lists stored) specifies the number of gamma parameters that will fill between gamma_min, and gamma_max. In the case of L0 penalty, lambda_grid must be a list of length 1. In case of L0L2/L0L1 lambda_grid can have any number of sub-lists stored. The ith element in lambda_grid should be a strictly +decreasing sequence of positive lambda values which are used by the algorithm for the ith value of gamma. For example, to fit an L0 model with the sequence of user-specified lambda values: 1, 1e-1, 1e-2, 1e-3, 1e-4, we run the following:

+
+
[113]:
+
+
+
+user_lambda_grid = [[1, 1e-1, 1e-2, 1e-3, 1e-4]]
+fit_grid = l0learn.fit(X, y, penalty="L0", lambda_grid=user_lambda_grid, max_support_size=1000, num_lambda=None, num_gamma=None)
+
+
+
+

To verify the results we print the fit object:

+
+
[114]:
+
+
+
+fit_grid
+# Use fit_grid.characteristics() for those without rich dispalys
+
+
+
+
+
[114]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconverged
01.00000-0.016000True
10.10000-0.016000True
20.0100100.016811True
30.0010620.018729True
40.00012670.051675True
+
+
+

Note that the \(\lambda\) values above are the desired values. For L0L2 and L0L1 penalties, the same can be done where the lambda_grid parameter.

+
+
[115]:
+
+
+
+user_lambda_grid_L2 = [[1, 1e-1, 1e-2, 1e-3, 1e-4],
+                       [10, 2, 1, 0.01, 0.002, 0.001, 1e-5],
+                       [1e-4, 1e-5]]
+
+# user_lambda_grid_L2[[i]] must be a sequence of positive decreasing reals.
+fit_grid_L2 = l0learn.fit(X, y, penalty="L0L2", lambda_grid=user_lambda_grid_L2, max_support_size=1000, num_lambda=None, num_gamma=None)
+
+
+
+
+
[116]:
+
+
+
+fit_grid_L2
+# Use fit_grid_L2.characteristics() for those without rich dispalys
+
+
+
+
+
[116]:
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
l0support_sizeinterceptconvergedl2
01.000000-0.016000True10.000000
10.100000-0.016000True10.000000
20.010000-0.016000True10.000000
30.001009-0.014394True10.000000
40.00010134-0.012180True10.000000
510.000000-0.016000True0.031623
62.000000-0.016000True0.031623
71.000000-0.016000True0.031623
80.01000100.015045True0.031623
90.00200280.001483True0.031623
100.00100580.002821True0.031623
110.000015820.021913True0.031623
120.000103110.048700True0.000100
130.000014110.047991False0.000100
+
+
+
+
+
+
+

More Details

+

For more details please inspect the doc strings of:

+ +
+
+

References

+

Hussein Hazimeh and Rahul Mazumder. Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms. Operations Research (2020).

+

Antoine Dedieu, Hussein Hazimeh, and Rahul Mazumder. Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives. JMLR (to appear).

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/tutorial.ipynb b/docs/tutorial.ipynb new file mode 100644 index 0000000..78fd59c --- /dev/null +++ b/docs/tutorial.ipynb @@ -0,0 +1,2358 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Introduction\n", + "`l0learn` is a fast toolkit for L0-regularized learning. L0 regularization selects the best subset of features and can outperform commonly used feature selection methods (e.g., L1 and MCP) under many sparse learning regimes. The toolkit can (approximately) solve the following three problems\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 \\quad \\quad (L0) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_1 \\quad (L0L1) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_2^2 \\quad (L0L2)\n", + "\\end{equation}\n", + "\n", + "where $\\ell$ is the loss function, $\\beta_0$ is the intercept, $\\beta$ is the vector of coefficients, and $||\\beta||_0$ denotes the L0 norm of $\\beta$, i.e., the number of non-zeros in $\\beta$. We support both regression and classification using either one of the following loss functions:\n", + "\n", + "* Squared error loss\n", + "* Logistic loss (logistic regression)\n", + "* Squared hinge loss (smoothed version of SVM).\n", + "\n", + "The parameter $\\lambda$ controls the strength of the L0 regularization (larger $\\lambda$ leads to less non-zeros). The parameter $\\gamma$ controls the strength of the shrinkage component (which is the L1 norm in case of L0L1 or squared L2 norm in case of L0L2); adding a shrinkage term to L0 can be very effective in avoiding overfitting and typically leads to better predictive models. The fitting is done over a grid of $\\lambda$ and $\\gamma$ values to generate a regularization path. \n", + "\n", + "The algorithms provided in l0learn` are based on cyclic coordinate descent and local combinatorial search. Many computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter $\\lambda$ in the path. For more details on the algorithms used, please refer to our paper [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919).\n", + "\n", + "The toolkit is implemented in C++ along with an easy-to-use Python interface. In this vignette, we provide a tutorial on using the Python interface. Particularly, we will demonstrate how use L0Learn's main functions for fitting models, cross-validation, and visualization.\n", + "\n", + "# Installation\n", + "L0Learn can be installed directly from pip by executing:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "```console\n", + "pip install l0learn\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "If you face installation issues, please refer to the [Installation Troubleshooting Wiki](https://github.com/hazimehh/L0Learn/wiki/Installation-Troubleshooting). If the issue is not resolved, you can submit an issue on [L0Learn's Github Repo](https://github.com/hazimehh/L0Learn).\n", + "\n", + "# Tutorial\n", + "To demonstrate how `l0learn` works, we will first generate a synthetic dataset and then proceed to fitting L0-regularized models. The synthetic dataset (y,X) will be generated from a sparse linear model as follows:\n", + "\n", + "* X is a 500x1000 design matrix with iid standard normal entries\n", + "* B is a 1000x1 vector with the first 10 entries set to 1 and the rest are zeros.\n", + "* e is a 500x1 vector with iid standard normal entries\n", + "* y is a 500x1 response vector such that y = XB + e\n", + "\n", + "This dataset can be generated in python as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = X@B + e" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We will use `l0learn` to estimate B from the data (y,X). First we load L0Learn:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import l0learn" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We will start by fitting a simple L0 model and then proceed to the case of L0L2 and L0L1.\n", + "\n", + "## Fitting L0 Regression Models\n", + "To fit a path of solutions for the L0-regularized model with at most 20 non-zeros using coordinate descent (CD), we use the [l0learn.fit](code.rst#l0learn.models.gen_synthetic) function as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "fit_model = l0learn.fit(X, y, penalty=\"L0\", max_support_size=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "This will generate solutions for a sequence of $\\lambda$ values (chosen automatically by the algorithm). To view the sequence of $\\lambda$ along with the associated support sizes (i.e., the number of non-zeros), we use the built-in rich display from [ipython Rich Display](https://ipython.readthedocs.io/en/stable/config/integrating.html+) for iPython Notebooks. When running this tutorial in a more standard python environment, use the function [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) to display the sequence of solutions." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
00.0795460-0.156704True
10.0787501-0.147182True
20.0658622-0.161024True
30.0504643-0.002500True
40.0445175-0.041058True
50.0416727-0.058013True
60.0397058-0.061685True
70.032715100.002157True
80.00021211-0.000857True
90.00018712-0.002161True
100.00017813-0.001199True
110.00015915-0.007959True
120.00014116-0.009603True
130.00013318-0.015697True
140.00013221-0.012732True
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model\n", + "# fit_model.characteristics()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "To extract the estimated B for particular values of $\\lambda$ and $\\gamma$, we use the function [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff). For example, the solution at $\\lambda = 0.032715$ (which corresponds to a support size of 10 + plus an intercept term) can be extracted using:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "<1001x1 sparse matrix of type ''\n", + "\twith 11 stored elements in Compressed Sparse Column format>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model.coeff(lambda_0=0.032715, gamma=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The output is a sparse matrix of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). Depending on the `include_intercept` parameter of [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff), The first element in the vector is the intercept and the rest are the B coefficients. Aside from the intercept, the only non-zeros in the above solution are coordinates 0, 1, 2, 3, ..., 9, which are the non-zero coordinates in the true support (used to generated the data). Thus, this solution successfully recovers the true support. Note that on some BLAS implementations, the `lambda` value we used above (i.e., `0.032715`) might be slightly different due to the limitations of numerical precision. Moreover, all the solutions in the regularization path can be extracted at once by calling [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) without specifying a `lambda_0` or `gamma` value.\n", + "\n", + "The sequence of $\\lambda$ generated by `L0Learn` is stored in the object `fit_model`. Specifically, `fit_model.lambda_0` is a list, where each element of the list is a sequence of $\\lambda$ values corresponding to a single value of $\\gamma$. When using an L0 penalty , which has only one value of $\\gamma$ (i.e., 0), we can access the sequence of $\\lambda$ values using `fit.lambda_0[0]`. Thus, $\\lambda=0.032715$ we used previously can be accessed using `fit_model.lambda_0[0][7]` (since it is the 8th value in the output of :code:`fit.characteristics()`). The previous solution can also be extracted using `fit_model.coeff(lambda_0=0.032, gamma=0)`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fit_model.lambda_0[0][7] = 0.03271533058913737\n" + ] + }, + { + "data": { + "text/plain": [ + "array([[0.00215713],\n", + " [1.02014176],\n", + " [0.97338278],\n", + " ...,\n", + " [0. ],\n", + " [0. ],\n", + " [0. ]])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(f\"fit_model.lambda_0[0][7] = {fit_model.lambda_0[0][7]}\")\n", + "fit_model.coeff(lambda_0=0.032715, gamma=0).toarray()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "We can make predictions using a specific solution in the grid using the function `fit_model.predict(newx, lambda, gamma)` where `newx` is a testing sample (vector or matrix). For example, to predict the response for the samples in the data matrix X using the solution with $\\lambda=0.0058037$, we call the prediction function as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-2.68272239]\n", + " [-3.667317 ]\n", + " [-1.77309853]\n", + " ...\n", + " [ 2.25545111]\n", + " [-0.77364234]\n", + " [-2.15002055]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model.predict(x=X, lambda_0=0.032715, gamma=0))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We can also visualize the regularization path by plotting the coefficients of the estimated B versus the support size (i.e., the number of non-zeros) using the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) method as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "ax = fit_model.plot(include_legend=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The legend of the plot presents the variables in the order they entered the regularization path. For example, variable 7 is the first variable to enter the path, and variable 6 is the second to enter. Thus, roughly speaking, we can view the first $k$ variables in the legend as the best subset of size $k$. To show the lines connecting the points in the plot, we can set the parameter :code:`show_lines=True` in the `plot` function, i.e., call :code:`fit.plot(fit, gamma=0, show_lines=True)`. Moreover, we note that the plot function returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) object, which can be further customized using the `matplotlib` package. In addition, both the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) and [l0learn.models.CVFitModel.cv_plot()](code.rst#l0learn.models.CVFitModel.cv_plot) accept :code:`**kwargs` parameter to allow for customization of the plotting behavior.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgYAAAEGCAYAAAAe1109AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8/fFQqAAAACXBIWXMAAAsTAAALEwEAmpwYAACWeUlEQVR4nOydd5iU1fXHP3f69t4rZZeFpXcEKWJBQFGwa2KJmkSNRKNR80usMWrsSYyJUWONig0LCiIiitJROkvbhe1sb7NT3/v7Y3ap22Z2Zhfkfp5nnt155733njs7O+95zz33fIWUEoVCoVAoFAoAXW8boFAoFAqF4sRBOQYKhUKhUCgOoRwDhUKhUCgUh1COgUKhUCgUikMox0ChUCgUCsUhDL1tgLfExsbKzMzM3jZDoVAoTio2bNhQKaWM62Yf8QaD4UVgMOrG8mRFA7a6XK7rR40adbCtE046xyAzM5P169f3thkKhUJxUiGE2N/dPgwGw4uJiYkD4+LianQ6ndrrfhKiaZqoqKgYVFZW9iJwflvnKI9PoVAoFF1lcFxcXL1yCk5edDqdjIuLq8MT9Wn7nB60R6FQKBQnNzrlFJz8tPwN273+K8dAoVAoFArFIZRjoFAoFIqThvfeey88MzNzcHp6+uA//OEPib1tT0/w0EMPxWdlZeX2798/98EHH4wHePnll6P69++fq9PpRn3zzTfB/hzvpEs+VCgUCsXJwRur90f/bdnulIoGuykuzOy4dXpW8VXjM6p97c/lcnHbbbelL1myZFffvn2dw4YNGzhv3rzaUaNG2fxpt8+seymaFY+l0HjQRGi8gyl3FTPmFz7PF2DdunWW1157LW7jxo07LBaLNmXKlOy5c+fWDR8+vPn999/fc8MNN2T6yfpDqIhBgFi0bxFnv3c2Q18dytnvnc2ifYt62ySFQqHoMd5YvT/6oU+3ZxxssJskcLDBbnro0+0Zb6zeH+1rn19//XVIRkaGfdCgQQ6LxSLnzp1b/d5770X6z+pusO6laJbck0FjuQkkNJabWHJPBute8nm+AFu2bAkaMWJEY1hYmGY0Gpk4cWLD22+/HTly5EjbsGHD7P4y/0hUxCAALNq3iPu/vx+b2+PEljaVcv/39wMwq++sXrRMoVAo/Mecf6wc0N5r20vrQ5xuKY48ZndpuscW70y7anxG9cF6m+GG19b3O/L1j26ZlNfReIWFhaaUlBRH6/PU1FTHmjVrQn2132temNbufCnbEoLmPGq+uOw6vrw/jTG/qKahzMBblx81X25c3uF8AYYPH9784IMPppSVlelDQkLk0qVLI4YNG9bk4wy6hHIMAsCzG5895BS0YnPbeHbjs8oxUCgUpwTHOgWtNNhcP83rzrFOQSv2+m7Nd+TIkbb58+eXTZ8+PTsoKEjLzc216vX67nTZKT/NP1AvU9ZU1ubx0qZS3JobvS6wf1SFQqHoCTq6wx/78JdDDjbYTccejw8zOwDiwy2uziIEx5KWluYoLi4+1GdRUdFREYSA09Ed/hPZQzzLCMcQmuCxLyzR1ZUIQVvcdtttlbfddlslwC233JKSmpoa0DmrHIMAEGGOaPe1WR/Owuq09qA1CoVC0fPcOj2r2GzQaUceMxt02q3Ts4p97XPKlClNBQUFlp07d5psNpv44IMPoufNm1fbbWP9wZS7ijGYj5ovBrPGlLt8nm8rxcXFBoDdu3ebFi1aFHn99dd3K6GxMwIWMRBCvAzMBg5KKdutsCSEGAOsAi6TUr4XKHt6ije2v0GtvRYdOjQOf0YsegvzsucRbAgm2OjZWfLqtlcZFjeM4fHDe8lahUKhCAytuw/8uSvBaDTy5JNPHpgxY0a22+3miiuuqBw9evSJsSOhdfeBn3clAJx//vn9amtrDQaDQT7zzDMHYmNj3a+99lrknXfemV5TU2O48MILswYOHGhduXLl7m7PAxBSBqaIlRBiMtAIvNaeYyCE0ANLARvwclccg9GjR8sTWSvhQP0BFuQtICsqi+d+fI6ypjISQxKZP3L+UfkFVqeVGe/PYG7WXH476re4NTdWl5UwUxiL9i3i2Y3PtttWoVAovEUIsUFKObo7fWzatKlg2LBhlf6ySdF7bNq0KXbYsGGZbb0WsIiBlPIbIUSbgx7Bb4D3gTGBsqMnKKgr4KO9H3HriFtJD0/njjF3ADCn/5x22wQbg1ly0RKcmhOA70q+444VdzA4ZjCbKjfhcHuWkNSOBoVCoVD0JL2WYyCESAEuBJ7vwrk3CiHWCyHWV1RUBN44L/m68Gve3/U+pU2lXrULMgQRbgoHIDU0lXP7nMu68nWHnIJWWnc0KBQKhUIRaHoz+fAZ4C4ppdbZiVLKF6SUo6WUo+PiuiUn7jeklJQ0lgBwde7VfDDnA5JDk33ur29kXx447QEEbe94KW0qPc5hUCgUCoXC3/SmYzAaeFsIUQBcBPxTCHFBL9rTZexuO3/87o9c/MnFlDeVI4QgNijWL30nhrRf+vv6L673yxgKhUKhULRHrzkGUso+UspMKWUm8B5wk5RyYW/Z01UqrBVct+Q6Pt77MVcNvIq4YP9GMOaPnI9FbznqmEVv4drca7lu8HUANLuamfvxXJbtX+bXsRUKhUKhCOR2xbeAqUCsEKIIuA8wAkgp/xWocf3NkTsEYoJisLvsuKSLp6Y+xVkZZ/l9vNYEw452JdTYaogPjifc7MlP2FOzh+9KvmNmn5l+d1QUCoVCcWoRyF0Jl3tx7jWBsqM7HKt5UNlciUDwmxG/CYhT0MqsvrM63IGQHJrMv8487FutLF7Jkxue5KkNTzEucRyz+81mevp0QowhAbNRoVAoeoOLL744c9myZRExMTGu3bt3b+ttewKN1WoV48aNy3E4HMLtdovzzjuv5umnny7ZuXOn6ZJLLulbW1trGDJkiPX999/Pt1gsh+oPvPLKK5HXXnttvxUrVuyYPHmyV1X1VOXDDmhL80AieXfXu71kUdtcM/gaPr7gY24YcgMHGg7wfyv/j6nvTOX3K37PN0XfHNoSqVAoFD3KupeieSJ7CPdHjuKJ7CHdVRoEuO666yo//vhjvxTy8Tfv5L0TPW3BtCFDXx06atqCaUPeyXun2/O1WCxy5cqVeXl5edu3bdu2fdmyZeHLli0Luf3221NvueWW8gMHDmyNiIhwPfvss4cS3WpqanT/+Mc/EoYOHeqT2JJyDDqgPc2D9o73Jn0i+nDLiFv4fO7nvH7u68zpP4fvS7/n5mU3M33BdJ778bneNlGhUJxKBEiG+Nxzz22Mi4tz+ctMf/FO3jvRf13314zK5kqTRFLZXGn667q/ZnTXOdDpdERERGgADodDuFwuIYRg1apVYddee20NwHXXXVf1ySefRLa2+d3vfpdyxx13lJnNZp8qGCoRpQ5IDElsszZBRzsHehshBMPjhzM8fjh3jbmLlcUr+XTfp9jdHtluTWq8su0Vzsk8h5TQlF62VqFQnNT0ggxxb3L5p5e3O9+dNTtDXJrrqPk63A7dMxueSbt0wKXVFdYKw61f3XrUfN+a/VaX5utyuRg8ePCgAwcOmK+++uqDAwcOtIeFhbmNRiMAmZmZjvJyj4DTypUrg4uLi02XXXZZ3VNPPeXTxUo5Bh0wf+T8o3IMwLNDYP7I+b1oVdcx6o1MS5/GtPRph47trtnNMxueISE4gZTQFOod9bg0F6tKVqkyzAqFwn8ESIb4ROVYp6CVRmdjt+drMBjYuXPn9srKSv2sWbP6bd682dLWeW63m9tvvz3t9ddfz+/WeN1p/FOnKzsETjYGRA9g6UVLDylAvr/rfZ7e8DRCCLSWWlOqDLNCoegSvSRD3Ft0dIc/bcG0IZXNlcfNNzYo1gEQFxzn6mqEoD1iY2Pdp59+esPKlStDGhoa9E6nE6PRSEFBgSkhIcFRW1ur3717t+WMM84YAFBZWWm86KKL+r/33nt7vElAVDkGnTA4djATUyay8IKFfHHRFz+JC2VCSAIWg8fhnJI2hWBj8CGnoBVVhlmhUHSLAMoQn4j8ativik1601HzNelN2q+G/apb8y0pKTFUVlbqARobG8Xy5cvDBw0aZBs/fnzDf//73yiAl19+OWb27Nm1MTEx7pqamk3FxcVbiouLtwwbNqzJW6cAlGPQKRXWCr468BWNjsbeNiUg9I3oi9XZ9memtKmUdWXrCJQCp0Kh+Akz5hfVnPPIfk+EQHgiBec8sr+7MsTnnXden0mTJuXk5+ebExIShj799NP+KTvbTS4dcGn178f8fn9sUKxDIIgNinX8fszv91864NJuzbewsNB4+umnD8jOzh40YsSIQdOmTau//PLL65588smiv//974np6emDa2pqDPPnz/eb6mXAZJcDxYkuu3wycvZ7Z7eZZCkQSCRXDrySu8fe3QuWKRQKf6FklxVH0pHssooYKNotw/zAaQ/wwGkPcG6fcwE4UH+ApzY8RbWtWw6wQqFQKE5gVPJhJyzIW8C6snU8PuXx3jYlYHQ1yXJD+Qb+t+N//GzgzwCos9cRbgpHiLaTjxUKhUJx8qEcg07YXLGZjQc39rYZAaezMswAF2ZdyBnpZxza0XDb17dRa6/lypwrmdl3JkGGoJ4wVaFQKBQBRC0ldEKNvYZoS7erWv5kaHUKpJTM7jsbgeD+Vfdz1ntn8dSGpyhpLOllCxUKhULRHVTEoBNqbDVEmaN624wTDiEEc7PmcmH/Cz1LDDv/x6vbXuXVba9yRtoZXDHwCkYnjFbLDAqFQnGSoRyDTqi2VZMWltbbZpywCCEYnTia0YmjKW0s5e28t3l/9/t8eeBLsqOy+evkv9Ivsl/nHSkUCoXihEAtJXRCjU0tJXSVpNAkbht1G0svWsoDpz1AqDH0kK7EpopNlDYevyVSoVAousqePXuM48aNy+7Xr19u//79cx966KH43rYp0FitVjFkyJCBAwYMGNS/f//c2267LfnI16+55pq04ODgEUcee/HFF6Na36Pzzjuvj7djqohBB9jddqwuK1EWtZTgDUGGIOZmzWVu1txDxx5c9SAmnYm3Zr916NiifYt+UuWmFQrF0byT9070vzb9K6WqucoUExTj+NWwXxV3p+CP0WjkySefLJo0aZK1pqZGN2LEiEEzZ86sHzVqlK3z1oGn+q23o6v++c8UV2WlyRAb64i56abi6Msv69b+7lbZ5YiICM1ut4sxY8YMWLZsWd306dObvvnmm+Da2tqjruNbtmwxP/nkk0mrV6/eGRcX5y4uLvb6Oq8cgw6osdUAKMfAD/z9jL8fej/r7HVcvuhyyprKcGpOQOkzKBQ/NVpliB1uhw44JEMMniqBvvSZkZHhzMjIcAJERUVp/fr1az5w4IDpRHAMqt96O/rgo49mSLtdB+CqqDAdfPTRDIDuOAftyS67XC7uvPPO1AULFuQPHDgwsvX85557Lu6GG244GBcX5wZISUnxWqI6YI6BEOJlYDZwUEo5uI3XrwTuAgTQAPxaSrkpUPb4Qmshn2izWkroLsmhySSHeiJg5dZySptKcWlHf15b9RmUY6BQnBz0lgwxQF5enmn79u3BU6ZM6bF69fkXX9LufG07d4bgPFpRUtrtuoqnnkqLvvyyaldFhaHwppuPmm+fdxf4JLt8xhlnND300EPxM2fOrG11lFrZs2ePGWDkyJE5brebP/3pTyUXXXRRfddnGdiIwSvAP4DX2nk9H5gipawRQpwLvACMC6A9XuPUnKSEphAbfEKU4v7JkB2VjVtzt/laWVNZD1ujUCgCQSBliOvq6nRz587t9+ijjxZGR0drnbfoAZxty0xrDQ1+l13+/PPPQxcuXBi1evXq4xwLt9st9u7da161alVefn6+cerUqTlTp07dFhsb2/aXblvjddfg9pBSfiOEyOzg9e+PeLoaSA2ULb4yLG4Yi+ct7m0zfpIkhiS2qc9g1pspaSw5FF1QKBQnLr0hQ2y328WsWbP6XXzxxdVXX311rbftu0NHd/i7T588xFVRcdx8DXFxjpafrq5GCNqjVXb5yy+/DNu/f78lMzNzCIDNZtOlp6cPPnDgwNakpCTHuHHjmsxms8zJyXH06dPHtm3bNvOUKVNOOtnlXwCft/eiEOJGIcR6IcT6ioqKHjTr1GHXmjJe/cN3PPerr3j1D9+xa01g79zb0mcwCANuzc2chXP4ZO8nAR1foVAElkDIEGuaxmWXXZaRnZ1tu//++8u7b6X/iLnppmJhPlpmWpjNWsxNN/lddnn06NHWysrKQ/LKFotFO3DgwFaAuXPn1q5YsSIMoLS01JCfn28ZMGCA3Zsxez35UAgxDY9jMKm9c6SUL+BZamD06NE9Jgf59s63+bb4W56b/pzXbXetKWPVR3tprLYTGm1mwpx+ZI9LDICV3WfXmjKWv7kTl8PzmW6strP8zZ0AAbO5PX2GkfEjeXz944dqHzg1J0adMSA2KBSKwNGaYOjPXQlLly4NXbhwYUxWVlZzTk7OIIAHHnig+NJLL63zl92+0ppg6O9dCYWFhcZrrrmmj9vtRkop5syZU3355Ze3O9+5c+fWL168OLxfv365er1ePvjgg4WJiYldXkaAAMsutywlfNpW8mHL60OBD4FzpZS7utJnT8ouv7njTVYWr+T5M5/3qt2xF1oAg0nHtCtzTijnQGqSyuJGPnr6B+zW4xNXQ6PNXP2Xib1g2WH+9N2fsLvsPDb5MVVFUaHoBkp2WXEkHcku91rEQAiRDnwA/KyrTkFPc+XAK7ly4JVet1v10d6jnAIAl0Nj1Ud7e90xaKyxkb+pkuK8Gop21WBvan8nS2O1HbdLQ2/onRUnKSUZ4Rk43I5DToFLc2HQ9XqgS6FQKH6yBHK74lvAVCBWCFEE3AcYAaSU/wLuBWKAf7Z86bu6682eKDRWt72c01htx9bkxBLSc6Fxe7OLvRsPkpIdRURcEKV76/jm7V2ERpvpMyyO1AFRrPpwL021x9scGmXmzftWM3RaKsPPTO8xm1sRQnD9kOsPPf++5HseWfMIZ6SdwecFn6vCSAqFQhEAArkr4fJOXr8euL6jc3qbaxZfw9DYodw++nav2oVGm9t1Dppq7VhCjAG7E2+qs1OcV0NwhJnUAVE4ml0sf30np1+azdBpqWQMjuGqhyYQHms5dBcuoM2lj9EzM6kqaSImJRTwRBsO7m8gc2gsOl3Ph/WNOiP19npe3vbyoWOqMJJCoVD4FxWT7YBdNbvIiszyut2EOf3avNCOO7/PoYvssld30FhjY8jUVPqOiEOv981JsDU6Kd5dQ/HOGoryaqgp8+xIyR6bQOqAKMKiLVxx/zgiE4IBMFkMmCxH/9lblzc6S5bc8X0paz/JJzwuiKHTUhl4WtJxfQWSMYljMBlMcIzPpQojKRQKhf9QjkE7ODUnDY4GnwSUWi+oy9/YicuptXmhTewbzqZldXzx4jZCIkzkTk5h0KRkQiLMne5oKNxezYHtVRTl1VBZ1AgSDGY9yf0jGXhaMqk5UcSkhh46PyoxpEs2d5b/MGpGBpEJwWxaVsjKBbtZ+0k+gyYmMWRaKuExQd6+TT5R3tT2DiVVGEmhUCj8g3IM2qHWVgvgs7Ji9rhEtqwoRm/UccFtI457fei0NIZMSWX/tiq2fF3E2k/yWf9ZAfEZYVQcaMDt8uwWaay289XrOzmwo5ozrxkEwA9L91O8u5akvhGMO68PKQOiic8M8znq0FV0eh1ZoxPIGp1A2b46Ni0rZNNXRWz6qoi+w+MYfmYaiX0jAmpDe4WRAJbuX8pZGWcFdHyFQqH4qaMcg3Zo1UnojoCStd7e4YVS6ASZQ2LJHBJLbbmVLSuK2PxV0XHnuV0aeavLmHxZNiaLgWk/G0hQqBGDSe+zbd0lsW8EiX0jaKi2sXl5EdtXlrB340HGzMpk7Hl9Azbu/JHzuf/7+7G5D2ummPVmYiwx/HXdX5mUMokgQ89ELxQKRc9itVrFuHHjchwOh3C73eK8886refrpp0t6265A0t6cP/roo7B77rknVdM0ERIS4n711VcLBg8ebG9ubhYXXXRRny1btgRHRka63n333X0DBgxweDOmcgzaocbePWVFKSXWegfB4cdVyGyTyIRgTr8ku03HoJXW9fywaEu75/Q0YdEWJs7rz5hZmexcVUZyViQAVcWN7N9WxeDJKX7NQ2ivMNLZmWdT2lhKkCEIp9vJtqptDI8f7rdxFQqF9/hbhrgjCWJ/2u0rW1YURa//rCDFWucwBUeYHKNnZhYPmZIaENnl+fPnZ3zwwQd7Ro4caXv00Ufj7rvvvqT333+/4Nlnn42NiIhwHThwYOsLL7wQdfvtt6cuWrRonzdjKsegHVolgn1dSnDa3LgcGsHhZq/atbejITTau356GpPFwNBph+UuCrZUsuHz/eRO8mgeuJ0aeqN/ljpm9Z3VZqJherhnS+XrO17nmQ3PsHDOQvpGBi56oVAo2icQMsTtSRCfCGxZURT93bt7MtwuTQdgrXOYvnt3TwZAd5yDjuZcW1urB6irq9MnJSU5AT799NPI+++/vwTg2muvrbnrrrvSNU1Dp+v6969yDNqhu0sJ1npP5CY4omsRg1ba29EwYU6/Dlp1n/fLqnlkXynFdicpZiP39E1iXqLvctOjZmSSMyEJc7ARKSXvP76BkEgzw6ankZIdiRAiYGWjLxtwGQnBCYecglpbLZGWyG73q1AojqY3ZIjbkiD2fQbe8e4j69qdb2VRY4jmlkfN1+3SdKsX7k0bMiW1uqnObvjsn5uPmu/F94zxWXb5X//6V8HcuXOzzGazFhoa6l63bt0OgPLyclOfPn0cAEajkdDQUHd5ebkhKSmp/Wp2x3CiiCidcFTbqhEIIky+JdOZggxMuLAf8RlhXrXLHpfItCtzDkUIQqPNAS+l/H5ZNXfkFVJkdyKBIruTO/IKeb+sWxEwQiI8c9DckozBMZTtq+Ojp39gwV/WsfzNnSx/Y+eh6EirPoM/xJuCjcGHIgo7qnZw9vtn8+KWF9uVelYoFAEgQDLErRLEBw4c2Lxx48aQdevWnRBrq8c6Ba04mt1+k10+cs5PPfVUwgcffLC7vLx88xVXXFH561//Oq274xwaz18d/dRIDklmWto09DrfEvyCw02MPCfDp7Zd2TrYHTQpOehwUWhzUGhzcM+uIpq1ozUzmjXJI/tKuxU1aEVv0DHu/L6MmpHBrnXlbFpWyPZvj88XCkTZ6KSQJE5POZ1nNz7LisIV/GXSX0gL99v/j0JxStObMsStEsSffPJJxJgxY2ydt+g+Hd3h//eulUOsdY7j5hscYXIAhESYXV2NELRH65w//vjjiB07dgS1Rkt+/vOf18yYMSMLICEhwZGfn2/q16+f0+l00tjYqE9ISOhytABUxKBd5mXP49kznvW5fVOdnfrKZgIpUtUeUkrqXYfvjl8prmRBy92/lJJBK7cy/PttnLdxNzdt30+9W2uzn2K70692GUx6Bk1M5rI/jW33nPYqRvpKpCWSJ6Y8wSOnP8Le2r3M+2Qe7+16r1f+LgrFqUQgZIjbkiAeOHBgjzgFnTF6Zmax3qA7ar56g04bPTPT77LLgwYNsjU2Nuo3b95sBvj000/D+/fvbwOYNWtW7csvvxwD8N///jdqwoQJDd7kF4CKGASMzV8V8eOXB/jV36d6ag57QWfr/VJKKp0uCpsdHGi56z/yUWRz0C/YzLIxOS391RBj0nNJYjRCCG5JjyfEoCfNYiLdYuKyTXspacMJSDEb2dbYzNulVdyakUCcyT8aD0KIDpMsG2vshESa/KamKIRgdt/ZjE4YzR9X/pEHVj3A14Vfc/9p9xMbFOuXMRQKxdEEQobYWwninqQ1wdDfuxLam7PT6dx/0UUX9RNCEBER4X7llVfyAebPn185b968Punp6YMjIiLc77zzzl5vxwyo7HIg6CnZ5Ss/u5LcmFz+MO4PPrWvLGqkurSR7DHehcVb1/uPDO2bhODMmDBeHuJJprt8016WVzcc1S7aqCfVYjp0sc8KsXBFUgwATk1i7EDboK0xg3SCJwakYdck9+8tZu34QUQaDexvtpNoNmL20gM9lvakqadcPoA1H+8jPTeGaVfldGuMttCkxv92/I9nNj5DsCGY+ybcx/SM6X4fR6E40VCyy4ojOSFll090xiWOIzUstfMT2yE2NZTYI8oSd5VH9pUet97vkJLFlfVIKRFCcGliNNNjwklvcQTSLCZCDe3nQnTkFACHohHtRSkuSIgiuKWq4i+37eeAzc6lidH8LDmWvsG+baPMHpdIfvFu1m5ajRsbeiyMHTaRrLEJaJokItZTpKipzs7OVaXknp5ySJVy8+bNLFu2jLq6OiIiIpg+fTpDhw7t0rg6oeOqQVcxIXkC93x7D3/67k+MThxNhDmwFRsVCoXiZEE5Bu1w68hbu9W+OK+GkEjzIfGiLrdrZ11fwqHQ+gUJvldjbI95idHtJhq2OgVSSu7pm8RrJZW8UFTB84UVnB4Vys+SY5kRG47JiyjC5s2bWb/jG9zCM183Ntbv+Iak/hEMnXj4Ir9/axWrF+5j/WcFnu2PqQ0sXb4Yt+bJpamrq+OjhR8DdNk5AOgX2Y83Z77J3rq9RJgj0KRGXnUeA2MGdrkPhUKh+CmiHIM2cGtuNKlh1Pu+pr7kpW30GRrrdTg8xWykqJ31/t5GCMGU6DCmRIdRbnfyVmkVr5dUceO2AuJMBi5PjObK5BgygjqPIixbtgyn8+h5Op1OPv/8cxwOB1JKz8MgyblQT8nuerZ/J6mIXI1mODrB1q25WPzZF145BgBGvZGcaM/f56M9H3Hf9/fx2rmvtVkxsTt1HvxdI+JEJVB1KRSnzmdIcWKgHIM2yK/L58KPL+TJKU9ydubZXrfXNImtoevlkI/knr5J3J5XiP2Y9f57+iZ53VcgSTAb+W1mIr/JSGB5dQOvl1TyjwMH+fuBgywY1o/To4+u39Dc3IzFYkEIwZo1a6iraztfqLm5mU8//fS44xaLhd88fBuPP7WizXbW5kY+/vhjwi0x9OmXQVqfZK8qfZ2TeQ7NrmaGxQ3z9Oe0Emz0RHveL6vmth0HaC02XmR3ctuOAwCdfjl3p+3tH23hE72DhmAdYVaN89wmnpozpEvz6U5bX9i1poynl+/hq9OCqAsOJsKqccbyPdwGnToHby7YTvGKMoLdEqtekDIlkSsvGRQwWwGeW7mPv9fXUmsRRNokvwmP5OZJJ2aVzPfLqrl9+wHsLSuCRXYnt2/v2mdIofAF5Ri0QatOgq/rzs0NDqTEJ8dgXmI0eY02/lZ4EAEn/N2BXgjOjAnnzJhwim0OFpRVMzTIQEFBAa8WllNQ38SwPVuoranh1ltvJTo6muDgYIx6gdN9fOJrmMXADTfdihACIQQ6ne7Q7xaLGaNmwKk/fkuuQLB9+3ZsNhtffw9ms5nkpGTqD+iJCo0jPjaR6NhwQiLNhEZaPD+jzJiCPP8CwcZgrhh4BQDFjcVc/unlXJ17NdfkXsOfdhZxrAKJA7h9xwHW1DWRbjFxS0YC4NkaGmnQH1ru+f3Owjbb3rWzkHCDHqNOEG8yMijUk1Oxo7GZSKOexxfn8W6wC2dL7khDiJ53XS6sH2/mT2fnIPEsL0kpCTXoiTYakFJS0OzgyaV5fBTsPq6t86PNPHXeEAyCLu340NwabrdEc0s0t9by8+jnQgiik0N45rt9fDoqGKfB44zVhej5dFQw4tt9PJgSik4vDj30et2h39/7aBerSgpYMiuaBouFMJuNc3bkwwIC5hw8t3IfjzbX4gzy2FobJHi0uRZW7uuWc+CWEpeUhxJza5wurG4Nl5Q4NM9rTilxaZ6fTikxCcHYSE8u0nc1DWiSQ071B+U1VDlcPLarGPsxPq5dwINbC0/Y7wXFyU3AHAMhxMvAbOCglHJwG68L4FlgJmAFrpFSbgyUPd7Q7XLIdZ5LQWvlP2/pH+op5PXduIE+J/f1JPX19ezcuZPi4mJMJSU8XVEBwDdZw2gIi2RGQgKjcrPZ5dAYIyVDwuqQYjkfycm4xeFvPL3UOMv+OeGLtoNOB8LzkOjAGAQXPMcYZwZrdPm4hXZEOx0THdn0C0+lWmelWt9Eja6RstJqqrVaqur3UF6VjOX7/oTrJc7gMpqskejcQURa9AybmET2+CRcmmTb6lJiBwUxMWYC/1r7L14rdFJtGQdtXEjtUvJOYSWRLti/tgyTQcfCMDeRUscBVzkhRj1NOq3Nto1S8rMt+QAMcuq4ymZEa3Tylzg3g5w69hkOOwWtOA2ChWEaC1dtP+p4n3o3ubWS8AoH/xsVjMWiHbpAH9n23XCNd1dsQkiJXgO92/Mzsc5Fco2LtIMuvhoSTHSTm+hGN4Nr4etUAwa3xKCBwX24ncENBk0igapgSeEAUxtj6lgy2IT5xR8QgJASIWn53fM4kGrnm2EJuPWer6KGoCAWDktA25xP+V/qqQoSaHpBk9uN2WLAaNLj0kGD043UC3QRRib1jSbcoGfrDwcpiDcyJyMGs06wZU8N+yzgRNLodBNkMaA36PjU1tDGe6vjiaZqvlnr4tUR/cCpcf/GAla57bgkuKTEhfT8jsSF56dJp2PL0GzsjQ6u37afPcHwXf8+NFY3c1lRCZstHe/6StEEK0Ljaaq284C9Gn2IkXeMkdSXW/lLaBNFwbp2q80c1LVdf0Sh6C6BjBi8AvwDeK2d188Fsloe44DnW372Ot0VUPJVJ6GVspYcg8QezCvY/OkLLNuwmzoZTISwMn1UFkNn39jmuY2NjSxfvpyhQ4eSkZFBVcF2PvtsMSEmHcmhMCixmRRdJXfUfYuprBRTYykVeyMYYVhIwr5yrmouZLx1ADJhHP/LiqbeYiHcZuOq3dWI/Fr27rDjcMSjuePRyzgsuljCot8gBhgoMzA5zaw37KNR2AiVFka7+tJPS6SqsBCDMJAkjKSKKIbr4tCEjlp9M0lXDyGpXwYb/7WYz6p2M278FBLC+mJbu528TSup+SGceC2C4qgQ/ukycv+W2dzMbP6OiYIMsLex6SOhWeNvXzeSqhfoAD3wawQah7/Ln5xsoTro+L9jhM3JvG+tDA3Rk6QJ+jV5vuRDY/XE2SVXTWgnaVVKbt1uI8+hoWmSIUZJhtVF/3oXbqmRuMnJU0PD2217SUEDTpORAqcOpw6idDYG1lgZVN1Ek17Hj+4ETq+uJqu2iaoQC43RSbgNBhqkAIMOt3Tj1utwHuHszMnLZ0tQZptDNpkNvDXFu7LgLr2BpQOjcBbV8Un/hHbOan2HJeGfryWhyc6B1BgWWhLIevsbQlwaP/SL54vMKHQShB70Tgd6h8TRjphXs1FPaVE1qxfuwCCBjHDCoi1YHTpMmiAUjQi9RrVNh9AgSqcRIyR5S1agQzA91sRwi578z1cjgCviTJxr0rPHDi63pI9RkCQlW9w6pFuSJSR9NKitrwfgzxaBAOpttQC8YvREhS6bYKGqjc9QjM2rYnY/KVwuF0OGDBmUmJjoWL58+Z7etqcnOHbO559/fp/NmzeHGI1GOXz48KY33nhjv9lslhUVFforrrgic//+/Waz2SxffvnlfG8rQwbMMZBSfiOEyOzglDnAa9JTSGG1ECJSCJEkpSwNlE1dpdUx8HUpwVrvKdzjy1ICQKndSYRBf2g3QKDZ/OkLfLJ+P05CAKiTIXyyfj8NVQ8RGp9OSXkFxdWNZOWOZMo552Pcu4TtG9aRGhtKRkYGqZXf8FteJMLRgKgWEBILYYkQnggpAyEskcjQJJ5PSeD1iiYesw9ETM9BAFrLVsr6oCBezk0mSV7OuWUuzEAzjTTrm7AGVRI2+z4Amt119BdJ9HccnXNhddcSn/4azeX52OxuGtxGbATTbE7AoY9irCEUjH1pDCqmb0MjU2eOx2yx8OjWT/guOYbsg/lE2JrID0pmVdRg3resJ9MK07cFkawL4smMHBzicPTGJO1cd3AnNmmkPiwYg1mHw+6gvrqZoEgdDoOLhmYb0ypr+CR1Kg5hOaKtjbMqvmbumVew4dt3qdC52RQKbqnhtkoKhSTcOYk60/EX+AhnAyliF3+4/XqkpvHow4+ww+1kR6tptRDhPKPdtklFKxmY3J8Lrr8Ep9PJww8/DMBOADdM2eY5dy9AI5y37keGJw3kgl9eeuj807PGcMYVMykrquBfr7yMUZNE9Ilpc8wQZyN319gYMWMC1WVVfLDoI8b2H0n26UMp3nmA+c2ONiMqDeYgIitWcGF9MBP7jaT/xGFU7z3AkhWLOXfMGUTn9qNw7VZWb/kGo8tBnUkSUyH4RaWOep2bBhNkFu7kxkKYddpsGuKSMRTu46uNS3lrwhnUtaGBEuGq5+zti1kZLBECwqokE6pg/NjZfFtpYZypnnXbvmHklNks3iuJqdiNwZHHx+Eti0VOiXDCh5EAEpyAE8aMnsmT39qYM0rPpm0rSRgymX+udDDNWECVpYgfo22AwBN/EcgQT5Qh2GzAYNBzVn0QH1jOPe4zNLXya2DMcfM4kQiEDDHAn//854T+/fs3NzY2+lazPkD8uPSz6NXvvZXSVFtjComMcoy/6PLi4WfN7PZ84fg5X3nlldULFy7MB5gzZ06fZ555Jvauu+6q+OMf/5g0dOhQ69KlS/f+8MMPlptuuil91apVu7wZqzdzDFKAwiOeF7Uc63XHoNpWTbgpHKPOtzv21ohBkI+OQbndSYKfqgx2hWUbdh9yClpxYmRpvhvy8zHiJImDhGhZAJhjMvn9iBWIXE9Gv3Hk5UQOmA5hSRASB23s5jAC5wHnpcA+q52zVm6nyXj0BcGmFzyXbWb2uAjCs5JIiYk8bi1c9CnHtT8Yg+7we+vSHIg+5STf/DlobqjcDSUboeQHz6NsKWyPgv7TGfPza7F++hD3bNrIMncEFSMmoUMyKTWB7NICEqprydm4jHrcbA4GKWsZl/Ah1zOCBfJKKokllkou4U1y47ewces4IiwlhMY0INEjUw1o6JEIpNRxfmIeCew4ru3YhLWMPuNuFi8uIjKhnCBzS/lsTSIlXKrbyyvyhuMuBhfzJta6Ug4ss5Iw4Xoo3E18Ug06gwvRErW+VBTxiry+zbaGCjtOsY6yLxsw555LZE0hYRElmAyev5lTOpFoyNZynUJSZ9jGi1+tolaLIaR+F6WhZTTW90HnNBFt3UlEXC2Xin28In9x3JhXiVcwRzSzZddy7E0xDBEFJMdsY7DuBrITo4jZV0qVOL76ZAyVnJH7JQgNc+hiZPnZhJpymJzcTIj+bvrIu0jsn4ihtp7g2C8RQgIaEjcIDYT0xG6EpEG/gL6Rf6JZ68OYWhs648u8KH99nK1XGl5mwmkrj7MF3uPWs1+nvjKdkIggnO4refTSLzhYHENx6S4c7g/baHMkH7H690s5eFAQEvkdLtdN/PinDezalUlV5T8w6r/ssHUOEE3BcZ+hUbE/AHd3MnbvESgZ4r179xqXLFkScc8995Q+/fTT7YWUepwfl34W/fWr/8lwO506gKbaGtPXr/4nA6C7zkFbc7700ksPZXCPHj26qaioyASQl5dnufvuu8sARowYYSsqKjIVFhYa0tLSuhxi6rJjIIQIllJauz4V/yGEuBG4ESA9PT3g49XYa3xeRgBoqnNgsugxmnxzZiscLpJ6cBmhTrZXa0Hy64vOIDYtC31YArQKSqWOQqSOOnxaZJrn0UX6BpuxtvPJK7cIkiYMOkrr4Uiybr6e3c+9iCM/gSB9BM3uOkSfcrJuvt5zgk4P8Tmex3BPMmF+YyNLD1ax9Mc9rK5txBk1lwibm2lxoZxlbGTaJ1cQnZCFTB3OuoQ+/HmjmV02I7P6mZmcosNkfoOJrGQiR180pAUSxo7H6PgfMbFlCCERQgOk50IlJOhk221b5n/OrfNoWno97oyjt272Awy4jrsYnGZYiUzQU1RVQrLx15hHaPSP2YE+9vD71Z+dGHC22fYDQxrDs4spqmsi23g+jYYKcifsRmfueC28Nn8Tb2jRjG8OY6RhDflf1RI/5EFs9ZUMPmsHaYABx3FjTtSvBD3k/7iO9+sjOPvHGIzDdlO2OgiXeRaX8DovtXGRvkS+gdtRitQE5moHyzfls9YayRl7Iogdk4/TuJ/Sg9UUrlxP0mgnUgqkBlLqQTO0/C5AQmpFA3+NepCaumjGF4dw2hV7QXD8+yNXUvR9gid+3/p3kjCouJKbd9xCRGEUg6oFp7n1GEYGs/qDv1Ozfy3BcS3RK9nSVLbmUEj0miSjpprLin9O+s4QsuubOc2cSNjUEHZ/9wE1eTuID05BL/Romhu7205L1RKQEp2QxM2sZqJo4zN0AqQf9YYM8c0335z217/+taiurq7HowVv/uG2dud7sCA/RHO7jp6v06n79n+vpA0/a2Z1Y0214aPHHzpqvlf+5ekuiSp1NGe73S7eeeedmKeeeqoQYPDgwc3vvvtu1IwZMxqXL18eXFpaai4oKDD51TEQQpwGvAiEAulCiGHAL6WUN3V1kHYoBo68mqS2HDsOKeULwAvgKYnczXE7pcZW43PiIXiSD4N9TDwE+Ghkf5rbETYKBBHCSp0MafN4wuApARkzXtMo1x//fx2veeZ99ZZ9hOn1vDb0+CzxQ05AOzg1SYndQUaQGacmOWvjPhrdGtnBFm5Ijees2HDGhIdg0AlPdKH/6dj2r8e05yvGovEx4IiKxxQyCkJGsqgpBIvleMl3uz2Eqy+6AqwzwFp1zKMarFUscSzEEHZ8XQp3o+dfb8yKZ3EVleEu8lyEpCbQNMHK0XFMDD3+YuBqNDLJHo24+H8YjEZuF/uw7qtAK/B8H2kCvhsYz8Tg49vam0w8OsCJjQGEzfwnQUFx3Jmwnfofa9BrAun2jI27xRa3AE2gxQwk6Nx7mTdkGtqjWRRv6EfYeTcS2bc/t4d9R9GbYeRdYGFiyPFjOhv1DFqczqjL7+bqCZNpfnos9Z9NIPb6X2JKiaFo2c2I8OMv0mMaVhO3KgUN0Mf358wzZzM5TI/rq38S77iAuP6zSehbTe1+QeruGBIx4hKSzbIJKTwKolKABtTE5XLa+LHYDY0Y1y7C1WBgYngbtjYYMNa4PW+iG1o72B8/hLG5I3AllWPYsAlTyGzMwTHkDk7i+zIXyQUmLFJPk6ZxULqQLVtGNCkASWNwGmMzJqAr24OhvJxQstHpjBjrynBXB1FbKpACT2KjCPZEm/Acs7hcRFibMIccryvisJ4AnkEHBEKG+K233oqIjY11nX766dZPP/3Uu+SVAHOsU9CKw9rebVDX6GzOV199dfr48eMbZ8yY0Qjw4IMPlt54443pOTk5g3JycppzcnKser3eq+tmVwx+GjgH+BhASrlJCDHZm0Ha4WPgFiHE23iSDutOhPwC8CwlpIf5HpkYNSMDm9V3ZUKdEIR0UOLY30wfldWSY3A4SmHEyfRRWQEb897Bfbh9awH2I/IozJrk3sF9AJiXEI25Jf/A5ta4ZNNepkeHMychkg11TccVezkrNoLwlvfsuq35HLA5WDE2B6NO8K/cTLKCzW0WXqoOyuBJ7Ve8VXqAxCDJ/WM1zowoxlTasgyxazFFMefQJ+cH9PrDd+Vut56ivQNh8d2w7sW2J2kKwyj1uEcb0RkP/19qToFxq92TZTPqGgz9z8JgDPLsvDAGgzEI47s34B4hjm+3UyPklk8gxKODwc8/JljKw+v0QuB8Ygr6cW70hsPOpdulw7XZSPhvPyRc6KBFPMp83WLi3HbPEox0e34e+t3l+T04GpI89R10c/9BWmgCJA8HTcN42XP00dzsfukxtPNtx9mrW2Ii/ZbfwKAzwe3C2Gck4bPmQE4ONJSjW2pkwvnfMdG48qh2cqmF0Tn1nnmNGwujz4eGctAq4axJEB8P5RWcY2yETEB41unPghbRMuF5CAGTL4aBs6F8O2xazKdfmtGf5z7OVteXZq4Y6tk2eujqDnDunZA+HvK/gYM/wEW/BpOJYZnJDMt2eM47pDlzzO8Al77AzLgBELkAzI/C9e8CcPG0XAhd1UZ7jmr/6soIkqZXHvf3rFgZ4Vmf60V6WoZ45cqVoUuXLo1MSUmJsNvtuqamJt2cOXP6fPTRR/neW+89Hd3h/+uXPxvSVFtz3HxDIqMcAKFR0a6uRgiOpKM5/+53v0uqrKw0LFmy5JBQUnR0tPbee+8VAGiaRlpa2pCcnByvZGs7FVESQqyRUo4TQvwgpRzRcmyTlHJYJ+3eAqYCsUA5cB+epWaklP9q2a74D2AGnu2K10opO1VH6gkRpf9s/g8JIQmc3+/8gI7TFlUOFw/vK+Gq5BhGhh9/Fx8ovNmV4C/eLankkT0llLq1Dus1HGi2c8uOA6yt89y1e+7DDtPqpm+bNJhoo4EV1Q00uzXOiQ3vcL++lJLz/rGSHaUN/HxCBr+dnk1E8DFLOLZ6Nj96FutSksnstxkpPZGC/fuGMKlyD0OvfgIq8yAoGoJjjnhEg8EMTw/mK2sDMscAkUAtiJ0uzggOg9u2tv/mbF7AV5/ejRwgDrfLk5wx+1EYeknHb+zmBXy88H6MQ90Yw1w4Gww4N+s5/4L7O2/rI3U3DWKtpsc9w4EhzIWrwYB+sYmxOjcR/9zu93bd4etbB2F3GJAz7IfGFIvNmE0upv4tMGN2h/dvGEN9rInE8ZUYQ104Gw2UrY4lvNLBvP+s63I/PS2idGyOAXhkiCde3H+/PxIQP/3007Ann3wy4UTZlXBsjgGA3mjUpl59w35/JSAeOeennnoq9vXXX4/99ttv80JDQw99JVZWVupDQ0M1i8Uin3zyydiVK1eGfvjhhwXH9tVdEaXCluUEKYQwAvOBHZ01klJe3snrEri5C+P3ODcMvaFb7fdsOEhsaqjXOgkAlU4XX1bVMyO2B0V9pKRoXx5njh/OkHN+1mPDXpwcy8XJncsepweZ+XhkFkU2B9PX5VF3TP6BBML1ukM3XVOiO44wrt5XxfC0SCxGPffOziUy2Eh2QjttLOEMjWiitqYAJ7B+7RzMdpjOSoZGWKHP6Z5He0y/lzM+uRW21B4+ZgyC6Y93POmhl3AGwLIHYUsRRKTC7Hu7dmEfegnnt7atK/e0vaCLbX0k4lf3M/bZ33HwQQsuqxFDsJv4EbVEzH8yIO26w9Tr7+fr5+/G+BczkfUmasPBOaqRqdc/GrAxu8O839zJ+8/8hT2vZuIwGDC5XCRHljPvt74pv/YUgZIhPlFpvfgHalfCsfz+97/PSEpKso8ePXogwOzZs2ueeOKJ0h9//NFy/fXX9wHIzs5ufvPNNwu87bsrEYNYPIWIzsRzc/YFMF9KWeXtYP4g0BEDt+amydVEmDGsS9XhjsVpd/PC/BWMv6Avo2Zk+t/AAOBurOTfT/+FgX1TmXbl7T0y5oGvNlC3upC+v5hMSELXEz2Tlv9IW59YAZROG95p+7yyBs555hv+OGsg15/exSp3mxew7uu7acwQTNlYiU7gubif97euXWw3L2i5SLdc4KcH9iLda/g6z954f062v4kf7FWyy4oj6Shi0KljcKIRaMcgvy6f8xeezyOnP8LsvrO9bq9pktoyK+YQg8+VD3sLqWkIL/QFusPmJxYSWRFF0n3jMAZbOm/Qwujvt7UpMpVqNrL+tNw22zQ73KzOr2LagHgAPttSyhk58ViMXc/jWPFuNpZqjXGllSfHhUShOAblGCiOpFtLCUKI/8LxN2lSyuu6b9qJR4Q5gjtG38HgmOOqOHcJnc5TO95XXi2u5JuaBl7MzfQpYtEdesopABBVGk2i3iunADwiU3fkFdLcBZEpKSWfbi7lkc92cLDBznd3n0FCuIWZQ7wTpGou24Mrxk2YmAa/bCfRUKFQKH4idCXH4EipOwtwIVASGHN6n2hLNFfnXu1z+6riRoryasiZkIQ5yPtdKhvrrWyst/aoU/DBE7diCotm9i/v75HxXA4Hoe5wrDHel8VoTU7sTIJ2W0kdD3yynbX51QxKCueZy0aQEO6dE9JKxdaPQQdR6YHZuqlQKBQnEp1euaSU7x/5vGW3QVvlwX4SVDZX0uBoICM8A53w/g66ZHctKxfsJmt0AgR5P36Z3UliD1Y9lPYmdjcGMaDnNkBQ8eNujDozlj6+LbXMS4xuV1WuusnBk1/k8dbaA0QEGfnLhUO4dEwaep3vjlbtwVUQB7GDvV9aUigUipMNXwovZAHx/jbkROGD3R/w9x/+zoarNmDSe1/S2FrvQAiwhPp2cS+1O+nfg4qKVTu/o5kg0tL79NiYNVsLCSeEmJH9Oj+5i7jcGm+s3s9TS3fR5HDz8wmZ3HZmG9sPfcBoiiXsYCrGYN+LXikUCsXJQldyDBo4VKMTCZQBdwXYrl6jxlZDqDHUJ6cAoKnOTlC4CZ2Pd6jlDieTokJ9ausLRXk/AJCaO6HHxnQVNmHXdKT0826tv5WFPxTz+JI8SmqbSY4M4s5zBrCzrIF/rdjLxP4x3HdebvvbD31g4EXP+60vhUKhONHpylLCCVV2MtBU26q7Vw653uGzqqLVrVHncveo3HJhcSlmEUJcev8eG9PUZMJmafYpj2LhD8Xc88EWmp2eWgbFtc3c88EW7jp3AP+6aiTn5Cb6NT/DZWtEZ7Sg0/em3phCoTiSlJSUISEhIW6dTofBYJBbt27ttLbOyUxb850/f37y559/HqnT6YiJiXG++eabBZmZmU5N07juuuvSvvrqqwiLxaK9/PLLBZMmTfIqoavdbzshxMiOGkopN3oz0MlCtx2DOgfB4b4tBZS1bMPrMcdASooaJKkhGroe2pHQUFZJqC6ShqTjtQe6wuNL8g45Ba00O93855t8vrv7DH+YeBT7PvsjReZPGT/6M4ITsv3ev0LxUyaQMsQrVqzYlZSU1GVhoJ6gcXVJdP2ywhStwWHShZkc4dPTikPHJwdkvvfdd1/Zs88+WwLw5z//Of4Pf/hD0v/+978D7777bsS+ffssBQUFW5cvXx5y0003pW/evHmnN2N1dBvUUekxCfj/W/gEoMZWQ1KIbyFu8EQMYlJ9WwootXvkmpN6KPnQXr6bg1oEOUmRPTIegLWsmgZZQ/igTJ/al9Q2e3W8u4QnjSI6fz+WuJ6LqCgUPwUCKUN8ItK4uiS69tP8DFpKQGsNDlPtp/kZAP5yDo4kOjr6kHhGU1OTrjVS+tFHH0VeeeWVVTqdjunTpzfV19cb9u/fb8zIyOiygE+7joGUclq3rD5JqbHVMChmkE9tpSZp7sZSQrnD4wwm9FDEoHjrSiQ60rKG9Mh4AAnDs0kY7vudd3JkEMVtOAHJkT5sAekCiRN+RuKEnisTrVCcTPSWDDHA9OnTs4QQXHvttRV33HFHjxRdKv/HD+3O11naFMKxipIuTVe3uCAtdHxytbveYah8bdtR8024ZUS35vub3/wm5d13340JCwtzr1ixIg+gtLTUmJmZ6Whtl5SU5PDWMehS/FgIMVgIcYkQ4uetj64OcDIhpaTa7vtSgq3JiaZJnx0DAWQGmUjqIcegMH83ACk9mHioad2Tk77znAEEHVOxMMio585z2v1/9Rl7XQVV25aguX1XylQoTlUCJUMMsHLlyp3bt2/f8cUXX+z+z3/+E//555/3XMZ2e7QjMy1tvstMt9LefP/+978Xl5WVbb7ooouqHn/8cb/tFuzKroT78KgkDgI+A87FU8fgNX8ZcaLQ6GzEpbmItnS9dv+RWOs9TpqvpZAvTIjiwoSe2xIXl9KXsbKUoJCe+Z9yO13s+8NS3Nl6cm4426c+LhiRAsAd727CpUlSWnYltB73J+XrXme37jkGNT1A0tir/N6/QnGy0xsyxAB9+vRxAqSkpLhmzZpVu2rVqpBzzz230Ze+vKGjO/ySh9cM0RqOl5nWhXlkpvXhJpc3EYIj6Wy+1113XfXMmTOznn766ZKkpCRnQUHBITtKS0tN3kQLoGsRg4uA6UCZlPJaYBjQg9J/PUeNrQbA54hBVGIwP3t4Aum5vjkWPc2gmTcw88Z7e2w8Z5MVW7SdoJTIbvUzZ3gyep3gxsl9+e7uMwLiFEBLYSM3xA6eFZD+FYqfMuMvurxYbzQeFSLUG43a+IsuL+5Ov/X19bqamhpd6+/Lly8PHzp0aGCSjLwgfHpaMQbd0SFRg04Ln54WkPlu2bLl0B3oggULIvv169cMcP7559e++eabMZqmsWzZspCwsDC3t45BV0IczVJKTQjhEkKEAweBNG8GOVmIMEfwp/F/YnjccJ/a6/Q6wmN8X+u+Zft+4k1G7u2f7HMfXcVWU4rb6SQkPj3gY7ViiQxnyD0XdLufikY7dpdGalRg8gpaadD2YKoOVoWNFAofCJQMcVFRkeHCCy/sD+B2u8W8efOqLrroonp/2NwdWhMM/b0rob35nnPOOf327dtnEULI1NRUx0svvbQf4JJLLqlbtGhRREZGxuCgoCDtxRdfLPB2zK44BuuFEJHAf4ANQCOwytuBTgYizBFcMsB3xbyindWUF9Qz4uwMnwocheh1BOt7ZtvgjqWv8dH2Zn5z/c+JSe2i/HA3qc0vISwlHr2pe0tuRTWem4OUACUcArgdzdhj6omq8S0RVaFQeJwDf+9AGDRokCMvL2+7P/v0F6Hjk6v9vQOhvfkuWbJkb1vn63Q6Xn/99QPdGbOjOgbPAf+TUt7UcuhfQojFQLiUcnN3Bj1RKW8qp9pWTVZUFgad9xevA9ur2fJ1ESPPyfBp/McG9FwgJm34NM6W3xOVnNljY5Y+v56iIMngB+Z0q59WxyA1KtgfZrVJzbYvkWaIjBkTsDEUCoXiRKSjq98u4AkhRBKwAHhLSvlDz5jVO3yy7xOe3fgsa69c65NjcNrc/oyd3afH5ZJ9ITZ7LLHZY3tsvMaD1YSKSBrifCtsdCRFNZ4iXikBXEqoyv8SIiEmZ2bAxlAoFIoTkXbj1lLKZ6WUE4ApQBXwshBipxDiPiFElzaiCyFmCCHyhBB7hBB3t/F6uhBiuRDiByHEZiFEr34Ln5N5Ds9Me4Ygg+8XHINJ3/lJbbC1wUruyq18U93g89hdxV5dzI6lr9NcVxHwsVo5uD4PIQThAxO73VdxTTNRwUZCzYErU1zfuBldo46w9FEBG0OhUChORDpd0JZS7pdSPialHAFcDlwAdFqXWgihB57Ds71xEHC5EOLYBds/Agta+r4M+Kd35vuXtLA0pqdP97n9d+/tZueqUp/altqdVDldhPRAjkHRhiW8891eSnZtCvhYrTTtqkBKSdzorG73VVTTHNBlBABrcAnBDXEnRfRHoVAo/EmnVyEhhEEIcZ4Q4k3gcyAPmNuFvscCe6SU+6SUDuBt4NjFZQmEt/weAZR02fIAsKZ0DT8e/NHn9ju+L+VggW/JsWWOntNJ8BQ2kj1a2IgKJ1ZRjzmi+zUThIB+cSF+MKptrGW7cUW7CDPnBmwMhUKhOFHpKPnwLDwRgpnAWjwX9hullF1dJE4BCo94XgSMO+ac+4EvhBC/AUKAM9ux5UbgRoD09MBtr3tqw1NEW6J5/kzvZXZdTjd2q4tgH4sbldqdCCC+B3QSiqoaiTcGYQkO3MX1SNwuFyHOcKyRXgl8tcsr1wY2N8IclcKQ8EcIyvJ/NUWFQqE40ekoYnAP8D0wUEp5vpTyf144BV3lcuAVKWUqHgfkdSHEcTZJKV+QUo6WUo6Oi4vzswmHqbHVdLvqYXCEjzoJdiexJgNGH7Y5eoNmb6TQHkpqTGBD8UdSuXUfJp0Fc+bJURdLbw4mfvQlhKUN621TFArFMVRWVupnzJjRt0+fPrl9+/bN/fLLL3vmDqcXaWvOs2bN6puTkzMoJydnUEpKypCcnJxDS/Vr1qwJGj58eE7//v1zs7OzB1mtVq8uLB2JKHVXPbGYowshpbYcO5JfADNaxlslhLAAsXiKKPU4NbYaosy+FbM55Bj4qJNQanf2iKpi5Y7vsGMmLaNnahcA1GzaTygWYob16XZf20vq+fOi7fxx1iAGJYd33sAH8j6YT1jiMJJPuy4g/SsUpwqBkCG+8cYb084+++z6xYsX77PZbKKxsbFnir90gXXr1kWvWLEipbGx0RQaGuqYMmVK8ZgxY7pd16CtOS9atGhf6+s33HBDakREhBvA6XTys5/9rM+rr76aP2HChOaysjK9yWSS3owXuLRuWAdkCSH64HEILgOuOOacA3jKLb8ihBgIWICeS5U/AqvTis1t87kcsrWue45Bmd1JqsW3tt5QlPcjAKk9mF/gKGzAoQmSc7pfp8HmcmNzujEZAhNZ0VxOSoyLiCzYpxwDhaIbBEKGuKqqSr9mzZqw9957rwDAYrFIi8Xi9pvR3WDdunXRS5YsyXC5XDqAxsZG05IlSzIAuuMcdDZnTdP45JNPopcuXZoH8MEHH0QMHDiwecKECc0AiYmJXr8/AXMMpJQuIcQtwBJAD7wspdwmhHgQWC+l/Bj4HfAfIcRteBIRr5FSeuXZ+Isau0cnobcElMocTkZHBD4iVlhShkUEEZPar/OT/UTCrMFYC6vQ+WHHxcj0KD64aaIfrGobncHI5LN+xNlUE7AxFIqfCj0tQ5yXl2eKjo52XXzxxZnbt28PHjp0aNN//vOfwvDw8O7JtnaRF154od35lpWVhWiadtR8XS6X7ssvv0wbM2ZMdUNDg+Gtt946ar433nhjp6JKnc15yZIlobGxsc4hQ4bYW843CyGYNGlSVnV1tWHu3LnVf/7zn8u9mWdXdiU81pVjbSGl/ExKmS2l7CelfLjl2L0tTgFSyu1SyolSymFSyuFSyi+8Md6ftAoo+ewY1NlBgCXM++UAu6ZR7XQHfkeClBTVS1JDJTpdz0XfEkZk0+f8HtwB0U30llAsMT9JORCFoucIgAyxy+USO3bsCL755psrduzYsT04OFj705/+1P3iKH7gWKegFbvd3q0b8M7m/MYbb0TPmzev+sjz161bF/ruu+/mr1mzJu/TTz+N+uijj8K8GbMrBp8F3HXMsXPbOHZSU23zvK8+LyXUOwgKNaL34a7Y5taYlxDFsLDAJgTaDu6jQkYwOKnnkgAPbt5D/c5i0meOwRTa/fn95q0f0Al49rIRfrDueLa+/XOETkfuJa8EpH+F4qdET8sQZ2ZmOhISEhxnnHFGE8Cll15a8+ijj/aYY9DRHf4TTzwxpLGx8bj5hoaGOgDCwsJcXYkQHEtHc3Y6nSxevDhq7dq1h7QUUlNTHePGjWtISkpyAZx11ll169evD54zZ06Xq+e1exUTQvxaCLEFGNBSlbD1kQ/85LQSuiu57HJqhET6towQYTTw3KAMpscEJpmuFUtCP+749XWMPueygI5zJAe/3oFlow7N6Z9lwB2l9didgYsaVhnW0OjcHbD+FYpThUDIEKenp7sSExMdmzZtMgN88cUX4QMGDLB101S/MGXKlGKDwXDUfA0GgzZlypRuyS53NOePPvoovG/fvrZ+/fodklW+8MIL63fu3BnU0NCgczqdfPfdd2G5ublevUcdRQz+h6eg0SPAkeWMG6SUflWPOhHo7lLCmdcMwtf0CE1KdD1UYS80wTeBJ1/J+eU5VO/YjyXKq0hWm0gpKaqxMjU7MFtWraW7PIWNalRhI4WiuwRKhvjvf//7gSuvvLKvw+EQ6enp9rfeeqvALwZ3k9YEw0DsSmhvzm+99Vb0xRdffFT/cXFx7ltuuaV8xIgRA4UQTJ8+ve6yyy6r82Y80ZWLWUt54wSOcCSklN2SdfSV0aNHy/Xr1/u937KmMvbV7mNC8oQeL4P778KD/DW/jB9OyyXc4JvWQlf4+sU/EZmUyfBZvwjYGIGkstHO6D9/yf3nDeKaid3f+ngsB5Y+yW79P8kNfZDEsVf6vX+FojcRQmyQUo7uTh+bNm0qGDZsWKW/bFL0Hps2bYodNmxYZluvdSX58BagHFgKLGp5fOpPA08EEkMSOS3lNJ+cAikln/97C3t/8K38wuDQIK5KjiEskDoJmptdZQ0UFRZ2fq6fqMrbz5ZHF1Kzyz8+ZKvcckqAdBJqDq4CN8QMOS8g/SsUCsXJQFeSD38LDJBSVgXYll7l26JvMevNjE3yvtyuy6FRW27F1ujs/OQ2mBgVxkQ/hNo7RKfnxj8+g9vlCuw4R1CxdjdRtTG4bf7JLyhucQxSAyS33Cj3Yq4KxhgU2FwPhUKhOJHpimNQCHi1PnEy8vym5wkzhfnkGBjNei6/91gZiK5TbncSYdBj6QFlRb0hkDWtjsZxoB6nFkFyrn/yGopqPFoLKQFwDNx2K/aYeqJqjhUAVSgUilOLrlwl9gFfCyEWAfbWg1LKpwJmVS/wtzP+htPt2x1/d5nzw25GhAXzfG5mwMb44p93YdUMXHDLwwEb41gM9Xqspga/FDYCz1JCRJCRcIv/6z1Ub1uKNENkTGAFmhQKheJEpyvf2Afw5BeYgLAjHj8pYoNiSQpN8qlt/uZK3v/rBhpr7J2ffAxSSsrsThICWdxISnZVOGiy91zl0OaaOkKJRMT5b15FNdaALSNUFywDIDZnVkD6VygUipOFTiMGUsoHAIQQwVJK/+jmnmA43A5e2voSU1KnMCjG+1By3UErZfvqMJq9vzOudbmxaZKkADoG1tI8KmUEQ3uysNG6XeiFjpDsBL/1OTglImA7RnR6M5aycEKnBaZwkkKhUJwsdOoYCCEmAC8BoUC6EGIY8Esp5U2BNq6nqGqu4p8//pO4oDifHANrnQO9QYcpyPv1+zK7Z/kikOWQi7d+B0Bq9pCAjXEsDXnlRBJB/Jhsv/X5u7PbLVPebbLmPE5WwHpXKBT+YtOmTeZLL730kOZAUVGR+fe//33xvffe2yuqvIGmvfmuWbMmdO/evRaAhoYGfVhYmHvnzp3bbTabuOqqqzI2b94cLITgySefLJw9e3aXqx5C13IMngHOAVr1DTYJISZ7M8iJTrW9++WQg8NNPt3NHnIMAii5XFiwF4GBlNzTAjbGschyB02yntRY/2T4a5pECAISMdBcDtAZelQ/QqE4FQiEDPGwYcPsO3fu3A7gcrlITEwcdtlll9X6xeBuUlT0ZnR+wT9SHI4Kk8kU5+iTeUtxauqVAZnvkY7QkbLLTz/9dCzArl27thcXFxvOPvvsrHPPPXeHXt/1Gjld+iaUUh67+f2EkLn0F92tethUZyc4wjfJ5FJH4CMGRZWNJJhsmIMCq8XQiuZ2E+QIwRnmv62RW4rrGHjvYr7d7X9V7sLlf2PFomxqdi73e98KxalKqwxxq35AqwzxunXrfPuibYOPP/44PD093Z6dne3wV5++UlT0ZvTuPQ9nOBwHTSBxOA6adu95OKOo6M2AzrdVdvnqq6+uBti+fXvQtGnT6gFSUlJc4eHh7m+++carL/8ubVcUQpwGSCGEEZgP7PBmkBOdQzoJZt8jBhFxviXFBXopQbM1UuQIZWiSb46LL1granEJB6YM/+WoRgQZuWpcBhnR/pemDoruQ9iOPoRmjPJ73wrFT5nekCE+krfeeiv6oosu6rEaO+vWXdjufBsad4RI6Txqvppm1+3Z+3haauqV1Xb7QcPmzb88ar5jxnzY7fkeK7s8bNgw66effhp54403Vu/du9e0devW4P3795uALucIdsUx+BXwLJACFANfADd3eSYnAf5QVkzqH+lT2zK7k2ijHnOAwtgVO1biwERaRr/OT/YToYkx5Dzm3+qBmbEh/HF2YGoMxI+aR/yoeQHpW6E4VQmUDHErNptNfPnllxFPPfVUkT/66y7HOgWtuN0NAZ3vsbLL8+fPr9yxY0fQkCFDBqWkpNhHjhzZ6M0yAnRtV0Il8JMuHF9jq8EgDISbvF8Pd7s1bI1OgsN9uyMvszsDm1+QtwmA1ME9l18QCCob7YSYDASZ/Ksl4XY0U5+/joisSSrHQKHwkt6QIW7lvffeixg0aJA1LS2tx8q5dnSH/+3KCUM8ywhHYzLFOwDM5niXtxGCI2lrvm3JLhuNRl566aVDy/8jRozIGTRokFfqih3JLv++5effhRB/O/bh3ZRObGrsNURaIn1KbGuu9ywF+OoYXJQYzY1pgVELBAiLSWRwlJ3olL4BG+NYtv7hA7Y95V85jd+/t5l5z3/v1z4BqrcuYWPxtRz48lG/961QnMoESoa4lbfffjv6kksuOWGUfvtk3lKs05mPmq9OZ9b6ZN4SsPm2Jbvc0NCgq6+v1wF8+OGH4Xq9Xo4aNcpvssuteQQ+SxkKIWbgWYbQAy9KKY/79hVCXALcD0hgk5TyCl/H85VqW7XPywhul5uEPuE+5xicHx/pU7uuMuCsqxlwVkCHOApN05BhOgyRFr/2W1RjJTPG//kF1fnLIApics71e98KxalMIGWI6+vrdStXrgx/9dVX93ffUv/QuvvA37sSoP35tiW7XFJSYjjnnHOydTqdTExMdP7vf//L93a8dh0DKeUnLT9f9bZTOCTV/BxwFlAErBNCfCyl3H7EOVnAPcBEKWWNECLel7G6S42thmizb4mjEXHBXHSXb0qmbinZ1WQj3WIiJAByy05rHS67laAo3yo6+oJOp2PIPRf4tU8pJUU1zUzsH+vXfgHqmzajN+gITRvu974VilOdMWPGVPvDETiW8PBwrba29kd/99tdUlOvrPaHI3As7c33/fffLzj22IABAxwFBQVbuzNeV2SXlwohIo94HiWEWNKFvscCe6SU+6SUDuBtYM4x59wAPCelrAGQUvZKgYrnz3yeRyf3fCi5zO5k2ro8PjhYE5D+8799n8ee/TeFm1cGpP+2aKqoQXP5dzdrrdWJ1eEmNQByy9aQMoIa4gNWUVGhUChONrqSbRUnpaxtfdJyEe/KnX0KHmXGVopajh1JNpAthPhOCLG6ZenhOIQQNwoh1gsh1ldU+H8fe5gpjNgg3+5GN31VyDsPr0XTpNdtIwx6/p2bweQASS7H9h/OGf1MJGSNDEj/bbHvb1+R98fP/NpnUYDklptKduKKchEeNNiv/SoUCsXJTFccA7cQIr31iRAiA08+gD8wAFnAVOBy4D9HRidakVK+IKUcLaUcHRfn30Q9p+bk6Q1Ps6lik0/tg0KNRCYEo9N5f8cZatAzJz6KjCCzT2N3RnS/kUz+2R8w9VRhI00jyB6CO8S/EYNWuWV/OwaV2z4BICp9ql/7VSgUipOZrjgG/wesFEK8LoR4A/gGT15AZxQDaUc8T205diRFwMdSSqeUMh/YBT1bsr7OXsdr218jr9q3XSTZYxM553rf7jj3WG2srGlAk/7ysw6j2ZvYtfwdbLU9tzpTs/sAFl0IxrRQv/Z7OGLgXwen9uBqcEPMYKWoqFAoFK106hhIKRcDI4F38OQJjJJSdiXHYB2QJYToI4QwAZfRordwBAvxRAsQQsTiWVrY11Xj/UFsUCwbr9rIvCzfCtzIblzU3y6t5vJN+wjE6vbBbd/yvxU7yFvdlT+Vf6j6wZP8GjkkvZMzvaOoxkqY2UBEkH/rPTSyF3NVCMYg/+g5KBQKxU+BjuoY5LT8HAmkAyUtj/SWYx0ipXQBtwBL8Gx9XCCl3CaEeFAIcX7LaUuAKiHEdmA5cKeUssfKW7YihECv821XwP/uX8M3b/kWbSizO0kwGwKS+Fa460cA0oZM9Hvf7WHLr8UlncQO7ePXfotrm0nx8zKC227FHtNAqJbp134VCoXiZKejOga3AzcCT7bxmgTO6KxzKeVnwGfHHLv3iN9lyzi3d8XYQLC+bD0f7f2I20bd5rWIkpSSxhobeqNvFfNK7U6STIHRMCgqKSdEWIhK9u9FuiMMtTqs+gb0Rv/e2V80Kg2rw7/FzYROz6DI+zFnpvq1X4VCEVgeeOCB+Ndffz1OCEFOTo71nXfeKQgODvb/euwJxEMPPRT/2muvxUkp+fnPf15x7733Hpw1a1bftmSXP/zww/A//vGPKU6nUxiNRvnII48UnX/++X6TXV7a8vMXUsoeDe/3JHk1eSzcs5DbR3nvmzjtblwOjeBw35IHy+xOBoX6904YACkpbBCkhske24bnaLASQgSNsY1+73vG4ES/96kzmkkc/zO/96tQKA7jbxni/Px84wsvvJCQl5e3NTQ0VM6cObPviy++GH3rrbf2eKS5LV4trox+qqAs5aDDZYo3GRy3ZyYWX50S2626BuvWrbO89tprcRs3btxhsVi0KVOmZM+dO7du0aJFh67LR8oux8fHOxctWrQnMzPTuW7dOsusWbOyDx48uNmbMTu61W1NMHzP+6mcPFTbqtEJHRHmCK/bWus8ype+Si6XOZwkmv2ir3EUTSU7qZbhpCUn+L3v9ijfkIde6AnO8m8RIpvTzdbiOr9HDPIXP0TJyv/4tU+FQnGYQMkQu91u0dTUpHM6nTQ3N+tSU1OdnbcKPK8WV0bfu6c4o9zhMkmg3OEy3bunOOPV4spuzXfLli1BI0aMaAwLC9OMRiMTJ05sePvttyNbXz9WdnnixInNmZmZToBRo0bZ7Ha7rrm52as7xI6uStVCiC+AvkKIY5MGkVKe30abk44aWw2R5kh0wvvlAGu9HfBNJ6HB5abJrZFo9v9SQtFWj6ZAavYwv/fdHvU7SokgjPjR/t1UsudgI7P/vpJ/XTXKr5GDAw1vYqmMJXnSDX7rU6E41ehpGeI+ffo4b7755rI+ffoMNZvN2umnn14/d+7c+u7NouvMWL+r3flua2wOcUp51HztmtQ9vLck7eqU2Opyu9Nw9Zb8o+a7eHR2pwlqw4cPb37wwQdTysrK9CEhIXLp0qURw4YNa2p9/VjZ5SN59dVXo3Jzc61BQUFeLbV05BjMxLMb4XXazjP4SVBjqyHK7JtOQlNrxMAHx6DU7nFyk8z+V1YsLNiLDgPJgyb4ve/2iJ84gOqgfFKTYvzab2pUEM9fOZKRGZF+7fe0maux1/pF20ShULRBIGSIKyoq9IsWLYrcs2fPlpiYGPesWbP6/vOf/4y+6aabel1M6VinoJV6t9atsPDIkSNt8+fPL5s+fXp2UFCQlpubaz1SRvlY2eVW1q9fb7n33ntTFi9evNvbMTsy+CUp5c+EEP+RUq7wtuOThe4IKFnrfV9KKG9xDAIhuVxU1USCyYTJEoD8hXaIG9qPuKH9Oj/RSyKDTZw7xP9aD8aQSIwhkX7vV6E4lehpGeJPPvkkPD093Z6cnOwCuOCCC2q///770J5yDDq6wx/23dYh5Q7XcfNNMBkcAAlmo6srEYK2uO222ypvu+22SoBbbrklJTU11QFtyy4D7N2713jRRRf1f+mll/Jzc3OPiyR0Rkfx81FCiGTgyhZ9hOgjH94OdKLSXcdApxdYgr2/uJc6WhwDP0cM3PYmih0hpAVAibA9Gksr2fvBdzRXe5X42iU2Hqhhbb5//+d3L7yDLW/3uIinQnFKEQgZ4szMTMfGjRtDGxoadJqm8dVXX4UNHDjQK0nhQHF7ZmKxWSeOmq9ZJ7TbMxO7HZosLi42AOzevdu0aNGiyOuvv74a2pZdrqys1M+cOTPrgQceKDr77LOb2uuzIzqKGPwLWAb0BTbAUXV4ZMvxk54ae43X2xRbsdY7CA43IXwohzwlKozXhvQhxeJfx0BvDuE3N1yLDEjZpLYp/XYrQRv11KUWEzQ2x699/3P5XopqrCz+7WS/9Xmw6UukTuv8RIVC4TOBkCE+44wzms4777yaoUOHDjQYDOTm5lpvv/12/wvo+EDr7gN/70oAOP/88/vV1tYaDAaDfOaZZw7Exsa6oW3Z5b/+9a/xBw4cMD/yyCPJjzzySDLAsmXLdqWkpHQ5g1t0VrlPCPG8lPLXPswlIIwePVquX7/eL325NBcjXh/Br4b9ipuH3+x1+23fFlNfaWPChf4PoZ9MOJvtHNywi8QxOej9HAGZ8cw3pEYF8eLVY/zSn9vWxIqvhxJdPZjhV3zklz4VipMBIcQGKaVvGvEtbNq0qWDYsGGV/rJJ0Xts2rQpdtiwYZltvdZpUoSU8tdCiElAlpTyvy2li8NatA1OahodjYSZwnyOGOSefqxYZNf5uroes07HhEj/6gqseeuvGIPDGTnnV37ttyOMQWZSJg3xe79SSoprmhnf138JjdXbliBNEBk7zm99KhQKxU+JTh0DIcR9wGhgAPBfwAS8AfRcrd0AEWmJ5PvLv/dZ78Dt1HyuevjYvjLCDXomDPevY7AjvwRLUDU9JbTssDaz659LiTsjh4SR2X7tu77ZRYPd5VdVxeqCryAKYgbO9FufCoVC8VOiK9soLgRGABsBpJQlQoiwgFrVw/hSHVDTJP+ev4KxszMZPdP7ssMvD8nEofm/iuc1f3gGp83q937b4+D6XURWRtFUWIW/vZHCAMgt11u3oNfrCU3tuRoPCoVCcTLRldtdR4umgQQQQvRcunuAWVe2jjtW3EGF1fvcFemWjJ3dh+SsSJ/GTjKbyAjyrZRyZxgt/pUn7oi6HSUAxI/q7/e+AyG3bA0uI6gxvsdKRSsUCsXJRlccgwVCiH8DkUKIG4AvgZ9ELdk6ex151Xk+VT3UG3WMnplJcpb3Wx1rnS6eLShnj9W/u2y+e+3PvPfM3d2SgvYWraSZZq2R0NQ4v/ddXOtxDFIi/RMxaCrZiSvKRXhQrl/6UygUip8iXUk+fEIIcRZQjyfP4F4p5dJOmp0UnJlxJmdmnOlTW0ezC4fNRXCEGZ2X2xULmh08kl9KTqiF/sEWn8Zvi11FFTgx9djdsJQSc7MFe4jX9TO6RFGNlRCTnkgf6kS0ReW2j0EP0enT/NKfQqFQ/BTp6q3yZmAF8DWwKWDWnETs/aGCV+/5nsZq7+/6y1qqHib4seqhu7meYkcoabE9t9JTt7+UYF0YhuTALF0U1TSTGhXsN0dHc9kwVpmIHjLLL/0pFIqe56GHHorPysrK7d+/f+6DDz4Y39v29ARtzXnWrFl9c3JyBuXk5AxKSUkZkpOTM+jINrt37zYFBwePuPfee71W0+vKroRLgMfxOAUC+LsQ4k4p5Umvuvjk+ieptlXz8KSHvW7bKqAU5ItOgsP/Ognl21biwkhqpv/X+tujcsNeLEDE4NSA9P/HWQOptfpPOK3PuffSh3v91p9CoegYf8sQtydBPHjw4MCELb3kjdX7o/+2bHdKRYPdFBdmdtw6Pav4qvEZPSq73MpvfvOb1ClTptT5MmZXIgb/B4yRUl4tpfw5MBb4ky+DnWhsq9pGUUORT22t9Q5MFj1Gk77zk4+hzO5ELyDW5D/J5cLdHrnttME9t4u0eW8VbukibkRgnJGMmBCGpUX6pS9N09A0Ve1QoegpAiFD3JkEcW/yxur90Q99uj3jYIPdJIGDDXbTQ59uz3hj9f4elV0GeP311yMzMjIcvpaL7sqVSSelPHjE8yq6uAQhhJgBPAvogRellI+2c9484D08Doh/yhp2gRpbDRnhGT61tdY5CI7wbVdBmd1JvMmI3o+5AEUlBwkVJiKSMv3WZ2foqiVNunoMFv9LRzfZXXywsYgp2fGkx3R/qaLqh4VsLbqLgUkPkDhW6SQoFP6gp2WIO5MgDjRz/rGy3fluL60PcbqPma9L0z22eGfaVeMzqg/W2ww3vLb+qPl+dMskv8su19XV6Z588snEFStW7HrggQd80qrvimOwWAixBHir5fmlwOedNRJC6IHngLOAImCdEOJjKeX2Y84LA+YDa7wx3B9U26oZHj/cp7atOgm+UGZ3+ldVUUoKGwVp4b7VZPAFze1GJ/W4YwPTf0FVE3/6aBv/vNLsF8dAZw4mrD6V0FFD/WCdQqHojEDIEHcmQdybHOsUtNJgc/Wo7PKdd96ZfMstt5RHRET4HCLtyq6EO4UQc4FJLYdekFJ+2IW+xwJ7pJT7AIQQbwNzgO3HnPcQ8BhwZ5et9gOa1Kiz1xFl9l1ZMTbVt6qFpXYn/YP9V8OgsXgHtTKMsckRfuuzM3R6PQMfOx/N5e78ZB8YmBjO2j9MJ8Tsn+WWmMEziBk8wy99KRQKD70hQ9yeBHFP0NEd/tiHvxxysMF+3Hzjw8wOgPhwi6srEYK28EZ2ecOGDSGLFi2Kuu+++1Lr6+v1Op0Oi8Wi/eEPf+hywZ52lwSEEP2FEBMBpJQfSClvl1LeDlQIIbqiGpQCFB7xvKjl2JFjjATSpJSLOupICHGjEGK9EGJ9RYV/hLTq7fW4pdt3ZcU6O8ERvkUMyh1Ov8otF279HoDU7OF+67Or6AyB8dZ1OkF8uMVvjkF9/gaVY6BQ9CCBkiFuT4K4t7l1elax2XC0bKvZoNNunZ7Vo7LLGzZsyCsuLt5SXFy85YYbbjg4f/78Um+cAug4YvAMcE8bx+taXjvPm4GORQihA54CrunsXCnlC8AL4FFX7M64rVTbPZ+lKIv3EQOnw43D5vZpKcHq1qhzuf3qGJiCwukf3EjSoAl+67MztjyyEDQY8n8XBKT/TzaVUFrXzI2Tu69c2VS0g3X5l5D64ywGXPg3P1inUCg6I1AyxO1JEPc2rbsP/L0rAbyTXfYHHTkGCVLKLccelFJuEUJkdqHvYiDtiOepLcdaCQMGA1+3rIsnAh8LIc7viQTEGlsN4JtjYK3zRK6Cw71fDgjSCbZNHIzBj6kA/aZcQr8pl/ivwy4gTDoI4A34J5tKKKhq8otjULn9EzBAVNqkzk9WKBR+4+qU2OruOgLHsmHDBp/C8T3BVeMzqv3hCBxLe3N+//33Czpq99RTT5X4Ml5HjkFkB691pUbtOiBLCNEHj0NwGXAoHVxKWQccSl0TQnwN3NFTuxJaHQNflhJMQXomXtSfxL7hXrcVQhDjx22Kbqcdp7UeS4T/SxJ3xODfnR/Q/luLG/mD2orVEAcxg5WiokKhUHRGR9sO17doIxyFEOJ6YENnHUspXcAtwBJgB7BASrlNCPGgECKwV5UuYNKbGBg9kNgg79Pqg0JNDD8znahE76sMbqhr4rF9pdT7KWmvbNMyHn36H+z+tiv5oP7B0dQc8PX6ohqr31QVG8U+zFWh6C3+lbhWKBSKnyId3br+FvhQCHElhx2B0YAJjxRzp0gpPwM+O+ZYm6XnpJRTu9Knv5icOpnJqZN9attUa8dhcxEZH4zwUidhU4OVZ/eXc1O6fyp5hsSmMTVzI4kDRvulv66Q9/wXBB000+cvZ6I3+C/60Upds5N6m8svjoHb1ogtuoGY6iF+sEyhUCh++rT7rS6lLAdOE0JMw5MLALBISvlVj1h2ArP1m2I2fF7Ar56bhrepAtelxvHz5FgMXjoU7RGZOYSp1/TsRU9XpeHQ2wPiFAAU+1FuuWrbYjBBZMy4bvelUCgUpwJdqWOwHFjeA7b0KI+tfYxyazlPTX3K67ZZYxKISQn1WlWxFX85BUhJwaqPSMyd2GM5Bi67gxB3BI2xDQEbo6jGCuCXiEF1wVcQBbG5s7vdl0KhUJwKdFVd8SdHbFAsCcFei04BEJ0UQv9Rvi0FPLinhP8U+qcWQ0PRdl754kc2Ln7TL/11hYM/7MKgMxLUt1vlvzukyI8Rg3rrVvR1ekJT1VKCQqFQdIVT1jH4xZBfcNfYu3xqW7ijmqriRp/aflxRw6YGq09tj7Nj6yoA0gYM90t/XaFuq2fHaezIwKk4Ftc2E2TUExXc/VoP1pAygpt8cwAVCsWJxZ49e4zjxo3L7tevX27//v1zH3rooXiA8vJy/WmnnZaVkZEx+LTTTsuqqKg4Meokn6Scso5Bd/jqtR38uKyw8xOPQZOScrvLb8WNivbvQ4+bpIHj/dJfV3AWN2HTmgjv45M2R5c42GAnNSqo27oPmqaRnXAXmQPn+8kyhULhDW+s3h899uEvh/S5e9GosQ9/OaS7SoNGo5Enn3yyaO/evdvWrVu346WXXorfsGGD5b777kuaOnVqw/79+7dOnTq14d577w3cF9QpQGCyx05wpJRMfHsi1w2+juuHXO9dW016lBV9qHpY5XThlNJvjkFhlZUkswGD2eKX/rqCucmMLag5oGJNf7tsODZn97dD6nQ6kif+wg8WKRQKb2mVIba7NB0cliGGw1UCvSUjI8OZkZHhBIiKitL69evXfODAAdPixYsjV6xYkQfwy1/+smrKlCkDOLqgnsILTknHoMHZQIOjAaPO+wu0zepE06RPjkGZ3VPOOskPjoGruZ4SZyhjU3ruT9hQUkGILpz6pMCqnAohCDJ1PxJYtOJ5hE5Hyum/9INVCoXiWHpDhriVvLw80/bt24OnTJnSWFVVZWh1GNLS0pxVVVWn5LXNX5ySSwndqXp4uByy746BPySXy7auxI2B1MysbvfVVQ6u2wVAxKDkgI3RaHcx/+0fWJvf/aqi+4v+TX7RP/1glUKh8JZAyRAD1NXV6ebOndvv0UcfLYyOjj4qvKjT6XpMfv6nyinpVXVLJ6He4xiE+KCsWOZocQz8EDEo2r0ZgLQhPVf/P3JAKmVF28geGbhiStWNDjbsr2FGbveXCMect5Tmij1+sEqhULRFb8gQ2+12MWvWrH4XX3xx9dVXX10LEBMT49q/f78xIyPDuX//fmN0dLTL234VhzklIwbVNt+VFa11dsA3AaVSuxMBxPshYlBYWkG4zkp4Yka3++oqMTkZ5N48E2Oof0oVt0V6TDAr7zqDc4ckdbsvU3gcEf16TnFSoVAcJhAyxJqmcdlll2VkZ2fb7r///vLW4+ecc07tv//97xiAf//73zEzZsyo9dlwxakdMYg2e7+U0NQSMQj2IWJQbncSazJg7G6BIykpbNSRFt5z4TKX3UnhF+tJGD+Q4LjIHhvXV/YvfZy6qg3kznsFvbHnkjMVCoWHQMgQL126NHThwoUxWVlZzTk5OYMAHnjggeIHHnig9MILL+yXkZERm5KS4vjwww/3+msepyKnpmNg795SgsGkw2j2PjlOAv2CvI80tMV1116Ly2H3S19doXLzHozfuSixbqL/pVMCNs4L3+zlhwO1PH/VqG71U172MdbQcuUUKBS9iL9liM8555xGKWWbIn6rVq3a5a9xTnVOSceg2lZNkCEIi8H7i0brVkVfklueykn3uk2bCEFk+iD/9NVFYgb1obzRRlJuZkDH2bi/lj0VvhWPOhJrSLkqbKRQKBQ+cEo6BjW2Gp92JACMOjcDW4PTzxZ5x+ZP/o3T5WTUhbf02JjGEAup00YEfJyi2u7LLTcWbcUd6SZcDu78ZIVCoVAcxSmZfDg4djBnZ57tU9uY5FBSBni/BGFza1z0wx4WV9T5NO6RbN2Rx48787vdjzds++dnlHy/NeDjFNU0d9sxqNz2KQDRGdP8YZJCoTiMpmma2gt4ktPyN2y3itwpGTG4cuCVPrfdtbaM2LQwopNCvGrX6NawaRoOKX0eu5XL73gCR2Ntt/vpKo1lVUQcCKOWAySfFri78Ea7i1qrk5TI7okn1VaugTiIGTzTT5YpFIoWtlZUVAyKi4ur0+l03f8yU/Q4mqaJioqKCKDdO71T0jHQpIZOeB8scTs1lr68nXHn9yE6qY9XbWNNBj4dle31mG0hdDrM4YFTNzyWivW7MALhOd3fQtgRxYdUFbsXMWgS+zBXhaK3hPrDLIVC0YLL5bq+rKzsxbKyssGcohHnnwAasNXlcrWrBxBQx0AIMQN4FtADL0opHz3m9duB6wEXUAFcJ6XcH0ibpJSM/994rs69mpuH3+xVW51ecNVD4zGae8+f+nHhP9iTf4ALbnqwxzQSmnZXEC4jSBzjH8emPYpqPKqT3XEMXLZGbDGNxFQN9ZdZCoWihVGjRh0Ezu9tOxSBJWAenxBCDzwHnAsMAi4XQhybSv8DMFpKORR4D/hroOxpxS3d/HzQzxkR530indAJIuKCfSqH/HpJJWes3UmT2+112yPZtTefwgZ6VDiJCjdNog5TmHfLJ95SdChi4PtSQvXWz8AIkbHj/GWWQqFQnFIEMhQ0FtgjpdwnpXQAbwNzjjxBSrlcSmlteboaSA2gPQAYdAZuGXELp6Wc5nXbqpJGfvjiALYm73cl7LXayW+2E6zrxlveC4WN3C4XIa5wXBHdVzvsjKIaK2aDjthQ7x2vVuwNZejr9MQOmu1HyxQKheLUIZCOQQpQeMTzopZj7fEL4PO2XhBC3CiEWC+EWF9RUdEto2wuG1XNVbg17+/cS/fU8f0He3A5vL9IltmdJJiN3RL3qDuwjQYZQmpyYNf6j6Ry8z6MOhOWzMiAjxURZGRi/9huvUdp025l6oW7CE1VWxUVCoXCF06I5BEhxFXAaODxtl6XUr4gpRwtpRwdFxfXrbHWlK5h6oKpbK/a7nXbVgGloHDvtQ7K7M5uqyoWblsFQNqA4d3qxxtqNntSPmJGeJds6Qu3nJHFy9eMCfg4CoVCoWifQDoGxUDaEc9TW44dhRDiTOD/gPOllAGv8dstAaV6B5ZQI3q9929bqd1JUjdVFYv252PARcLAnhMGchY2YteaicxO6/zkXqaxaCtff5jNgeXP9rYpCoVCcdISSMdgHZAlhOgjhDABlwEfH3mCEGIE8G88TsHBANpyiFadBF8qH1rr7D4lHkopKXd4lhK6Q2F1M8nmZgwm/+gtdAVhg2ZzE7ru5EZ0gSa7i9P/+hULf/BZeA23vYHgxkSCY/v70TKFQqE4tQjYvjsppUsIcQuwBM92xZellNuEEA8C66WUH+NZOggF3m1ZVz4gpQzoVpgaWw1mvZkgg/db4qz1Dp8cg1qXG5smuxUxcFrrKHWGMj7Ve/Gm7jD44QtxNgderMnmdDMyPYrYUN+dnoh+Exjb7xs/WqVQKBSnHgHdkC+l/Az47Jhj9x7x+5mBHL8tqm3VRFmifEpws9Y5SM6K9Lpdmd2ziyGxG45B6dZv0dCTlhnYWgJtYfSTImRHxISaefay7mkxWA/uJTi+n58sUigUilOTEyL5sCepsdUQZfY+v0BK6XPEoNUxSOpG8qGUkGmpJ3XIJJ/78JbtLyxmy30form6V3uhK7jc3dsO6bI1suqHs9ny9hV+skihUChOTU65ksi+Kis6ml24XRrBEd47Bha9jtOjQkmx+L4/P2PcbK4Z17N786VLQ2igMwR++eLxJXm8v7GYdf833adoTmtho7BoVfFQoVAousOp5xjYa8iMyPS6XVOdZ6uiLxGDCZGhvDvc94Q4qWk4mxswhUT43Icv5N7UcyJERTXNhFsMPtcwqNq/HKJQhY0UCoWim5xySwmtOQbeEpkQzDWPTSRzaGwArOqYugPbeOTxJ9ny2Us9Nqbb6ULTAl/tsJWiGisp3dBIaLBuRV+nV4WNFAqFopucUo6BJjVuHHojk1Mne91WpxOERJgxWbwPsvxyWwE/37zP63aHxjZZmJSmJzGre8l53rD7ra/Zd/dSGop6ZBcpRTXN3dJIsIaWE9yU4EeLFAqF4tTklFpK0Akd1w9pV2myQwp3VlO2t46RMzK8LnA0KjwYu+a7dHl4chbTf3Gfz+19wXmgAROhhCQHPkJidbioanL4rKrYWLQVd4SbcDnEz5YpFArFqccpFTGwOq0UNRThdHsvglSyu5YNi/ej03m/Bn5jWjy/yfD9brb4x69wNNb43N4XjA0GrKbGgBc2Aig+pKrom2NQue0TAKIzzvCbTQqFQnGqcko5BhsPbuTcD85lW9U2r9uOO68vNzwz2evkOE1KrN3YiudsquWlhctZ8XbPlfm1VtURQgQiwfddFN7QXbnl2so14ISY3Bn+NEuhUChOSU6ppYT+kf158LQHyQzP9Km9LxoJJXYno1dt5+mcNC5PivG+/daVnsJGfXqusFHFul3ohSA0u2fW7ItqPMrbab4uJYh8LNWh6C2h/jRLoThhKF17D/uq38Vm1LA4dfSNvpiksY/0tlmKnyinlGOQGJLIhVkX+tR25Xu7iUoIJvf0jpSjj6e8pbhRnI/FjYp2bwUgdcjpPrX3hYa8ciJkOPFjBvTIeEU1zZgMOp/LIfdNvwVNc/nZKoW3LPzofR5f00yJFkmyrpY7xwVxwZx5nTfcvACWPQh1RRCRCtPvhaGXBNTWk+lCW7r2HnbULkCaAAQ2k2RH7QJYywlrs+Lk5pRyDPbV7cPqtDI41vstbbvWlNF3uPeSz6Wt5ZBNvr3VhWUVROl0hMb3oLphhZMm6kiLDuuR4YanRXLdxD4+5W8AJE+6wc8WKbxl4Ufvc88qQTOercDFWhT3rLID73fsHGxewML33+Rx+x2UEEOyrYo733+TCyBgzkHp2nv47849vL/vPqpsUcRYapjX92Ou5Z7DF1opQQiky4601yHddnA7kZrdc0zz/I7biXQ7MSeOQQRF4azeibN0HcFZc8EUgq3oWxzl60DznCelE6m5PM81J1K6QXMSM/5hCI6mYcer2AuWEnvWy2AwUb3+L+yoXsCrlZfzzZ4JaDbQWWBy/1Vc63yHJJRjoPA/p5Rj8MrWV1hZvJKvLvnKq3Zut0Zzg9O3csiOVp0EH1QZNY2iJgN9I71u6jOay02wI5SmiKYeG/PcIUmcOyTJp7YH17+Lq7mGxInX90iipKJtHl/TfMgpaKUZM39dU4PF/So66UaHC710o5OHf36/dS8vOK/Bgef/o5g47rJfg/XD97li6CW4a0to+OxejLgwShdGXAjcoLlBaiDdnov46Gth8DyoPQALroYz/g/6nwlFG+Cjmw+fq7l5NSKLV3ZdgUPzjFlli+aVnVdgkG9zpfkh8ir+y8TMv2HuP5t9626hoLnz74tJ7mcx95/NgbwnKWj+iukJEyC2P/l7HqdE6zynabr1dxAcTVHFQqrEJia5HWAwcaBhKa9WXs7X2ycgNBCAtMHX2ycgB8FUr/9SCkXnnFKOQY2txqfiRs31not7cIT3oe4yuxOjEMQYvS8rXHtgG40yiNTkSK/b+krVzv2YdBZc6T2n4ljRYCc21ORT1cOCHX/DGlJOsu7GAFim6Ai3y83mLRv55ocdFGttlxkv1SL51dqO/q4Djztix8RjzedxBZBf2ciZm49e/jPixiA0jMKNqeXn/Agdlw2Gwno3vy65ht+X6JjcH7bX6flr3XWgd6LpbbgNzazfPeiQU9CKQzPxTv5sZoyKZfEeA30yU+gDHDTMYNmebBA6hBAIoTviIRBCjxCCrOxc0oGaiKtZXXA6Iw1xRAGVUXewfnc5QqdH6PSADl3L70J4fup0ekYFpxMJ1Cc9yrc/7CX9YC3paaFsEQ/x9c56xDHbnYUG3+6Z0MlfSKHwjVPKMai2V/ukk2Ct98gO+yqglGD2rdRv0bbVAKQNHOV1W18xhQVxMKWe5PHDemS8ZoebMQ9/yZ3nDODmad6XjR46622aSrYEwDJFWxQV7efbNev4dk81K2ujqJfBCKKw4MDG8Y5zkq6WF6+fgoYOTehxo0dDhxuBW5Nc/p9VeO6Dj6aeEABikzO57zwjTreG0y1xuDRc2uHfnW4Nl1uSnOuJOOnCE4nPHIQ5tQ8AdUY9+4XE6TTidgQhRSp2d9v/x1W2KBotY/i4GC41ZtAHKDeM5739wUgkUkLr5VlKicQTrACYOTmCdOCALZP/bKrjqrMNRAFba1J4fn3DMSNJwNXy8CDc3/GbmdNYsx/+vVFywLyN59NS2VoZgnDVtWmvZmvzsELRbU4px6DGVkNKrHfJgwDW+hadBB8ElErtTpJMvm37K9yfjxFB/IBxPrX3hYiMJCJ+M6vHxpNIHpyTy8h07yM5AJboFCzR3v9NFV1n7ZqVfLZ2J9+Um9jnigGCSNIFMyOmgskD4pk4djwrVq/hnlV2mo9wDoKw8/txQQzq235+TEqwpNh6vGOQHOy54kYGm7h2Yp8u25ocYebmIU8S7h4K/IFx/Qfyr7nvEx09kaio0zAYQhj/wPuUNVuOa5sYZOe0/rHsfOjcQ8cuGJHCBSO6/vm6cEQKFwxPofU+4MbJffnZ6EQK9xewsaiYrdX17La7OKA3UxEZS7PF4wB92FzDb4BfTu1PucXK5LR4AO47fwgLtpQh7cdvedZZ1NKZIjCcco6BbxED3wWUyuxOBoX6tg2vsMZGigX0xp6pJwBQtOJHogdnEhwT2SPjBZsM/HxCpk9ty9a8SWXB52TNeApzRLx/DTuFKSzYwydff88vLrsYsyWIZRt38HZxDONDy7kqQ2PyqKH0GzADoT+83HTBnFTgfR5fU0OJFkGyrq5LuxLuPG8k97z3A83uw85BkF5y53kjO7XT5Wqkuvo7Kqu+wumsZdjQfyOEjvDwoQQH9wVApzMwYMD9R7W7+/zx3P3eBmzuw/Zb9G7uPn98F96djhFCsHrLZvbX1nHZ5NPR6wTnLvqK3XEpYIqHxHhMbhepLhuTTXqGRJkZlZzI4ChP1U6zQc/Dkw6XPg82Gbh8Wl/e/GLvUcsJUie4YmrfbturULTFKeMYONwOGp2NRJm9vzO11nmWEkLCfcgxcDg5w+xbdv9Fl/0Mh/XYMGTgsNU2oH1WR/6PK8md3zMqhcW1zVjtLvrFhXq9K6Fsz7tUxWxhoNl3jQUFlBUX8u3aNYwanEvfrIHs3JXHX3fGMGHLRkaMmchNl8zh9qBgzMEd14m4YM48Lpjj3ditd+OPL8mjpLaZ5Mgg7jxnQLt36VZrPpWVy6msWk5t7TqkdGIwhBMTMwUp3QihJyvrD34dsy0ctmZ27N/PhqISttbUs98lefeSOeh0Oh7fW8wWUxiXtZx7dnI8k3UaIxJjGJmYQGawGZ0XS4t/merZNvz2inzczW70QXoum9Ln0HGFwt8IKX2v4d9p50LMAJ4F9MCLUspHj3ndDLwGjAKqgEullAUd9Tl69Gi5fv16r+y4+OnnWF+XeWirz+iIAt697eaAtu3OmJc8/Rzrjmg7JqKABV1s6ysn05i9Yuszz7OuNv3wmJEHWPDbX3ep7dynn+WHuv6H2o6I2MMHt83vvN0zT/FD7YDD7SLz+OC3t3fN3qceY1394MP2hm9lwe13AdDc1Mjadav4ZlsB35YZ2eX0bMO9Z2AFv7z6GmzWRuprq4hPzujSWK3c9t//44v0qVSJGGJkFWcf+Jqnr32403b/98a9LEw6/VC7C0q/5eGrHjz0emNjHiUlC6isWk5z834AQkKyiI2ZRkzMNCIiRqLTeXeP88Srb/KfyGTqwiKJaKjlhtoS7rj6yjbPraiu5vvde/mxooqdVjsFwkhpaCQ2y2GHNNxu5ZsJg0mMCOeH0nKkycSI6EifZcQDgRBig5RydG/boTjxCZhjIITQA7uAs4AiYB1wuZRy+xHn3AQMlVL+SghxGXChlPLSjvr11jG4+OnnWFuRiThiiU7qYGxc5xdqX9t2Z8xLnn6ONW20HRcXuIvfyTRmr9j6zPOsOZh+/JjxnTsHc59+lg0V/Y9rOyquY+dg7jNPseHggOPbxXfuHFzy1GOsqRx8XNt0SzEZAtY2xePAiAknY0PKmZxu5vQRueTkjjxqecAbbvvv//F+xnk4xOG1e5O0MW//Jx06B//3xr28njzzuHZX1y7gD+feTlBQKmXln7Bjx11ERY0nJmYasTHTCApK9clO8DgFzyRl4Tpiic7gdPDb0t3ccfWVbNyXz783b+cPE8eRERfLXZ9/yasWj5iY2ekgxd5EPz3kRoQwIimB0SlJxPiwHbmnUY6BoqsE0jGYANwvpTyn5fk9AFLKR444Z0nLOauEEAagDIiTHRjlrWPQ5/5FyLaydw0wOK6szTbPzpxEv6wBZN67CBzetTVKNz/WprQ9phEGx7bdrpWtlYnQhsaTsMCoyEJsouMKirGuRl75neeCc9Ez/yLa1cQLd/wOgPOefRHZRgb41orEIxOkD2OAC6K28czvfs/a1St5cE1eh2MD9HdVHXX+MHclD99+F2/877+8XXH4StXumCYoeHAW777zKq+WuTjDWMvtN/2Ov//rGZbYwzq0dXBcGWeFuJl//Y38878v8Vk9XJ4UzJWXXM5fnn+O7x3HJ5wdy7Hn3z2yPz9b1tjuZ2hgbNFRh/5v/EAmjZ/MHX9/gm3uMHbWpnbQtuC4w29efRnRUTEdf/Zi9rRpu1G6+PD237X/mQfCTI3EhdQSG1JHZEgTet3hf7UQqfHENX8G4Pev/BEjOh6+xnPn/ttX/ohdtJ/s9kXaWTSJ45fMYrQKtk0/i9vevA/Zxt3zJ4nT22wXIhv4s/N7Lj/nLmqry3n8w0UI2XGy3biUJM479xzqa2v463sfcWZWX6ZOmcz+ggL+8+XXh857IyX7qLv9VoKtjeybNYmFP27mpion/46zcN7QXLYUl7C1spox6Wn0iww/oaIA3qAcA0VXCWSOQQpQeMTzIuDY9PpD50gpXUKIOiAGqDzyJCHEjcCNAOnp6V4Zodna2gwFuGBraWKbbZrq6wGQDu/bYhRIZ1uXX5DODtq1nkPbY2o22FCb1u4XfitB0Yevmhtq04gKtx56vrksCeGFHyhdsNHgWXfN27OTraWdFyGqT7IcfX5Lk22lB9la0XnFSdlyMdxUXM7Wg7lEJXjs32QVbD3YwXvX8jeJTswH4IdaK1vL+/KD3MOVwHqHsdP3Hjju/K35e9FsCe1+DnaUHX3nunnvbiaNn8x6dwwFZfHt/j09bTOPO9xs8whKdfjZK29nW6fR06K9z7wEKqYNoKLt1qS69h/6fWXSMCzu5kPPv0ibSrXOewnuKuHRB1mQdB5u0fWvmyZCWbwvksuB2tomXurbeTJi+a4fOQ+orq7hxX7DYfePTJ0ymb35+z3PO8Ea5NkhcHZqMpurNhGVOQaAISnJDElJ7rLtCsXJzkmRfCilfAF4ATwRA2/a6iy0fTE1wx1xu9tsk5rh8V90ZpD2ttv+PrHttnqdjkdL+7U5pjDDH5PabtfKn0uzoI0xdRb4c2YJDlfHmgBhIYcTxP6cWUJU1OFdGPf3O9BmmwcK09scU5jhoSGei8GsGXOAjzocGyA9Oe2o8wf0zwHgV3PnMei7rw+d96fdSW3PsyW/81cXzWPwdysYOmQSAHefO40zf1zP3TsT2myHGZ4Y3szQ3OkA3DNnJuds+oFxozzFce6bNobde/d2av+x50+ZdB6P7Vvb7mfoiRHNR/0XTZ3gyb57YuogCooKuXN9ULttnz6tmWNvPmMiPRdSYabdeT42poHQkOOjH3q9J5rU3mdeZ4F3w9uX7zabDl/4/5aSjjhCNOzFGDNud/ttb6x1U9OG4xAjq5BS8mlCcxut4Ioye5vtomQV5433JAo7wpxcUfUKA6NzGBA9kLSwFEQb0YuIIZ6E2dS0NFZTSPQIz99i4oRxrC4pOXTeWVsP0BAWeXz7hloA3Js2UXHzLQS/9T+CR4ygafVqGpZ+iXlANpacHMxZWeiCfNtppFCcDPzklxJUjkHnnExjnkg5BlMTK3ikPofI2X0JGhbXZoi5vRyDETFFzKsdysR5/ekzLPa4tu3lGAxLLGBkdQUjR45k2rRphIUdH4ZvL8dgXOzhBER/01GOwaB+v2D9/mpunZ5FbnLEUe3ayzH4WclnPHjFfeh1er4v+Z77v7+f0qZSACLNkYxKGMXohNGMThxNdlQ2ug6WOY6lsxwDrbkZ286dWAYORGexUPPWWxx8/Ak0a0v0TQhMGRmYc3Kw5AzAnD0AS84ADElJJ/Qyg1pKUHSVQDoGBjzJh9OBYjzJh1dIKbcdcc7NwJAjkg/nSik7VE5RuxICw8k05omyK+GNi39GzYe7cRY1Ys6KJPqSAejDjk9Ca2tXwjNnX8XK9/ZQU9pEyoBIJl2cRWzq0Rf5tnYlvPnLX7NixQrWrl2LwWDg9NNPZ/z48RiNR+eedLQrIVC0tyvh5ZX5PP3lLhpsLs4elMCt07MYnHLYQehsV0IrxY3FrCtbx/qy9awvX09xYzEAYaYwRsWP4r7T7iM2qGvLHd7sSgCPbomzqAhbXh72nXnY8nZiz9uFs/Dwamn2urXow8JoXLECV3UNkRde0MV3rmdQjoGiqwR6u+JM4Bk82xVfllI+LIR4EFgvpfxYCGEBXgdGANXAZVLKfR316YtjoFAECqlJmlaX0rSujLhfD0Nn6npWv+bW2L6yhDUf5yOl5OpHJmI0d619VVUVX3zxBXl5eURGRnL22WczaNAgX6cRcOqanfz3u3xeWplPg83FWYMSmH+Mg+AtZU1lHkehfD1bK7fy9uy3MeqMPPfjc+yt3cuTU55s9w5+0b5FPLvxWcqaykgMSWT+yPnM6ut9xU93YyP2Xbtw7D9wyBEomv9b7Hv20G/RpwD8f3v3Hh5VfSZw/PuemWRCAoSAJNxCuAhBeEQCQauyElFEqQ9eH6zr7urjrd3HbluVp3XVdUFt1/pstd3t7VF0da1rt94KW62AVFSKXEQ0KAhCDMj9IknMbTIz590/zslkcoOgxBky74cnz5w5F847Pw457/wu57f3X+4jVvsFWcXjCI0rJqu4mOCgQV977YIlBqarujUx6A6WGJhUpK4ijqCRGIef/Zg+MwoJDe/bpWPD9REOflbLsOI8VJXNq/Yy9swCgl2YeKuiooIlS5ZQVFTE7Nmz4+u3rtnHO4u2U/t5mN79Q5x92WjGnnXszpdfRXl5OcuXL6e6uprc3FwuuOACJk6c2Gqf6oYIT/21kidWVlDTGOXC0woozvycQ1vWk6VhGiVE8eRzuHHO9C8dx8KNC6msruTBad4Ii5uX3kzQCXpNDwWl7KjZwYJVDxBJ6ECUISEemLbgSyUHbanrEjtyhOAAr7/InnvvpX71GiK7WkawBHJzCRUXEyr2miGyTj+drLFjO/07VzxxPxmP/YF+1TGqcgNEbp1L2U33HVdclhiYrrLEwJgTKLK/jkNPfUTe1WPJGt0PgLoNB6hZUkmsKkygX4i+s0aQU9LxI5x3bTnCokc3MPPG8QR21uC+u58sVRpFcEoLGH11+5uH67pEo1EyMzOprKxkxdJVxLbB4awd1EuYbA2R3ziKc66a3m3JQXl5OYv+uJiY29I5NuAEuezyOe2SA4CaRi9B+NOKdzjLqSSQ0CEipg4jp0znxjnT2VfdSCTmEnCEoCM4jhAQ7zXoCAH/JyPQcR8DVeWhtQ+xZu8atle3dD69uvIqesVC8fJpCIRZNuZtVnxrCRsObKCwTyGDcgbREG3go0Ne62fzN3zxx3wkvh/Sewj52fk0RhvZcmQLw/sMJy8rj9qmWj6t9kbKUFuPVOxEtu1Atu+EbZVQsRNpDJM1cwYj//NX1EXq2Hnv3QyceQmnXHgxVY1VvLFwAfpeLtuGtcR76q4w/c91jis5sMTAdNVJMSrBmJNFRkEOg+4sRYLejerQs5to3PQ5xLwEPFYVpuolb2RKR8nBsOI8rpw3mboN+wm8u5+QCIjQC4iu28eHXzSRP2M46voz/ClkZgUYMNQbjbJ982fs2vUp2itGzL/Z1kuYnb0+pmlRA1UNk1FVXPW2ueoyYGAe4yZ6z91/+/V1DBo8kDETRtDUFGHl62v9mQTVP58bX0YVV5XhI4bx2mtLWyUFADE3yuJF/8euyn2I+DdUR7zHAQucJnDQ2dEqKQAIiMv29auoPm8S9z7+CruONKIi3myGiD/DocRnO+wzYBCL7pxFbW0t33t8GU7ffBbedC41NTXc8tvXOdIwCkdOZWigETL3cnZdPaJCvROOl0/AdZi59W+47YllrM64izum3sE1o6/hlmf+xBbnYe8zi7Z+TVieN3Ue10+4nvmvruSVI7fz8HkPM33oTO5fuoQ/H57f+h85BDJevB9XGVgd4EeTpzKoron/fmcZ41csJTx4AJkl03l+xQvkb+hPeaESk5Z4Pyp0KF7twk3HeYEa0wWWGBhzgjUnBRrTVklBM424VC/eDlEXBDIL+5BRkIPbEKVh02EGjsyldv0Bgm3aoIMi9Nr8OX9972DC9L/Qa2RfZt85hVhdhMa3HTKDDvXS+ilZrih7MivZs6KyXbwDswsZN3EUGonxl7f/TNHAYsZMGEF9TQNvrV12zM97YPdp1DfUdvjwhGgswtr3VnV6bKCTZvYMwny8oZJhdVsZdoyHCubnerUgm5dvYnj1BwwbciEA615YzWn15d5OCrhAxFvUNg/0iIlLSIMM3r2GK067h1kjzuP1ny/i1MbNnMrRmxeqRo7mwqIZvPrA7+kV3cKsSXdTWlDKkgd/R57s5jquxQFEBUFwkPifJlX2lAzm9NKLePMnL1ArB1j+4Hy+X3I+bz3yAuLUUz5M40leYryVg49/7hZjusISA2O6iQSkXVLQzG2IcuRFr+Yg99JRZBTkEKsJc+T5rfT/23FkqdLuIQdAyBGm5rT+bxss8eY6iOyp5QzXZZ109AAEQGF2pAQQHCV+c8qd4U3GU19+iCubptJ/ul/1/8ER5obPQfBvagIiDuIIIhJf7jNzPP/1uwrqOzhvjhtibmYZiv/1PrEMgBcjb1LntD8uW0MMqxD+rvG8+Ldz7yNo/MauQNSBAWXew4/y9wX5pns2Red6D9IqOtybvHApLhATRcU759Lg+g6TmDBRztCJTC+bSf+cPgzOHsTkOsH16ypc/5xuS30BYQdmX3Qpp/Tux47+BQw7GObvL7mazKwM+vTLp6A6Ft/XTaxp8P+IA3eXXUl+Tg5NA7PJqM7k9nOuoF9mgJrcIFV1UWK0n3IZ6LC8jTkRrI+BMd1o70NriVW1/wXu9M0k/x/P8JZ7BXGygmjUJVbThNM7g+33raKjR+g0AkW3Twb1OjyiEMwL4WRn4DZEiRys5z+e/DV1tH/CUY5m8d25t/jHKbiKukrW2DwCvTOJHKgnvK2K7Mn5OFlBwjtqCFdUQcxrssD1j1H1Eh4/hr4zi3jrxy+zMuPjVt9sA+owLTKOiacn9DFo8/um/MONnR531twyIgcbkAwHCSb8ZEjLcmaA0EhvZEOsJowqBHO9b9IadcERpM2snQ/f92/xZoRE2W6IH97/zx2UenKdqHitj4HpKqsxMKYb9Z01gqqXPkEjLTc+yXDIvWQkwbzWTy+UoEOwv7fOKS0gum5fq+aEqCoydRAZBTkdnsvpFSQ0vC9lk6fx2vq/tLvZlk2ZRq8JAzqNNSM/m4z8ljkEQkV9CRV1bWTF6FAhEoZ3gxXUSiO9NYvS6ChGhQoZcO24zo9bUNXpcdmTOu6g2ZlAm2nRm5t02ioZPpnVn61pVz4lw4/92OVkGNpvAhXV77eLd2i/CUmMyvRklhgY042aOxh2dVRCs9FXj2U7EEkclTB1UIejEtqaOsd7jPSb762kVhvpLVlMnzItvr47DJgzGnkhxqlNLfNpaEDoP2d0txz3Vcy8eRYshA07N1AvjWRrFiXDS7z1Kei6O+bw7COwu+qj+KiEof0mcN0dc5IdmumhrCnBGHNCHM+wzBNxnDk+1pRguspqDIwxJ0ROSf6XuqF/2eOMMd2j6zOPGGOMMabHs8TAGGOMMXGWGBhjjDEmzhIDY4wxxsRZYmCMMcaYuJNuuKKIHAR2fMnDTwEOncBweiIro6Oz8jk2K6OjS1b5FKnqwCSc15xkTrrE4KsQkXdtHO/RWRkdnZXPsVkZHZ2Vj0l11pRgjDHGmDhLDIwxxhgTl26JwWPJDuAkYGV0dFY+x2ZldHRWPialpVUfA2OMMcYcXbrVGBhjjDHmKCwxMMYYY0xc2iQGInKxiGwRkW0icley40lFIlIpIhtF5H0RSfu5rUXkSRE5ICIfJqzrLyLLROQT/zUvmTEmWydlNF9EdvvX0fsiMjuZMSaTiBSKyBsisklEPhKR7/vr7ToyKSstEgMRCQC/Ai4BxgPXisj45EaVss5X1Uk2zhqAp4CL26y7C1iuqmOA5f77dPYU7csI4FH/Opqkqq9+zTGlkihwp6qOB74B3Ob/7rHryKSstEgMgDOBbapaoapNwO+By5Ick0lxqvoW8Hmb1ZcBT/vLTwOXf50xpZpOysj4VHWvqr7nL38BbAaGYteRSWHpkhgMBT5LeL/LX2daU2CpiKwXkVuTHUyKKlDVvf7yPqAgmcGksO+KSLnf1GDV5ICIjABKgDXYdWRSWLokBqZrpqnqZLwml9tE5LxkB5TK1Bvra+N92/sNMBqYBOwFfpbUaFKAiPQGXgR+oKo1idvsOjKpJl0Sg91AYcL7Yf46k0BVd/uvB4CX8ZpgTGv7RWQwgP96IMnxpBxV3a+qMVV1gcdJ8+tIRDLwkoJnVfUlf7VdRyZlpUtisA4YIyIjRSQT+BawOMkxpRQRyRGRPs3LwEXAh0c/Ki0tBq73l68HFiUxlpTUfMPzXUEaX0ciIsATwGZVfSRhk11HJmWlzZMP/SFTPwcCwJOq+uPkRpRaRGQUXi0BQBD4n3QvIxF5DijDmyZ3P/CvwB+BPwDD8ab/nquqadv5rpMyKsNrRlCgEvh2Qnt6WhGRacDbwEbA9VffjdfPwK4jk5LSJjEwxhhjzLGlS1OCMcYYY7rAEgNjjDHGxFliYIwxxpg4SwyMMcYYE2eJgTHGGGPiLDEwPY6I3OPPZFfuz+53VhJj+YGIZHey7VIR2SAiH/iz733bX/8dEfmHrzdSY4zx2HBF06OIyNnAI0CZqoZF5BQgU1X3JCGWALAdKFXVQ222ZeCNXz9TVXeJSAgYoapbvu44jTEmkdUYmJ5mMHBIVcMAqnqoOSkQkUo/UUBESkVkhb88X0SeEZF3ROQTEbnFX18mIm+JyCsiskVEfisijr/tWhHZKCIfishPm08uIrUi8jMR+QC4BxgCvCEib7SJsw/eg6QO+3GGm5MCP555IjLEr/Fo/omJSJGIDBSRF0Vknf9zbncVpjEm/VhiYHqapUChiGwVkV+LyPQuHjcRmAGcDdwnIkP89WcC/wSMx5sY6Ep/20/9/ScBU0Xkcn//HGCNqp6hqvcDe4DzVfX8xJP5T7lbDOwQkedE5LrmpCNhnz2qOklVJ+HNOfCiqu4AfgE8qqpTgauAhV38jMYYc0yWGJgeRVVrgSnArcBB4H9F5IYuHLpIVRv8Kv83aJn4Z62qVqhqDHgOmAZMBVao6kFVjQLPAs0zUcbwJszpSqw3AxcAa4F5wJMd7efXCNwC3OivuhD4pYi8j5dc9PVn7zPGmK8smOwAjDnR/Jv4CmCFiGzEm6TmKSBKSzKc1fawTt53tr4zjf75uxrrRmCjiDwDfArckLjdn5DoCWCOn/SA9xm+oaqNXT2PMcZ0ldUYmB5FRIpFZEzCqkl4nfzAm9Bnir98VZtDLxORLBEZgDcJ0Dp//Zn+rJwOcA2wEu8b/nQROcXvYHgt8GYnIX2B15+gbZy9RaSskzib98kAngd+pKpbEzYtxWveaN5vUifnNsaY42aJgelpegNP+8P/yvH6Bsz3ty0AfiEi7+JV+Scqx2tCWA08kDCKYR3wS2Az3jf6l/2ZAu/y9/8AWK+qnU2b+xjwWgedDwX4od+p8X0/thva7HMOUAosSOiAOAT4HlDqD8fcBHznWIVijDFdZcMVTdoTkflArar+e5v1ZcA8Vb00CWEZY0xSWI2BMcYYY+KsxsAYY4wxcVZjYIwxxpg4SwyMMcYYE2eJgTHGGGPiLDEwxhhjTJwlBsYYY4yJ+384q9ov/iUunwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model.plot(show_lines=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Fitting L0L2 and L0L1 Regression Models\n", + "We have demonstrated the simple case of using an L0 penalty. We can also fit more elaborate models that combine L0 regularization with shrinkage-inducing penalties like the L1 norm or squared L2 norm. Adding shrinkage helps in avoiding overfitting and typically improves the predictive performance of the models. Next, we will discuss how to fit a model using the L0L2 penalty for a two-dimensional grid of :math:`\\lambda` and :math:`\\gamma` values. Recall that by default, `l0learn` automatically selects the :math:`\\lambda` sequence, so we only need to specify the :math:`\\gamma` sequence. Suppose we want to fit an L0L2 model with a maximum of 20 non-zeros and a sequence of 5 :math:`\\gamma` values ranging between 0.0001 and 10. We can do so by calling [l0learn.fit](code.rst#l0learn.fit) with :code:`penalty=\"L0L2\"`, :code:`num_gamma=5`, :code:`gamma_min=0.0001`, and :code:`gamma_max=10` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "fit_model_2 = l0learn.fit(X, y, penalty=\"L0L2\", num_gamma = 5, gamma_min = 0.0001, gamma_max = 10, max_support_size=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "`l0learn` will generate a grid of 5 :math:`\\gamma` values equi-spaced on the logarithmic scale between 0.0001 and 10. Similar to the case for L0, we can display a summary of the regularization path using the [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) function as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconvergedl2
00.0037880-0.156704True10.0000
10.0037501-0.156250True10.0000
20.0029112-0.148003True10.0000
30.0026543-0.148650True10.0000
40.0025973-0.148650True10.0000
..................
1280.000216100.002130True0.0001
1290.00017313-0.003684True0.0001
1300.00016713-0.003685True0.0001
1310.00013418-0.015724True0.0001
1320.00013021-0.012762True0.0001
\n", + "

133 rows × 5 columns

\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2 # Using ipython Rich Display\n", + "# fit_model_2.characteristics() # For non Rich Display" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The sequence of $\\gamma$ values can be accessed using `fit_model_2.gamma`. To extract a solution we use the [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) method. For example, extracting the solution at $\\lambda=0.0016$ and $\\gamma=10$ can be done using:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "<1001x1 sparse matrix of type ''\n", + "\twith 11 stored elements in Compressed Sparse Column format>" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2.coeff(lambda_0=0.0016, gamma=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Similarly, we can predict the response at this pair of $\\lambda$ and $\\gamma$ for the matrix X using" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-0.31499242]\n", + " [-0.3474209 ]\n", + " [-0.23997924]\n", + " ...\n", + " [-0.06707991]\n", + " [-0.18562493]\n", + " [-0.25608131]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_2.predict(x=X, lambda_0=0.0016, gamma=10))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The regularization path can also be plotted at a specific $\\gamma$ using `fit_model_2.plot(gamma=10)`. Here we can see the influence of ratio of $\\gamma$ and $\\lambda$. Since $\\gamma$ is so large ($10$) maintaining sparisity, then $\\lambda$ can be quite small $0.0016$ which this results in a very stable estimate of the magnitude of the coeffs." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 3. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 10. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 11. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 12. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_2.plot(gamma=10, show_lines=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "Finally, we note that fitting an L0L1 model can be done by just changing the `penalty` to \"L0L1\" in the above (in this case `gamma_max` will be ignored since it is automatically selected by the toolkit; see the reference manual for more details.)\n", + "\n", + "## Higher-quality Solutions using Local Search\n", + "By default, `l0learn` uses coordinate descent (CD) to fit models. Since the objective function is non-convex, the choice of the optimization algorithm can have a significant effect on the solution quality (different algorithms can lead to solutions with very different objective values). A more elaborate algorithm based on combinatorial search can be used by setting the parameter `algorithm=\"CDPSI\"` in the call to `l0learn.fit`. `CDPSI` typically leads to higher-quality solutions compared to CD, especially when the features are highly correlated. CDPSI is slower than CD, however, for typical applications it terminates in the order of seconds.\n", + "\n", + "## Cross-validation\n", + "We will demonstrate how to use K-fold cross-validation (CV) to select the optimal values of the tuning parameters $\\lambda$ and $\\gamma$. To perform CV, we use the [l0learn.cvfit](code.rst#l0learn.cvfit) function, which takes the same parameters as [l0learn.fit](code.rst#l0learn.fit), in addition to the number of folds using the `num_folds` parameter and a seed value using the `seed` parameter (this is used when randomly shuffling the data before performing CV).\n", + "\n", + "For example, to perform 5-fold CV using the `L0L2` penalty (over a range of 5 `gamma` values between 0.0001 and 0.1) with a maximum of 50 non-zeros, we run:" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "cv_fit_result = l0learn.cvfit(X, y, num_folds=5, seed=1, penalty=\"L0L2\", num_gamma=5, gamma_min=0.0001, gamma_max=0.1, max_support_size=50)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Note that the object returned during cross validation is [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel) which subclasses [l0learn.models.FitModel](code.rst#l0learn.models.FitModel) and thus has the same methods and underlinying structure. The cross-validation errors can be accessed using the `cv_means` attribute of a `CVFitModel`: `cv_fit_result.cv_means` is a list where the ith element, `cv_fit_result.cv_means[i]`, stores the cross-validation errors for the ith value of gamma `cv_fit_result.gamma[i]`). To find the minimum cross-validation error for every `gamma`, we apply the :code:`np.argmin` function for every element in the list :`cv_fit_result.cv_means`, as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[(0, 8, 0.5313128699361661),\n", + " (1, 8, 0.2669789604993652),\n", + " (2, 8, 0.2558807301729078),\n", + " (3, 20, 0.25555788170828786),\n", + " (4, 19, 0.2555564968851251)]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gamma_mins = [(i, np.argmin(cv_mean), np.min(cv_mean)) for i, cv_mean in enumerate(cv_fit_result.cv_means)]\n", + "gamma_mins" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The above output indicates that the 5th value of gamma achieves the lowest CV error (`=0.255`). We can plot the CV errors against the support size for the 5th value of gamma, i.e., `gamma = cv_fit_result.gamma[4]`, using:" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "cv_fit_result.cv_plot(gamma = cv_fit_result.gamma[4])\n", + "cv_fit_result.cv_sds" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The above plot is produced using the [matplotlib](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) package and returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) which can be further customized by the user. We can also note that we have error bars in the cross validation error which is stored in `cv_sds` attribute and can be accessed with `cv_fit_result.cv_sds`. To extract the optimal $\\lambda$ (i.e., the one with minimum CV error) in this plot, we execute the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 19 0.2555564968851251\n", + "Optimal lambda = 0.0016080760437896327\n" + ] + } + ], + "source": [ + "optimal_gamma_index, optimal_lambda_index, min_error = min(gamma_mins, key = lambda t: t[2])\n", + "print(optimal_gamma_index, optimal_lambda_index, min_error)\n", + "print(\"Optimal lambda = \", fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "To print the solution corresponding to the optimal gamma/lambda pair:" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "<1001x1 sparse matrix of type ''\n", + "\twith 11 stored elements in Compressed Sparse Column format>" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The optimal solution (above) selected by cross-validation correctly recovers the support of the true vector of coefficients used to generate the model." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.01994648 1. ]\n", + " [0.97317979 1. ]\n", + " [0.99813347 1. ]\n", + " [0.99669481 1. ]\n", + " [1.01128182 1. ]\n", + " [1.00190748 1. ]\n", + " [1.01272103 1. ]\n", + " [0.99204841 1. ]\n", + " [0.99607406 1. ]\n", + " [1.0266543 1. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " ...\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]]\n" + ] + } + ], + "source": [ + "beta_vector = cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index],\n", + " include_intercept=False).toarray()\n", + "\n", + "with np.printoptions(threshold=30, edgeitems=15):\n", + " print(np.hstack([beta_vector, B.reshape(-1, 1)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Fitting Classification Models\n", + "All the commands and plots we have seen in the case of regression extend to classification. We currently support logistic regression (using the parameter `loss=\"Logistic\"`) and a smoothed version of SVM (using the parameter `loss=\"SquaredHinge\"`). To give some examples, we first generate a synthetic classification dataset (similar to the one we generated in the case of regression):" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = np.sign(X@B + e)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)\n", + "\n", + "An L0-regularized logistic regression model can be fit by specificying `loss = \"Logistic\"` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconvergedl2
025.2253361-0.036989True1.000000e-07
119.4320532-0.043199True1.000000e-07
218.9286032-0.043230True1.000000e-07
315.1428822-0.043245True1.000000e-07
412.11430690.004044True1.000000e-07
57.411211100.188422False1.000000e-07
67.188875100.219990False1.000000e-07
75.751100100.234899False1.000000e-07
84.600880100.242631False1.000000e-07
93.680704100.243655True1.000000e-07
102.944563100.243993True1.000000e-07
112.355651100.244295True1.000000e-07
121.884520100.244520True1.000000e-07
131.507616100.244716True1.000000e-07
141.206093100.244886True1.000000e-07
150.964874100.245011True1.000000e-07
160.771900100.245133True1.000000e-07
170.617520120.178144False1.000000e-07
180.598994120.196406False1.000000e-07
190.479195120.208883False1.000000e-07
200.383356150.192033False1.000000e-07
210.371856150.221470False1.000000e-07
220.297484150.246182False1.000000e-07
230.23798816-0.110626False1.000000e-07
240.23084816-0.118558False1.000000e-07
250.18467816-0.124533False1.000000e-07
260.14774321-0.211002False1.000000e-07
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'Logistic', 'intercept': True, 'penalty': 'L0L2'})" + ] + }, + "execution_count": 117, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_3 = l0learn.fit(X,y,loss=\"Logistic\", max_support_size=20)\n", + "fit_model_3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The output above indicates that $\\gamma=10^{-7}$ by default we use a small ridge regularization (with $\\gamma=10^{-7}$) to ensure the existence of a unique solution. To extract the coefficients of the solution with $\\lambda = 8.69435$ we use the following code. Notice that we can ignore the specification of gamma as there is only one gamma used in L0 Logistic regression:" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "np.where(fit_model_3.coeff(lambda_0=7.411211).toarray() > 0)[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "The above indicates that the 10 non-zeros in the estimated model match those we used in generating the data (i.e, L0 regularization correctly recovered the true support). We can also make predictions at the latter $\\lambda$ using:" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.69583037e-04]\n", + " [4.92440655e-06]\n", + " [3.92195535e-03]\n", + " ...\n", + " [9.99161941e-01]\n", + " [1.69035746e-01]\n", + " [9.99171256e-04]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_3.predict(X, lambda_0=7.411211))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Each row in the above is the probability that the corresponding sample belongs to class $1$. Other models (i.e., L0L2 and L0L1) can be similarly fit by specifying `loss = \"Logistic\"`.\n", + "\n", + "Finally, we note that L0Learn also supports a smoothed version of SVM by using squared hinge loss `loss = \"SquaredHinge\"`. The only difference from logistic regression is that the `predict` function returns $\\beta_0 + \\langle x, \\beta \\rangle$ (where $x$ is the testing sample), instead of returning probabilities. The latter predictions can be assigned to the appropriate classes by using a thresholding function (e.g., the sign function)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Advanced Options\n", + "\n", + "### Sparse Matrix Support\n", + "Starting in version 2.0.0, L0Learn supports sparse matrices of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). If your sparse matrix uses a different storage format, please convert it to `csc_matrix` before using it in `l0learn`. `l0learn` keeps the matrix sparse internally and thus is highly efficient if the matrix is sufficiently sparse. The API for sparse matrices is the same as that of dense matrices, so all the demonstrations in this vignette also apply for sparse matrices. For example, we can fit an L0-regularized model on a sparse matrix as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
00.03189200.009325True
10.03157310.004882True
20.0206062-0.002187True
30.0144423-0.004111True
40.0139324-0.002556True
50.01085450.002987True
60.00928660.002048True
70.0091917-0.001371True
80.0087718-0.000533True
90.00815190.000064True
100.006480110.001587True
110.00636412-0.003636True
120.00576013-0.003866True
130.00556015-0.004211True
140.005264170.001497True
150.00463718-0.000797True
160.00451519-0.002634True
170.004113220.001419True
\n", + "
" + ], + "text/plain": [ + " l0 support_size intercept converged\n", + "0 0.031892 0 0.009325 True\n", + "1 0.031573 1 0.004882 True\n", + "2 0.020606 2 -0.002187 True\n", + "3 0.014442 3 -0.004111 True\n", + "4 0.013932 4 -0.002556 True\n", + "5 0.010854 5 0.002987 True\n", + "6 0.009286 6 0.002048 True\n", + "7 0.009191 7 -0.001371 True\n", + "8 0.008771 8 -0.000533 True\n", + "9 0.008151 9 0.000064 True\n", + "10 0.006480 11 0.001587 True\n", + "11 0.006364 12 -0.003636 True\n", + "12 0.005760 13 -0.003866 True\n", + "13 0.005560 15 -0.004211 True\n", + "14 0.005264 17 0.001497 True\n", + "15 0.004637 18 -0.000797 True\n", + "16 0.004515 19 -0.002634 True\n", + "17 0.004113 22 0.001419 True" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from scipy.sparse import random\n", + "from scipy.stats import norm\n", + "\n", + "\n", + "X_sparse = random(n, p, density=0.01, format='csc', data_rvs=norm().rvs)\n", + "y_sparse = (X_sparse@B + e)\n", + "\n", + "fit_model_sparse = l0learn.fit(X_sparse, y_sparse, penalty=\"L0\", max_support_size=20)\n", + "\n", + "fit_model_sparse.characteristics()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Selection on Subset of Variables\n", + "In certain applications, it is desirable to always include some of the variables in the model and perform variable selection on others. `l0learn` supports this option through the `exclude_first_k` parameter. Specifically, setting `exclude_first_k = K` (where K is a non-negative integer) instructs `l0learn` to exclude the first K variables in the data matrix `X` from the L0-norm penalty (those K variables will still be penalized using the L2 or L1 norm penalties.). For example, below we fit an `L0` model and exclude the first 3 variables from selection by setting `excludeFirstK = 3`:" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
00.0504643-0.017599True
10.0443334-0.021333True
20.0327705-0.027624True
30.0293677-0.029115True
40.0247108-0.021199True
50.02139390.010249True
60.014785100.016812True
\n", + "
" + ], + "text/plain": [ + " l0 support_size intercept converged\n", + "0 0.050464 3 -0.017599 True\n", + "1 0.044333 4 -0.021333 True\n", + "2 0.032770 5 -0.027624 True\n", + "3 0.029367 7 -0.029115 True\n", + "4 0.024710 8 -0.021199 True\n", + "5 0.021393 9 0.010249 True\n", + "6 0.014785 10 0.016812 True" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_k = l0learn.fit(X, y, penalty=\"L0\", max_support_size=10, exclude_first_k=3)\n", + "fit_model_k.characteristics()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Plotting the regularization path:" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_k.plot(show_lines=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We can see in the plot above that first 3 variables, (0, 1, 2) are included in all the solutions of the path\n", + "\n", + "### Coefficient Bounds\n", + "Starting in version 2.0.0, `l0learn` supports bounds for CD algorithms for all losses and penalties. (We plan to support bound constraints for the CDPSI algorithm in the future). By default, `l0learn` does not apply bounds, i.e., it assumes $-\\infty <= \\beta_i <= \\infty$ for all i. Users can supply the same bounds for all coefficients by setting the parameters `lows` and `highs` to scalar values (these should satisfy: `lows <= 0`, `lows != highs`, and `highs >= 0`). To use different bounds for the coefficients, `lows` and `highs` can be both set to vectors of length `p` (where the i-th entry corresponds to the bound on coefficient i).\n", + "\n", + "All of the following examples are valid." + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", highs=0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5, highs=0.5)\n", + "\n", + "max_value = 0.25\n", + "highs_array = max_value*np.ones(p)\n", + "highs_array[0] = 0.1\n", + "fit_model_bounds = l0learn.fit(X, y, penalty=\"L0\", lows=-0.1, highs=highs_array, max_support_size=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We can see the coefficients are subject to the bounds." + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 0. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 6. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 8. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "maximum value of coefficients = 0.25 <= 0.25\n", + "maximum value of first coefficient = 0.1 <= 0.1\n", + "minimum value of coefficient = -0.1 >= -0.1\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_bounds.plot(show_lines=True)\n", + "\n", + "print(f\"maximum value of coefficients = {np.max(fit_model_bounds.coeffs[0])} <= {max_value}\")\n", + "print(f\"maximum value of first coefficient = {np.max(fit_model_bounds.coeffs[0][0, :])} <= 0.1\")\n", + "print(f\"minimum value of coefficient = {np.min(fit_model_bounds.coeffs[0])} >= -0.1\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### User-specified Lambda Grids\n", + "By default, `l0learn` selects the sequence of lambda values in an efficient manner to avoid wasted computation (since close $\\lambda$ values can typically lead to the same solution). Advanced users of the toolkit can change this default behavior and supply their own sequence of $\\lambda$ values. This can be done supplying the $\\lambda$ values through the parameter `lambda_grid`. When `lambda_grid` is supplied, we require `num_gamma` and `num_lambda` to be `None` to ensure the is no ambiguity in the solution path requested.\n", + "\n", + "Specifically, the value assigned to `lambda_grid` should be a list of lists/arrays of decreasing positive values (floats). The length of `lambda_grid` (the number of lists stored) specifies the number of gamma parameters that will fill between `gamma_min`, and `gamma_max`. In the case of L0 penalty, `lambda_grid` must be a list of length 1. In case of L0L2/L0L1 `lambda_grid` can have any number of sub-lists stored. The ith element in `lambda_grid` should be a **strictly decreasing** sequence of positive lambda values which are used by the algorithm for the ith value of gamma. For example, to fit an L0 model with the sequence of user-specified lambda values: 1, 1e-1, 1e-2, 1e-3, 1e-4, we run the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "user_lambda_grid = [[1, 1e-1, 1e-2, 1e-3, 1e-4]]\n", + "fit_grid = l0learn.fit(X, y, penalty=\"L0\", lambda_grid=user_lambda_grid, max_support_size=1000, num_lambda=None, num_gamma=None)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "To verify the results we print the fit object:" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
01.00000-0.016000True
10.10000-0.016000True
20.0100100.016811True
30.0010620.018729True
40.00012670.051675True
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid\n", + "# Use fit_grid.characteristics() for those without rich dispalys" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Note that the $\\lambda$ values above are the desired values. For L0L2 and L0L1 penalties, the same can be done where the `lambda_grid` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "user_lambda_grid_L2 = [[1, 1e-1, 1e-2, 1e-3, 1e-4],\n", + " [10, 2, 1, 0.01, 0.002, 0.001, 1e-5],\n", + " [1e-4, 1e-5]]\n", + "\n", + "# user_lambda_grid_L2[[i]] must be a sequence of positive decreasing reals.\n", + "fit_grid_L2 = l0learn.fit(X, y, penalty=\"L0L2\", lambda_grid=user_lambda_grid_L2, max_support_size=1000, num_lambda=None, num_gamma=None)" + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconvergedl2
01.000000-0.016000True10.000000
10.100000-0.016000True10.000000
20.010000-0.016000True10.000000
30.001009-0.014394True10.000000
40.00010134-0.012180True10.000000
510.000000-0.016000True0.031623
62.000000-0.016000True0.031623
71.000000-0.016000True0.031623
80.01000100.015045True0.031623
90.00200280.001483True0.031623
100.00100580.002821True0.031623
110.000015820.021913True0.031623
120.000103110.048700True0.000100
130.000014110.047991False0.000100
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})" + ] + }, + "execution_count": 116, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid_L2\n", + "# Use fit_grid_L2.characteristics() for those without rich dispalys" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# More Details\n", + "For more details please inspect the doc strings of:\n", + "\n", + "* [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel)\n", + "* [l0learn.models.FitModel](code.rst#l0learn.models.FitModel)\n", + "* [l0learn.fit](code.rst#l0learn.fit)\n", + "* [l0learn.cvfit](code.rst#l0learn.cvfit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# References\n", + "Hussein Hazimeh and Rahul Mazumder. [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919). Operations Research (2020).\n", + "\n", + "Antoine Dedieu, Hussein Hazimeh, and Rahul Mazumder. [Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives](https://arxiv.org/abs/2001.06471). JMLR (to appear)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt new file mode 100644 index 0000000..43a614e --- /dev/null +++ b/python/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.18) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +IF (NOT DEFINED L0LEARN_VERSION_INFO) + SET(L0LEARN_VERSION_INFO "0.0.0") +ENDIF () + +SET(MODNAME "l0learn") + +PROJECT( + ${MODNAME} + VERSION ${L0LEARN_VERSION_INFO} + DESCRIPTION "Python bindings for L0Learn CD and CDPSI optimization." + LANGUAGES CXX +) + +set(BUILD_SHARED_LIBS OFF CACHE BOOL "build shared library" FORCE) + +ADD_SUBDIRECTORY(external/pybind11) +ADD_SUBDIRECTORY(external/armadillo-code) +ADD_SUBDIRECTORY(external/carma) + +# Add Armadillo and pybind11 +if($ENV{CIBUILDWHEEL}) + set(ALLOW_OPENBLAS_MACOS ON CACHE BOOL "Allow detection of OpenBLAS on macOS" FORCE) + target_compile_definitions(armadillo PRIVATE -DARMA_USE_LAPACK) + target_compile_definitions(armadillo PRIVATE -DARMA_USE_BLAS) +endif() + +INCLUDE_DIRECTORIES(l0learn) +INCLUDE_DIRECTORIES(l0learn/src) +INCLUDE_DIRECTORIES(l0learn/src/include) + +pybind11_add_module(${MODNAME} MODULE + l0learn/pyl0learn.cpp + l0learn/src/Normalize.cpp) + +TARGET_LINK_LIBRARIES(${MODNAME} PRIVATE carma::carma) +TARGET_LINK_LIBRARIES(${MODNAME} PRIVATE armadillo) + +# Add -fPIC for Armadillo (and OpenBLAS if compiled) +if(NOT MSVC) + # clang on Windows does not support -fPIC + if(NOT WIN32) + target_compile_options(armadillo PRIVATE -fPIC) + endif() +endif() + +TARGET_INCLUDE_DIRECTORIES(${MODNAME} + PUBLIC + $ + $ + ) + +TARGET_COMPILE_OPTIONS(${MODNAME} + PUBLIC + "$<$:${PROJECT_RELEASE_FLAGS}>" + ) + +TARGET_COMPILE_DEFINITIONS(${MODNAME} + PUBLIC + "$<$:${PROJECT_RELEASE_DEFINITIONS}>" + ) + + +INSTALL(TARGETS ${MODNAME} DESTINATION .) diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 0000000..25dd6a4 --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2021 +COPYRIGHT HOLDER: Hussein Hazimeh \ No newline at end of file diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..b2e603a --- /dev/null +++ b/python/README.md @@ -0,0 +1,97 @@ +# l0learn: Fast Best Subset Selection + +![example workflow](https://github.com/TNonet/L0Learn/actions/workflows/python.yml/badge.svg) + +### Hussein Hazimeh, Rahul Mazumder, and Tim Nonet +### Massachusetts Institute of Technology + +## Introduction +L0Learn is a highly efficient framework for solving L0-regularized learning problems. It can (approximately) solve the following three problems, where the empirical loss is penalized by combinations of the L0, L1, and L2 norms: + + + +We support both regression (using squared error loss) and classification (using logistic or squared hinge loss). Optimization is done using coordinate descent and local combinatorial search over a grid of regularization parameter(s) values. Several computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter λ in the path. We describe the details of the algorithms in our paper: *Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms* ([link](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919)). + +The toolkit is implemented in C++11 and can often run faster than popular sparse learning toolkits (see our experiments in the paper above). We also provide an easy-to-use R interface; see the section below for installation and usage of the R package. + +**NEW: Version 2 (03/2021) adds support for sparse matrices and box constraints on the coefficients.** + +## Package Installation +`l0learn` comes pre-packaged with a version of [Amardillo](http://arma.sourceforge.net/download.html) +`l0learn` Currently is only supported on Linux and MacOS. Windows support is an active area of development. + +The latest version (v2.0.3) can be installed from pip as follows: +```bash +pip install l0learn +``` + +## Documentation +Documentation can be found [here](https://tnonet.github.io/L0Learn/tutorial.html) + +# Source Code and Installing from Source +Alternatively, `l0learn` can be build from source +```bash +git clone https://github.com/TNonet/L0Learn.git +cd python +``` + +To install, ensure the proper packages are installed from `pyproject.toml` build from source with the following: +```bash +pip install ".[test]" +``` + +To test, run the following command: +```bash +python -m pytest +``` + +# Change Log +L0Learn's changelog can be accessed from [here](https://github.com/hazimehh/L0Learn/blob/master/ChangeLog). + + +## Usage +For a tutorial, please refer to l0learn's Vignette(Link to be added). For a detailed description of the API, check the Documentation(link to be added). + +## FAQ +#### Which penalty to use? +Pure L0 regularization can overfit when the signal strength in the data is relatively low. Adding L2 regularization can alleviate this problem and lead to competitive models (see the experiments in our paper). Thus, in practice, **we strongly recommend using the L0L2 penalty**. Ideally, the parameter gamma (for L2 regularization) should be tuned over a sufficiently large interval, and this can be performed using L0Learn's built-in [cross-validation method](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#cross-validation). + +#### Which algorithm to use? +By default, L0Learn uses a coordinate descent-based algorithm, which achieves competitive run times compared to popular sparse learning toolkits. This can work well for many applications. We also offer a local search algorithm which is guarantteed to return higher quality solutions, at the expense of an increase in the run time. We recommend using the local search algorithm if your problem has highly correlated features or the number of samples is much smaller than the number of features---see the [local search section of the Vignette](https://cran.r-project.org/web/packages/L0Learn/vignettes/L0Learn-vignette.html#higher-quality_solutions_using_local_search) for how to use this algorithm. + +#### How to certify optimality? +While for many challenging statistical instances L0Learn leads to optimal solutions, it cannot provide certificates of optimality. Such certificates can be provided via Integer Programming. Our toolkit [L0BnB](https://github.com/alisaab/l0bnb) is a scalable integer programming framework for L0-regularized regression, which can provide such certificates and potentially improve upon the solutions of L0Learn (if they are sub-optimal). We recommend using L0Learn first to obtain a candidtate solution (or a pool of solutions) and then checking optimality using L0BnB. + + +## Citing L0Learn +If you find L0Learn useful in your research, please consider citing the following two papers. + +**Paper 1:** +``` +@article{doi:10.1287/opre.2019.1919, +author = {Hazimeh, Hussein and Mazumder, Rahul}, +title = {Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms}, +journal = {Operations Research}, +volume = {68}, +number = {5}, +pages = {1517-1537}, +year = {2020}, +doi = {10.1287/opre.2019.1919}, +URL = {https://doi.org/10.1287/opre.2019.1919}, +eprint = {https://doi.org/10.1287/opre.2019.1919} +} +``` + +**Paper 2:** +``` +@article{JMLR:v22:19-1049, + author = {Antoine Dedieu and Hussein Hazimeh and Rahul Mazumder}, + title = {Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives}, + journal = {Journal of Machine Learning Research}, + year = {2021}, + volume = {22}, + number = {135}, + pages = {1-47}, + url = {http://jmlr.org/papers/v22/19-1049.html} +} +``` \ No newline at end of file diff --git a/python/Untitled.ipynb b/python/Untitled.ipynb new file mode 100644 index 0000000..cbed03d --- /dev/null +++ b/python/Untitled.ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "import numpy as np\n", + "from scipy.sparse import csc_matrix\n", + "\n", + "import l0learn\n", + "\n", + "N = 50" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "x = np.random.random(size=(N, N))\n", + "x_sparse = csc_matrix(x)\n", + "y = np.random.random(size=(N,))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "model_fit = l0learn.fit(x, y, intercept=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
03.509812e-0100.0True
13.474714e-0110.0True
22.120829e-0220.0True
34.689658e-0330.0True
42.577396e-0350.0True
51.139058e-03100.0True
61.112107e-03120.0True
77.572906e-04170.0False
86.631129e-04170.0True
93.371410e-04220.0False
102.231749e-04250.0False
111.535537e-04280.0False
124.323626e-05330.0False
132.592606e-05370.0False
141.513707e-05360.0False
151.454129e-05360.0True
169.571525e-06400.0False
173.786050e-06440.0False
181.537428e-06410.0False
199.192078e-07450.0False
206.081844e-07450.0False
212.825029e-08480.0False
222.120165e-08490.0False
236.824058e-09500.0False
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': False, 'penalty': 'L0'})" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_fit" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "lambda_grid = [[10, 12] , [13, 10]]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[10.0, 12.0], [13.0, 10.0]]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[[float(i) for i in lst] for lst in lambda_grid]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/_tutorial.rst b/python/_tutorial.rst new file mode 100644 index 0000000..b282719 --- /dev/null +++ b/python/_tutorial.rst @@ -0,0 +1,300 @@ +l0learn starter guide +===================== + +Introduction +============ +`l0learn` is a fast toolkit for L0-regularized learning. L0 regularization selects the best subset of features and can outperform commonly used feature selection methods (e.g., L1 and MCP) under many sparse learning regimes. The toolkit can (approximately) solve the following three problems. + +.. math:: + \\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 \quad \quad (L0) + +.. math:: + \\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 + \gamma||\beta||_1 \quad (L0L1) + +.. math:: + \\min_{\beta_0, \beta} \sum_{i=1}^{n} \ell(y_i, \beta_0+ \langle x_i, \beta \rangle) + \lambda ||\beta||_0 + \gamma||\beta||_2^2 \quad (L0L2) + +where :math:`\ell` is the loss function, :math:`\beta_0` is the intercept, :math:`\beta` is the vector of coefficients, and :math:`||\beta||_0` denotes the L0 norm of :math:`\beta`, i.e., the number of non-zeros in :math:`\beta`. We support both regression and classification using either one of the following loss functions: + +* Squared error loss +* Logistic loss (logistic regression) +* Squared hinge loss (smoothed version of SVM). + +The parameter :math:`\lambda` controls the strength of the L0 regularization (larger :math:`\lambda` leads to less non-zeros). The parameter :math:`\gamma` controls the strength of the shrinkage component (which is the L1 norm in case of L0L1 or squared L2 norm in case of L0L2); adding a shrinkage term to L0 can be very effective in avoiding overfitting and typically leads to better predictive models. The fitting is done over a grid of :math:`\lambda` and :math:`\gamma` values to generate a regularization path. + +The algorithms provided in `l0learn` are based on cyclic coordinate descent and local combinatorial search. Many computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter :math:`\lambda` in the path. For more details on the algorithms used, please refer to our paper [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919). + +The toolkit is implemented in C++ along with an easy-to-use Python interface. In this tutorial, we provide a tutorial on using the Python interface. Particularly, we will demonstrate how use L0Learn's main functions for fitting models, cross-validation, and visualization. + +Installation +============ +L0Learn can be installed directly from pip by running the following command in terminal: + +.. code-block:: console + + pip install l0learn + +If you face installation issues, please refer to the [Installation Troubleshooting Wiki](https://github.com/hazimehh/L0Learn/wiki/Installation-Troubleshooting). If the issue is not resolved, you can submit an issue on [L0Learn's Github Repo](https://github.com/hazimehh/L0Learn). + +Tutorial +======== +To demonstrate how `l0learn` works, we will first generate a synthetic dataset and then proceed to fitting L0-regularized models. The synthetic dataset (y,X) will be generated from a sparse linear model as follows: + +* X is a 500x1000 design matrix with iid standard normal entries +* B is a 1000x1 vector with the first 10 entries set to 1 and the rest are zeros. +* e is a 500x1 vector with iid standard normal entries +* y is a 500x1 response vector such that y = XB + e + +This dataset can be generated in Python as follows: + +.. code-block:: python + + import numpy as np + np.random.seed(4) # fix the seed to get a reproducible result + n, p, k = 500, 1000, 100 + X = np.random.normal(size=(n, p)) + B = np.zeros(p) + B[:k] = 1 + e = np.random.normal(n) + y = X@B + e + +More expressive and complete functions for generating datasets can be found are available in :py:mod:`l0learn.models`. The available functions are: + +* :py:meth:`l0learn.models.gen_synthetic` +* :py:meth:`l0learn.models.gen_synthetic_high_corr` +* :py:meth:`l0learn.models.gen_synthetic_logistic` + +We will use L0Learn to estimate B from the data (y,X). First we load L0Learn: + +.. code-block:: python + + from l0learn import fit + + +We will start by fitting a simple L0 model and then proceed to the case of L0L2 and L0L1. + +Fitting L0 Regression Models +============================ + +To fit a path of solutions for the L0-regularized model with at most 20 non-zeros using coordinate descent (CD), we use the :py:meth:`l0learn.models.fit` function as follows: + +.. code-block:: python + + fit = fit(X, y, penalty="L0", max_support_size=20) + + +This will generate solutions for a sequence of :math:`\lambda` values (chosen automatically by the algorithm). To view the sequence of :math:`\lambda` along with the associated support sizes (i.e., the number of non-zeros), we use the built in rich display from `ipython Rich Display `_ in iPython Notebooks. When running this tutorial in a more standard python environment, use the function :py:meth:`l0learn.models.FitModel.characteristics` to display the sequence of solutions. + +.. code-block:: python + + fit # will render as an pandas DataFrame. + + +To extract the estimated B for particular values of :math:`\lambda` and :math:`\gamma`, we use the function :py:meth:`l0learn.models.FitModel.coeff`. For example, the solution at :math:`\lambda = 0.0325142` (which corresponds to a support size of 10) can be extracted using: + +.. code-block:: python + + fit.coeff(lambda_0=0.0325) + +The output is a sparse matrix of type `scipy.sparse.csc_matrix `. Depending on the `include_intercept` parameter of :py:meth:`l0learn.models.FitModel.coeff`, The first element in the vector is the intercept and the rest are the B coefficients. Aside from the intercept, the only non-zeros in the above solution are coordinates 0, 1, 2, 3, ..., 9, which are the non-zero coordinates in the true support (used to generated the data). Thus, this solution successfully recovers the true support. Note that on some BLAS implementations, the `lambda` value we used above (i.e., `0.0325142`) might be slightly different due to the limitations of numerical precision. Moreover, all the solutions in the regularization path can be extracted at once by calling :code:`fit.coeff()`. + +The sequence of :math:`\lambda` generated by `l0learn` is stored in the object :code:`fit`. Specifically, :code:`fit.lambda_0` is a list, where each element of the list is a sequence of :math:`\lambda` values corresponding to a single value of :math:`\gamma`. When using an L0 penalty , which has only one value of :math:`\gamma` (i.e., 0), we can access the sequence of :math:`\lambda` values using :code:`fit.lambda_0[0]`. Thus, :math:`\lambda=0.0325142` we used previously can be accessed using :code:`fit.lambda_0[1][6]` (since it is the 7th value in the output of :code:`fit.characteristics()`). So the previous solution can also be extracted using :code:`fit.coeff(lambda_0=fit.lambda_0[1][6], gamma=0)`. + +We can make predictions using a specific solution in the grid using the function :code:`fit.predict(newx, lambda, gamma)` where :code:`newx` is a testing sample (vector or matrix). For example, to predict the response for the samples in the data matrix X using the solution with :math:`\lambda=0.0325142`, we call the prediction function as follows: + +.. code-block:: python + + fit.predict(x=X, lambda_0=0.0325142, gamma=0) + +We can also visualize the regularization path by plotting the coefficients of the estimated B versus the support size (i.e., the number of non-zeros) using the :py:meth:`l0learn.models.FitModel.plot` method as follows: + +.. code-block:: python + + fit.plot(fit, gamma=0) + +The legend of the plot presents the variables in the order they entered the regularization path. For example, variable 7 is the first variable to enter the path, and variable 6 is the second to enter. Thus, roughly speaking, we can view the first $k$ variables in the legend as the best subset of size $k$. To show the lines connecting the points in the plot, we can set the parameter :code:`show_lines=True` in the `plot` function, i.e., call :code:`fit.plot(fit, gamma=0, show_lines=True)`. Moreover, we note that the plot function returns a :code:`matplotlib.axes._subplots.AxesSubplot` object, which can be further customized using the :code:`matplotlib` package. In addition, both the :py:meth:`l0learn.models.FitModel.plot` and :py:meth:`l0learn.models.CVFitModel.cv_plot` accept :code:`**kwargs` parameter to allow for customization of the plotting behavior. + +Fitting L0L2 and L0L1 Regression Models +======================================= +We have demonstrated the simple case of using an L0 penalty. We can also fit more elaborate models that combine L0 regularization with shrinkage-inducing penalties like the L1 norm or squared L2 norm. Adding shrinkage helps in avoiding overfitting and typically improves the predictive performance of the models. Next, we will discuss how to fit a model using the L0L2 penalty for a two-dimensional grid of :math:`\lambda` and :math:`\gamma` values. Recall that by default, `l0learn` automatically selects the :math:`\lambda` sequence, so we only need to specify the :math:`\gamma` sequence. Suppose we want to fit an L0L2 model with a maximum of 20 non-zeros and a sequence of 5 :math:`\gamma` values ranging between 0.0001 and 10. We can do so by calling :py:meth:`l0learn.fit` with :code:`penalty="L0L2"`, :code:`num_gamma=5`, :code:`gamma_min=0.0001`, and :code:`gamma_max=10` as follows: + +.. code-block:: python + + fit = fit(X, y, penalty="L0L2", num_gamma = 5, gamma_min = 0.0001, gamma_max = 10, max_support_size=20) + +`l0learn` will generate a grid of 5 :math:`\gamma` values equi-spaced on the logarithmic scale between 0.0001 and 10. Similar to the case for L0, we can display a summary of the regularization path using the :code:`fit.characteristics()` function as follows: + +.. code-block:: python + + fit # Using ipython Rich Display + # fit.characteristics() # For non Rich Display + + +The sequence of :math:`\gamma` values can be accessed using :code:`fit.gamma`. To extract a solution we use the :py:meth:`l0learn.models.FitModel.coeff` method. For example, extracting the solution at `:math:`\lambda=0.0011539` and :math:`\gamma=10` can be done using + +.. code-block:: python + + fit.coeff(lambda_0=0.0011539, gamma=10) # Using ipython Rich Display + + +Similarly, we can predict the response at this pair of :math:`\lambda` and :math:`\gamma` for the matrix X using + +.. code-block:: python + + fit.predict(x=X, lambda_0=0.0011539, gamma=10) + +The regularization path can also be plot at a specific :math:`\gamma` using :code:`fit.plot(gamma)`. Finally, we note that fitting an L0L1 model can be done by just changing the `penalty` to "L0L1" in the above (in this case `gamma_max` will be ignored since it is automatically selected by the toolkit; see the reference manual for more details.) + +Higher-quality Solutions using Local Search +=========================================== +By default, `l0learn` uses coordinate descent (CD) to fit models. Since the objective function is non-convex, the choice of the optimization algorithm can have a significant effect on the solution quality (different algorithms can lead to solutions with very different objective values). A more elaborate algorithm based on combinatorial search can be used by setting the parameter `algorithm="CDPSI"` in the call to :py:meth:`l0learn.fit`. `CDPSI` typically leads to higher-quality solutions compared to CD, especially when the features are highly correlated. `CDPSI` is slower than `CD`, however, for typical applications it terminates in the order of seconds. + +Cross-validation +================ + +We will demonstrate how to use K-fold cross-validation (CV) to select the optimal values of the tuning parameters :math:`\lambda` and math:`\gamma`. To perform CV, we use the :py:meth:`l0learn.cvfit` function, which takes the same parameters as :code:`l0learn.fit`, in addition to the number of folds using the :code:`num_folds` parameter and a seed value using the :code:`seed` parameter (this is used when randomly shuffling the data before performing CV). + +For example, to perform 5-fold CV using the `L0L2` penalty (over a range of 5 `gamma` values between 0.0001 and 0.1) with a maximum of 50 non-zeros, we run: + +.. code-block:: python + + cv_fit_result = cvfit(X, y, num_folds=5, seed=1, penalty="L0L2", num_gamma=5, gamma_min=0.0001, gamma_max=0.1, max_support_size=50) + +Note that the object :py:class:`l0learn.models.CVFitModel` subclasses :py:class:`l0learn.models.FitModel` and thus has the same methods and underlinying structure which is output of running :py:meth:`l0learn.cvfit` on (y,X). The cross-validation errors can be accessed using the `cv_means` attribute of `cvfit`: `cvfit.cv_means` is a list where the ith element, :code:`cvfit.cv_means[i]`, stores the cross-validation errors for the ith value of gamma (:code:`cvfit.gamma[i]`). To find the minimum cross-validation error for every `gamma`, we apply the :code:`np.argmin` function for every element in the list :code:`cvfit.cv_means`, as follows: + +.. code-block:: python + + gamma_mins = [(np.argmin(cv_mean), np.min(cv_mean)) for cv_mean in cv_fit_result.cv_means] + gamma_mins + +The above output indicates that the 3rd value of gamma achieves the lowest CV error (`=0.9899542`). We can plot the CV errors against the support size for the 4th value of gamma, i.e., `gamma = cvfit$fit$gamma[4]`, using: + + +```{r, fig.height = 4.7, fig.width = 7, out.width="90%", dpi=300} +plot(cvfit, gamma=cvfit$fit$gamma[4]) +``` + +The above plot is produced using the `ggplot2` package and can be further customized by the user. To extract the optimal $\lambda$ (i.e., the one with minimum CV error) in this plot, we execute the following: +```{r} +optimalGammaIndex = 4 # index of the optimal gamma identified previously +optimalLambdaIndex = which.min(cvfit$cvMeans[[optimalGammaIndex]]) +optimalLambda = cvfit$fit$lambda[[optimalGammaIndex]][optimalLambdaIndex] +optimalLambda +``` +To print the solution corresponding to the optimal gamma/lambda pair: +```{r output.lines=15} +coef(cvfit, lambda=optimalLambda, gamma=cvfit$fit$gamma[4]) +``` +The optimal solution (above) selected by cross-validation correctly recovers the support of the true vector of coefficients used to generated the model. + +## Fitting Classification Models +All the commands and plots we have seen in the case of regression extend to classification. We currently support logistic regression (using the parameter `loss = "Logistic"`) and a smoothed version of SVM (using the parameter `loss="SquaredHinge"`). To give some examples, we first generate a synthetic classification dataset (similar to the one we generated in the case of regression): +```{r} +set.seed(1) # fix the seed to get a reproducible result +X = matrix(rnorm(500*1000),nrow=500,ncol=1000) +B = c(rep(1,10),rep(0,990)) +e = rnorm(500) +y = sign(X%*%B + e) +``` +An L0-regularized logistic regression model can be fit by specificying `loss = "Logistic"` as follows: +```{r output.lines=15} +fit = L0Learn.fit(X,y,loss="Logistic") +print(fit) +``` +The output above indicates that $\gamma=10^{-7}$---by default we use a small ridge regularization (with $\gamma=10^{-7}$) to ensure the existence of a solution. To extract the coefficients of the solution with $\lambda = 8.69435$: +```{r output.lines=15} +coef(fit, lambda=8.69435, gamma=1e-7) +``` +The above indicates that the 10 non-zeros in the estimated model match those we used in generating the data (i.e, L0 regularization correctly recovered the true support). We can also make predictions at the latter $\lambda$ using: +```{r output.lines=15} +predict(fit, newx=X, lambda=8.69435, gamma=1e-7) +``` +Each row in the above is the probability that the corresponding sample belongs to class $1$. Other models (i.e., L0L2 and L0L1) can be similarly fit by specifying `loss = "Logistic"`. + +Finally, we note that L0Learn also supports a smoothed version of SVM by using squared hinge loss (`loss = "SquaredHinge"`). The only difference from logistic regression is that the `predict` function returns $\beta_0 + \langle x, \beta \rangle$ (where $x$ is the testing sample), instead of returning probabilities. The latter predictions can be assigned to the appropriate classes by using a thresholding function (e.g., the sign function). + + +## Advanced Options + +### Sparse Matrix Support +Starting in version 2.0.0, L0Learn supports sparse matrices of type dgCMatrix. If your sparse matrix uses a different storage format, please convert it to dgCMatrix before using it in L0Learn. L0Learn keeps the matrix sparse internally and thus is highly efficient if the matrix is sufficiently sparse. The API for sparse matrices is the same as that of dense matrices, so all the demonstrations in this vignette also apply for sparse matrices. For example, we can fit an L0-regularized model on a sparse matrix as follows: +```{r} + +# As an example, we generate a random, sparse matrix with +# 500 samples, 1000 features, and 10% nonzero entries. +X_sparse <- Matrix::rsparsematrix(nrow=500, ncol=1000, density=0.1, rand.x = rnorm) +# Below we generate the response using the same linear model as before, +# but with the sparse data matrix X_sparse. +y_sparseX <- as.vector(X_sparse%*%B + e) + +# Call L0Learn. +fit_sparse <- L0Learn.fit(X_sparse, y_sparseX, penalty="L0") + +# Note: In the setup above, X_sparse is of type dgCMatrix. +# If your sparse matrix is of a different type, convert it +# to dgCMatrix before calling L0Learn, e.g., using: X_sparse <- as(X_sparse, "dgCMatrix"). +``` + +### Selection on Subset of Variables +In certain applications, it is desirable to always include some of the variables in the model and perform variable selection on others. `L0Learn` supports this option through the `excludeFirstK` parameter. Specifically, setting `excludeFirstK = K` (where K is a non-negative integer) instructs `L0Learn` to exclude the first K variables in the data matrix `X` from the L0-norm penalty (those K variables will still be penalized using the L2 or L1 norm penalties.). For example, below we fit an `L0` model and exclude the first 3 variables from selection by setting `excludeFirstK = 3`: +```{r} +fit <- L0Learn.fit(X, y, penalty="L0", maxSuppSize=10, excludeFirstK=3) +``` +Plotting the regularization path: +```{r, fig.height = 4.7, fig.width = 7, out.width="90%", dpi=300} +plot(fit, gamma=0) +``` + +We can see in the plot above that first 3 variables are included in all the solutions of the path. + +### Coefficient Bounds +Starting in version 2.0.0, L0Learn supports bounds for CD algorithms for all losses and penalties. (We plan to support bound constraints for the CDPSI algorithm in the future). By default, L0Learn does not apply bounds, i.e., it assumes $-\infty <= \beta_i <= \infty$ for all i. Users can supply the same bounds for all coefficients by setting the parameters `lows` and `highs` to scalar values (these should satisfy: `lows <= 0`, `lows != highs`, and `highs >= 0`). To use different bounds for the coefficients, `lows` and `highs` can be both set to vectors of length `p` (where the i-th entry corresponds to the bound on coefficient i). + +All of the following examples are valid. +```{r} +L0Learn.fit(X, y, penalty="L0", lows=-0.5) +L0Learn.fit(X, y, penalty="L0", highs=0.5) +L0Learn.fit(X, y, penalty="L0", lows=-0.5, highs=0.5) + +low_vector <- c(rep(-0.1, 500), rep(-0.125, 500)) +fit <- L0Learn.fit(X, y, penalty="L0", lows=low_vector, highs=0.25) +``` + +We can see the coefficients are subject to the bounds. +```{r} +print(max(fit$beta[[1]])) +print(min(fit$beta[[1]][1:500, ])) +print(min(fit$beta[[1]][501:1000, ])) +``` + +### User-specified Lambda Grids +By default, `L0Learn` selects the sequence of lambda values in an efficient manner to avoid wasted computation (since close $\lambda$ values can typically lead to the same solution). Advanced users of the toolkit can change this default behavior and supply their own sequence of $\lambda$ values. This can be done supplying the $\lambda$ values through the parameter `lambdaGrid`. L0Learn versions before 2.0.0 would also require setting the `autoLambda` parameter to `FALSE`. This parameter remains in version 2.0.0 for backwards compatibility, but is no longer needed or used. + +Specifically, the value assigned to `lambdaGrid` should be a list of lists of decreasing positive values (doubles). The length of `lambdaGrid` (the number of lists stored) specifies the number of gamma parameters that will fill between `gammaMin`, and `gammaMax`. In the case of L0 penalty, `lambdaGrid` must be a list of length 1. In case of L0L2/L0L1 `lambdaGrid` can have any number of sub-lists stored. The length of `lambdaGrid` (the number of lists stored) specifies the number of gamma parameters that will fill between `gammaMin`, and `gammaMax`. The ith element in `LambdaGrid` should be a **decreasing** sequence of positive lambda values which are used by the algorithm for the ith value of gamma. For example, to fit an L0 model with the sequence of user-specified lambda values: 1, 1e-1, 1e-2, 1e-3, 1e-4, we run the following: +```{r} +userLambda <- list() +userLambda[[1]] <- c(1, 1e-1, 1e-2, 1e-3, 1e-4) +fit <- L0Learn.fit(X, y, penalty="L0", lambdaGrid=userLambda, maxSuppSize=1000) +``` +To verify the results we print the fit object: +```{r} +print(fit) +``` +Note that the $\lambda$ values above are the desired values. For L0L2 and L0L1 penalties, the same can be done where the `lambdaGrid` parameter. +```{r} +userLambda <- list() +userLambda[[1]] <- c(1, 1e-1, 1e-2, 1e-3, 1e-4) +userLambda[[2]] <- c(10, 2, 1, 0.01, 0.002, 0.001, 1e-5) +userLambda[[3]] <- c(1e-4, 1e-5) +# userLambda[[i]] must be a vector of positive decreasing reals. +fit <- L0Learn.fit(X, y, penalty="L0L2", lambdaGrid=userLambda, maxSuppSize=1000) +``` + +```{r} +print(fit) +``` + +# References +Hussein Hazimeh and Rahul Mazumder. [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919). Operations Research (2020). + +Antoine Dedieu, Hussein Hazimeh, and Rahul Mazumder. [Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives](https://arxiv.org/abs/2001.06471). JMLR (to appear). diff --git a/python/appveyor.yml b/python/appveyor.yml new file mode 100644 index 0000000..ac070ac --- /dev/null +++ b/python/appveyor.yml @@ -0,0 +1,62 @@ +branches: + only: + - use-carma + +skip_non_tags: true + +environment: + matrix: + - job_name: "python37-x64-macos-mojave-cp37" + APPVEYOR_BUILD_WORKER_IMAGE: macos-mojave + CIBW_BUILD: "cp37-*" + - job_name: "python37-x64-macos-mojave-cp38" + APPVEYOR_BUILD_WORKER_IMAGE: macos-mojave + CIBW_BUILD: "cp38-*" + - job_name: "python37-x64-macos-mojave-cp39" + APPVEYOR_BUILD_WORKER_IMAGE: macos-mojave + CIBW_BUILD: "cp39-*" + - job_name: "python37-x64-macos-cp310" + APPVEYOR_BUILD_WORKER_IMAGE: macos + CIBW_BUILD: "cp310-*" + - job_name: "python37-x64-vs2015-cp37" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + CIBW_BUILD: "cp37-*" + - job_name: "python37-x64-vs2015-cp38" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + CIBW_BUILD: "cp38-*" + - job_name: "python37-x64-vs2015-cp39" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + CIBW_BUILD: "cp39-*" + - job_name: "python37-x64-vs2015-cp310" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + CIBW_BUILD: "cp310-*" + - job_name: "python37-x64-ubuntu-cp37" + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + CIBW_BUILD: "cp37-*" + - job_name: "python37-x64-ubuntu-cp38" + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + CIBW_BUILD: "cp38-*" + - job_name: "python37-x64-ubuntu-cp39" + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + CIBW_BUILD: "cp39-*" + - job_name: "python37-x64-ubuntu-cp310" + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + CIBW_BUILD: "cp310-*" + + CIBW_BEFORE_ALL: "pip install numpy" + CIBW_BEFORE_ALL_LINUX: "yum resolvedep python3-devel \ yum install python3-devel" + CIBW_BEFORE_TEST: "pip install pytest numpy hypothesis" + CIBW_TEST_COMMAND: "pytest {package}/tests" + +stack: python 3.7 + +init: + - cmd: set PATH=C:\Python37;C:\Python37\Scripts;%PATH% + +install: + - git submodule update --init --recursive + - python -m pip install cibuildwheel==2.5.0 + +build_script: | + cd python + python -m cibuildwheel --output-dir wheelhouse diff --git a/python/cibuilwheel.sh b/python/cibuilwheel.sh new file mode 100644 index 0000000..1d5a5b9 --- /dev/null +++ b/python/cibuilwheel.sh @@ -0,0 +1,8 @@ +python -m pip install cibuildwheel==2.5.0 + +export CIBW_SKIP="pp* *-win32 *-manylinux_i686 *musllinux*" +export CIBW_BEFORE_ALL_LINUX="yum install -y lapack-devel || apt-get -y liblapack-dev || apk add --update-cache --no-cache lapack-dev && bash scripts/install_linux_libs.sh" +export CIBW_BEFORE_TEST="pip install pytest numpy hypothesis" +export CIBW_TEST_COMMAND="python -u {package}/scripts/test_sparse.py" +export CIBW_BUILD_VERBOSITY=3 +python -m cibuildwheel --output-dir wheelhouse --platform linux diff --git a/python/doc/.nojekyll b/python/doc/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/python/doc/Makefile b/python/doc/Makefile new file mode 100644 index 0000000..d04a206 --- /dev/null +++ b/python/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS += +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/python/doc/code.rst b/python/doc/code.rst new file mode 100644 index 0000000..10e6f5b --- /dev/null +++ b/python/doc/code.rst @@ -0,0 +1,41 @@ +`fit` function +---------------- + +.. autofunction:: l0learn.fit + + +`cvfit` function +---------------- + +.. autofunction:: l0learn.cvfit + + +FitModels +--------- +.. autoclass:: l0learn.models.FitModel + :members: + + +CVFitModels +----------- +.. autoclass:: l0learn.models.CVFitModel + :members: + + +Generating Functions +-------------------- +.. autofunction:: l0learn.models.gen_synthetic +.. autofunction:: l0learn.models.gen_synthetic_high_corr +.. autofunction:: l0learn.models.gen_synthetic_logistic + +Scoring Functions +-------------------- +These functions are called by :py:meth:`l0learn.models.FitModel.score` and :py:meth:`l0learn.models.CVFitModel.score`. + +.. autofunction:: l0learn.models.regularization_loss +.. autofunction:: l0learn.models.squared_error +.. autofunction:: l0learn.models.logistic_loss +.. autofunction:: l0learn.models.squared_hinge_loss + + + diff --git a/python/doc/conf.py b/python/doc/conf.py new file mode 100644 index 0000000..7cc6648 --- /dev/null +++ b/python/doc/conf.py @@ -0,0 +1,56 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +# -- Project information ----------------------------------------------------- + +project = "l0learn" +copyright = "2021, Hussein Hazimeh, Rahul Mazumder, and Tim Nonet" +author = "Hussein Hazimeh, Rahul Mazumder, and Tim Nonet" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "nbsphinx", + "sphinx.ext.mathjax", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/python/doc/index.rst b/python/doc/index.rst new file mode 100644 index 0000000..18e9975 --- /dev/null +++ b/python/doc/index.rst @@ -0,0 +1,21 @@ +.. l0learn documentation master file, created by + sphinx-quickstart on Wed Nov 17 07:46:32 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to l0learn's documentation! +=================================== + +.. toctree:: + :maxdepth: 2 + + tutorial.ipynb + code + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/python/doc/make.bat b/python/doc/make.bat new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/python/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/python/doc/tutorial.ipynb b/python/doc/tutorial.ipynb new file mode 100644 index 0000000..fce1fac --- /dev/null +++ b/python/doc/tutorial.ipynb @@ -0,0 +1,1338 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Introduction\n", + "`l0learn` is a fast toolkit for L0-regularized learning. L0 regularization selects the best subset of features and can outperform commonly used feature selection methods (e.g., L1 and MCP) under many sparse learning regimes. The toolkit can (approximately) solve the following three problems\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 \\quad \\quad (L0) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_1 \\quad (L0L1) \n", + "\\end{equation}\n", + "\n", + "\n", + "\\begin{equation}\n", + "\\min_{\\beta_0, \\beta} \\sum_{i=1}^{n} \\ell(y_i, \\beta_0+ \\langle x_i, \\beta \\rangle) + \\lambda ||\\beta||_0 + \\gamma||\\beta||_2^2 \\quad (L0L2)\n", + "\\end{equation}\n", + "\n", + "where $\\ell$ is the loss function, $\\beta_0$ is the intercept, $\\beta$ is the vector of coefficients, and $||\\beta||_0$ denotes the L0 norm of $\\beta$, i.e., the number of non-zeros in $\\beta$. We support both regression and classification using either one of the following loss functions:\n", + "\n", + "* Squared error loss\n", + "* Logistic loss (logistic regression)\n", + "* Squared hinge loss (smoothed version of SVM).\n", + "\n", + "The parameter $\\lambda$ controls the strength of the L0 regularization (larger $\\lambda$ leads to less non-zeros). The parameter $\\gamma$ controls the strength of the shrinkage component (which is the L1 norm in case of L0L1 or squared L2 norm in case of L0L2); adding a shrinkage term to L0 can be very effective in avoiding overfitting and typically leads to better predictive models. The fitting is done over a grid of $\\lambda$ and $\\gamma$ values to generate a regularization path. \n", + "\n", + "The algorithms provided in l0learn` are based on cyclic coordinate descent and local combinatorial search. Many computational tricks and heuristics are used to speed up the algorithms and improve the solution quality. These heuristics include warm starts, active set convergence, correlation screening, greedy cycling order, and efficient methods for updating the residuals through exploiting sparsity and problem dimensions. Moreover, we employed a new computationally efficient method for dynamically selecting the regularization parameter $\\lambda$ in the path. For more details on the algorithms used, please refer to our paper [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919).\n", + "\n", + "The toolkit is implemented in C++ along with an easy-to-use Python interface. In this vignette, we provide a tutorial on using the Python interface. Particularly, we will demonstrate how use L0Learn's main functions for fitting models, cross-validation, and visualization.\n", + "\n", + "# Installation\n", + "L0Learn can be installed directly from pip by executing:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "```console\n", + "pip install l0learn\n", + "```" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "If you face installation issues, please refer to the [Installation Troubleshooting Wiki](https://github.com/hazimehh/L0Learn/wiki/Installation-Troubleshooting). If the issue is not resolved, you can submit an issue on [L0Learn's Github Repo](https://github.com/hazimehh/L0Learn).\n", + "\n", + "# Tutorial\n", + "To demonstrate how `l0learn` works, we will first generate a synthetic dataset and then proceed to fitting L0-regularized models. The synthetic dataset (y,X) will be generated from a sparse linear model as follows:\n", + "\n", + "* X is a 500x1000 design matrix with iid standard normal entries\n", + "* B is a 1000x1 vector with the first 10 entries set to 1 and the rest are zeros.\n", + "* e is a 500x1 vector with iid standard normal entries\n", + "* y is a 500x1 response vector such that y = XB + e\n", + "\n", + "This dataset can be generated in python as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 1, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = X@B + e" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "We will use `l0learn` to estimate B from the data (y,X). First we load L0Learn:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [], + "source": [ + "import l0learn" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We will start by fitting a simple L0 model and then proceed to the case of L0L2 and L0L1.\n", + "\n", + "## Fitting L0 Regression Models\n", + "To fit a path of solutions for the L0-regularized model with at most 20 non-zeros using coordinate descent (CD), we use the [l0learn.fit](code.rst#l0learn.models.gen_synthetic) function as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [], + "source": [ + "fit_model = l0learn.fit(X, y, penalty=\"L0\", max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "This will generate solutions for a sequence of $\\lambda$ values (chosen automatically by the algorithm). To view the sequence of $\\lambda$ along with the associated support sizes (i.e., the number of non-zeros), we use the built-in rich display from [ipython Rich Display](https://ipython.readthedocs.io/en/stable/config/integrating.html+) for iPython Notebooks. When running this tutorial in a more standard python environment, use the function [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) to display the sequence of solutions." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.0795460-0.156704True
10.0787501-0.147182True
20.0658622-0.161024True
30.0504643-0.002500True
40.0445175-0.041058True
50.0416727-0.058013True
60.0397058-0.061685True
70.032715100.002157True
80.00021211-0.000857True
90.00018712-0.002161True
100.00017813-0.001199True
110.00015915-0.007959True
120.00014116-0.009603True
130.00013318-0.015697True
140.00013221-0.012732True
\n
" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model\n", + "# fit_model.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To extract the estimated B for particular values of $\\lambda$ and $\\gamma$, we use the function [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff). For example, the solution at $\\lambda = 0.032715$ (which corresponds to a support size of 10 + plus an intercept term) can be extracted using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 6, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model.coeff(lambda_0=0.032715, gamma=0)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The output is a sparse matrix of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). Depending on the `include_intercept` parameter of [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff), The first element in the vector is the intercept and the rest are the B coefficients. Aside from the intercept, the only non-zeros in the above solution are coordinates 0, 1, 2, 3, ..., 9, which are the non-zero coordinates in the true support (used to generated the data). Thus, this solution successfully recovers the true support. Note that on some BLAS implementations, the `lambda` value we used above (i.e., `0.032715`) might be slightly different due to the limitations of numerical precision. Moreover, all the solutions in the regularization path can be extracted at once by calling [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) without specifying a `lambda_0` or `gamma` value.\n", + "\n", + "The sequence of $\\lambda$ generated by `L0Learn` is stored in the object `fit_model`. Specifically, `fit_model.lambda_0` is a list, where each element of the list is a sequence of $\\lambda$ values corresponding to a single value of $\\gamma$. When using an L0 penalty , which has only one value of $\\gamma$ (i.e., 0), we can access the sequence of $\\lambda$ values using `fit.lambda_0[0]`. Thus, $\\lambda=0.032715$ we used previously can be accessed using `fit_model.lambda_0[0][7]` (since it is the 8th value in the output of :code:`fit.characteristics()`). The previous solution can also be extracted using `fit_model.coeff(lambda_0=0.032, gamma=0)`." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 8, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fit_model.lambda_0[0][7] = 0.03271533058913737\n" + ] + }, + { + "data": { + "text/plain": "array([[0.00215713],\n [1.02014176],\n [0.97338278],\n ...,\n [0. ],\n [0. ],\n [0. ]])" + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(f\"fit_model.lambda_0[0][7] = {fit_model.lambda_0[0][7]}\")\n", + "fit_model.coeff(lambda_0=0.032715, gamma=0).toarray()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can make predictions using a specific solution in the grid using the function `fit_model.predict(newx, lambda, gamma)` where `newx` is a testing sample (vector or matrix). For example, to predict the response for the samples in the data matrix X using the solution with $\\lambda=0.0058037$, we call the prediction function as follows:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 10, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-2.68272239]\n", + " [-3.667317 ]\n", + " [-1.77309853]\n", + " ...\n", + " [ 2.25545111]\n", + " [-0.77364234]\n", + " [-2.15002055]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model.predict(x=X, lambda_0=0.032715, gamma=0))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can also visualize the regularization path by plotting the coefficients of the estimated B versus the support size (i.e., the number of non-zeros) using the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) method as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 9, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "ax = fit_model.plot(include_legend=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The legend of the plot presents the variables in the order they entered the regularization path. For example, variable 7 is the first variable to enter the path, and variable 6 is the second to enter. Thus, roughly speaking, we can view the first $k$ variables in the legend as the best subset of size $k$. To show the lines connecting the points in the plot, we can set the parameter :code:`show_lines=True` in the `plot` function, i.e., call :code:`fit.plot(fit, gamma=0, show_lines=True)`. Moreover, we note that the plot function returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) object, which can be further customized using the `matplotlib` package. In addition, both the [l0learn.models.FitModel.plot()](code.rst#l0learn.models.FitModel.plot) and [l0learn.models.CVFitModel.cv_plot()](code.rst#l0learn.models.CVFitModel.cv_plot) accept :code:`**kwargs` parameter to allow for customization of the plotting behavior.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 11, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model.plot(show_lines=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Fitting L0L2 and L0L1 Regression Models\n", + "We have demonstrated the simple case of using an L0 penalty. We can also fit more elaborate models that combine L0 regularization with shrinkage-inducing penalties like the L1 norm or squared L2 norm. Adding shrinkage helps in avoiding overfitting and typically improves the predictive performance of the models. Next, we will discuss how to fit a model using the L0L2 penalty for a two-dimensional grid of :math:`\\lambda` and :math:`\\gamma` values. Recall that by default, `l0learn` automatically selects the :math:`\\lambda` sequence, so we only need to specify the :math:`\\gamma` sequence. Suppose we want to fit an L0L2 model with a maximum of 20 non-zeros and a sequence of 5 :math:`\\gamma` values ranging between 0.0001 and 10. We can do so by calling [l0learn.fit](code.rst#l0learn.fit) with :code:`penalty=\"L0L2\"`, :code:`num_gamma=5`, :code:`gamma_min=0.0001`, and :code:`gamma_max=10` as follows:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 12, + "outputs": [], + "source": [ + "fit_model_2 = l0learn.fit(X, y, penalty=\"L0L2\", num_gamma = 5, gamma_min = 0.0001, gamma_max = 10, max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "`l0learn` will generate a grid of 5 :math:`\\gamma` values equi-spaced on the logarithmic scale between 0.0001 and 10. Similar to the case for L0, we can display a summary of the regularization path using the [l0learn.models.FitModel.characteristics](code.rst#l0learn.models.FitModel.characteristics) function as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 14, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
00.0037880-0.156704True10.0000
10.0037501-0.156250True10.0000
20.0029112-0.148003True10.0000
30.0026543-0.148650True10.0000
40.0025973-0.148650True10.0000
..................
1280.000216100.002130True0.0001
1290.00017313-0.003684True0.0001
1300.00016713-0.003685True0.0001
1310.00013418-0.015724True0.0001
1320.00013021-0.012762True0.0001
\n

133 rows × 5 columns

\n
" + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2 # Using ipython Rich Display\n", + "# fit_model_2.characteristics() # For non Rich Display" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The sequence of $\\gamma$ values can be accessed using `fit_model_2.gamma`. To extract a solution we use the [l0learn.models.FitModel.coeff](code.rst#l0learn.models.FitModel.coeff) method. For example, extracting the solution at $\\lambda=0.0016$ and $\\gamma=10$ can be done using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 40, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_2.coeff(lambda_0=0.0016, gamma=10)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Similarly, we can predict the response at this pair of $\\lambda$ and $\\gamma$ for the matrix X using" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 41, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-0.31499242]\n", + " [-0.3474209 ]\n", + " [-0.23997924]\n", + " ...\n", + " [-0.06707991]\n", + " [-0.18562493]\n", + " [-0.25608131]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_2.predict(x=X, lambda_0=0.0016, gamma=10))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The regularization path can also be plotted at a specific $\\gamma$ using `fit_model_2.plot(gamma=10)`. Here we can see the influence of ratio of $\\gamma$ and $\\lambda$. Since $\\gamma$ is so large ($10$) maintaining sparisity, then $\\lambda$ can be quite small $0.0016$ which this results in a very stable estimate of the magnitude of the coeffs." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 44, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 3. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 10. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 11. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 12. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "data": { + "text/plain": "" + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_2.plot(gamma=10, show_lines=True)\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we note that fitting an L0L1 model can be done by just changing the `penalty` to \"L0L1\" in the above (in this case `gamma_max` will be ignored since it is automatically selected by the toolkit; see the reference manual for more details.)\n", + "\n", + "## Higher-quality Solutions using Local Search\n", + "By default, `l0learn` uses coordinate descent (CD) to fit models. Since the objective function is non-convex, the choice of the optimization algorithm can have a significant effect on the solution quality (different algorithms can lead to solutions with very different objective values). A more elaborate algorithm based on combinatorial search can be used by setting the parameter `algorithm=\"CDPSI\"` in the call to `l0learn.fit`. `CDPSI` typically leads to higher-quality solutions compared to CD, especially when the features are highly correlated. CDPSI is slower than CD, however, for typical applications it terminates in the order of seconds.\n", + "\n", + "## Cross-validation\n", + "We will demonstrate how to use K-fold cross-validation (CV) to select the optimal values of the tuning parameters $\\lambda$ and $\\gamma$. To perform CV, we use the [l0learn.cvfit](code.rst#l0learn.cvfit) function, which takes the same parameters as [l0learn.fit](code.rst#l0learn.fit), in addition to the number of folds using the `num_folds` parameter and a seed value using the `seed` parameter (this is used when randomly shuffling the data before performing CV).\n", + "\n", + "For example, to perform 5-fold CV using the `L0L2` penalty (over a range of 5 `gamma` values between 0.0001 and 0.1) with a maximum of 50 non-zeros, we run:" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 45, + "outputs": [], + "source": [ + "cv_fit_result = l0learn.cvfit(X, y, num_folds=5, seed=1, penalty=\"L0L2\", num_gamma=5, gamma_min=0.0001, gamma_max=0.1, max_support_size=50)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Note that the object returned during cross validation is [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel) which subclasses [l0learn.models.FitModel](code.rst#l0learn.models.FitModel) and thus has the same methods and underlinying structure. The cross-validation errors can be accessed using the `cv_means` attribute of a `CVFitModel`: `cv_fit_result.cv_means` is a list where the ith element, `cv_fit_result.cv_means[i]`, stores the cross-validation errors for the ith value of gamma `cv_fit_result.gamma[i]`). To find the minimum cross-validation error for every `gamma`, we apply the :code:`np.argmin` function for every element in the list :`cv_fit_result.cv_means`, as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 52, + "outputs": [ + { + "data": { + "text/plain": "[(0, 8, 0.5313128699361661),\n (1, 8, 0.2669789604993652),\n (2, 8, 0.2558807301729078),\n (3, 20, 0.25555788170828786),\n (4, 19, 0.2555564968851251)]" + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gamma_mins = [(i, np.argmin(cv_mean), np.min(cv_mean)) for i, cv_mean in enumerate(cv_fit_result.cv_means)]\n", + "gamma_mins" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above output indicates that the 5th value of gamma achieves the lowest CV error (`=0.255`). We can plot the CV errors against the support size for the 5th value of gamma, i.e., `gamma = cv_fit_result.gamma[4]`, using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 50, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "cv_fit_result.cv_plot(gamma = cv_fit_result.gamma[4])\n", + "cv_fit_result.cv_sds" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above plot is produced using the [matplotlib](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) package and returns a [matplotlib.axes._subplots.AxesSubplot](https://pandas.pydata.org/pandas-docs/version/0.21/visualization.html) which can be further customized by the user. We can also note that we have error bars in the cross validation error which is stored in `cv_sds` attribute and can be accessed with `cv_fit_result.cv_sds`. To extract the optimal $\\lambda$ (i.e., the one with minimum CV error) in this plot, we execute the following:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 57, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 19 0.2555564968851251\n", + "Optimal lambda = 0.0016080760437896327\n" + ] + } + ], + "source": [ + "optimal_gamma_index, optimal_lambda_index, min_error = min(gamma_mins, key = lambda t: t[2])\n", + "print(optimal_gamma_index, optimal_lambda_index, min_error)\n", + "print(\"Optimal lambda = \", fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index])" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To print the solution corresponding to the optimal gamma/lambda pair:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 58, + "outputs": [ + { + "data": { + "text/plain": "<1001x1 sparse matrix of type ''\n\twith 11 stored elements in Compressed Sparse Column format>" + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index])" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The optimal solution (above) selected by cross-validation correctly recovers the support of the true vector of coefficients used to generate the model." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 70, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.01994648 1. ]\n", + " [0.97317979 1. ]\n", + " [0.99813347 1. ]\n", + " [0.99669481 1. ]\n", + " [1.01128182 1. ]\n", + " [1.00190748 1. ]\n", + " [1.01272103 1. ]\n", + " [0.99204841 1. ]\n", + " [0.99607406 1. ]\n", + " [1.0266543 1. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " ...\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]\n", + " [0. 0. ]]\n" + ] + } + ], + "source": [ + "beta_vector = cv_fit_result.coeff(lambda_0=fit_model_2.lambda_0[optimal_gamma_index][optimal_lambda_index],\n", + " gamma=fit_model_2.gamma[optimal_gamma_index],\n", + " include_intercept=False).toarray()\n", + "\n", + "with np.printoptions(threshold=30, edgeitems=15):\n", + " print(np.hstack([beta_vector, B.reshape(-1, 1)]))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "## Fitting Classification Models\n", + "All the commands and plots we have seen in the case of regression extend to classification. We currently support logistic regression (using the parameter `loss=\"Logistic\"`) and a smoothed version of SVM (using the parameter `loss=\"SquaredHinge\"`). To give some examples, we first generate a synthetic classification dataset (similar to the one we generated in the case of regression):" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 72, + "outputs": [], + "source": [ + "import numpy as np\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = np.sign(X@B + e)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "More expressive and complete functions for generating datasets can be found are available in [l0learn.models](code.rst#l0learn.models). The available functions are:\n", + "\n", + "* [l0learn.models.gen_synthetic()](code.rst#l0learn.models.gen_synthetic)\n", + "* [l0learn.models.gen_synthetic_high_corr()](code.rst#l0learn.models.gen_synthetic_high_corr)\n", + "* [l0learn.models.gen_synthetic_logistic()](code.rst#l0learn.models.gen_synthetic_logistic)\n", + "\n", + "An L0-regularized logistic regression model can be fit by specificying `loss = \"Logistic\"` as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 117, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'Logistic', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
025.2253361-0.036989True1.000000e-07
119.4320532-0.043199True1.000000e-07
218.9286032-0.043230True1.000000e-07
315.1428822-0.043245True1.000000e-07
412.11430690.004044True1.000000e-07
57.411211100.188422False1.000000e-07
67.188875100.219990False1.000000e-07
75.751100100.234899False1.000000e-07
84.600880100.242631False1.000000e-07
93.680704100.243655True1.000000e-07
102.944563100.243993True1.000000e-07
112.355651100.244295True1.000000e-07
121.884520100.244520True1.000000e-07
131.507616100.244716True1.000000e-07
141.206093100.244886True1.000000e-07
150.964874100.245011True1.000000e-07
160.771900100.245133True1.000000e-07
170.617520120.178144False1.000000e-07
180.598994120.196406False1.000000e-07
190.479195120.208883False1.000000e-07
200.383356150.192033False1.000000e-07
210.371856150.221470False1.000000e-07
220.297484150.246182False1.000000e-07
230.23798816-0.110626False1.000000e-07
240.23084816-0.118558False1.000000e-07
250.18467816-0.124533False1.000000e-07
260.14774321-0.211002False1.000000e-07
\n
" + }, + "execution_count": 117, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_3 = l0learn.fit(X,y,loss=\"Logistic\", max_support_size=20)\n", + "fit_model_3" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The output above indicates that $\\gamma=10^{-7}$ by default we use a small ridge regularization (with $\\gamma=10^{-7}$) to ensure the existence of a unique solution. To extract the coefficients of the solution with $\\lambda = 8.69435$ we use the following code. Notice that we can ignore the specification of gamma as there is only one gamma used in L0 Logistic regression:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 83, + "outputs": [], + "source": [ + "np.where(fit_model_3.coeff(lambda_0=7.411211).toarray() > 0)[0]" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "The above indicates that the 10 non-zeros in the estimated model match those we used in generating the data (i.e, L0 regularization correctly recovered the true support). We can also make predictions at the latter $\\lambda$ using:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 84, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.69583037e-04]\n", + " [4.92440655e-06]\n", + " [3.92195535e-03]\n", + " ...\n", + " [9.99161941e-01]\n", + " [1.69035746e-01]\n", + " [9.99171256e-04]]\n" + ] + } + ], + "source": [ + "with np.printoptions(threshold=10):\n", + " print(fit_model_3.predict(X, lambda_0=7.411211))" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Each row in the above is the probability that the corresponding sample belongs to class $1$. Other models (i.e., L0L2 and L0L1) can be similarly fit by specifying `loss = \"Logistic\"`.\n", + "\n", + "Finally, we note that L0Learn also supports a smoothed version of SVM by using squared hinge loss `loss = \"SquaredHinge\"`. The only difference from logistic regression is that the `predict` function returns $\\beta_0 + \\langle x, \\beta \\rangle$ (where $x$ is the testing sample), instead of returning probabilities. The latter predictions can be assigned to the appropriate classes by using a thresholding function (e.g., the sign function)." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Advanced Options\n", + "\n", + "### Sparse Matrix Support\n", + "Starting in version 2.0.0, L0Learn supports sparse matrices of type [scipy.sparse.csc_matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html). If your sparse matrix uses a different storage format, please convert it to `csc_matrix` before using it in `l0learn`. `l0learn` keeps the matrix sparse internally and thus is highly efficient if the matrix is sufficiently sparse. The API for sparse matrices is the same as that of dense matrices, so all the demonstrations in this vignette also apply for sparse matrices. For example, we can fit an L0-regularized model on a sparse matrix as follows:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 90, + "outputs": [ + { + "data": { + "text/plain": " l0 support_size intercept converged\n0 0.031892 0 0.009325 True\n1 0.031573 1 0.004882 True\n2 0.020606 2 -0.002187 True\n3 0.014442 3 -0.004111 True\n4 0.013932 4 -0.002556 True\n5 0.010854 5 0.002987 True\n6 0.009286 6 0.002048 True\n7 0.009191 7 -0.001371 True\n8 0.008771 8 -0.000533 True\n9 0.008151 9 0.000064 True\n10 0.006480 11 0.001587 True\n11 0.006364 12 -0.003636 True\n12 0.005760 13 -0.003866 True\n13 0.005560 15 -0.004211 True\n14 0.005264 17 0.001497 True\n15 0.004637 18 -0.000797 True\n16 0.004515 19 -0.002634 True\n17 0.004113 22 0.001419 True", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.03189200.009325True
10.03157310.004882True
20.0206062-0.002187True
30.0144423-0.004111True
40.0139324-0.002556True
50.01085450.002987True
60.00928660.002048True
70.0091917-0.001371True
80.0087718-0.000533True
90.00815190.000064True
100.006480110.001587True
110.00636412-0.003636True
120.00576013-0.003866True
130.00556015-0.004211True
140.005264170.001497True
150.00463718-0.000797True
160.00451519-0.002634True
170.004113220.001419True
\n
" + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from scipy.sparse import random\n", + "from scipy.stats import norm\n", + "\n", + "\n", + "X_sparse = random(n, p, density=0.01, format='csc', data_rvs=norm().rvs)\n", + "y_sparse = (X_sparse@B + e)\n", + "\n", + "fit_model_sparse = l0learn.fit(X_sparse, y_sparse, penalty=\"L0\", max_support_size=20)\n", + "\n", + "fit_model_sparse.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Selection on Subset of Variables\n", + "In certain applications, it is desirable to always include some of the variables in the model and perform variable selection on others. `l0learn` supports this option through the `exclude_first_k` parameter. Specifically, setting `exclude_first_k = K` (where K is a non-negative integer) instructs `l0learn` to exclude the first K variables in the data matrix `X` from the L0-norm penalty (those K variables will still be penalized using the L2 or L1 norm penalties.). For example, below we fit an `L0` model and exclude the first 3 variables from selection by setting `excludeFirstK = 3`:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 94, + "outputs": [ + { + "data": { + "text/plain": " l0 support_size intercept converged\n0 0.050464 3 -0.017599 True\n1 0.044333 4 -0.021333 True\n2 0.032770 5 -0.027624 True\n3 0.029367 7 -0.029115 True\n4 0.024710 8 -0.021199 True\n5 0.021393 9 0.010249 True\n6 0.014785 10 0.016812 True", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
00.0504643-0.017599True
10.0443334-0.021333True
20.0327705-0.027624True
30.0293677-0.029115True
40.0247108-0.021199True
50.02139390.010249True
60.014785100.016812True
\n
" + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model_k = l0learn.fit(X, y, penalty=\"L0\", max_support_size=10, exclude_first_k=3)\n", + "fit_model_k.characteristics()" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Plotting the regularization path:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 93, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAcMAAAEGCAYAAAAZo/7ZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8/fFQqAAAACXBIWXMAAAsTAAALEwEAmpwYAACOPElEQVR4nOydd3gU19m377O9SVpV1BuI3osBAwaMsXHFvSaxnThOYjtxnC/NKX7jlDdOs+M49ps4dorj3sEVYwzYYMD03gRCoF5X0u5q65zvj5WEJASsALEaMfd17bWa2Tmzj6Td+c15zlOElBINDQ0NDY1zGV2sDdDQ0NDQ0Ig1mhhqaGhoaJzzaGKooaGhoXHOo4mhhoaGhsY5jyaGGhoaGhrnPIZYG3CmSElJkfn5+bE2Q0NDQ0NVbNy4sU5KmXqa50gzGAzPAKPpn5MsBdgRCoXumjRpUk1PBwwYMczPz2fDhg2xNkNDQ0NDVQghSk/3HAaD4Zn09PQRqampjTqdrt/l6ymKImpra0dWVVU9A1zV0zH9UcE1NDQ0NNTF6NTU1Ob+KIQAOp1OpqamNhGZufZ8zFm0R0NDQ0NjYKLrr0LYTpt9x9U8TQw1NDQ0NM55NDHU0NDQ0BgQvP766/H5+fmjc3NzR//kJz9J783YARNAo6GhoaGhDp5fW5r0l2X7s2pb/KbUOHPgO/OKyr80La/hdM4ZCoV44IEHcpcsWbKvsLAwOG7cuBHXXXeda9KkSb5oxmszQxXx3sH3uPj1ixn7n7Fc/PrFvHfwvVibpKGhodErnl9bmvSrd3fl1bT4TRKoafGbfvXurrzn15Ymnc55V6xYYc/Ly/OPHDkyYLFY5LXXXtvw+uuvO6Mdr80MVcJ7B9/jF5//Al84cpNT6ankF5//AoDLCy+PoWUaGhoaXVn411XDjvfarspmezAsRed9/pCi+92He3K+NC2voabZZ/j6cxsGd3590X0z957sPY8cOWLKysoKtG9nZ2cH1q1b54jWZm1mqBIe3/R4hxC24wv7eHzT4zGySENDQ6P3dBfCdlp8oZhOzrSZYT8nGA5i1Bup8lT1+Hqlp5Ir37qS/IR8ChIKKIgvoCChgPz4fJwW59k1VkNDQ4MTz+TO+83HY2pa/Kbu+9PizAGAtHhLKJqZYHdycnIC5eXlHectKyvrMlM8GZoY9kOa/E0sP7Kcj0s/ZmvtVpZev5R0ezqVnspjjnUYHRQlFlHSVMLq8tUElWDHa4nmRG4ZcQvfGvctpJR8Vv4ZI5JGkGo7rcpLGhoaGqfMd+YVlf/q3V15/pDS4Zk0G3TKd+YVlZ/OeWfPnu05dOiQZc+ePab8/Pzgm2++mfTCCy8cjHa8Job9hEZfI58c/oSlpUtZV7mOkAyRYc/gqsFX4Q/7uX/i/V3WDAEsegs/m/azjjXDsBKmwl1BSXMJJU2RR5YjC4AGXwP3LruXH5/3Y24bcRvl7nJ+/8XvI7PItlllfnw+CeaEmPz+Ghoa5wbtUaNnOprUaDTypz/96fCCBQuGhsNhbr311rrJkydHFUkKIKTs10UDomby5MlSbbVJmwPNfFjyIR+VfsSGqg2EZZhsRzbz8+dzcd7FjEoehRBH3evvHXyPxzc9TpWninR7OvdPvD/q4JlgOMiuhl2k29IZZB/Ezrqd/GTVTzjccpiQEuo4LsmS1CGMBQkFXJR3UYegamhoDDyEEBullJNP5xxbt249NG7cuLozZVNfsXXr1pRx48bl9/SaNjM8y1R7qmkNtZKfkI/L5+JXa39Ffnw+Xx39VS7Ov5hhicO6CGBnLi+8/JQjR416I+NSx3Vsj0oZxaKrFxFSQpS7yztmkoeaD1HSVMInhz+h0d/I0MShZDmy+LTsUx7b+BiPz32c3PhcjrQcweVzkZ+QT5wprsf3PB3x1tDQ0DibaGJ4FvAEPdiNdhSpcMt7tzAhbQJ/mvMncuNzeefqd8iLzzuuAPY1Bp2BvPg88uLzmJMzp8trLp8Lq9EKgNVgJTsum0RLIgBv7X+Lf2z/BwAp1pQus8n8+HxKmkp4YvMTWiqIhoaGKtDEsI840nyEpYeXsvTQUup8dSy5bgk6oePh8x8mOy6747j8hPzYGXkSOkejTkmfwpT0KR3bNw67kdEpoztmkiVNJSw5tITmQPNxz9eeCqKJoYaGRn9DE8MzSElTCUtLl7K0dCl7GvYAMCp5FDcPu5mQEsKkNzEre1aMrTwzpNvTSbd3Lf0npaTR30hJUwl3fHhHj+PaU0R+v/73+EI+ihKLKHIWUZRYpAXvaGhoxAxNDE+TKk8Vb+1/i49KP6LYVQzAuNRxfH/y98+54BMhBEmWJJIsSWTYM3pMBWkX0CpPFWsr1/Lavtc6XhtkGxQRxzaBHJUyisKEwrNmv4aGxrmLJoa9RErJ3sa92A12cuJzqHBX8H9b/4+Jgyby4/N+zLzcecfMmM5FjpcKcv/E+wF4dM6jSCmp9lazv3E/+1372d+4n32N+1hbuZaQEmJ+3nwenfMoAA+veZjZ2bOZkzOH9gjoWK2zamhoDDw0MYyCdvdfkiWJ1lArX3r/S1xXdB0PTn2Q8Wnj+eTGT0ixpsTazH5F+7rgiaJJhRAd7tbO7uOgEqS0qbRj2xv0sqZiDfnx+QBUe6u5etHVDHEO6eJmHZo4VHO1amico9xwww35y5YtS0hOTg7t379/Z2/Hn/N5hscL/1ekwrbabXxU+hEfl35MkiWJl694GYDPyz9nePJwkiynVWRdo5dIKRFCUOWp4tntz3bMJjsH7aTZ0iLC6BzKlYOvpCixKIYWa2j0f2KSZ7j+2SRW/i4Ld40JR1qA2T8qZ8rXTivp/oMPPnDExcUpd955Z8HxxFDLMzwOPXWCeGj1QywqXsQB1wFqWmsw6oycn3k+8/Pmd1yMz886P8aWn5u0u0XT7en8dNpPgYhA1nhrOoSx3eX6fOXzTEmfQlFiEWsq1vDIF4/w2JzHKHQWUuutxR/2k+nIRCe0WvUaGmeV9c8mseTBPEL+yJfPXW1iyYN5AKcjiJdeeql77969x9Q8jZZzWgx76gQRUAKsqVzDvNx5XJR3EbOzZx83qVzjxOxbV8WaRQdwN/hxJJmZvnAwQ6ee2fVUIQSD7IMYZB/EzKyZHftDSghJxOth1pvJjc8l2ZoMwOv7XueprU9hM9gYkjiki5tVc7VqaJwBnp573BZOVG23owS7LviH/Do+/kUOU77WQEuVgZdu6dLCibuX97pwd2/pUzEUQiwAHgf0wDNSyke6vf5N4F4gDLiBu6WUu9peexD4Wttr35FSLjnT9h2vE4RA8Oe5fz7Tb3dOsW9dFctf2EMooADgbvCz/IVIusmZFsSeMOiOfrQnDprIxEETO7YXFCwg1ZbKvsZ97G/cz8eHP+aN/W90vJ5mjbhaH7/wccx6M/Wt9ThMDsx6c5/braEx4OkuhO34mwdmCychhB54EpgPlAHrhRCL28WujRellH9rO/4q4FFggRBiJHAzMArIBD4WQgyVUobPpI3H6wShRYP2TCgYxucOYrYZMZr1NNe1UrqjnqLJg7A4jJRsrWXb8jJ8niD15W6k0m18QOGz1/ZTMD4Vo1kfm18CIq2uEgo6tqWU1LbWdnGz1nhrOsTvf9f9L3sb9/LuNe8C8EHJB5j0JoY6h5IVl6W5WjU0unOimdwfh47BXX2sO9MxKNJuKS49dDZmgt3pSyU+DyiWUh4EEEK8DCwEOsRQStm5XIkdaI/mWQi8LKX0AyVCiOK28605kwaeLPy/v3Gm3Y7hoIKrxovdacZiN9JU62XfF9X43EFa3UF8niA+d+TR6gkS8kfuRS6/Zyz5Y1NoqPDw6cv7SMuLx+IwooQlQX8Yh9NM3RF3j+/pcwcJtIYwmvUUb6yhqqSJ868dgk4XuzQJIQRptjTSbGnMyJpxzOvXFl1Lo7+xY/vPG/9MhacCiJSpa3ezdo5sbS9bp6Gh0Y3ZPyrvsmYIYDArzP7RabVwOl36UgyzgCOdtsuAqd0PEkLcC3wPMAEXdhq7ttvYY7LXhRB3A3cD5Obm9trAaML/+wsncjsWnTcIIQShQJjqQ81HxayToEW2A/g8QSZekseoWVk01bby8q++4OK7RlE0eRAtDX6+eKcEk0WPxWHE4jBhizeRlGmPbNuNWB1GkrLsAGQPT+TO38/E4jACMHhiGoMnpgHwn5+sxt3gP+b3sMWbsCVEbgrryloo3V7PzOsjEZ9L/7WTpppWUnLiSM1xkJobR1KmHYMxdrNI4BiBfHPhmxS7irvMJJcdXtbF1Xpl4ZX876z/BWDJoSWMTBpJTnzOWbVbQ6Nf0h4kc4ajSa+88sqCtWvXxjU2NhoGDRo09sc//nHFAw88EHWEa5+lVgghrgcWSCnvatv+MjBVSnnfcY6/FbhESnm7EOKvwFop5fNtrz0LfCClfP1476fGFk694XjigoDpVw9m4iV5NNe18t+fdZ08G80RYbM6jB2CNvS8dPJGJxP0hyndUU96YTyORAtKWEFK0BtO3+3XXbwBDCYdc28b3mU22x6hC7Dxw0Mc3tlAXZmbQGukrZTQCZIybG0CGceggnjSC/tfgIuUkrrWug5xzHRkMj9vPu6Am+kvTec7E77D18d+nQZfA79e++uO9I+hiZqrVSO2aC2cIvTlzLAc6HwrnN2273i8DPzfKY4d8PQohAASUnIcANidZhZ+d3yb6JmwOAwnnFUZzXqGTErr2Nbpz9wFuV3wTubW7VxFZtKCfCYtyEdKSXOdj7ojLdQebqH2iJsjuxrYu7aK3FFJXPnt8QB8+so+soqcHbPRWCKEINWWSqottUvqjc1oY/HVi7EbI7PpWm8texr28HHpxx3RrlaD9ZgCAqOSR+EwOY77flp7LA2NM0tfiuF6oEgIUUBEyG4Gbu18gBCiSEq5v23zcqD958XAi0KIR4kE0BQBX/Shrf0eR5K5R0F0JJnJHRlJGdAbdGQP7z+FAIZOTT+lNU0hBAmpVhJSrV2EztPkJ+iLrFuGwwpHdjVgdRgZDPg8QV765TpSsuNIzXWQmhNHSk4c8SmWmJZt0wldl2CdYUnDeP/a9/EGvRxwHWC/a39HVOvyw8t5c/+bADw+93EuzL2QPQ17eOfAO9wx6g5SbalAz/mxWnssDY3To8/EUEoZEkLcBywhklrxTynlTiHEL4ENUsrFwH1CiIuAINAI3N42dqcQ4lUiwTYh4N4zHUmqNqYvHMwn/91NOHTUrW0w6Zi+cPAJRg0s7AlmaPOQ6vU6bnt4Wked0lBAIWdEEnVHWjiyuwGpRPabrAZSsiPrj6k5DrKHJ2F3xj5Fwma0MSZ1DGNSx3Tsk1JS76tnX+M+RiaNBOCA6wCv7H2FO0ffCcC/dvyLxzc9Trjb10Frj6WhcXr0aV6HlPJ94P1u+x7q9PNxwzallL8BftN31qmLoVPTKdleR/GGGoA+S2JXG+2zPkeimYvuiAhIKBimvtwTcbMecVN3pIWdn5YTCipc8vXRDJmURl1ZC9tXljNpQR7xydZY/godCCFIsaZ0qXN7eeHlLMhf0LGmmB2XfYwQtlPpqeyyBquhoRE953QFGrUR8odxDrJx28PTYm1Kv8Zg1DMoP55B+fEd+5SwQmO1F0eiBYDmWh8HNtYw5bJ8ALYuO8Ku1RWk5Bx1sabmODDbjLH4Fbqg1x1d952fN/+47bEALn7jYq4afBXfnvDts2WehsaAQBNDlaAokoripi4BLxrRo9PrSM48GpBSOCGVgvFHZ2CORDNxyRbK9zSyb111x/74FEuHMKbkxJE7KjmmOZHQc36sWW/mqsFXUdtaS7XnqP1Pb3uaWVmzGJE8IhamamioBk0MVUJ9eSTdILPIGWtTBgyd3YmdcyS9zYE2F2sLdUfc1B5p4eDmWkwWPXc9egEQmUmGQwoTL8k763afLD+2fR211lvL09uexm60MyJ5BC2BFnbX72bioIldytVpaAwEiouLjbfddltBXV2dUQjB7bffXvvzn/+8Jtrx2jdCJVTsdwFoYngWsMWbyB2VTO6o5I59gdYQzfWtiLZZYVVJE0FfuEMMX//dBnR6QWpOHKm5ETdrYoYN/RlMV+nM5YWXHzdYpl3kU22prLxpZcf+Tw5/ws9W/4wkSxLzcucxP28+U9KnaMKocdZ5Ze8rSX/b+res+tZ6U7I1OfDNcd8sv2nYTaeVdG80GvnTn/5UNnPmTG9jY6NuwoQJIy+77LLmSZMm+U4+WhND1dBU7SUu2UJckiXWppyTRKJSj3YvueSu0R0zMCklafnx1BxqZteqCkLBSKEBvUFHUqa9w8WaWeQkOev4uYN9QXt+I0TWG60GK0tLl/LuwXd5bd9rOM3ODmE8L+M8jLrYr5FqDGxe2ftK0u/X/z4vEA7oAOpa60y/X//7PIDTEcS8vLxgXl5eECAxMVEZPHhw6+HDh03RiuE539xXTQR8IUwW7f6lP6MoEle1t0ska+2RFvyeEOMuzGHmjUWEQwqf/Hc3o2ZmklkUmxqmvpCP1eWr+aj0I1aWrcQT9BBvimd+3nwemv6QVhHnHKIvKtDc8u4tx23htKdxjz2khI5ZeHcYHaE1t67ZWuutNXznk+90yRl76YqXelW4e+/evaY5c+YM27lz586kpKSOMlhac98BgiaE/R+dTpCUYScpw87Q8yL7pJS4G48WTPC4/FTsc5E/JhLAU3WwiY+e2RmJZM09Gslqd5qPmyZxukXbLQYL8/LmMS9vHv6wn8/LP2dp6VIafA0dQvjPHf9kTMoYpqRPOcW/xpmxVWNg0ZMQAriD7jNygWtqatJde+21gx955JEjnYXwZGhXVxWwd10VBzbVcNGdIzVBVCFCiC7u7fgUK7f/dkaHm1Vv0DGoMJ66I25KttV19G6xOIwdLtbU3DhyRyZhthnPeK9Is97M3Ny5zM2d27HPH/bzn53/4bqi65iSPoVgOMin5Z8yI3MGFkP0rvpY97XUiA0nmsnNfXXumLrWumNaOKVYUwIAqbbUUG9ngu34/X5x+eWXD77hhhsabr/9dldvxmpXVhUQCoRpbQnGtAegxpmnI9AlN45L7hoNRFzh9WVuatuiWOuOtLB12RGUsOTmn5+H2WZk1Wv7uxRAh7Zeka/uo8uiR/uaJiCAYdMyAKgsduFpCnSk6RzeWY/b1a3Un4S/ZjxPsDnIzs/KKfEc4MHq72I1WFkgrmd88gQunTcTq8HK/g3V+D1BOq+4tP/8xTsHe7R1zdsHNDE8R/nmuG+Wd14zBDDpTco3x33ztOpPK4rCzTffnDd06FDfL37xi+qTj+iKJoYqYNSsLEbNOqaDlcYAxGQxkDHEScYQZ8e+cFChodJDYroNgFZ3sMexPk+Ij/+1q8fXdDrRIYa7Pq+kbHdDhxhuXXaEw7tOHLdgTzTz9Lef5qPSjwi+42RL4AiP1M1mZtZMRiy7jEBt79YYO7uNP/nvbkxWA3FJli4Ps92gVdMZgLQHyZzpaNKlS5c63n777eSioqLW4cOHjwR4+OGHy2+66aamaMZrYtjPUcIKQie0i8I5jN6oIzX3aCSrI8lMddNaWp0NSIMBEQphdSWR6pjKNd+bGDmo7eNy9GNz9PNz/jWDCV1xtHj4vDtGEg4du7TSeazQRWrDTs+cjnt0K1trtmGqvYqPSz/ms7zPMRdYmJoxlZ9M/QlWgzUyVsArv16Pp/usE7DGRaJWFUVSdaCJ5nof4WBXGwxmPXFtxRAGT0hj5MxMACqKXSSm27A6jm2Wfq7R9M471Dz2Z0KVlRgyMkh74LskXHllrM06KTcNu6nhdMWvO5dccolbSrnxVMdrYtjP2buumjVvFXPjT6Z0lBLTOHdRFAV9+m68hiZoS4OQRiPe5CYsaftwDpp50nNY47qKiC2+d6LiiLMyI24qMwZP5cHzHmRzzWY+Kv2Ig66DJCXGI4Tg+V3Pk25P5/xrRvPhP14n6FkNSgvo4jDaZzDz+uuByIz11l9ECq773EFaGny0NPhwN/hpqffR0uijpd6HtzkARPI93/rjJqZfE+nh2dLgY+k/d+JItHSkHsUlWXAkmYlLsgzoNfamd96h8ucPIX2RzIFQRQWVP4+UflaDIPY3Bu4nZYBQUexCKm0dGzQGHIqi4PV68Xq9mEwmnE4ngUCAzz//nMLCQnJzc6mrq+PVV1/F4/Hg9XojgTe6bm5JnY59VQf42ze/AnoDismCCYXx8y5h8hXX4Pd6efO3/8OkyxcydNpMmmtrWPavv6HXG9Dp9W0PAzqDHp1OH3nWGxgyZRrZw0fhbW5i+7IlDDlvOslZObTU11GyeQM6vR6bXs+1+hnonLM5sPELdDody754jfzEQiqVjfg8n6Fvn/QpLfg8H7KhLMjQqQ90mC+EwBpnwhpnIi0vnuOhN+i48jvjSEiNFFcPBcIIIaguaeLAxhoUpWuqmNlmwJFkYdpVheSPTaHVHaB8r4usoc5jbgraWfLmZ3yxdTVhfOixcN64GVxy7aze/3P7mJo//LFDCNuRPh81j/1ZE8NTQBPDfk7FvkYyhiR0VD7RUAfV1dUYjUaSkpIIhUJ8+umnHWLW+bm1tbVjzLRp01iwYAEAK1aswGQykZubi9lsJikpiZycHGw2G599+mlnH2YHUm+gcMJkGlv97HZ5KLKbsDsTKS0tZdPGjbgMFspr60iqq0P6/bTU16GEQiiKghIOoYTCkedwGCUcJhwO4RyUQfbwUXgaG1j18nMkZWaTnJVD3ZFSlv7jr8f9/SPhQPspM+3DpHS1Va9AyVsf8Ye3PgGTAb3ZhNFqJSEuiYS4JIwWKyarFbPVxriLLyc+JRVXdRW1pQfJHzeR3JHJ+NxuWhrqsCfYuPqB8QidDkWReJsCuNtmk0dnmb6O4LPa0haW/GMH13x/ItY4E8Uba1jz9oG2GaWZ8vqDHHRtgLZ7jTA+1mxZBkguufaC0/lInBFkKIQwGAg3NRGqqaE0N5dt48bitdmweb2M3bqNvCNHYm2mKtHEsB/jbvTRXOdj7NycWJtyzlNVVUVLS8sxYtb5OTs7m2uvvRaA5557jmHDhnHVVVeh0+lYtWoVFosFu92OzWYjLS0Nm83WsW2320lLiwS0mEwmfv7zn6PXRy7gcXFx3HzzzR22fL5iOWH9sV9dvRLm4m98B6/Xy9jSUvLz87FarWzdupXiAwfwSD3VG7ewcuMWhBAkZg0hJSWF5ORkkpOTGT16NBZLz674lNx87v/vm+jabMoZNZa7/+/fyLBC+DhCqoTCvPqrB3s8nzmoo3K4jmCrByXgwhjQMdirA28At6eJxuY6rIqRodNnUalr4O1FT2JYdpDCB79MxqA86pduYM97H3acz2S1YrJYMVltkZ+tkZ8v/ub9WB1xHN6xjXVvLWXiZddy88/Pw++tonT7IVpcQWxJLlrdBhordBw2bzr2qqiDtVuWk5htoe6wh8M7G5l6xWCsdjM1JS00lHtJSRqE3WEDQwhFFyQlNRmL3YTOIDFadTjiregN+tNa+6986H8Ilh0h95//RJ+QwOERI1g3ejSyreSf125n3dSp6BIT0cqy9x5NDPsxFcUuQKtHeqaQUhIIBI4rZgaDgQsvvBCAV155BUVRuOWWWzq2GxsbO86l0+mw2WwdQpaRkUFGRkbH69deey1xcXEdx/7sZz9D1921eQLahbAnBukVKhSlq6tUUZgyJjIfs9lsjBhx9HI4btw4xo0bR2trK/X19dTX11NXV9fx88GDBwmFQgwfPhyAzz//nB07dvC1r30NvV5PVVUViqKQnJyMoc0ug9FIXNLRrh/Hw2cDq7fn/X96eBEAgXCAutY67EY7CeYEyt3lvLr3Va4ruo60uBx2HP6E90wbEDN9NG7+NVIHSU1GUkabsSkmEkQc8cAEZxGWsBFXSwM1LTXYmky43W527d3Hzs8/o/xAMUfCOtxuN9VHDuPz+5F6AwiB9fA+DJ5m5PBJdA42akfqJO+/f7Q166J3tnV5PX4t6IIG/PHQmhzCWRqHTthoTTbSai/rOE4ndCAFJosBvU6PEgKpCKYWXkScM44y10FqGsu4eMZlhPdsZ+eW1fjGjcRsMxNKSkRYLez56CMMBgNfjBtL9/phUq9jw4gRXHzS/4xGdzQx7MdU7G/CaNGTnH1261mqBSklPp+vi8tx2LBIFaitW7dSVVXFJZdcAsDrr7/Onj17CIVCPZ5Lr9eTlpbWIYa5ubl0LlW4cOHCDgG02+1YLJYT3uUPHtylmlSvhPBE+DxuLN4WClMzKW32ENbp0SthpowZzYIbbz7hWKvVSnZ2NtnZ2V32K4pCc3Mzdnukjqndbic5OblDkJcvX87evZEc6Li4uI6ZZOdZpdPp7FHAC6+4iJKPNhJKzkEaTYhgAEP9EQovntRxjElvItOR2bGd5cjigUlH1xMvzL2QGTfPoM5VR3lDOdWuaupd9bhaXHg8HnxeH3VJdUy+/EuYPWaefvpp1uRv5YU7X6CxrJF33nkHgHBSIluKt6A36zE59Tj0dqxGM3ajmcyxQ0gwWPlg3Qak8dj1eREMMC7VyZw77iYcDrP8uWeoPnSAUChMOBRG+JsJBwLoFDMWn52Qt4G4pHQmzL2H6rpMqg+uRiJIzZ9Mc4OXpprN+L1epBAgdGzc8RBCSgKJqYQcibzx2XvoDNkEMkYT2HUQncEHOh2Kokf5fC2S4xdWCcioi65odEITw35MxX4XGYOdMe+fd6ps27aNZcuW0dTUREJCAvPmzWPs2LHHPV5RIl9inU5HY2MjlZWVDB8+HJ1Ox/bt29m7d+8xs7n2Me387Gc/w2AwUFNTw8GDBzv25+XlER8ff4xrsv3ZZDJ1Ebfp06d3OW9+fv4Z+IucPha7gy8/8jhSyg6X5emi0+lwOp0d2+0zyXbmz5/PuHHjuswmd+7cia9T8EZSUhLf+c53ANi0aRM2m43hw4czumge+3c0dSThS5OZUOYQRg25ELfbjcfjwePxYLPZSE9PJxgM8t577zF8+HCGDx9OfX09Tz31FOFwuEfbbTYbifZEbhh1AwUJBXiNXi6YcwFX5lxJkiWJ+Lx4zr/pfL5o+AJXwEWtt5ZqbzV1rXWEZadztsCaW9aw5IN3CKfmQKeGyihhdA0VXP2b/+3Ydf0DPzrGFikl4VCIcDBAKBBASokjMQmA+rLRSEUhJTcfgJLNQ/G5WwgGAvhLS2mRCp69+wjWlhK21EBGFo7CPAbPuxwpdBze9jqJ6ZkYrJPxt4YoXvckxYoJaepZuM9FvF6vmDp16vBAICDC4bC48sorGx977LGKaMdrYthPaW0J0FjpYdjUQbE25ZTYtm0b77zzDsFgJEG8qamJRYsWUVxcTGJi4nGDSe655x5SU1PZs2cPS5Ys4Yc//CE2m42GhgYqKiqw2Ww4nU4yMzN7FLX2Gdj8+fOZP39+hz1Tppxefc3+QM2hg8SnpGFxOBDbXoVlv4SmMkjIhnkPwdgb++R9U1JSSEnp6hKVUuL1ejvEsfMs+vPPP2fQoEEMHz6cZcuW0b0XgJTw1ltvddk3ceJErrrqKvR6PSUlJR0uZ4fDwbRp07Db7djtdhwOR8fPNpvtmNmozWbjwjkXdmybTCYuHnExF3dzHCpSocHXQK23ltrWWmq9tdiNdjbk7OG8QwqhlKyjM9m6ctbl7+WaRddQlFjE0MShDE0cSpGziHR7esdNlBACg9GIwWjEbLN3eb/k7Nwu2wUTJhNuaqLkxhsxlh4mzmjEMWcO8VdcgWP2Bei6rd0Om3pfl+2pV/6J333jG7QOSjtGuC31ZzR9r09oeOnlpPqnnsoK1dWZDCkpgeR77ilPuuXm0zLcYrHIVatW7U1ISFD8fr+YMmXKsGXLljXNmzfPE814TQz7KUfXC2PT1eB0WbZsWYcQthMOh9m2LbLWYrVaOy5oKSkp5ObmYrfbMZsjd7qjR4+moKCgY3v27NnMnj377P4S/YhwKMQ7j/4WR1IyN107Ed75DgTbIlGbjkS2oc8EsTtCiA5Rys3teqG/5557CAQis5OmpuMX/7j00ks7xC0xMfI51+l0PPDAURep2WzuclNzptAJHSnWFFKsKYzoFG7SOjSedexl0t4aHD49HkuYdcMaqc3XMcmRxdaarXxQ8kHH8XHGOIoSi5ieOZ1vjvsmEKnratb3nArV+MqrhKqrSf3Ot9EnJGCfNh3r3d8gbv5F6OOPn1LSE1OmX8q61W8TSM3oEG5TbSVTZlzd+z/IWaThpZeTah55JE/6/TqAUG2tqeaRR/IATkcQdTodCQkJCkAgEBChUEj0JmBJE8N+SnKmg6kLC0nLizv5wf2QE10EO0dKHo+4uLiOAJRzEikh5Is8rIlsX7YEV3UlcxdeDMsePiqE7QRbIzPFsySGSiCM4glGHt4QxiwHeruRQFkLnvVVxM+PND12YMHNse3kHFiYMnYSOmv/ugTdP/F+fuH7Ba9nHS2TadFb+MW0hzqaKbcEWih2FbOvYR/7XfvZ17iPIy1H0xmueOsK5uXO48fn/ZhQYyNr33iKjOtuJi8hD9+uXQRKSpBSIoQg4+FfnLKtF95xNQBbl76KEmpGZ4hn3PwbO/bHkpIbbjxuCyffnj12gsEuKiX9fl3to4/mJN1yc0OottZw5J57uyy6F7z2alSFu0OhEKNHjx55+PBh8+23315z4YUXRjUrhD4WQyHEAuBxQA88I6V8pNvr3wPuAkJALfBVKWVp22thYHvboYellFf1pa39DecgG5MvzY+1GadMQkJCj4KYkJBwUiHs17T7/ISA1kbwNkDQGxGjY57bHtPvjbiydrwJZethwW8j51jxCBxc0e34Tj8jwZpE4P5drHnjJbKS9RTseRSayvCEZtMcup0wKeipI97wH+xNK+G/10JiHjjzIDEfUopg0KgT/0phBSQIgw7FG8R3wIU5Nx59gplAWQstn5WjeNuEzxNC8QaR3UqnJd85CuuwJMLNAVp31uM4PxO9w8TkQCGfGfcQFkeP10sdk4OFVDy8BkOaDVNuHM4rB6PrB4Xo2wXv8U2PU+WpIt2ezv0T7+/YDxBnimNC2gQmpE04ZrwiFW7Jv5YRuz0c+fe9uD/9lORQiAfqXqI8y8yQCQUMmT+Mop3/ibhaE4tIsaaccsrFhXdc3S/Er1d0E8J2lJaW09Yjg8HAnj17dtXV1ekvv/zywevXr7dMmTIltp3uhRB64ElgPlAGrBdCLJZSdq4kvBmYLKX0CiG+BfweuKnttVYp5fi+sq8/E/CFqNjnInOoU7XlpObNm8c7i94iGD66YGTUC+bNm9c3byglhINHxSTUSYxSisCSAI2lULoahl8Blng4vBb2LTm+kLWf47bXwJkLa56Ej34GD5aByQ4r/wBrnzy5bZO/CmYH1OyKvF+7GEoF9EawZIDBAkYbGK1tj7afzXFsem8R3iYXC+//OiIrFc8L/8DluxVJZF0pTBqu0LdBb8LmqUWW7QFfAzrRipJ1Ia3j/w/FGyS8cTGKKQMlblhE2FzNhP06pF/iXDgYx/RMQk0BGl7YQ9JtI7CNMSMDYQJlLehtRvTxZowZDnR2AzqbEb3diM5mRGc3YEyPrJFZRyZjHZnc8asPi8+HZthgOIhb+HBIC5NDhQy15eKYnkngcDOBkiaEKbLW63r3IIonSNJNkYmFDEuE/uwGkF1eeHkX8YsGGQziWbOGpnff5fyPlyG9XnxpaTi/dCuu2WP5emqY/a5i9jXuY23FWhYfWNwx1ml28qPzfsQVhVfgCXo46DrI0KShx3W1qoETzeT2z7pgTKi29pjyP4bU1EDbcyjameDxSElJCc+aNavlnXfeSYi5GALnAcVSyoMAQoiXgYVAhxhKKZd3On4t8KU+tEc1VOx38d5T21j4wASyh6lzzTDXu50LlDVsYCRNxJFAC/PkF4z1pIAvLyJOrY1wZP2xQtRZyIJeGHcr5E6F6p3w/g/h4l9B1kTY+wEs/s7R42TPEYd8+W0YPBcqNsHb34J7JkbEsGIzfP5EzyJktII9LfIs2mYsmRNh1v+jIw9tzHWQOf74Qtb+bGoLprjwZ5FHO3N/ctK/o7e5ifVP3cWQKdPIPH8hAM0hP7LbV1dioVl/L9Y7Z1HxP2tImJ9B3EgvSmOQxuf2Rw4SI9GbFXRhPzqbHqP7UyyiCZ2hGdOKg7BTYowvZNC0Yei9JXAwF3PqCDJ+cOrBR/GX5FP0ZpAhgaM5mMKoI+HyQuwTIkUG2l2GAMKsR4SPziKr/7IJoROY8uIx5cVjzo1Dn3TitJazSdjtpvbRx2j+8EPCDQ3o4uNJuPxy4q+4AtvkSQi9ngw4Jgne5XN1uFj3N+4n2xFJd9las5VvfPwNnr34Wc7LOI9ttdtYVb6qI3An25GNvlPAzHsH3zvhLLY/knzPPeWd1wwBhNmsJN9zz2m1cKqoqDCYTCaZkpISdrvdYvny5fHf//73q6Id35dimAV0rgtUBkw9wfFfAz7otG0RQmwg4kJ9REr5dvcBQoi7gbuBYxbx1Uz2sESufmACgwp6t6AecwIeqN4FVdvY9NEiPpPn8UP+hpW2rgUKsGQ7mONh4pehrhhevKHncwl9REQMFsifBUw9Kkrtrsq4DBh+2YmFyGiD9LZ0jiEXwf1bIa4tp23qN2Hat6L//fKmRx7tZE2KPPqQdW+9StDnZ+bNt3fsC3t7/tqGvQaESU/CFYWYCxIgw4E+TZL+Qz86uxFh0h0VkXAIKvyR2bLrEDQawVWKqFiLsel12NJ2YzHjfpj/S/C3wEu3RLaL5ke2q3dFXLKOQT2WhwMignd4Lc3rFMJKInpdI/GTddgnzOg4prOwJbStNUJEJK2jUwiUNuPdVINnbSUAOocRU2485ry4iEhmORDGs+di9e3bR7Cigrg5c9BZrbhXrcI+bSrxV1yBfeZMdKaTFz53WpxMSZ/ClPSuNxojk0fy5zl/ZmTySAC2123n79v+jtKWO2jRWxjsHMzQxKEEw0E+OLSEsIwEqlV6Kvn5qv8B6NeC2B4kc6ajSY8cOWK84447CsLhMFJKsXDhwoZbbrklqvZN0E8CaIQQXwImA53DBfOklOVCiELgEyHEdinlgc7jpJRPA08DTJ48uXsxBtViMOnJUsOMMOSHtU9B1fbIo7444voDirmFbKqOCmFn8s6PPKeNgLuW9SxkeuOx49KGw53vHd3OHA+Zj0dvrzku8minn8wujkdTTTVbP3qPUXMuIjn7aEk+XbwJpfnYXDK904wQgriZR3tfCr3AkNRDiTW9AXLOizy6Ew5Bczm4SsHR1oDX1xz5fyttIlm5Ff7ddsE1WCNu5I61yrb1SmceVG7Dv+hB3FsshLx6DLYw5iof9gLfSYN9hBAd4igVSbDaS6C0OeJaLW3Gt6seAMfMLJxXFCJDCq276jEPdqK39/D5iZKmJ39Kzb/eJOSWGByCtDuvxX7jdzGkpgJQ+/hf8O3ehWP2bIRez+AP3kecoXVwp8XJvLyjSwm3jbiNa4uu5aDrIPsa90Vmkq79rCxbSYPvWO0ISj+/XftovxZDiAji6Ypfd6ZOndq6e/funht6RkFfimE50LmoZnbbvi4IIS4CfgrMllJ2XDmllOVtzweFECuACcCB7uMHGkF/mA0fHGL4tHQS0+0nH9DXKAp468ERuRDw1jfBngIX/xr0JvjssYjLM30MjL4O0sfgiR9CxdMvMpc1x54vIQeS2wLFzA7Innz2fheV8fmrzyOEjvNvuLXLfn2i+RgxFEYd8Zfkn5k31hvaBO3oLI2ELLhr6dHtQaPg1teg8VBENNufD68D/9Gb8bqyFOrW2ZDhiEcs5DVQs85G+C8PknrZMtAZIu+nM0ZugHSGtmdjZP+kO8GegqjZialsHaZJt8G0DKjaTri8gkCDBUPcAThwiECtkYa3IflSE9ahFoIuHb5aJ6b8BExJYQRBcERcs4SDgIgENnW6KWp68qdUPvUGMhxpyBhyQ8UTb8ATbzL4448xZWcx6AffRxcff9S128cBYVaDlVEpoxiVMopDdR7217gpsbj5y8Gre7yfawrU9Kk9A5W+FMP1QJEQooCICN4MdPlWCyEmAH8HFkgpazrtTwS8Ukq/ECIFmEEkuGbAU1XSxKYPS8kscp59MQz6oHY3VG47Otur3gHxWXDfF5FjTPbIzA0iF5H/t/vomlgbB7ZtAwSD9VXQeRnPaI0kh2tExbiLLydrxGjiko8mvPsPNxMsbcE8IpFQpZewy4/eaSb+kvyONbizgjURhh6nAmZrY5v7tZTGL32/QwjbkWEdDWsNmEyrSRiiIINBKlcBigSUyA0YEiQw/FBkNl+7B1t4Lc6/X48UBiq+//+It2wlLttHyK+jfFM8UurBlkv9liqE0opMmQe5d7a9aQh96ADWeRdiSNLheuybJCZtxD4ogN9tpnZrHAgd7nId+vRpmEddg7AmIVsb8O98C6VmHboVD8GXnsWUnw+rHoOGkogb32Du+dmWHHHjQ+Q7pdMfjextKgeh6zTGAjodYUVS2dRKdmLkO/byF4fZWubit9dGXP0PvrmdNQcjM+KUIVb8xm4pNoA5ZD3lf+u5TJ+JoZQyJIS4D1hCJLXin1LKnUKIXwIbpJSLgT8ADuC1trus9hSKEcDfhRAKkWYqj3SLQh2wVOxzIQRkDE44O2+4823Y+35E+Gr3Hg1CMcVB+mgYfxtkHC3NxeV/6jredKxgHzhwAKvVSuaCn8InvzorVVIGIplDh5M5dHjHtlQkTe8cRBdnJPnm4ejM/WKV4xikxUmx3kDm4NGEvMeWLQNQQoInK+eysugGMlOM3FvzA/RCoNML9EKg1+sw6ARi256OMcZLvxP5XCoKrZVhrDf8AK66CFlTQ+u63wAgmhWg7aagaj+0/AWdPRthTUeXNQz36goISyj4KS69G3+iF0vuOgIbVwISffpwLBO+jDBEIjmFLRnLhC/j2ywxWDqtxFRsjkQjh3wR93Goh4DF5KKjYvjBjyJieMe7kbJt/7ocg6uky+FBDPilEQtGlIQ4dHkzqE74IXurWpBv3IVIHc4PF3wNIQQj1/+Mdw9U8duUOHyd6t5aFIXvNka9TKbRiT79Nkkp3wfe77bvoU4/X3SccZ8DY/rStv5KxX4XqblxZy6lQkpwHYa49Mhd6JaXYOUjcO8Xke0jX0DJZxE35/DLI8/pY8CZf2wD2ShQFIXi4mIGDx6Mbtz1MO6mkw/S6ELZ7h3s/mwFs269A4vjaJH2UH0rwbpWnFcU9jshbPYF+by4jpX7alm5t5aKJh9P3TaRNLsdu+fYthVumw3/Hd8gr8lHZZOPb175C+rcR9eX9TrBvl9fil4neHJ5MXurWvjLLZG8vjUlDSh/ewUSLFicFmyZBoZ8El2fBhlUCFS4I2uPpc0Earwk3nEthV8VuN49SMvKg4huKQ3CYMY8+lq4/uqjO298rtuJJYQDXcVRRmZ5q4vruXzer7Eadfx7dQm/X7KX2aGrSBRuzASw6UIMsglSrZIUiyTRpOB06tGlFnH/rCLuv6gIXnsGpGRCblsswVufc42nCTNBHk90UmXQkx4Kc3+ji8s8x84WNU5O//pGneOEgwrVJc2MnpPV8wEnq0cZCkDd3q5uzqrtkTWcr34USU9wpEH2lEg0oMEciRRc8L89v98pUF1djcfjYciQIWfsnOcaNYdKKN2+mTnGu7rsN6bayPjBZEQ/yD1VFMmuyuYO8dt4uJGwIokzG5gxJIVvz0tlcn4i2++4H8tTj6DvVKA0qNfTfOd3eXjh6C7n9IfC1DT7qXC10uAJoG8rUN+9e/0fluxl82FXx3a8xUCm00p6goWMBCsZCRaK0hxcOiaSzhEIKZgMkRs7YdRhzovHnHdspLY+zoTQ9xwJqrMmUfGrtehskRxLndWAzmZASbVSWZRASb0H7+4GDnp9rGv189PLRjA5I55txbV8/7WtFN07g3GZToq8ddw8JZeC1BEUJNspSLWTEW85eTH+G/7Vdfs7mxCPjebypiNc3v1mI0Hrf3oqxP5bpdFB9aFmwiGFzCHOY1/c9mrP9ShbqqB2D1Rtg5o9oLTVAzXaIusTYyJBLR3BEEPmRR7t9NAk9nQoLi4Gjm1hpBE9Ey+9krEXLcBgPBoRGSh3Y8ywo7OdepTk6RJWJHqdoDUQZvYfllPTEpnJjcqM5xsXFDJ7aCoT8xIx6o96FOZ9+ytsfucVOHIIIRUa7IkE7vgm8779lWPObzboyUmykZNk67L/2/OKumw/ccsEyhpbqWxqpbLJR6UrMrusbGple1kT9Z4AE3OdHWJ41V9XMSTNwV9vnQjAb9/fjd1sICPB0iGimQlW4mZnU7PyCGbvsW2+QgYdCaOTcbv8lFU0I/xhTEHJLhniZ0si38nXcBA2wq48C1JKKn69jlFhhc+siRhe2U+Nzcgwq4ERNiO6mhA6tweTokPnjKzxBcpa0DvN6B0nT82I/HEfwvPGqzT7bzlaicj8EvZ52lLEqaCJYT+iYr8L6NbM11MP9fsjlU96qke55klQQpAxFqbPa3Nzjo1EbOrOXu5VO6WlpaSnp5/bdUVPESUcprJ4H1nDRnQRwlCTn5r/20LcrGwSzlTEaBR0Tob/9kub8fpDPHvHFKwmPddPymZwqoNZQ1NIi+shdaMNpbUVa3U5zi/dRvpPT15kIBqyE20dASY94QuGcfuPCtrNU3JIdkRcn4oieXNzObUtx6b8JFiNTGuF72PB2qnBbyuSJ4Sfv1xTRGOtm2//fS0FmTYKUuwUpDj4W4qNghQH2VIwwWzgtiQLUpG4L8lHaY3UblW8QZTWEGF3kGBta6SknS+MfWo6lmFJyLCk5q9biJuXS8L8PMLNAar/vBGd1YDoNBONPEe2g7UT8IbSaS8C0VGJKDyCfhCHHhNCoRBjxowZmZ6eHli+fHlxb8ae82L49uZy/rBkLxWuVjKdVn5wyTCunnAcN2VfEQ5CQwkVW0pJdgawLP1uRADr9kUi806Euxr+p7Hf5MzdcsstuN3uWJuhSnauXMZHf/8LN/3iEbJHHHUh6uNNJF0/FFNB3wdV1bT4+HRfZO1vR3kTH39vNnqdYEKOk0CnyjA/XDD8BGc5imftWqTfj2PO2es4YjHqsXRKwr9jRkHHzzqdYP1PL8IXDFPd7KPC5aOquTXy3OTjv2tLCSP5JhbSENQg+Rs+lgVD/AUoTHWw4Wc9hjp0QegEcbNOfB2RYRmpC9tG8h2jjuaE6sA6LjUipK0RMQ3Vt6J4Q0hfiKMt7kW3c+poXnLo7EYWnwLbV5YlbXj/UJa3KWCyJZgCky/LLx8zO/u08w5//etfDxoyZEir2+3u9UzgnBbDtzeX8+Cb22kNRiIoy12tPPhmpDZ4nwrijjcAAaOvjSy8/2EwSmsLlTXPM8K6HIqXRiLRRi6ElKGRn9/5DrRUHnuuhOx+I4QQ6RifkHCWImEHEMGAn89fe4GMIcPIGn60sHb77Mw2vm8ubsGwwqbSRla0rf3tqmwGIDXOzOyhqbh9IRJsRr46s+AkZ+oZ94qV6Gw2bP2sn6TFqCcv2U5ectc51Cd7avjY1crHdL2hy3Ke+XQFoRcdOYpCL7AOT+p4Te8wkbiw53V3qUikL0TFL9f2+HrY1UOhi37E9pVlSatfK84LhxQdgLcpYFr9WnEewOkI4oEDB4xLlixJePDBBysfe+yxXjeCjVoMhRA2KeWxYWEq5g9L9nYIYTutwTC/WLyT4RlxFKXFdSziR03bLK9jZldXHPlZ6OGrbdXmNvwrUslj9LURIZv7M9wBJ9Z3bWRecx9M++Ox553/y65rhtDv8vY+/fRTgsFg3xXjHsBs+fBd3A31XPbt73e4JqUiqX16G/aJg7Cfl35G329HeRNPfLKf1cX1uP0hDDrBpLxEfrhgGHOGpjEiI+60639KKXGvWIF9xoyoSpT1B35wybAuN8gAVqOeH1xy3I5EZx2hEwibEb3T3KPw6Z2xL/D92m/XH/cPVlfmtith2eXDFQ4purVvH8gZMzu7wdPkN7z/1LYuQQc3PDjlpIW777333pzf//73ZU1NTae0PnRSMRRCnA88QyQfMFcIMQ74hpTynlN5w/5EhavnEGRXa5AFf/4Mu0nPuBwn43OcTMhNZFZRylH3S2tjJPEYYOO/Ye+HEfFrPNS1YLRjUGR217mNzo3PRaq2tDP1buKBr8yKXAB7pD1q9Cx1Nz8VGhsbj2noq3FyfB43X7z9GgXjJ5Ez8mhGkWd9FYFDzTjOzzzt92hqDfKXZfu5cHgaM4ZEkvh3lDdz1fhMZg9N5fzBycRZzmxwTtjlwpiZiePCC09+cD+h3SMU86WTKIi/JJ+XVh/kr4Umqi2CQT7JfQcD3DIjP9amnZDuQthOoDV8yp7Kl156KSElJSU0a9Ys77vvvntKAQvRvPljwCXAYgAp5VYhxAWn8mb9jUynlUnNS/mh4VUyRR0VMoXfh25knWMeP750OFtL66g+tJuDn+1HoZwpP/gtFmcSxa/+jMLd/4f4aSXCYIK6/ZFSVINGwahrIi2DkosgZUhX0WvHlnTsvjbEiWaiY2/sV+LXnYULFyLlccRc47isX/Q6Pq+HmbccLcattIZo/ugQpvx4rGNSTjD6WKSUHKr3snJvDTaTgRun5GAz6Vm0pZyMBAszhqQwKjOeVT+a26fdHwyJieS/9GKfnb+vuHpCVr8Uv+58mGHgN6OstLYtIFZZBb8ZZSUxw8B1MbbtRDO5f/1o1RhvU+AYV4EtwRQAsCeYQ9HMBDuzatUqx9KlS51ZWVkJfr9f5/F4dAsXLixYtGhRyclHR4hKiaWUR7p9aY7TK0dd/HnkfkZvfAariNR5zBZ1/Mn4d9yW5SSuCnJNQ0lkltd+w9x6DziTeN87Ar3pdu5tmwH+b/hLBHNuZUJuIhNynGQnWnt1kZGK5KVffcHYudmMvqD/fwl7IhwOo9fr+01rHbXQ0lDHpvcXM2LmHNLyCzv2Ny87jOIN4bxycI9/0+6BX9+5cAjJDnMk729fLYcbIisaFw5P48YpORj1OtY+OA9DW9rD2fg/KT4fOsvxI001Tg1FSp4pq+UvpTUdQthOK5LfHqzkuvTj33DHmsmX5Zd3XjME0Bt0yuTL8k+5hdOTTz5Z/uSTT5YDvPvuu3F/+tOfBvVGCCE6MTzS5iqVQggjcD+wu/fm9j+mHHgCRNeCx0YRJtG9H4ZdBiOv7nGW9507bqPFdyO0hb+X1ntYua+Wf60+BECKw8T4nEQm5DqZkONkbI4TxwkqhgR8IVJzHNji1bGu0hOvvPIKRqORG244TksmjR5Z8/pLKIrCjBtv69gXrPXi/rwC++R0TFmOLscHwwrvbq3gJ2/t6BL49aO2wC+rUc/5g5P5+qwCLhia2iVAxKDvfUWhUyVUX0/x3AtJ/+XDOK+++qy970BASkmFP8hej499Hh/7vJHnIruFx4bnohOCJw/XUBc8Nh8SoNzfv5cq2oNk+iKa9HSIRgy/CTxOpD9hOfARcG9fGnXWaCrreb8Shpv+e8KhnddX/v7lyYTCCnuqWthyxMXmwy42H2nk493VQCRG5vbp+fziqsi64YFaNwXJ9o6qE2abkflfHXXsm6iEYDDIwYMHmTSpb3v7qQ1FkXgCIcwGPSaDjgZPgH3VLYzLdmI16Vm3ZTfbP1mKbtQMntrYiNdfh8cf4sr9HnKQ/KSimurHKnn2jslkJ9p4+tMD/O/7e8hIsBwT+AWRm7DVP74Qs+Hs55d2R4bDJH75S1hGjoy1Karg+Yp61jd5OsTP0ynlIsVoYKjdQp7l6M3yZ1NHcOEXeyjrQfiyzLErzBAtY2ZnN/SV+F1xxRUtV1xxRUtvx51UDKWUdcBtJztOlSRkRyq59LS/lxj0OkZnJTA6K4EvTYtUe2nyBtlS5mLz4UaK0iJruvVuP/P+tJKfXDacuy8YTFNrkLW7a5hUlEzKCZKX+zOHDx8mFAqpuuqMlBJfUMETCOHxh/D4wx0/Fw2KI8tpparJxxubyrhibAZ5yXY2H27kmc9KcPtDeAMh3P4w3kDbWH+oQ7Ce/9pUZhalsOZAPfe+uIkl372AYelx7GyQrEuYyPbmQsKrD+EwGzhfGChyG3jLKZA2IwUmQ4dLc1JeEv9v/lAeXbqvx9+h3h3oF0IIYExLY9APfhBrM/oFYSk54gtQ4vUzNzlSBu4XxeWscblZMjkSdLm4ppE9Hh/D7BZuTk9iqN0SedgsJJuOvUzHG/Q8WJjB9/ceobVT0J1VJ3iwMOPs/GIDjGiiSf8FHBMVIaX8ap9YdDaZ91Cfpisk2IzMHprK7KGpHfvMRj2P3jiOcTlOAL44WM/Wf+zhVWOY/dlGJuQejV4dmRHfUVMR+kmBgB4oLi5Gr9eTn5/fZX9f29ve7qZduLydBMwTCOP1R34+ryCZmUUpNHgC/OC1rXxpeh5zh6Wxq6KZO//9Rce44wXy/vbaMdxyXi41LT7+sGQvwwbFkZdsxxsIs7e6BbtJj91sIMtpwm7WYzMZcLQ928168pIj1VLOK0jixbumkp0YyVm7eeZQrpv2U2xmPUa9DhlWqP7zJrDAfd+diDB0dWtOyktkUl4iL68/QnkPkdCZfZALdyrIQADvps3YJk1EGPv/LOVMEZaS0tYA+zy+iIvTG3ku9vrwtX249s8aQ5xBz3C7pUu6/H/HFmLuZWH89nXB3x6spNwfJMts5MHCjH69XtificZN+m6nny3ANUBF35hzlolBuoLDbODaiUdnnmPi7eyTginj0zCZwqw72MCiLZE/r8mgY1RmPBNyEslLtvLIB3vPfoGAKCguLiYvLw9Tp1yy4xU0qG3xc+mY9I5yWou3VrTNxCIzqsgMK4Q3EO6YcZ0/OIV75w5BSsnEXy3l9vPz+e5FQ2n0Bpj5u+UntE0I+LYQzCxKQS8EVc0+WgMRm5w2I3OGpmE3G3oUMbvZgM1kIL9NzEZlJrD31wswta29zRiSwsffi76ySmqcmdQ4M1JKPnr6CYrOO5+C8Uddy4o3hD7ehGNm1jFC2Jn+ngvn3biRw3d+leynniRORWkV7bxR1XBSgakLhFjX5GZOUhx2vZ5nymr51YEK/J3uqLLMRobaLcxITGGY3cIwmwVLm+DdnJHc5Xy9FcJ2rktP0sTvDBGNm/SNzttCiJeAVX1m0dkmxukK9YciFT9uvnwo3xwUuehWNrWy5bCLzUdcbDns4sUvSnFaTT0WCPj5oh38d20pipTItn6oSIkiQRLZZzXqef1b5wPw2w92s7/azT/viFQEeeCVLWw54kJKiYSj55F07MtJtPHqN6cDcPdzGwB4+iuRDvXXPf4xYxprWVZr4w//syQyHmgNhI9xJ7QGwzzy4R52VjTx55sj7Xh+9Pq2Lr+XxajDbjJgM+uxmwzYOwUeCSG4bmI2Y7IigUzxFiO/v25s5Fizoe14fcd4h9mAxaDvWJtNsBl57zuzOs6X6bTyu+vHRv2/0usE+jNQ77W1pZmy3TtJzc0HjoqhPs5Eyl0n71zW33Ph3CtWIEwm7NOmxdqUXvNGVUMX12OZP8gDe47wXq0Lg07HN7JTmZRgZ2Ozh6/tOMR7E4uYlGBnpN3K17JSGWo3M8xupchmxtFPXNYa0XEqSY5FdHTP1DhdKva7sMWbSEg76uLKSLCSMcbaUXU/FFYo+ukHPY5v8YWwGvUdFdmEEOhEpGKhEAIBWExHv5SpDnPHzAggP9lOSJEIiIxrGyOEQLSdJzXuaEWLKflJXaq/TU0J4m2EsSOGMc6WgK5t/DOreo5qDiuyS63I9++fhdWox2bWYzPqTxrx+LMrjgZkmAw6bpyivnY1tvgE7vjjk11yMj0bqjEPcWKIsnpIf86Fc69YiW3aVHS24xfT7q/89mBllzU4gICUvF/XTL7V1BHBOd3p4MNJQxluj6zzn5/o4PxExzHn01AP0awZthCZcIi25yqg5/bVGr1CSknFfheZRc4T5n0Z9DoyndYe14mynFaev2tq1O9516zCLtv3X1R0nCN75usXdB2fb2zhSFwc37txepff4YMdVce1d3zbeilAQcq5VV+/5tBBnIPSMVmPCkXYE8S1uBj7eRk4ryg8wej+j7+khEBpKYm3H9uiSQ0cLy1BAGunHb0RizfoGR+vPrHXOD4ndVRLKeOklPGdnod2d51qnBot9T7cjf6uLZuOww8uGYbV2NXtEut1onA4zIEDBxgyZMgxYt4f7Y01oWCQRX/8DYsf/W2X/Xq7kUHfnUT8heqb5XbHvWIlAHGzz16XijPJ8dIS1JCuoAFZWVljhg4dOnL48OEjR48ePaI3Y487MxRCTDzRQCnlpt68kcax9Ni/8Dj0x3UiKSVXXnklTqfzmNf6o72xZtvHH9BcW838u46W9Q27A+jsxqOte1SOe8UKzEVFGLPU+X++OyeVh4q7xgdq6Qpnni1L309a+/pLWR5Xo8nuTAxMu/6W8vHzLzsjeYcrV67cl5GR0XNFghNwIjfpn07wmgTUFybWz6jY78JsN5CUEZ2rsL+tExkMBkaPHn3c1/ubvbHE7/Wy9o2XyRk1lrxxkftMGVSoeWor1uFJOK9Sb45mO+HmZrwbN5J8552xNuWUuWZQImtdbjY1e6kOhLR0hT5gy9L3k1b85x954WBQB+BxNZpW/OcfeQBnShBPheOKoZRy7tk05FykfL+LzCHOExfn7sds2bKFnJwckpOTT37wOc6Gd9+itaWZC269o8Ol3LKqnHCDD8vIgXGh9axeDaEQjrlzYm3KKZNqMvLPMepet+0PvPCTB467HlJzqMSuhENdi10Hg7rPXvx3zvj5lzW4GxsMi/7wqy53h7f972NRF+6eN29ekRCCO++8s/b73/9+XbTjokpuEUKMFkLcKIT4SvsjynELhBB7hRDFQogf9/D694QQu4QQ24QQy4QQeZ1eu10Isb/tcXv3sWpHSsmcW4cxYX5urE05JVpbW3n77bfZsWNHrE3p93hcjWx89y2GTp1B+pChAISb/bQsP4xlVDKWIYkxtvDM4N28GX1CAtZx42JtyilR6Q+wpK4Jv6Kc/GCNU6a7ELYT8HpPu9n8qlWr9uzatWv3Rx99tP8f//hH2gcffBB1iG800aT/A8wBRgLvA5cSyTN87iTj9MCTwHygDFgvhFgspdzV6bDNwGQppVcI8S3g98BNQogk4H+AyURcshvbxjZG+4v1d4QQ5IxQ74zAarXywAMPoNdruVQnY+2brxAKBphx89F7yKYPDyHDEudlp9ZBvj8y6MEHSb7rro7u7Wrj7WoXDx+oYN20EeRZY98gV82caCb3t298eYzH1XhMVwK7MzEA4EhMCvVmJtiZgoKCIEBWVlbo8ssvd61Zs8Z+6aWXuqMZG83M8HpgHlAlpbwTGAf00KTvGM4DiqWUB6WUAeBlYGHnA6SUy6WU3rbNtUB7aZZLgKVSyoY2AVwKLIjiPVVDybY6KvarW9sTEhJwOLTcqhPhqqpk28cfMubCi0nKjKyfBo604N1UQ9ysLAzJ/aOE2plACIExTb0pyHdlp7JowhBNCPuYadffUq43GrtMv/VGozLt+ltOuYUTQHNzs66xsVHX/vPy5cvjx44d23MH9x6IZlraKqVUhBAhIUQ8UANEEwOeBXSugl0GnCgh7mtAe2Z5T2OPicQQQtwN3A2Qm6sud+O6RQexO01kFqnPRaYoCosWLWLcuHEUFmrrKydi9avPo9PrmX7dLUDEPe565wC6OCNxc9WfStFO/bP/xH/wABm//rVqe1oadYKpTu3mrq9pD5I509GkZWVlhmuuuWYIQDgcFtddd1399ddf3xzt+GjEcIMQwgn8A9gIuIE1p2Ls8RBCfImIS7RXyUlSyqeBpwEmT56sqhbr1/5gIj53/+47djyqq6vZunUrBQUDx8XXV4y58GJyRo3BkRQJMmrdUkvgcAuJ1w9Fd4Iel2pD8bhRmptVK4QvVNRT2urnx4UZ6FT6O6iJ8fMvazjTkaMjR44M7N27d9fJj+yZE+UZPgm8KKVsT4r6mxDiQyBeSrktinOX03UGmd22r/v7XAT8FJgtpfR3Gjun29gVUbynajBZDJgs6rwYFhcXA6i6ZdPZInf0OHJHRwJKFH8Y1wclGLMd2Caq153YE6nf+U6sTTgt/lVeh1knNCE8hznRmuE+4I9CiENCiN8LISZIKQ9FKYQA64EiIUSBEMIE3Aws7nyAEGIC8HfgKillTaeXlgAXCyEShRCJwMVt+wYEOz4tZ9OS0libccoUFxeTnp5OXFxcrE3ptxzZuY3l//kHfq+3Y5/QCRzTM3BeOVi16TQ9oXg8Xeqsqo0DXh873K0sTHPG2hSNGHJcMZRSPi6lnE7EdVkP/FMIsUcI8T9CiKEnO7GUMgTcR0TEdgOvSil3CiF+KYS4qu2wPwAO4DUhxBYhxOK2sQ3Ar4gI6nrgl237BgQ7Pyvn8M76WJtxSvh8Po4cOcKQIUNibUq/prJ4Hwc2rEVv6NR1w6gjfm4u5rz4GFp25in/3v/jyNfuirUZp8ziGhcAV6Q6Y2qHRmyJpoVTKfA74HdtM7l/Ag8BJ42fllK+TyQdo/O+hzr9fNEJxv6z7b0GFP7WEHVlbqZclh9rU06JkpISFEXRXKQn4byF1zNhwRUY2no8ut45gLkgAevolBhbdmZRWlvxrF2L88bYtUE7XRbXuDgvwU6m5Zhof41ziJOmVgghDEKIK4UQLxCJ9twLXNvnlg1QKotdIKOrR9ofOXDgACaTiZycgRMJeSZRwmGqD0bWVI3mSL1RxR/Gf6CJYLX3RENViWftWqTfj2OOOgtz7/P42O3xcZXmIj3nOa4YCiHmCyH+SSSt4evAe8BgKeXNUspFZ8vAgUZlsQudTjCoMJpUzf6FlJLi4mIKCgowGNQZ/NPX7Fi+lOcf/C6VxUdzhnVmPWnfnkDc7OwTjFQn7hUr0dls2KZMibUpp8TiGhcCzUWqceKZ4YPA58AIKeVVUsoXpZSes2TXgKViv4u0/DiMJvVV6aivr8flcmnrhcch6Pfx+esvkjlsJOmDI8vq/tJmlNYQQi8QhqiqH6oGKSXulSuxz5iBzqROF+PiGhdTE+ykq7RFU2XVIlavnsWyT4awevUsKqvO7XlKXV2dfsGCBYUFBQWjCgsLR3388cdRN0w9UaFurSvFGSYYCFNzqIXxKq1HGggEKCws1MTwOGx6fzGexgau+O6PEEKg+ELUP7cLU148KV8ZefITqAz/3r2EqqpwfPvbsTbllNjjaWWf18f/Fqmzs0pl1SL27PkpihIpsuLzV7Bnz08ByEhfeKKhMce9tiKpedmRLKUlYNLFmQLx83LKHdMyTztI8u677865+OKLmz/88MODPp9PuN3uqO9ANV/XWaTqYBOKIlW7XpiZmclXvqLODuZ9TWtLM+sXv0HhpPPIHj4KgOZlh1G8QeLnqfPm52S4V6wAwDH7gtgacoqYhY4vZyar1kV68MAfO4SwHUVp5eCBP/ZrMXSvrUhyvVuSR0jRASgtAZPr3ZI8gNMRxPr6ev26deviXn/99UMAFotFWiyWcLTjNTE8i1TsdyEEZAxW33phKBQiEAhgs9libUq/ZN3br+Fv9TKrrRh3sNaLe3UF9snpmLIGZokv9/IVWMaOxZCizgjZApuZPwxTbyCYz1/Zq/1nk+q/bj5uC6dgpcdOWHZNtA0puqYPD+U4pmU2hJsDhrrndnYJVx9034STFu7eu3evKSkpKXTDDTfk79q1yzZ27FjPP/7xjyPx8fFRtSGJJpr0d9Hs0zg5er2O3NHJmKzquwc5dOgQf/jDHygtVW+xgL6iua6GLUveZdQF80jJzQeg6b2SSF7hxXknHqxSQvX1tG7bptpZYZkvwKYmdRcLsJgzerW/39BdCNuQvvBpXRhDoZDYvXu37d57763dvXv3LpvNpvz85z9Pj3Z8NG8+H/hRt32X9rBP4yRMVmluIUBSUhIXXHABGRn9/IsWAz5/7UUAzr/xVgB8exvw7Wkg4bIC9HHqDCw5Gfr4eHKffQZjrjrF/vmKev5SWs22GaNJManv5hSgcPD3u6wZAuh0VgoHfz+GVkU40Uyu4jfrxigtgWO+GLo4UwBAH28KRTMT7E5+fn5g0KBBgQsvvNADcNNNNzU+8sgjUYvhiVIrviWE2A4Ma2u+2/4oAaItyabRhhJWd8PQpKQk5s6di0mlUYN9Rd2RUnat/ITxF19OfEoaMqzgevcghhQrjvMzY21enyGMRuznn48pW53BJ9/KSeX5sYWqFUIAmzWPROc0zOZ0QGAxZzJ8+G/69XohQPy8nHIMuq4XRINOiZ+Xc1otnHJzc0Pp6emBrVu3mgE++uij+GHDhvmiHX+iT8KLRJLsfwt07lLfMpBKo50tNn10mF2rKrj1f6ZiUFlahdvtpqKigoKCAoxGdYag9xX2xCSmXHUtk6+M1KFwr6kkVNtK8u0jB1wqRTsyEKD2qadIuGoh5kJ1di5JMBq4MFndZfHKyp/H1bSeWTPXoNerZy2/PUimL6JJn3jiicO33XZbYSAQELm5uf6XXnrpULRjT5Ra0QQ0Abe0da0f1Ha8QwjhkFIePl3DzyWSsxwUjktVnRAC7N27l3feeYd77rmHNBU3b+0LrI44Zt16BwBKIEzzssOYi5xYhifF1rA+xLdvP/XPPIt17FhViuHzFfUEFIWvZqfG2pRTJhhspqbmAzIyrlWVELbjmJbZcCbErzvnn39+644dO3afytiT+giEEPcBvwCqgfaprQTGnsobnqsUjE2hYKw6o+6Ki4uJj48nNVW9F48zjZSSZc8+xbDps8gZFfkq6Ex6Uu4Yhc5mUG1fv2iwjh7F0DWfI8zq6wgvpeQvpdUMtplVLYZV1YtQFB+ZmTfF2pQBQzQO8+8Cw6SU6myz0A9odQcIBxUciZZYm9JrwuEwBw8eZNSoUQP6At9bPK5GSrZsJDWvgJxRY5GKROjEgOtIcTz0Km3ftbWllcO+AN/NHxRrU04ZKSUVFS8TFzeK+LjRsTZnwBDNosYRIu5SjVNkz5oq/vPg53ibA7E2pdeUlZXh9/u1LhXdcCQmcedjf2f03IuRUlL3rx00fXgo1mb1Of6SEg596Uu07twZa1NOicU1LgwCLk1RX65vO80t23C795CZeXOsTRlQRDMzPAisEEK8B7R3okdK+WifWTXAqNjvIiHNii1efZGYBw4cQAhBYWFhrE3pN9QdPkRCegZGU8RNKEMKxlQb+kT1uQ17i3vFSlo3bESf4Iy1Kb1GSsni2kZmJ8aTaFRvFGlF+cvodFbSB10Za1MGFNHMDA8DSwETENfpoREFUpFUFrtUW4KtuLiY7OxsrFZrrE3pF4QCAd585GHee/wPHfuEQYfzqsE4pg78HEz3ypWYi4pUmVKxudlLmS+o6nZNoZCb6pp3GTToCgwG7TJ8Jommue/DAEIIm5Ry4DVk62PqKzz4vSFViqHH46GiooK5c+fG2pR+w5aP3qOlvpYF93wXAM/GagyJFswqbMnVW8ItLXg3bCD5zjtjbcopsajWhVEIFqSod123uvpdwmEvWVrgzBknmnJs04UQu4A9bdvjhBBP9bllA4SK/S5Anc18Dxw4AKB1qWjD7/Ww7q1XyRs7gdzR4wg3+3EtKqZl1WnlCqsGz+rVEAqpspGvIiXv1riYkxRHgopdpAkJE8jPv4/4+PGxNqXfsXXrVvPw4cNHtj8cDseEX/7yl1HngkXzqfgzcAmwGEBKuVUIoc6ChDGgYr8LR5KZ+GT1uRkrKyux2WxaCbY21i9+A5+7pSOvsOnDQ8iwxHmZ+nLtTgX38hXoExKwjhsXa1N6zcZmL+X+IA8Wqvuz7HAMw+E4bg1s1bB+/fqklStXZrndbpPD4QjMnj27fMqUKaeVdzhu3Dj/nj17dkGksUB6evq4m2++2RXt+KhukaSUR7qF1UfdFuNcRkpJRbGLnBGJsTbllLjkkkuYNWsWOt3ArKTSG9yNDWx8bxHDzr+AQQWDCRxpwbupBsfsbAwp6rvR6S0yHMb96afYL7gAYVDfzMooBJenJnCJiqNIq6oWY7PlEx+v7hTv9evXJy1ZsiQvFArpANxut2nJkiV5AKcriO0sXrw4Pjc31z906NCoQ/ij+VQfEUKcD0ghhBG4HzilDP9zDVe1l9bmAJlDnLE25ZTRWjZFWPP6iyjhEDNv+jJSSlzvHEDnMBI/V70tgHqDb/t2wo2NqnSRAoyPt/HsaPXO4KUMU1z8CImJ0xg1qv8H8j/99NPHnb5WVVXZFUXpMrsKhUK6jz/+OGfKlCkNLS0thpdeeqlLLtfdd9/dq8LdL730UtL111/fq9z4aG75vwncC2QB5cD4tu2TIoRYIITYK4QoFkL8uIfXLxBCbBJChIQQ13d7LSyE2NL2WBzN+/U32tcLs4aqb2a4bt06Xn31VcJhzQnQUFHO9k8+YuxFC3CmZ9C6pZbA4RYSFuSjs6hvlnQqtKxYAXo9jpkzY21KrynzBTjiU1+Ob2eE0DNt2hIGD/lhrE05bboLYTt+v/+MfJl8Pp/4+OOPE7785S839mZcNNGkdcBtvTWorZ7pk0RaQJUB64UQi6WUuzoddhi4A+ip50irlHJ8b9+3P1E0eRCOJAsJaepzo4VCIYLBIHq9+mqpnmlWv/JfDEYT0669GSUQpumDEoxZDmwT1VvFpLdYx48n5RvfQJ+gPjfjU4dreKmynp0zx2DTq9flbzDEqSad4kQzuT/+8Y9j3G73MUnXDocjABAXFxfq7UywM6+//nrCyJEjvTk5OaHejDuuGAohfiil/L0Q4gkitUi7IKX8zknOfR5QLKU82Ha+l4GFQIcYSikPtb2m7v5Gx8FkNZA3KjnWZpwSM2bMYMaMGbE2o18wavY88saMx+5MpOmjQ4SbAyTdOhyhO3fK08XNmUPcnDmxNuOU+EZOKjMSHaoVQo+nmB07H2DkiEeIixsVa3NOm9mzZ5d3XjMEMBgMyuzZs89IWPbLL7+cdOONN/Z67fFEM8P2dcENp2YSWURKubVTBkztxXiLEGIDEAIekVK+3f0AIcTdwN0Aubm5p2hm3+Bu9LP78wpGnJ+hupqkgUAAo9Go1SJto3DiFCBSaca7qQbruFTM+eqbIZ0qvn370NlsmLKzY23KKZFnNZNnVW91oPKKV/B49mM2DwxPRHuQzJmOJgVobm7WrVq1Kv4///lPaW/HnqiF0zttz/85HeNOgzwpZbkQohD4RAixXUp5oPMBUsqngacBJk+efMzsNZbUlDbzxbslFI5PxaGyJcOPPvqIQ4cOce+9957Tgli6fQtHdm5j6jU3YjRbEAYdg747ERkakI6M41L7p0fxHzzI4I+WqO7z8EJFPYlGPZelOmNtyimhKH6qqt4iNeUiTCZ1dr3piSlTpjScqcjRzsTHxysul2vLqYyNJul+qRDC2Wk7UQixJIpzlwOdQ+2y2/ZFhZSyvO35ILACmBDt2P5A4fhUvvbHWSRl2GNtSq+QUlJcXExycrLqLnxnmvI9O9mzeiVCpyfk8iPDCjqLAb1DfTVmT4dBP3mQjF//WnWfh7CU/PZgJW9Vu2JtyilTU/sRwWCj1qrpLBCNEz1VSulq35BSNgLRZPWvB4qEEAVCCBNwM22J+yejTXDNbT+nADPotNaoFix2o+rWlerr63G5XFrVGeD8G27jK79/Ar3eQP1/dlL3H9V9BM8Iprw87FPPi7UZvWaNy01dMKTqWqQVFa9gsWSTlKSt3/c10YhhWAjRsSAnhMijh4Ca7kgpQ8B9wBIi64+vSil3CiF+KYS4qu1cU4QQZcANwN+FEO19YUYAG4QQW4HlRNYMVXMl8jT5eecvW6guaY61Kb2muLgYOLdLsIVDQeqORJYcTFYbCIi/KA/H+Zkxtuzs43r9dZo/jMYR1P9YXOPCptcxL1mdtUi93lIaG9eQmXkjQqgz+EdNRJPX8VNglRBiJSCAWbQFrZwMKeX7wPvd9j3U6ef1RNyn3cd9DoyJ5j36IxX7XRze1cDUhepre9TuIk1MVNlC5xlk27IlfPKvv/PlRx4nLb8QIQRWlUYFnw5SSmqf+CvWsWOIX3BJrM3pFSFF8m6ti4uT41UbRVpR+SpC6MnMuP7kB2ucNtHkGX4ohJgITGvb9d223EON41Cx34XRrCcl2xFrU3pFMBjk0KFDTJo0KdamxIyAr5W1b7xM9vBRpOYV0PTRIYROEDcvV3VrZqeLf88eQtXVOOZ8O9am9JrVLjcNwbBqXaSKEqSy8nWSk+cOmCjS/s5xb5mEEMPbnicCuUBF2yO3bZ/GcajY7yJjcAI6ld2RHj58mFAodE53td/43tt4m1zMuvUOQnWttKwoI+Tyn3NCCOBesQIAxwXqq8u/uKYRu17H3CR1ukjr65cTCNRprZrOIieaGX6PiDv0Tz28JoEL+8QileNzB2mo8FA0RX13c8XFxej1evLz82NtSkzwNjexfvGbDJkyncyhw6n7906EUUfCJfmxNi0muFesxDJmDIbU1Fib0iuCiuT92iYWpCRgVdkNaTvJyXMYM+YpkpLUdyMSSx5++OG0//73v6lCCIYPH+595ZVXDtlstqjS7k70SVna9vw1KeXcbg9NCI9DRbELUGf/wuLiYvLy8jCZzq3UgXbWvfkKIb+fmTd/Bd/eBnx7Goi/MBd93Ln39wjV19O6bZsqC3OvamyhMaReFymATmciLfUSdLqBWfu2rOyFpM9WTR+z7JMhkz5bNX1MWdkLSad7zpKSEuPTTz89aMuWLbv279+/MxwOi2eeeSbq855IDB9se3799Ew8t6jY70Jv1DEoT33umSuuuII5Ki25dbo01VSzden7jJ57EUkZWbjePYgh2YJjxrkXQQrg/vQzkBKHCj8PQsBMp4PZieqo49mdI2X/peTQk0jZr+qInDHKyl5I2l/8m7xAoMYEkkCgxrS/+Dd5Z0IQw+Gw8Hg8umAwSGtrqy47OzsY7dgT3XY0CCE+Agp76hohpbzqVIwd6FTsd5FeEI/eqD73TF5eXqxNiBmfv/o8QuiYfsOtuNdWEqptJfkrIxEG9f0fzwTuFSswpKZiGTky1qb0mjlJ8cxR6VohQHPzVoLBBlWvU69ff81xWzi1uHfbpQx2+eUUxa8rPvCHnOzs2xr8/hrDtm3f6BK4MGXKWyct3F1QUBC89957qwoKCsaazWZl1qxZzddee23U+W0n+qZfBjwE1BJZN+z+0OhGoDVE3ZEWMlToIt28eTOlpb0u5zcgqC0tYdeqFUy49Eps5gSalx7GXOTEMuK0b1RViQwE8KxahWPObNVdkCv9AVpC6m47NmrkHxk75m+xNqPP6C6E7YTDLaflE66trdW/9957zuLi4u1VVVXbvF6v7qmnnor6S3yiN39WSvllIcQ/pJQrT8fIcwV/a4iC8ankqOwiqigKH3/8McOGDTsnZ4e2BCcTL72K8xbeQPPSUmQghPOKQtUJwZkiVFeHedgwHHPVFxrwvwcrWdnQwpbzR6FT4f8vGGzGaIxHp1P3OvWJZnKfrZo+JuIi7YrJlBYAMJvTQtHMBLvzzjvvxOfm5vozMzNDAFdffbXr888/d9xzzz1R1UA90cxwkhAiE7itrTxaUudHbw09F4hLsnDpN8aorrO9Tqfj/vvv58IL1XfxOxPYnYnMvf3rmG12wk1+7FMzMA5SV03ZM4kxM5P8F18g7sK5sTal19yZlcLDQ7JUKYR+fw2rVk+lonJgh2kU5N9XrtOZu1S71+nMSkH+fafVwik/Pz+wadMmR0tLi05RFD755JO4ESNG+KIdf6KZ4d+AZUAhsJFI9Zl2ZNt+jU743EEsDmOszTglTCbTORdFKqVk2T//xshZc8kcGulPmHL7KGT43OpK0Z2w24Peoc6bgYnxdibGq9P2ysrXUZQAzoSBXfQiO/u2BoCSQ3/NCgRqTSZTaqAg/77y9v2nyoUXXui58sorG8eOHTvCYDAwatQo7/e+973aaMefqIXTX4C/CCH+T0r5rdMx8lwgGAjzrx+t4rwrC5i0ID/W5vSKRYsWkZuby4QJqmoMctq01NdSvH4NgwoGk2zORO8wYki2IlSam3Ym8JeUcPCqhWQ9+ifi58+PtTm94pXKBgptZqYkqE8MpVQor3iVROc0bLaCWJvT52Rn39ZwuuLXE4899ljFY489VnEqY0/6rZdSfksIMVMIcSdEukgIIQb+f6uXSEUy/ZrBqlsv9Hg8bN68meZm9RUVP13iU9L42p+fZuQFF+J6q5j6F/cM2HD2aNGZzSR95ctYR6mro7ovrPDT/WW8VFkfa1NOicbGNfh8R7RWTTHkpNE7Qoj/ASYDw4B/ASbgeSJtlTTaMFkMjL8o9+QH9jMOHIj0Sz7XulTUlx3BmZ6B0WIBIOWrowi7g+ds0Ew7xsxMBv3gB7E2o9csb2jGHVZYmKbOAvPlFS9jMDhJTVVXQfSBRDT+oGuAqwAPgJSyAlBnNmsfUr63EW9zINZm9Jri4mJsNhsZGRmxNuWsEQz4ef03P+ODv/4JGQwjpUQfb8aUqa7C6measNuDZ+06ZDDqPOV+w+IaF0lGPTOc6vsfBgL11NYuJSPjGvR6c6zNOWeJRgwDMuI7kgBCCPU55PuYcEjhnb9uZdOH6srTUxSFAwcOUFhYiE537qyTbf7gHdwN9Yy7+DIa3z5A3bM7kMq57R4F8Kz6jMN33EHrtm2xNqVXeMMKS+qbuTzViUFlzbQBKqveRMqg5iKNMdFcAV8VQvwdcAohvg58DPyjb81SFzWlLYSDiurqkVZXV+PxeM4pF6nP7eaLRa9RMGEyg+Ly8W6sxpjlQKjwInqmcS9fgT4hAev48bE2pVd8Ut+MN6xwVaoz1qb0GiklFRWvkpAwEYe9KNbmnNNE08/wj0KI+UAzkXXDh6SUS08y7JyiYn8jABlFCTG2pHe0d7U/l1o2fbHoNfxeLzNv/gqudw6gcxiJn5sTa7NijgyHcX/6KfYLLkDo9bE2p1csrnWRYjQwXYUuUq/3IK2tR8jL+0asTTnnidY3tg1YCawAtvaZNSqlYr+LpEw7Voe68vSKi4tJT08nLu7cWAJuqa9j8wfvMHLmHBwuB4HDLSRcko/OMjA7A/SG1m3bCDc2qq5LhSccZmldM5enJqjSRWq3D2bmjNUMSrsi1qYMCH71q1+lFRUVjRoyZMioX/7yl2m9GXtSMRRC3Ah8AdwA3AisE0Jcf2qmDjyUsELlgSbVVZ3x+XwcOXLknHKRrnn9RaRUmH7NrTS9X4Ixy4Ftkvr6TvYF7hUrQa/HMXNmrE3pFR/XN9OqKKps19SexmMyJaPXW2JszdnlP+V1SeNW7xiTsXzLpHGrd4z5T3ndaeekrV+/3vLcc8+lbtq0affu3bt3fvjhh84dO3ZEHZEUzczwp8AUKeXtUsqvAOcBPz9VgwcadWVugr6w6tYLA4EAY8aMYdiw4xaXH1DUlx9hx/KPGTf/MsROP+HmAM4rC7W1wjbcK1dimzgRfYK6XP1BRTIp3sY0FbpIy8qeY8PGmwiFWmJtylnlP+V1SQ8Vl+dVB0ImCVQHQqaHisvzTlcQt2/fbp0wYYI7Li5OMRqNzJgxo+Xll192Rjs+Gv+QTkpZ02m7nujdqwOeiv0uQH3NfOPj47nmmmtibcZZY9VLz2G0mJk87xqa/r4P67hUzPnquvD3FcHKSvx79pCmwvzC69OTuD5dXYUu2jEY4jGb0zAYBt4yxYIN+457l73T3WoPStnlLtSvSN1vDlTk3J6V0lDtDxpu317SJZDhw8lDT1q4e/z48a2//OUvs6qqqvR2u10uXbo0Ydy4cZ5obY5G1D4UQiwRQtwhhLgDeA/4IJqTCyEWCCH2CiGKhRA/7uH1C4QQm4QQoe6uVyHE7UKI/W2P26N5v1hQsd9FQqoVu1M9+UFSSmpra8+paivDZ1zA7C99jcBndQgBCZfmx9qkfoN7ZaQpjWPunJja0VtqA0FCKk6Jyci4hjGjn4i1GWed7kLYTnNYOa3F+4kTJ/ruv//+qnnz5g2dO3du0ahRo7z6XgSDRRNN+gMhxLVA+2LC01LKt042TgihB54E5gNlwHohxGIp5a5Ohx0G7gC+321sEtBe+UYCG9vGNp78Vzp7SCmpLG6iYFxKrE3pFXV1dTz55JMsXLjwnKlHOmz6LKSUuFdVYMqOw+A8t9ZoTkTrtu0Yc3MxFairyuIP95ZxxBfg4ynqc/U3NW3C4Rg5YNcKTzSTG7d6x5jqQOiYaMNBJkMAYJDZGIpmJtgTDzzwQN0DDzxQB3DfffdlZWdnR10J5bhiKIQYAgySUq6WUr4JvNm2f6YQYrCU8sBJzn0eUCylPNg27mVgIdAhhlLKQ22vdW8TcAmwVErZ0Pb6UmAB8FK0v9jZQAjBrQ9PJRRQV5cDu93OlVdeeU6kVBzatpmq4n1MvuIaDCYTcbOyYm1SvyPjN78m3NioulJ0t2Qk4VJhI99QyM3mLbeTnn4tw4c9HGtzzjrfy08vf6i4PM+vyA7PpFknlO/lp59WCyeA8vJyQ1ZWVmj//v2m9957z7l+/fo90Y490czwz8CDPexvanvtypOcOws40mm7DJgapV09je2XVzG1pVMA2Gw2Jk0a2G1i2indtpniL9YwuuAChNRhm5Cmuot+XyOEwJCkvnW3i1PUueZbXf0O4bCXjPRzZ82+M7dnpTQAPHqoKqsmEDKlmQyB7+Wnl7fvPx2uuuqqwS6Xy2AwGOSf//znwykpKVHfLZ1IDAdJKbd33yml3C6EyD8VQ880Qoi7gbsBcnPPfpHsTUtKMdsMjFLRbCMYDLJt2zaGDRuGw6G+CLzeMvtLX2XqNTfifq0UpTWEbUKvUo8GPDWP/RmlpZn0hx6KtSm94q3qRsbF2Si0qWetvp3yipdx2IcRHz8u1qbEjNuzUhrOhPh1Z+PGjafkXoUTB9A4T/CaNYpzlwOdS3tkt+2LhqjGSimfllJOllJOTk1NjfLUZ47DO+s7oknVQmlpKe+88w6VlZWxNqVPCQWDNFZGPjIWu4Pkr4wk+SsjtVlhN2QwiBJQV4H5pmCI+3cf5j8VdbE2pde0tOykpWUHmZk3aZ/FfsaJZoYbhBBfl1J2qUMqhLgL2BjFudcDRW29D8uBm4Fbo7RrCfC/Qoj2fiwX07PLNqZc/b2JKCrril5cXIxerycvLy/WpvQp2z7+gBXPPcNXfvE4iVlZ6B0m9HZjrM3qdwz6ofrSKZbUNxOQkoUqTLQvr3gFnc5MevrVsTZFoxsnEsPvAm8JIW7jqPhNJtLP8KTObillSAhxHxFh0wP/lFLuFEL8EtggpVwshJgCvAUkAlcKIR6WUo6SUjYIIX5FRFABftkeTNPf0KmsK3pxcTF5eXmYTOpb64wWv9fL2jdeJmfkGFjrpaZiC+k/nHxOd7DviXBLCzqHQ3UzlEXVLrItRibE2WJtSq8Ih71UVS0iLfVSjEZ1rneeAEVRFKHT6fptrouiKAI47uzluGIopawGzhdCzAVGt+1+T0r5SbRvLqV8H3i/276HOv28nogLtKex/wT+Ge17nW0+e2UfQX+YC78yItamRI3L5aKuro6JEyfG2pQ+ZcO7b9Ha0syMC27B90EDCZfma0LYA0fu+jqGtDSyn/hLrE2JGlcwxMrGZu7OVl8gVHXN+4TDbjKzbo61KX3Bjtra2pGpqalN/VEQFUURtbW1CcCO4x0TTZ7hcmD5mTRsIFCytY60fHVVjjgXutp7XI1sfPcthk6dhW6jH5FswTFDPQFOZ4tQQwOt27aRct+9sTalV3xQ10RIospapBUVr2CzDcaZMDnWppxxQqHQXVVVVc9UVVWNpn9WKFOAHaFQ6K7jHaCV6z8FmutbaWnwMX6+ulr/FBcXEx8fTyyCjc4Wa998mVAwwHkjriS4sj4SNGPoj9/N2OL+9FOQEsecObE2pVcsrnGRZzExLi6aGL7+QzjsRUqFzMwbVTejjYZJkybVAFfF2o7TQRPDU6Cyox5p4okP7EeEw2EOHjzIqFGjBuSXEcBVVcm2jz9k/JzLCH3RhHmIE8sI9eXPnQ3cK1ZiSEvDMnJkrE2JmoZgiM8aW/hWjvpcpHq9jSmT30BKdQXcnUtot8ynQMV+F2abgeRMe6xNiZqysjL8fv+AdpGufvV5dAYDoxNnIf0hnFcUqu6ieTaQgQCeVatwzL5AVX+fD2rV6SJVFD/BoAsAIbRLbn9F+8+cAhXFTWQMcaqq/U9dXR1Go5ECldWfjJbqg8XsWb2SaXOvJ7C5AfvUDIzp6rlZOZt4N21CcbtV5yJtCoUZF2dltENdLtKamiWsWj0dt/uU88E1zgKam7SXeJr8uKq9jJyRGWtTesWkSZMYO3YsRuPAzLWzJTgZN/8y8oMjCFlaib9oYOdRng7u5SsQJhP2adNibUqvuCc3jW/lpKpqNgsQFzeK3Jy7sNuLYm2KxgnQxLCXdPQvHOqMqR2nwkAVQoC45BQuuuseAuVuwo0+LcH+BLhXrMA2dSo6u3pmzi2hMA69TnVCCGC3D2bw4P8XazM0ToLmJu0llftdGMx6UnPUU9dzx44dPPPMM7S0DLyO2lJKPvn336kuiaSNmLIcWEerq6XW2cRfUkKgtBTHnNmxNqVX3LG9hDt2lMTajF5TXf0eLteGWJuhEQXazLCXpBXEY7YbVVV5RqfTYTKZsKtoJhAtrupKdq9aSY4yFOP6EInXFWkJ9ifAlJVF7j+fxVykLpfd1YOcmFQWfKIoAfbue5iEhAk4nQMvt3CgIQZKt/PJkyfLDRu0O7BzEb/Xg+/zWkI1rSTfqp6KQBoDm5qaD9m+417GjX2GlJS5sTbnuAghNkopz3m1VtetVozxNPlpdaurwr/P5yMYDMbajD6hoaIcRQljttlJuCifpFuGx9qkfk24pYWaPz1K4PDhWJvSKz6sbaI+EIq1Gb2mvOJlzOZ0kpMviLUpGlGgiWEv2Lz0MP958HPCIfUkzq5fv57f/e53+Hy+WJtyRgn6fLz68I/59Iln8O1rBFBlcMXZxLdzF/X/+heh2tpYmxI1Vf4gd+4o4V/l6mrX1NpaRkPDKjIzbkAIfazN0YgCbc2wFwybmk5KtgO9isp7FRcXk5KSgsViibUpZ5RNHyzG42pkSHgsDa/uJf2HU9CZtIvOibBPm8rQNZ+js6mn28O7tS4k6ku0r6h8DYDMzBtjbIlGtKjnqt4PSM2JY/i0jFibETU+n48jR44MuKozrS3NfLHodaaMuRJqQiRckq8JYZTo4+IQevX8rRbXuBhhtzDUrp6bOUUJUVn5OsnJF2CxqCsf+VxGE8Moaaj0ULKtjnBQPS7SkpISFEUZcGK47q1XUfxhChmDMcuBbdKgWJvU72ndupVDX/oS/oMHY21K1FT4AnzR5FHdrLCh4VP8/ioyM2+KtSkavUATwyjZu66KD/+2HUVRT/RtcXExJpOJ7OweW0aqkubaGrYseZdZY28EdxjnlYWqKosXK1qWL6d18xYMycmxNiVq3q11AepzkZZXvIzJlEJK8oWxNkWjF2hrhlFSud9Fal4cRrM6XExSSoqLiyksLMRgGDj/5s9fewGrIZ40TxbWcSmY8wdcx/A+wb1iJbYJE9AnqOfvtajGxWiHlcE29bhIpZTEx40h0TkVnU6rgqQmtJlhFIQCYaoPNZNZ5Iy1KVFTX19PU1MTgwcPjrUpZ4y6w4fY+eknXDDsJoQQJFyaH2uTVEGwshL/nj045s6JtSlRc8QXYGOzV3WzQiEEBQXfJjf3a7E2RaOXaGIYBVUlzShhqSoxLC4uBgZWV/vPXn6OzPjBxDXHEzc7G4NTPTOGWOJe+SmAqrpUvFvjAtTlIpVSoa7uExRlYOb1DnQ0MYyCiv0uEJAxWD0upuLiYpKTk0lMVE8D4hMhpaTovPOZOnQh+gQzjgsGzjpoX+NesQJjTg6mwsJYmxI1Ff4AE+Nt5FvNsTYlahob17B129eprVsaa1M0ToGBs5jUh1Tsd5GS7cBsU88awOWXX05zc3OszThjCCEYPecilPPDhOp9WipFlCitrXjWrMF5442qKkrwq6JsgioKVgNwOqcydszfSU6eFWtTNE4BbWZ4EsIhheqDTapykQIkJiaSlzcwevod2rKRjYsXEfIF0Jn0mDIGXsHxvsKzbh3S78cxWz1dKgJKJH3JqLIoYZ3OQGrqReh06pnNahylT8VQCLFACLFXCFEshPhxD6+bhRCvtL2+TgiR37Y/XwjRKoTY0vb4W1/aeSJqD7cQCiqqEsOtW7eybdu2WJtxxije+AXeTyup/etWlEA41uaoCveKFQibDdt5U2JtStQs3FTMT/aVxdqMXlFR8SoHDj6KlNrnU630mZtURAryPQnMB8qA9UKIxVLKXZ0O+xrQKKUcIoS4Gfgd0J6pekBKOb6v7IuW2sORHoCZQ5yxNaQXbN68Gb1ez9ixY2Ntyhnhoq99i6at5YjakOYe7SWOmTMx5eSiM5libUpUKFIyNzmOAhWtFUopKT38NEZjEqLwe7E2R+MU6cs1w/OAYinlQQAhxMvAQqCzGC4EftH28+vAX0U/W9gYMyebwRPTsMap42ICcPvttw+IwtyhQABvs4v4lDQSxmXF2hxVEnfRRbE2oVfohOCHBeopeQjgcn2B11vCyBH3xNoUjdOgL92kWcCRTttlbft6PEZKGQKagPYSGQVCiM1CiJVCiB5XpIUQdwshNgghNtT2YSV+W7x6hBAiwSZWqzXWZpw2W5a8y7s/+g3Vr+/Q3KOnQOv2HQTK1OVuXNXYgl9RT8lDgIqKVzAY4khLuzTWpmicBv01gKYSyJVSTgC+B7wohIjvfpCU8mkp5WQp5eTU1NQzbkRdmZsP/radxirPGT93X/Hee++xbNmyWJtx2vg8br54+w0mD1qAUtKqlVw7Bap/8xvKv/tArM2ImgNeH9dvOcBz5fWxNiVqgkEXNbUfkD7oavR69d+Ansv0pZu0HMjptJ3dtq+nY8qEEAYgAaiXUkrADyCl3CiEOAAMBc5qK3tvk5+6shbVlGALh8Ns27aNUaNGxdqU02b94jfI1g3GpjhwXlaIUFHbrP5C5iO/JexyxdqMqFnclmh/eap68nkrq95CUQJaUe4BQF9eYdYDRUKIAiGECbgZWNztmMXA7W0/Xw98IqWUQojUtgAchBCFQBFw1svt545K5su/Ph9HojoqnZSVleH3+1VfdcbdUM+ODz9ibMpczEOcWEYmxdokVWLKz8c6fnyszYiaxTUuzkuwk2lRx7KElJKKileIjxtLXNyIWJujcZr0mRi2rQHeBywBdgOvSil3CiF+KYS4qu2wZ4FkIUQxEXdoe/rFBcA2IcQWIoE135RSNvSVrcexn8gEVT0UFxe31UYsiLUpp8WaN15ihGMqBgw4ryhUVbJ4f6HhuedoUZG7fJ/Hx26PT1Xl15qbN+Px7NdmhQOEPq1AI6V8H3i/276HOv3sA27oYdwbwBt9advJaKj0sOixzVx812iyh6mjpFlxcTHZ2dmqDp5pqCjn8GebuDjzDuzTMjCmawn2vUUGAtT+5QniL11A3Lx5sTYnKhbXuBDAFanOWJsSNeUVr6DX2xg06IpYm6JxBtDKsR2Hin0uWluCxCWpw0XqdruprKxk7ty5sTbltFj90nNMSJ6HzmIg/qKBUUHnbOPdtAnF7VZVYe7FNS6mJthJN6un5GFW5s04nVMwGByxNkXjDKBFJRyHimIXdqeZ+BR1iOHBtg7mal4vrCreh3tHNWnmXBLm56G3q+fC2J9wL1+BMJmwT5sWa1OiYo+nlX1edblIARISJpCZcX2szdA4Q2hi2ANSSir2u8gscqpmvaq4uBibzUZGhroSljtjiYtnRNFM9KkW7NPU+3vEGveKFdjOOw+dXR0u5kXVLnSoy0VaUvJX3O69sTZD4wyiuUl7oKm2FW9TQDX1SBVF4cCBAwwePBidTr33N85B6Yz/yfUo/hBCr97fI5b4S0oIlJaS+OUvx9qUqNnr8THd6SBNJS5Sn6+CQ6VPYjAm4HAMi7U5GmcITQx7oGK/C0A1YhgKhZg4cSI5OTknP7gfIhWFVf/+D8MnX0Dq2MHozNrH8lRxr1gJgGOOerpU/HNMAZ6QeioMWSyZzJyxFp1OHeKtER3aVacHKva7sMYZSUy3xdqUqDCZTMxTSdRgTzRUlCG3ePEdKEcZkotORX0j+xvulSsxFw3BlK2O5sdSSoQQ2A3qKGzRbq/RqJ7CABrRofmieqBiv4vMIepZLywvLycYDMbajFMmOTuXST+5heSbRmhCeBqEW1rwbtigmihSKSUXb9jHE6XVsTYlaior32D9+msIBM5q2rPGWUATw260NPhoqfeRoRIXqc/n49lnn+XTTz+NtSmnRGNlBUpYwZ6ehG3sma8vey4Rqq3DOnYsDpWk17QqkvHxNrJVUnEGoKLiZUJhL0ajOnKPNaJHc5N2Q2/QMfWqAvJGJZ/84H6AwWDg5ptvJilJfSXLAq1ePv313xmZcj6F35+LXkVtsvoj5sIC8l98IdZmRI1Nr+MPw9Szzu1276WpeTNFQ36iGq+RRvRoYtgNW7yJyZepp5yZwWBg6NChsTbjlNi4aBEjrOdhdcSj03IKTwupKCheL3qHOhLApZRsd7cyxmFVjbCUV7yCECbS06+JtSkafYDmJu1G2Z4G/K2hWJsRFVJKVq9eTV/2cuwrvE0u3J+VYzPEk3r9SK1F02ni27aNfdPPx7NmTaxNiYpt7lYu3rCPN6sbY21KVITDfqqq3iY1dT4mk/q8MBonRxPDTnia/Cz68xZ2fta901T/pK6ujqVLl1JaWhprU3rNhlffoMg+EX2RHXOBFpl3uuiTkkj68pexjBwZa1OiYlG1C4OAC5OPaVPaL6mt/ZBQqIksrSj3gEVzk3bCYjey8IEJJKSqo9B1cXExAIMHD46xJb2jqaYKwzYFnUNP6nXquHj3d0y5uQz64Q9ibUZUSClZXNvI7MR4Eo3quASVV7yC1ZJLYuL0WJui0UdoM8NO6A06soclqqY494EDB0hOTiYxUV2RbZufW0SufTjW6WkYnOr4W/dnQg0NeNauQ6okvWZzi5cyX1A1tUi93hJcrnVkZt6EENolc6Ci/Wc7sXXZEaoPNcfajKgIBoMcOnRIdYW5qw8eILk8mZAxRPICdQb+9DdaPvqIw3fcQeDw4VibEhWLalwYhWBBijpcpJWVbyCEgYyM62JtikYfoolhGz5PkFWv7+fIrvpYmxIVpaWlhEIh1Ynh3v9+TKJ5EIlXDEZnUkfVkf6Oe/kKjDk5mAoLY23KSVGk5N0aF3OS4khQiYs0P//bTBj/HGazlgc7kNHEsI3KYhdI9dQjLS4uRq/Xk5ennp5/UkpS8wsIpIaIP089+WX9GaW1Fc/atTjmzFFFisKmZi/l/iALVeIiBdDrzSQmTo21GRp9jDpuzc4CFftd6A060vLV4bopLi4mPz8fk0k9iepCCEbcfnGszRhQeNatQ/r9qinMvaimEbNOcEmKOiKId+/5CfHx47Qo0nMAbWbYRsV+F4MK4jEY+7/rzuVyUVdXpyoXacmnX7D33x8TDqojh1MtuFesQGezYZsyJdamRMW6Jg9zk+KIU0FhbkUJ4PUewu+vibUpGmcBbWYIBHwhao+4mbRAHS7HxsZG7Ha7qsSwcdUhkppSUDxB9E7tY3cmkFLiXrES+4zz0anEQ/DBpKG4gupo16TTmZg08UWkVGJtisZZQLsqAVUHmpCKJHOIM9amREVBQQH/7//9P1WsEbUz/sfX4zlUi9GpjhxONeDfu5dQVRWOb98Xa1OiRi8Eyab+f9lRlBChkAuTKUVLpzhH6NP/shBigRBirxCiWAjx4x5eNwshXml7fZ0QIr/Taw+27d8rhLikr2z8yXOPcmnVZn51o5MFlZv4yXOP9tVbnTYPvfAoo5Z9TPonmxiz/BP+58XHYm3SCfnDi08wus3escs/4W9rX421SQOG/1vyDyaXHWHuUy8wMyGe/1vyj1ibdFye3buU0cs/If2TzRQtX8Wze5fG2qQTUlm1iFWrp/PZqql8tmoalVWLYm2Sxlmgz8RQCKEHngQuBUYCtwghupcb+RrQKKUcAjwG/K5t7EjgZmAUsAB4qu18Z5SfPPcoz2efT70uBYSOel0Kz2ef3y8F8aEXHuXfGV1t/XfG+Tz0Qv+zFSJC+ET6edS12VunS+GJ9PP4w4tPxNo01fN/S/7Bb41jqNOnRv62+lR+axzTLwXx2b1LebginjqSQAhacPBwRXy/FcTKqkXs2fNTgsFIv8JAoJY9e36qCeI5gJBS9s2JhZgO/EJKeUnb9oMAUsrfdjpmSdsxa4QQBqAKSAV+3PnYzscd7/0mT54sN2zY0CsbRy37OCIu3dDLEIOUYxuO3ldXxldv/gZ/fuEJnhtUyCMhHxcvuI6fv/gY76WdPIG8+/GvZecyePgYvv3qn1mdVHTCsdW6QYTFse6lZKWOC5qKWR+fe8LxBhlm7fwrAfjWm3/joC2RJQsiEXK3Lf4ne23pJxyfHGrpcnxAp+e1K24H4PIPXqTa6OxyfKUurUd7U5Q6CvzlZIe9/N+V3wLggiWv49WduBLNFN+RLsfPClTzmyvvxeNtYfbqlSccC3CJr7TL8beGyvjepd9k38Et3Hqg7KTjux9/v66eL8+7nZUbPuD/NZ58Daz78b9xhLhk+tW8tuJFfhc8eQRz5+O/qwzt8W+rlyEydT3nyf5n7BBGJg3mH3uW8nSVwpJpU0myOPnttnd4s+Hk642dj1/caGDN7EsB+P7Gt1jZYjvuuAqZTLiH1ZgUGtgx90J2736QpubNJ3xvszmdCeP/DcDu3Q+iKH5GjYrcBG7ddjetrScuNhDnGNnleKs1l6FFPwNg/YbrCYfdHcd6vSVIeWyQl8WcyYwZn53wfdSKEGKjlHJyrO2INX3pvM8CjnTaLgO6J+t0HCOlDAkhmoDktv1ru43N6v4GQoi7gbsBcnNPLAY9US96rj4fRk96sO6Y/XHmyAU7wWgmPViHzTooso2ux+O70/14kykigIkh5aTjK8yZx/0dkgNBsv0n7ryt7xQEkBwI4tW1dGyn+n149CcenxD0dTk+2KnLRLrPjVHpGmRQZs3o8Tx1IolZ4f2ki6M3YTnBRlpPMvHvfnyaIdLySQfkBk4e7df9+OS2/6XZaIlqfPfjnc44ABwWO7mB4pOO7368w5INQILFRq7n5O/f+fiwt+e/VRg9+UZPj69Z9BHBSzJZ/n979x6bVX3Hcfz9KVBa0MlGp9Khgo7AFjJREe+KCvMyYo2aDS8TpuIlDG9BpzGZyKKZids0MdvixEm8bQ5mJFskeIGZ6RS8cBO8REUtcmllm1cqtN/98ftVHp72aR9W2nNOz/eVND3Pec7T88lJ2+9zfs85vy/D+jXQJx7vvftXM6xf57Muldp+38rKkvsE+KBpb2jno+1GGxRyVdWyffsnbTco0K9yR2/RqqpaWlq+/OpxdfX+VKjjYl5VteNfR3X1/vTvv+ON38ABw2lu/uKrx5999la7P2Nr04YO9+GyrzvPDM8BTjWzS+LjHwNHmNlPC7ZZHbepj4/fJhTMWcALZvZgXD8HeMLM5pXa3+48Mxzc0shrJ0/YpZ/V3bKUFWD000+FIdIiNS2NrE5h3iwZ/dSTYYi0SE1zA6snTEwgUWmjFz8ThkiLtJ4Zps1zzx3H1qYP26z3M8PerzsvoFkPFE4zMjSua3ebOEy6F/BRma/tsrr1K6m0rTutq7St1K1fubt31WVnb2w/69kb05cVYMqmtVRa007rKq2JKZvWJpSo95jevK7d34XpzeuSCdSBa2qbqaTo94AmrqlN5+0VBx40k4qKna94rqio5sCDZiaUyPWU7iyGy4ARkoZLqiRcELOgaJsFwJS4fA7wjIVT1QXA5Hi16XBgBLB0dwe87cJruaD+eQa3NIK1MLilkQvqn+e2C6/d3bvqstnnX8vUDTtnnbrheWafn76sANedN4MZG5dSE/PWtDQyY+NSrjtvRtLRMu+KU6Zx47ZV1DQ3hGPb3MCN21ZxxSnTko7WxsUjJ3Jz7cfUsCVkZQs3137MxSPTdQbbasi+dYwadStV/WsBUdW/llGjbmXIvnVJR3PdrNuGSQEknQ7cCfQB7jOzWyXNBl4yswWSqoAHgEOALcBkM3snvvYm4CJgO3C1mT3R0b7+n2FS55zLOx8mDbq1GPYkL4bOObfrvBgGPrWCc8653PNi6JxzLve8GDrnnMs9L4bOOedyr9dcQCOpAXivCz+iBuh8Gpl0yFJWyFbeLGWFbOXNUlbIVt6uZD3AzNrO4pAzvaYYdpWkl7JyRVWWskK28mYpK2Qrb5ayQrbyZilrWvkwqXPOudzzYuiccy73vBjucE/SAXZBlrJCtvJmKStkK2+WskK28mYpayr5Z4bOOedyz88MnXPO5Z4XQ+ecc7mX62IoqUrSUkkrJL0m6ZakM3VGUh9Jr0r6W9JZOiNpnaRVkpZLSv0s6pIGSZon6XVJayUdlXSm9kgaGY9p69fHkq5OOldHJF0T/8ZWS3okdqxJJUlXxZyvpfG4SrpP0ubYHL113TckPSnprfj960lmzKJcF0OgCTjJzA4GxgCnSjoy2UidugrIUofcE81sTEbugboLWGhmo4CDSelxNrM34jEdAxwGfA48lmyq0iR9C7gSGGtmowkt3SYnm6p9kkYD04BxhN+BSZK+nWyqNu4HTi1adwPwtJmNAJ6Oj90uyHUxtODT+LBf/ErtFUWShgI/AO5NOktvI2kv4HhgDoCZfWlm/0k0VHlOBt42s67MvtQT+gLVkvoCA4APE85TyneAF83sczPbDvwDOCvhTDsxs2cJ/V8L1QFz4/Jc4MyezNQb5LoYwlfDjsuBzcCTZvZiwpE6cidwPdCScI5yGbBI0suSLk06TCeGAw3AH+Mw9L2SBiYdqgyTgUeSDtERM1sP3AG8D2wA/mtmi5JNVdJq4DhJgyUNAE4H9ks4Uzn2MbMNcXkjsE+SYbIo98XQzJrjcNNQYFwcJkkdSZOAzWb2ctJZdsGxZnYocBowXdLxSQfqQF/gUOB3ZnYI8BkpH2qSVAmcAfwl6SwdiZ9f1RHecNQCAyVdkGyq9pnZWuB2YBGwEFgONCeZaVdZuF8utSNcaZX7YtgqDoktpu1YfFocA5whaR3wJ+AkSQ8mG6lj8YwAM9tM+ExrXLKJOlQP1BeMDMwjFMc0Ow14xcw2JR2kExOAd82swcy2AX8Fjk44U0lmNsfMDjOz44F/A28mnakMmyQNAYjfNyecJ3NyXQwlfVPSoLhcDUwEXk80VAlmdqOZDTWzYYShsWfMLJXvrgEkDZS0Z+sy8H3CEFQqmdlG4ANJI+Oqk4E1CUYqx7mkfIg0eh84UtIASSIc21RenAQgae/4fX/C54UPJ5uoLAuAKXF5CvB4glkyqW/SARI2BJgrqQ/hjcGjZpb6WxYyYh/gsfC/j77Aw2a2MNlInZoBPBSHH98BfpJwnpLiG4yJwGVJZ+mMmb0oaR7wCrAdeJV0Tx82X9JgYBswPW0XUkl6BBgP1EiqB24Gfgk8KuliQiu7HyaXMJt8OjbnnHO5l+thUueccw68GDrnnHNeDJ1zzjkvhs4553LPi6Fzzrnc82LockXSTbEbwcrY8eGIBLNcHaf8au+5SXFauBWS1ki6LK6/XNKFPZvUud7Pb61wuRFbMv0aGG9mTZJqgEoz6/FJo+O9rW8TOjk0Fj3Xj3Cv2Dgzq5fUHxhmZm/0dE7n8sLPDF2eDAEazawJwMwaWwth7L1YE5fHSloSl2dJekDSv2KvuGlx/XhJz0r6u6Q3JP1eUkV87tzYx3G1pNtbdy7pU0m/krQCuIkwT+diSYuLcu5JmKjgo5izqbUQxjwzJdUW9TRslnRAnFVpvqRl8euY7jqYzvUmXgxdniwC9pP0pqTfSjqhzNd9DzgJOAr4uaTauH4cYdaa7wIHAWfF526P248BDpd0Ztx+IKE90MFmNpvQxuhEMzuxcGdmtoUwvdZ7sRHu+a2FtmCbDwt6Gv4BmB/bON0F/MbMDgfOxtt9OVcWL4YuN2LvysOASwntmv4saWoZL33czL6Iw5mL2THh+FIze8fMmglzhB4LHA4siZNSbwceIvRJhND9YH6ZWS8hzOG5FJgJ3NfedvHMbxpwUVw1Abg7tiVbAHxN0h7l7NO5PMv73KQuZ2LhWgIskbSKMKnx/YQ5M1vfHFYVv6zE41LrS9ka919u1lXAKkkPAO8CUwufj90J5gBnFDSprgCONLOt5e7HOednhi5HJI2UNKJg1RjChSoA6whnjRCGFwvVSaqKkzePB5bF9eMkDY9DmD8C/kk4kztBUk28SOZcQrf09nxC+HywOOceksaXyNm6TT9CH8OfmVlhi6FFhKHb1u3GlNi3c66AF0OXJ3sQupSskbSS8FnfrPjcLcBdkl6ibTPXlYTh0ReAXxRcfboMuJvQjuhd4LHYbfyGuP0K4GUzK9VO5x5gYTsX0Ai4Pl6Yszxmm1q0zdHAWOCWgotoaoErgbHx1pE1wOWdHRTnnN9a4VyHJM0CPjWzO4rWjwdmmtmkBGI553YzPzN0zjmXe35m6JxzLvf8zNA551zueTF0zjmXe14MnXPO5Z4XQ+ecc7nnxdA551zu/Q9h/Y88Ps8S/AAAAABJRU5ErkJggg==\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_k.plot(show_lines=True)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can see in the plot above that first 3 variables, (0, 1, 2) are included in all the solutions of the path\n", + "\n", + "### Coefficient Bounds\n", + "Starting in version 2.0.0, `l0learn` supports bounds for CD algorithms for all losses and penalties. (We plan to support bound constraints for the CDPSI algorithm in the future). By default, `l0learn` does not apply bounds, i.e., it assumes $-\\infty <= \\beta_i <= \\infty$ for all i. Users can supply the same bounds for all coefficients by setting the parameters `lows` and `highs` to scalar values (these should satisfy: `lows <= 0`, `lows != highs`, and `highs >= 0`). To use different bounds for the coefficients, `lows` and `highs` can be both set to vectors of length `p` (where the i-th entry corresponds to the bound on coefficient i).\n", + "\n", + "All of the following examples are valid." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 107, + "outputs": [], + "source": [ + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", highs=0.5)\n", + "l0learn.fit(X, y, penalty=\"L0\", lows=-0.5, highs=0.5)\n", + "\n", + "max_value = 0.25\n", + "highs_array = max_value*np.ones(p)\n", + "highs_array[0] = 0.1\n", + "fit_model_bounds = l0learn.fit(X, y, penalty=\"L0\", lows=-0.1, highs=highs_array, max_support_size=20)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "We can see the coefficients are subject to the bounds." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 110, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 0. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 6. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n", + "/Users/tnonet/Documents/GitHub/L0Learn/python/l0learn/models.py:438: UserWarning: Duplicate solution seen at support size 8. Plotting only first solution\n", + " warnings.warn(f\"Duplicate solution seen at support size {support_size}. Plotting only first solution\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "maximum value of coefficients = 0.25 <= 0.25\n", + "maximum value of first coefficient = 0.1 <= 0.1\n", + "minimum value of coefficient = -0.1 >= -0.1\n" + ] + }, + { + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fit_model_bounds.plot(show_lines=True)\n", + "\n", + "print(f\"maximum value of coefficients = {np.max(fit_model_bounds.coeffs[0])} <= {max_value}\")\n", + "print(f\"maximum value of first coefficient = {np.max(fit_model_bounds.coeffs[0][0, :])} <= 0.1\")\n", + "print(f\"minimum value of coefficient = {np.min(fit_model_bounds.coeffs[0])} >= -0.1\")\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### User-specified Lambda Grids\n", + "By default, `l0learn` selects the sequence of lambda values in an efficient manner to avoid wasted computation (since close $\\lambda$ values can typically lead to the same solution). Advanced users of the toolkit can change this default behavior and supply their own sequence of $\\lambda$ values. This can be done supplying the $\\lambda$ values through the parameter `lambda_grid`. When `lambda_grid` is supplied, we require `num_gamma` and `num_lambda` to be `None` to ensure the is no ambiguity in the solution path requested.\n", + "\n", + "Specifically, the value assigned to `lambda_grid` should be a list of lists/arrays of decreasing positive values (floats). The length of `lambda_grid` (the number of lists stored) specifies the number of gamma parameters that will fill between `gamma_min`, and `gamma_max`. In the case of L0 penalty, `lambda_grid` must be a list of length 1. In case of L0L2/L0L1 `lambda_grid` can have any number of sub-lists stored. The ith element in `lambda_grid` should be a **strictly decreasing** sequence of positive lambda values which are used by the algorithm for the ith value of gamma. For example, to fit an L0 model with the sequence of user-specified lambda values: 1, 1e-1, 1e-2, 1e-3, 1e-4, we run the following:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 113, + "outputs": [], + "source": [ + "user_lambda_grid = [[1, 1e-1, 1e-2, 1e-3, 1e-4]]\n", + "fit_grid = l0learn.fit(X, y, penalty=\"L0\", lambda_grid=user_lambda_grid, max_support_size=1000, num_lambda=None, num_gamma=None)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To verify the results we print the fit object:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 114, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconverged
01.00000-0.016000True
10.10000-0.016000True
20.0100100.016811True
30.0010620.018729True
40.00012670.051675True
\n
" + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid\n", + "# Use fit_grid.characteristics() for those without rich dispalys" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "Note that the $\\lambda$ values above are the desired values. For L0L2 and L0L1 penalties, the same can be done where the `lambda_grid` parameter." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 115, + "outputs": [], + "source": [ + "user_lambda_grid_L2 = [[1, 1e-1, 1e-2, 1e-3, 1e-4],\n", + " [10, 2, 1, 0.01, 0.002, 0.001, 1e-5],\n", + " [1e-4, 1e-5]]\n", + "\n", + "# user_lambda_grid_L2[[i]] must be a sequence of positive decreasing reals.\n", + "fit_grid_L2 = l0learn.fit(X, y, penalty=\"L0L2\", lambda_grid=user_lambda_grid_L2, max_support_size=1000, num_lambda=None, num_gamma=None)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 116, + "outputs": [ + { + "data": { + "text/plain": "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0L2'})", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
l0support_sizeinterceptconvergedl2
01.000000-0.016000True10.000000
10.100000-0.016000True10.000000
20.010000-0.016000True10.000000
30.001009-0.014394True10.000000
40.00010134-0.012180True10.000000
510.000000-0.016000True0.031623
62.000000-0.016000True0.031623
71.000000-0.016000True0.031623
80.01000100.015045True0.031623
90.00200280.001483True0.031623
100.00100580.002821True0.031623
110.000015820.021913True0.031623
120.000103110.048700True0.000100
130.000014110.047991False0.000100
\n
" + }, + "execution_count": 116, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_grid_L2\n", + "# Use fit_grid_L2.characteristics() for those without rich dispalys" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "# More Details\n", + "For more details please inspect the doc strings of:\n", + "\n", + "* [l0learn.models.CVFitModel](code.rst#l0learn.models.CVFitModel)\n", + "* [l0learn.models.FitModel](code.rst#l0learn.models.FitModel)\n", + "* [l0learn.fit](code.rst#l0learn.fit)\n", + "* [l0learn.cvfit](code.rst#l0learn.cvfit)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "# References\n", + "Hussein Hazimeh and Rahul Mazumder. [Fast Best Subset Selection: Coordinate Descent and Local Combinatorial Optimization Algorithms](https://pubsonline.informs.org/doi/10.1287/opre.2019.1919). Operations Research (2020).\n", + "\n", + "Antoine Dedieu, Hussein Hazimeh, and Rahul Mazumder. [Learning Sparse Classifiers: Continuous and Mixed Integer Optimization Perspectives](https://arxiv.org/abs/2001.06471). JMLR (to appear)." + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/python/external/armadillo-code b/python/external/armadillo-code new file mode 160000 index 0000000..d4d6069 --- /dev/null +++ b/python/external/armadillo-code @@ -0,0 +1 @@ +Subproject commit d4d6069f12b1cd24d2646914984fff2c598a4645 diff --git a/python/external/carma b/python/external/carma new file mode 160000 index 0000000..79e3669 --- /dev/null +++ b/python/external/carma @@ -0,0 +1 @@ +Subproject commit 79e36697d6d9ba7cb20b8dabacbaadc59d0bc9dd diff --git a/python/external/pybind11 b/python/external/pybind11 new file mode 160000 index 0000000..914c06f --- /dev/null +++ b/python/external/pybind11 @@ -0,0 +1 @@ +Subproject commit 914c06fb252b6cc3727d0eedab6736e88a3fcb01 diff --git a/python/l0learn/__init__.py b/python/l0learn/__init__.py new file mode 100644 index 0000000..967c6bd --- /dev/null +++ b/python/l0learn/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa + +from .interface import fit, cvfit diff --git a/python/l0learn/interface.py b/python/l0learn/interface.py new file mode 100644 index 0000000..3ab421d --- /dev/null +++ b/python/l0learn/interface.py @@ -0,0 +1,917 @@ +from typing import Union, List, Sequence, Dict, Any, Optional + +from .models import FitModel, CVFitModel +from .l0learn import ( + _L0LearnFit_sparse, + _L0LearnFit_dense, + _L0LearnCV_dense, + _L0LearnCV_sparse, +) +from scipy.sparse import csc_matrix +import numpy as np +from warnings import warn + +SUPPORTED_LOSS = ("SquaredError", "Logistic", "SquaredHinge") +CLASSIFICATION_LOSS = SUPPORTED_LOSS[1], SUPPORTED_LOSS[2] +SUPPORTED_PENALTY = ("L0", "L0L1", "L0L2") +SUPPORTED_ALGORITHM = ("CD", "CDPSI") + + +def _fit_check( + X: Union[np.ndarray, csc_matrix], + y: np.ndarray, + loss: str, + penalty: str, + algorithm: str, + max_support_size: int, + num_lambda: Union[int, None], + num_gamma: Union[int, None], + gamma_max: float, + gamma_min: float, + partial_sort: bool, + max_iter: int, + rtol: float, + atol: float, + active_set: bool, + active_set_num: int, + max_swaps: int, + scale_down_factor: float, + screen_size: int, + lambda_grid: Union[List[Sequence[float]], None], + exclude_first_k: int, + intercept: bool, + lows: Union[np.ndarray, float], + highs: Union[np.ndarray, float], +) -> Dict[str, Any]: + if isinstance(X, (np.ndarray, csc_matrix)): + if X.dtype != np.float64: + raise ValueError( + f"expected X to have dtype {np.float64}, but got {X.dtype}" + ) + if X.ndim != 2: + raise ValueError(f"expected X to be 2D, but got {X.ndim}D") + if not np.prod(X.shape): + raise ValueError( + f"expected X to have non-degenerate axis, but got {X.shape}" + ) + # if isinstance(X, np.ndarray): + # if not X.flags.contiguous: + # raise ValueError(f"expected X to be contiguous, but is not.") + # else: # isinstance(X, csc_matrix): + # if not (X.indptr.flags.contiguous and X.indices.flags.contiguous and X.data.flags.continuous): + # raise ValueError(f"expected X to have contiguous `indptr`, `indices`, and `data`, but is not.") + else: + raise ValueError( + f"expected X to be a {np.ndarray} or a {csc_matrix}, but got {type(X)}." + ) + + n, p = X.shape + if ( + not isinstance(y, np.ndarray) + or not np.isrealobj(y) + or y.ndim != 1 + or len(y) != n + ): + raise ValueError(f"expected y to be a 1D real numpy, but got {y}.") + if loss not in SUPPORTED_LOSS: + raise ValueError( + f"expected loss parameter to be on of {SUPPORTED_LOSS}, but got {loss}" + ) + if penalty not in SUPPORTED_PENALTY: + raise ValueError( + f"expected penalty parameter to be on of {SUPPORTED_PENALTY}, but got {penalty}" + ) + if algorithm not in SUPPORTED_ALGORITHM: + raise ValueError( + f"expected algorithm parameter to be on of {SUPPORTED_ALGORITHM}, but got {algorithm}" + ) + if not isinstance(max_support_size, int) or 1 > max_support_size: + raise ValueError( + f"expected max_support_size parameter to be a positive integer, but got {max_support_size}" + ) + max_support_size = min(p, max_support_size) + + if gamma_max < 0: + raise ValueError( + f"expected gamma_max parameter to be a positive float, but got {gamma_max}" + ) + if gamma_min < 0 or gamma_min > gamma_max: + raise ValueError( + f"expected gamma_max parameter to be a positive float less than gamma_max," + f" but got {gamma_min}" + ) + if not isinstance(partial_sort, bool): + raise ValueError( + f"expected partial_sort parameter to be a bool, but got {partial_sort}" + ) + if not isinstance(max_iter, int) or max_iter < 1: + raise ValueError( + f"expected max_iter parameter to be a positive integer, but got {max_iter}" + ) + if rtol < 0 or rtol >= 1: + raise ValueError(f"expected rtol parameter to exist in [0, 1), but got {rtol}") + if atol < 0: + raise ValueError( + f"expected atol parameter to exist in [0, INF), but got {atol}" + ) + if not isinstance(active_set, bool): + raise ValueError( + f"expected active_set parameter to be a bool, but got {active_set}" + ) + if not isinstance(active_set_num, int) or active_set_num < 1: + raise ValueError( + f"expected active_set_num parameter to be a positive integer, but got {active_set_num}" + ) + if not isinstance(max_swaps, int) or max_swaps < 1: + raise ValueError( + f"expected max_swaps parameter to be a positive integer, but got {max_swaps}" + ) + if not (0 < scale_down_factor < 1): + raise ValueError( + f"expected scale_down_factor parameter to exist in (0, 1), but got {scale_down_factor}" + ) + if not isinstance(screen_size, int) or screen_size < 1: + raise ValueError( + f"expected screen_size parameter to be a positive integer, but got {screen_size}" + ) + screen_size = min(screen_size, p) + + if not isinstance(exclude_first_k, int) or not (0 <= exclude_first_k <= p): + raise ValueError( + f"expected exclude_first_k parameter to be a positive integer less than {p}, " + f"but got {exclude_first_k}" + ) + if not isinstance(intercept, bool): + raise ValueError( + f"expected intercept parameter to be a bool, " f"but got {intercept}" + ) + + if loss in CLASSIFICATION_LOSS: + unique_items = sorted(np.unique(y)) + if len(unique_items) != 2: + raise ValueError( + f"expected y vector to only have two unique values (Binary Classification), " + f"but got {unique_items}" + ) + else: + a, *_ = unique_items # a is the lower value + y = np.copy(y) + first_value = y == a + second_value = y != a + y[first_value] = -1.0 + y[second_value] = 1.0 + if y.dtype != np.float64: + y = y.astype(float) + + if penalty == "L0": + # Pure L0 is not supported for classification + # Below we add a small L2 component. + + if lambda_grid is not None and len(lambda_grid) != 1: + # If this error checking was left to the lower section, it would confuse users as + # we are converting L0 to L0L2 with small L2 penalty. + # Here we must check if lambdaGrid is supplied (And thus use 'autolambda') + # If 'lambdaGrid' is supplied, we must only supply 1 list of lambda values + raise ValueError( + f"L0 Penalty requires 'lambda_grid' to be a list of length 1, but got {lambda_grid}." + ) + + penalty = "L0L2" + gamma_max = 1e-7 + gamma_min = 1e-7 + elif penalty != "L0" and num_gamma == 1: + warn( + f"num_gamma set to 1 with {penalty} penalty. Only one {penalty[2:]} penalty value will be fit." + ) + + if y.dtype != np.float64: + raise ValueError( + f"expected y vector to have type {np.float64}, but got {y.dtype}" + ) + + if lambda_grid is None: + lambda_grid = [[0.0]] + auto_lambda = True + if not isinstance(num_lambda, int) or num_lambda < 1: + raise ValueError( + f"expected num_lambda to a positive integer when lambda_grid is None, but got {num_lambda}." + ) + if not isinstance(num_gamma, int) or num_gamma < 1: + raise ValueError( + f"expected num_gamma to a positive integer when lambda_grid is None, but got {num_gamma}." + ) + if penalty == "L0" and num_gamma != 1: + raise ValueError( + f"expected num_gamma to 1 when penalty = 'L0', but got {num_gamma}." + ) + else: # lambda_grid should be a List[List[float]] + if num_gamma is not None: + raise ValueError( + f"expected num_gamma to be None if lambda_grid is specified by the user, " + f"but got {num_gamma}" + ) + num_gamma = len(lambda_grid) + + if num_lambda is not None: + raise ValueError( + f"expected num_lambda to be None if lambda_grid is specified by the user, " + f"but got {num_lambda}" + ) + num_lambda = 0 # This value is ignored. + auto_lambda = False + + if penalty == "L0" and num_gamma != 1: + raise ValueError( + f"expected lambda_grid to of length 1 when penalty = 'L0', but got {len(lambda_grid)}" + ) + + for i, sub_lambda_grid in enumerate(lambda_grid): + if sub_lambda_grid[0] <= 0: + raise ValueError( + f"Expected all values of lambda_grid to be positive, " + f"but got lambda_grid[{i}] containing a negative value" + ) + if any(np.diff(sub_lambda_grid) >= 0): + raise ValueError( + f"Expected each element of lambda_grid to be a list of decreasing value, " + f"but got lambda_grid[{i}] containing an increasing value." + ) + + n, p = X.shape + with_bounds = False + + if isinstance(lows, float): + if lows > 0: + raise ValueError( + f"expected lows to be a non-positive float, but got {lows}" + ) + elif lows > -float("inf"): + with_bounds = True + elif ( + isinstance(lows, np.ndarray) + and lows.ndim == 1 + and len(lows) == p + and all(lows <= 0) + ): + with_bounds = True + else: + raise ValueError( + f"expected lows to be a non-positive float, or a 1D numpy array of length {p} of non-positives " + f"floats, but got {lows}" + ) + + if isinstance(highs, float): + if highs < 0: + raise ValueError( + f"expected highs to be a non-negative float, but got {highs}" + ) + if highs < float("inf"): + with_bounds = True + elif ( + isinstance(highs, np.ndarray) + and highs.ndim == 1 + and len(highs) == p + and all(highs >= 0) + ): + with_bounds = True + else: + raise ValueError( + f"expected highs to be a non-negative float, or a 1D numpy array of length {p} of " + f"non-negative floats, but got {highs}" + ) + + if with_bounds: + if isinstance(lows, float): + lows = np.ones(p) * lows + if isinstance(highs, float): + highs = np.ones(p) * highs + + if any(lows >= highs): + bad_bounds = np.argwhere(lows >= highs) + raise ValueError( + f"expected to be high to be elementwise greater than lows, " + f"but got indices {bad_bounds[0]} where that is not the case " + ) + else: + lows = np.array([0.0]) + highs = np.array(([0.0])) + + return { + "max_support_size": max_support_size, + "screen_size": screen_size, + "y": y, + "penalty": penalty, + "gamma_max": float(gamma_max), + "gamma_min": float(gamma_min), + "lambda_grid": [[float(i) for i in lst] for lst in lambda_grid], + "num_gamma": num_gamma, + "num_lambda": num_lambda, + "auto_lambda": auto_lambda, + "with_bounds": with_bounds, + "lows": lows.astype("float"), + "highs": highs.astype("float"), + } + + +def fit( + X: Union[np.ndarray, csc_matrix], + y: np.ndarray, + loss: str = "SquaredError", + penalty: str = "L0", + algorithm: str = "CD", + max_support_size: int = 100, + num_lambda: Optional[int] = 100, + num_gamma: Optional[int] = 1, + gamma_max: float = 10.0, + gamma_min: float = 0.0001, + partial_sort: bool = True, + max_iter: int = 200, + rtol: float = 1e-6, + atol: float = 1e-9, + active_set: bool = True, + active_set_num: int = 3, + max_swaps: int = 100, + scale_down_factor: float = 0.8, + screen_size: int = 1000, + lambda_grid: Optional[List[Sequence[float]]] = None, + exclude_first_k: int = 0, + intercept: bool = True, + lows: Union[np.ndarray, float] = -float("inf"), + highs: Union[np.ndarray, float] = +float("inf"), +) -> FitModel: + """ + Computes the regularization path for the specified loss function and penalty function. + + Parameters + ---------- + X : np.ndarray or csc_matrix of shape (N, P) + Data Matrix where rows of X are observations and columns of X are features + + y : np.ndarray of shape (P) + The response vector where y[i] corresponds to X[i, :] + For classification, a binary vector (-1, 1) is requried . + + loss : str + The loss function. Currently supports the choices: + "SquaredError" (for regression), + "Logistic" (for logistic regression), and + "SquaredHinge" (for smooth SVM). + + penalty : str + The type of regularization. + This can take either one of the following choices: + "L0", + "L0L2", and + "L0L1" + + algorithm : str + The type of algorithm used to minimize the objective function. Currently "CD" and "CDPSI" are are supported. + "CD" is a variant of cyclic coordinate descent and runs very fast. "CDPSI" performs local combinatorial search + on top of CD and typically achieves higher quality solutions (at the expense of increased running time). + + max_support_size : int + Must be greater than 0. + The maximum support size at which to terminate the regularization path. We recommend setting this to a small + fraction of min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically selects a small portion of non-zeros. + + num_lambda : int, optional + The number of lambda values to select in the regularization path. + This value must be None if lambda_grid is supplied.When supplied, must be greater than 0. + Note: lambda is the regularization parameter corresponding to the L0 norm. + + num_gamma: int, optional + The number of gamma values to select in the regularization path. + This value must be None if lambda_grid is supplied. When supplied, must be greater than 0. + Note: gamma is the regularization parameter corresponding to L1 or L2, depending on the chosen penalty). + + gamma_max : float + The maximum value of gamma when using the L0L2 penalty. + This value must be greater than 0. + + Note: For the L0L1 penalty this is automatically selected. + + gamma_min : float + The minimum value of Gamma when using the L0L2 penalty. + This value must be greater than 0 but less than gamma_max. + Note: For the L0L1 penalty, the minimum value of gamma in the grid is set to gammaMin * gammaMax. + + partial_sort : bool + If TRUE partial sorting will be used for sorting the coordinates to do greedy cycling (see our paper for + for details). Otherwise, full sorting is used. #TODO: Add link for paper + + max_iter : int + The maximum number of iterations (full cycles) for CD per grid point. The algorithm may not use the full number + of iteration per grid point if convergence is found (defined by rtol and atol parameter) + Must be greater than 0 + + rtol : float + The relative tolerance which decides when to terminate optimization as based on the relative change in the + objective between iterations. + Must be greater than 0 and less than 1. + + atol : float + The absolute tolerance which decides when to terminate optimization as based on the absolute L2 norm of the + residuals + Must be greater than 0 + + active_set : bool + If TRUE, performs active set updates. (see our paper for for details). #TODO: Add link for paper + + active_set_num : int + The number of consecutive times a support should appear before declaring support stabilization. + (see our paper for for details). #TODO: Add link for paper + + Must be greater than 0. + + max_swaps : int + The maximum number of swaps used by CDPSI for each grid point. + Must be greater than 0. Ignored by CD algorithims. + + scale_down_factor : float + Roughly amount each lambda value is scaled by between grid points. Larger values lead to closer lambdas and + typically to smaller gaps between the support sizes. + + For details, see our paper - Section 5 on Adaptive Selection of Tuning Parameters). #TODO: Add link for paper + + Must be greater than 0 and less than 1 (strictly for both.) + + screen_size : int + The number of coordinates to cycle over when performing initial correlation screening. #TODO: Add link for paper + + Must be greater than 0 and less than number of columns of X. + + lambda_grid : list of list of floats + A grid of lambda values to use in computing the regularization path. This is by default an empty list + and is ignored. When specified, lambda_grid should be a list of list of floats, where the ith element + (corresponding to the ith gamma) should be a decreasing sequence of lambda values. The length of this sequence + is directly the number of lambdas to be tried for that gamma. + + In the the "L0" penalty case, lambda_grid should be a list of 1. + In the "L0LX" penalty cases, lambda_grid can be a list of any length. The length of lambda_grid will be the + number of gamma values tried. + + See the example notebook for more details. + + Note: When lambda_grid is supplied, num_gamma and num_lambda must be None. + + exclude_first_k : int + The first exclude_first_k features in X will be excluded from variable selection. In other words, the first + exclude_first_k variables will not be included in the L0-norm penalty however they will be included in the + L1 or L2 norm penalties, if they are specified. + + Must be a positive integer less than the columns of X. + + intercept : bool + If False, no intercept term is included or fit in the regularization path + Intercept terms are not regularized by L0 or L1/L2. + + lows : np array or float + Lower bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of + size p (number of columns of X) where lows[i] is the lower bound for coefficient i. + + Lower bounds can not be above 0 (i.e. we can not specify that all coefficients must be larger than a > 0). + Lower bounds can be set to 0 iff the corresponding upper bound for that coefficient is also not 0. + + highs : np array or float + Upper bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of + size p (number of columns of X) where highs[i] is the upper bound for coefficient i. + + Upper bounds can not be below 0 (i.e. we can not specify that all coefficients must be smaller than a < 0). + Upper bounds can be set to 0 iff the corresponding lower bound for that coefficient is also not 0. + + Returns + ------- + fit_model : l0learn.models.FitModel + FitModel instance containing all relevant information from the solution path. + + See Also + ------- + l0learn.cvfit + l0learn.models.FitModel + + Examples + -------- + >>>fit_model = l0learn.fit(X, y, penalty="L0", max_support_size=20) + """ + check = _fit_check( + X=X, + y=y, + loss=loss, + penalty=penalty, + algorithm=algorithm, + max_support_size=max_support_size, + num_lambda=num_lambda, + num_gamma=num_gamma, + gamma_max=gamma_max, + gamma_min=gamma_min, + partial_sort=partial_sort, + max_iter=max_iter, + rtol=rtol, + atol=atol, + active_set=active_set, + active_set_num=active_set_num, + max_swaps=max_swaps, + scale_down_factor=scale_down_factor, + screen_size=screen_size, + lambda_grid=lambda_grid, + exclude_first_k=exclude_first_k, + intercept=intercept, + lows=lows, + highs=highs, + ) + + max_support_size = check["max_support_size"] + screen_size = check["screen_size"] + y = check["y"] + penalty = check["penalty"] + gamma_max = check["gamma_max"] + gamma_min = check["gamma_min"] + lambda_grid = check["lambda_grid"] + num_gamma = check["num_gamma"] + num_lambda = check["num_lambda"] + auto_lambda = check["auto_lambda"] + with_bounds = check["with_bounds"] + lows = check["lows"] + highs = check["highs"] + + if isinstance(X, np.ndarray): + c_results = _L0LearnFit_dense( + X, # const T &X, + y, # const arma::vec &y, + loss, # const std::string Loss, + penalty, # const std::string Penalty, + algorithm, # const std::string Algorithm, + max_support_size, # const std::size_t NnzStopNum, + num_lambda, # const std::size_t G_ncols, + num_gamma, # const std::size_t G_nrows, + gamma_max, # const double Lambda2Max, + gamma_min, # const double Lambda2Min, + partial_sort, # const bool PartialSort, + max_iter, # const std::size_t MaxIters, + rtol, # const double rtol, + atol, # const double atol, + active_set, # const bool ActiveSet, + active_set_num, # const std::size_t ActiveSetNum, + max_swaps, # const std::size_t MaxNumSwaps, + scale_down_factor, # const double ScaleDownFactor, + screen_size, # const std::size_t ScreenSize, + not auto_lambda, # const bool LambdaU, + lambda_grid, # const std::vector> &Lambdas, + exclude_first_k, # const std::size_t ExcludeFirstK, + intercept, # const bool Intercept, + with_bounds, # const bool withBounds, + lows, # const arma::vec &Lows, + highs, + ) # const arma::vec &Highs + elif isinstance(X, csc_matrix): + c_results = _L0LearnFit_sparse( + X, # const T &X, + y, # const arma::vec &y, + loss, # const std::string Loss, + penalty, # const std::string Penalty, + algorithm, # const std::string Algorithm, + max_support_size, # const std::size_t NnzStopNum, + num_lambda, # const std::size_t G_ncols, + num_gamma, # const std::size_t G_nrows, + gamma_max, # const double Lambda2Max, + gamma_min, # const double Lambda2Min, + partial_sort, # const bool PartialSort, + max_iter, # const std::size_t MaxIters, + rtol, # const double rtol, + atol, # const double atol, + active_set, # const bool ActiveSet, + active_set_num, # const std::size_t ActiveSetNum, + max_swaps, # const std::size_t MaxNumSwaps, + scale_down_factor, # const double ScaleDownFactor, + screen_size, # const std::size_t ScreenSize, + not auto_lambda, # const bool LambdaU, + lambda_grid, # const std::vector> &Lambdas, + exclude_first_k, # const std::size_t ExcludeFirstK, + intercept, # const bool Intercept, + with_bounds, # const bool withBounds, + lows, # const arma::vec &Lows, + highs, + ) # const arma::vec &Highs + + results = FitModel( + settings={"loss": loss, "intercept": intercept, "penalty": penalty}, + lambda_0=c_results.Lambda0, + gamma=c_results.Lambda12, + support_size=c_results.NnzCount, + coeffs=c_results.Beta, + intercepts=c_results.Intercept, + converged=c_results.Converged, + ) + return results + + +def cvfit( + X: Union[np.ndarray, csc_matrix], + y: np.ndarray, + loss: str = "SquaredError", + penalty: str = "L0", + algorithm: str = "CD", + num_folds: int = 10, + seed: int = 1, + max_support_size: int = 100, + num_lambda: Optional[int] = 100, + num_gamma: Optional[int] = 1, + gamma_max: float = 10.0, + gamma_min: float = 0.0001, + partial_sort: bool = True, + max_iter: int = 200, + rtol: float = 1e-6, + atol: float = 1e-9, + active_set: bool = True, + active_set_num: int = 3, + max_swaps: int = 100, + scale_down_factor: float = 0.8, + screen_size: int = 1000, + lambda_grid: Optional[List[Sequence[float]]] = None, + exclude_first_k: int = 0, + intercept: bool = True, + lows: Union[np.ndarray, float] = -float("inf"), + highs: Union[np.ndarray, float] = +float("inf"), +) -> CVFitModel: + """Computes the regularization path for the specified loss function and penalty function and performs K-fold + cross-validation. + + Parameters + ---------- + X : np.ndarray or csc_matrix of shape (N, P) + Data Matrix where rows of X are observations and columns of X are features + + y : np.ndarray of shape (P) + The response vector where y[i] corresponds to X[i, :] + For classification, a binary vector (-1, 1) is requried . + + loss : str + The loss function. Currently supports the choices: + "SquaredError" (for regression), + "Logistic" (for logistic regression), and + "SquaredHinge" (for smooth SVM). + + penalty : str + The type of regularization. + This can take either one of the following choices: + "L0", + "L0L2", and + "L0L1" + + algorithm : str + The type of algorithm used to minimize the objective function. Currently "CD" and "CDPSI" are are supported. + "CD" is a variant of cyclic coordinate descent and runs very fast. "CDPSI" performs local combinatorial search + on top of CD and typically achieves higher quality solutions (at the expense of increased running time). + + num_folds : int + Must be greater than 1 and less than N (number of . + The number of folds for cross-validation. + + max_support_size : int + Must be greater than 0. + The maximum support size at which to terminate the regularization path. We recommend setting this to a small + fraction of min(n,p) (e.g. 0.05 * min(n,p)) as L0 regularization typically selects a small portion of non-zeros. + + num_lambda : int, optional + The number of lambda values to select in the regularization path. + This value must be None if lambda_grid is supplied.When supplied, must be greater than 0. + Note: lambda is the regularization parameter corresponding to the L0 norm. + + num_gamma: int, optional + The number of gamma values to select in the regularization path. + This value must be None if lambda_grid is supplied. When supplied, must be greater than 0. + Note: gamma is the regularization parameter corresponding to L1 or L2, depending on the chosen penalty). + + gamma_max : float + The maximum value of gamma when using the L0L2 penalty. + This value must be greater than 0. + + Note: For the L0L1 penalty this is automatically selected. + + gamma_min : float + The minimum value of Gamma when using the L0L2 penalty. + This value must be greater than 0 but less than gamma_max. + Note: For the L0L1 penalty, the minimum value of gamma in the grid is set to gammaMin * gammaMax. + + partial_sort : bool + If TRUE partial sorting will be used for sorting the coordinates to do greedy cycling (see our paper for + for details). Otherwise, full sorting is used. #TODO: Add link for paper + + max_iter : int + The maximum number of iterations (full cycles) for CD per grid point. The algorithm may not use the full number + of iteration per grid point if convergence is found (defined by rtol and atol parameter) + Must be greater than 0 + + rtol : float + The relative tolerance which decides when to terminate optimization as based on the relative change in the + objective between iterations. + Must be greater than 0 and less than 1. + + atol : float + The absolute tolerance which decides when to terminate optimization as based on the absolute L2 norm of the + residuals + Must be greater than 0 + + active_set : bool + If TRUE, performs active set updates. (see our paper for for details). #TODO: Add link for paper + + active_set_num : int + The number of consecutive times a support should appear before declaring support stabilization. + (see our paper for for details). #TODO: Add link for paper + + Must be greater than 0. + + max_swaps : int + The maximum number of swaps used by CDPSI for each grid point. + Must be greater than 0. Ignored by CD algorithims. + + scale_down_factor : float + Roughly amount each lambda value is scaled by between grid points. Larger values lead to closer lambdas and + typically to smaller gaps between the support sizes. + + For details, see our paper - Section 5 on Adaptive Selection of Tuning Parameters). #TODO: Add link for paper + + Must be greater than 0 and less than 1 (strictly for both.) + + screen_size : int + The number of coordinates to cycle over when performing initial correlation screening. #TODO: Add link for paper + + Must be greater than 0 and less than number of columns of X. + + lambda_grid : list of list of floats + A grid of lambda values to use in computing the regularization path. This is by default an empty list + and is ignored. When specified, lambda_grid should be a list of list of floats, where the ith element + (corresponding to the ith gamma) should be a decreasing sequence of lambda values. The length of this sequence + is directly the number of lambdas to be tried for that gamma. + + In the the "L0" penalty case, lambda_grid should be a list of 1. + In the "L0LX" penalty cases, lambda_grid can be a list of any length. The length of lambda_grid will be the + number of gamma values tried. + + See the example notebook for more details. + + Note: When lambda_grid is supplied, num_gamma and num_lambda must be None. + + exclude_first_k : int + The first exclude_first_k features in X will be excluded from variable selection. In other words, the first + exclude_first_k variables will not be included in the L0-norm penalty however they will be included in the + L1 or L2 norm penalties, if they are specified. + + Must be a positive integer less than the columns of X. + + intercept : bool + If False, no intercept term is included or fit in the regularization path + Intercept terms are not regularized by L0 or L1/L2. + + lows : np array or float + Lower bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of + size p (number of columns of X) where lows[i] is the lower bound for coefficient i. + + Lower bounds can not be above 0 (i.e. we can not specify that all coefficients must be larger than a > 0). + Lower bounds can be set to 0 iff the corresponding upper bound for that coefficient is also not 0. + + highs : np array or float + Upper bounds for coefficients. Either a scalar for all coefficients to have the same bound or a vector of + size p (number of columns of X) where highs[i] is the upper bound for coefficient i. + + Upper bounds can not be below 0 (i.e. we can not specify that all coefficients must be smaller than a < 0). + Upper bounds can be set to 0 iff the corresponding lower bound for that coefficient is also not 0. + + Returns + ------- + fit_model : l0learn.models.FitModel + FitModel instance containing all relevant information from the solution path. + + See Also + ------- + l0learn.cvfit + l0learn.models.FitModel + + Examples + -------- + >>>fit_model = l0learn.fit(X, y, penalty="L0", max_support_size=20) + """ + + check = _fit_check( + X=X, + y=y, + loss=loss, + penalty=penalty, + algorithm=algorithm, + max_support_size=max_support_size, + num_lambda=num_lambda, + num_gamma=num_gamma, + gamma_max=gamma_max, + gamma_min=gamma_min, + partial_sort=partial_sort, + max_iter=max_iter, + rtol=rtol, + atol=atol, + active_set=active_set, + active_set_num=active_set_num, + max_swaps=max_swaps, + scale_down_factor=scale_down_factor, + screen_size=screen_size, + lambda_grid=lambda_grid, + exclude_first_k=exclude_first_k, + intercept=intercept, + lows=lows, + highs=highs, + ) + + max_support_size = check["max_support_size"] + screen_size = check["screen_size"] + y = check["y"] + penalty = check["penalty"] + gamma_max = check["gamma_max"] + gamma_min = check["gamma_min"] + lambda_grid = check["lambda_grid"] + num_gamma = check["num_gamma"] + num_lambda = check["num_lambda"] + auto_lambda = check["auto_lambda"] + with_bounds = check["with_bounds"] + lows = check["lows"] + highs = check["highs"] + + n, p = X.shape + + if not isinstance(num_folds, int) or num_folds < 2 or num_folds > n: + raise ValueError( + f"expected num_folds parameter to be a positive integer less than {n}, but got {num_folds}" + ) + + if isinstance(X, np.ndarray): + c_results = _L0LearnCV_dense( + X, + y, + loss, + penalty, + algorithm, + max_support_size, + num_lambda, + num_gamma, + gamma_max, + gamma_min, + partial_sort, + max_iter, + rtol, + atol, + active_set, + active_set_num, + max_swaps, + scale_down_factor, + screen_size, + not auto_lambda, + lambda_grid, + num_folds, + seed, + exclude_first_k, + intercept, + with_bounds, + lows, + highs, + ) + elif isinstance(X, csc_matrix): + c_results = _L0LearnCV_sparse( + X, + y, + loss, + penalty, + algorithm, + max_support_size, + num_lambda, + num_gamma, + gamma_max, + gamma_min, + partial_sort, + max_iter, + rtol, + atol, + active_set, + active_set_num, + max_swaps, + scale_down_factor, + screen_size, + auto_lambda, + lambda_grid, + num_folds, + seed, + exclude_first_k, + intercept, + with_bounds, + lows, + highs, + ) + + results = CVFitModel( + settings={"loss": loss, "intercept": intercept, "penalty": penalty}, + lambda_0=c_results.Lambda0, + gamma=c_results.Lambda12, + support_size=c_results.NnzCount, + coeffs=c_results.Beta, + intercepts=c_results.Intercept, + converged=c_results.Converged, + cv_means=c_results.CVMeans, + cv_sds=c_results.CVSDs, + ) + return results diff --git a/python/l0learn/models.py b/python/l0learn/models.py new file mode 100644 index 0000000..cd329c6 --- /dev/null +++ b/python/l0learn/models.py @@ -0,0 +1,1020 @@ +""" +.. module:: models +""" +import warnings +from dataclasses import dataclass, field +from typing import List, Dict, Any, Optional, Tuple, Union, Sequence + +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt +from scipy.sparse import csc_matrix, vstack, hstack, find +from scipy.stats import multivariate_normal, binom + + +def regularization_loss( + coeffs: csc_matrix, + l0: Union[float, Sequence[float]] = 0, + l1: Union[float, Sequence[float]] = 0, + l2: Union[float, Sequence[float]] = 0, +) -> Union[float, np.ndarray]: + """ + Calculates the regularization loss for a path of (or individual) solution(s). + + Parameters + ---------- + coeffs + l0: + l1 + l2 + + Returns + ------- + loss : float or np.ndarray of shape (K,) where K is the number of solutions in coeffs + Let the shape of coeffs be (P, K) and l0, l1, and l2 by either a float or a sequence of length K + Then loss will be of shape: (K, ) with the following layout: + loss[i] = l0[i]*||coeffs[:, i||_0 + l1[i]*||coeffs[:, i||_1 + l2[i]*||coeffs[:, i||_2 + + Notes + ----- + + """ + l0 = np.asarray(l0) + l1 = np.asarray(l1) + l2 = np.asarray(l2) + + _, num_solutions = coeffs.shape + + infer_penalties = l0 + l1 + l2 + + if infer_penalties.ndim == 0: + num_penalties = num_solutions + elif infer_penalties.ndim == 1: + num_penalties = len(infer_penalties) + else: + raise ValueError( + f"expected penalties l0, l1, and l2 mutually broadcast to a float or a 1D array," + f" but got {infer_penalties.ndim}D array." + ) + + if num_penalties != num_solutions: + raise ValueError( + f"expected number of penalties to be equal to the number of solutions if multiple penalties " + f"are provided, but got {num_penalties} penalties and {num_solutions} solutions." + ) + + l0 = np.broadcast_to(l0, [num_penalties]) + l1 = np.broadcast_to(l1, [num_penalties]) + l2 = np.broadcast_to(l2, [num_penalties]) + + if num_solutions == 1: + _, _, values = find(coeffs) + + return float( + l0 * len(values) + l1 * sum(abs(values)) + sum((np.sqrt(l2) * values) ** 2) + ) + else: # TODO: Implement this regularization loss better than a for loop over num_solutions! + _, num_solutions = coeffs.shape + loss = np.zeros(num_solutions) + + for solution_index in range(num_solutions): + loss[solution_index] = regularization_loss( + coeffs[:, solution_index], + l0=l0[solution_index], + l1=l1[solution_index], + l2=l2[solution_index], + ) + + return loss + + +# def broadcast_with_regularization_loss( +# metric_func: Callable[[Any], Tuple[np.ndarray, np.ndarray]]) -> Callable[[Any], np.ndarray]: +# @wraps(metric_func) +# def reshape_metric_loss(*args, **kwargs) -> np.ndarray: +# metric_loss, reg_loss = metric_func(*args, **kwargs) +# +# if reg_loss.ndim == 0: +# return metric_loss + reg_loss +# elif reg_loss.ndim == 2 and metric_loss.ndim == 2: +# # reg_loss of shape (l, k) +# # metric_loss of shape (m, k) +# return metric_loss[:, np.newaxis, :] + reg_loss # result of shape (m, l, k) +# elif reg_loss.ndim == 1 and metric_loss.ndim == 1: +# # reg_loss of shape (l,) +# # squared_residuals of shape (m, ) +# return metric_loss[:, np.newaxis] + reg_loss +# else: +# raise ValueError("This should not happen") +# +# return reshape_metric_loss + + +def squared_error( + y_true: np.ndarray, + y_pred: np.ndarray, + coeffs: Optional[csc_matrix] = None, + l0: Union[float, Sequence[float]] = 0, + l1: Union[float, Sequence[float]] = 0, + l2: Union[float, Sequence[float]] = 0, +) -> np.ndarray: + """ + Calculates Squared Error loss of solution with optional regularization + + Parameters + ---------- + y_true : np.ndarray of shape (m, ) + y_pred : np.ndarray of shape (m, ) or (m, k) + coeffs : np.ndarray of shape (p, k), optional + l0 : float or sequence of floats of shape (l) + l1 : float or sequence of floats of shape (l) + l2 : float or sequence of floats of shape (l) + + Returns + ------- + squared_error : np.ndarray + Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D + + """ + reg_loss = 0 + if coeffs is not None: + reg_loss = regularization_loss(coeffs=coeffs, l0=l0, l1=l1, l2=l2) + + if y_pred.ndim == 2: + y_true = y_true[:, np.newaxis] + + squared_residuals = 0.5 * np.square(y_true - y_pred).sum(axis=0) + + return squared_residuals + reg_loss + + +def logistic_loss( + y_true: np.ndarray, + y_pred: np.ndarray, + coeffs: Optional[csc_matrix] = None, + l0: float = 0, + l1: float = 0, + l2: float = 0, + eps: float = 1e-15, +) -> np.ndarray: + """ + Calculates Logistic Loss of solution with optional regularization + + Parameters + ---------- + y_true : np.ndarray of shape (m, ) + y_pred : np.ndarray of shape (m, ) or (m, k) + coeffs : np.ndarray of shape (p, k), optional + l0 : float or sequence of floats of shape (l) + l1 : float or sequence of floats of shape (l) + l2 : float or sequence of floats of shape (l) + eps: float, default=1e-15 + Logistic loss is undefined for p=0 or p=1, so probabilities are clipped to max(eps, min(1 - eps, p)). + + + Returns + ------- + logistic_loss : np.ndarray + Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D + + """ + # TODO: Check this formula. If there is an error here, there might be an error in the C++ code for Logistic. + + reg_loss = 0 + if coeffs is not None: + reg_loss = regularization_loss(coeffs=coeffs, l0=l0, l1=l1, l2=l2) + + # C++ Src Code: + # ExpyXB = arma::exp(this->y % (*(this->X) * this->B + this->b0)); + # return arma::sum(arma::log(1 + 1 / expyXB)) + regularization + + if y_pred.ndim == 2: + y_true = y_true[:, np.newaxis] + + y_pred = np.clip(y_pred, eps, 1 - eps) + + log_loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)).sum( + axis=0 + ) + + return log_loss + reg_loss + + +def squared_hinge_loss( + y_true: np.ndarray, + y_pred: np.ndarray, + coeffs: Optional[csc_matrix] = None, + l0: float = 0, + l1: float = 0, + l2: float = 0, +) -> np.ndarray: + """ + Calculates Logistic Loss of solution with optional regularization + + Parameters + ---------- + y_true : np.ndarray of shape (m, ) + y_pred : np.ndarray of shape (m, ) or (m, k) + coeffs : np.ndarray of shape (p, k), optional + l0 : float or sequence of floats of shape (l) + l1 : float or sequence of floats of shape (l) + l2 : float or sequence of floats of shape (l) + + Returns + ------- + squared_hinge_loss : np.ndarray + Shape (,) if y_pred is 1D or = (k,) if y_pred is 2D + + """ + # TODO: Check this formula. If there is an error here, there might be an error in the C++ code for Logistic. + + reg_loss = 0 + if coeffs is not None: + reg_loss = regularization_loss(coeffs=coeffs, l0=l0, l1=l1, l2=l2) + + # C++ Src Code: + # onemyxb = 1 - this->y % (*(this->X) * this->B + this->b0); + # arma::uvec indices = arma::find(onemyxb > 0); + # return arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) + this->lambda0 * n_nonzero( + # B) + this->lambda1 * arma::norm(B, 1) + this->lambda2 * l2norm * l2norm; + if y_pred.ndim == 2: + y_true = y_true[:, np.newaxis] + + square_one_minus_y_XB = np.square(np.max(1 - y_true * y_pred, 0)).sum(axis=0) + return square_one_minus_y_XB + reg_loss + + +@dataclass(frozen=True, repr=False, eq=False) +class FitModel: + """FitModel returned by calling l0learn.fit(...)""" + + settings: Dict[str, Any] + lambda_0: List[List[float]] = field(repr=False) + gamma: List[float] = field(repr=False) + support_size: List[List[int]] = field(repr=False) + coeffs: List[csc_matrix] = field(repr=False) + intercepts: List[List[float]] = field(repr=False) + converged: List[List[bool]] = field(repr=False) + + def _characteristics_as_pandas_table( + self, + new_data: Optional[ + Tuple[ + List[float], + List[List[float]], + List[List[int]], + List[List[float]], + List[List[bool]], + ] + ] = None, + ) -> pd.DataFrame: + """Formats FitModel's data as a Pandas DataFrame where each row is a solution in the regularization path. + The DataFrame is in of solutions being founnd. + + Parameters + ---------- + new_data : Optional Tuple + Should be a valid subset of `self`'s instance values that represent a consecutive subsequence of solutions + + Returns + ------- + characteristics_table : pd.DataFrame: + Pandas DataFrame of characteristics of the solution path referenced by new_data, if provided, or by the + instance values of self. Specifically: + (self.gamma, self.lambda_0, self.support_size, self.intercepts, self.converged) + """ + if new_data is not None: + gamma, lambda_0, support_size, intercepts, converged = new_data + else: + gamma, lambda_0, support_size, intercepts, converged = ( + self.gamma, + self.lambda_0, + self.support_size, + self.intercepts, + self.converged, + ) + tables = [] + for ( + gamma, + lambda_sequence, + support_sequence, + intercept_sequence, + converged_sequence, + ) in zip(gamma, lambda_0, support_size, intercepts, converged): + + data = { + "l0": lambda_sequence, + "support_size": support_sequence, + "intercept": intercept_sequence, + "converged": converged_sequence, + } + + if len(self.settings["penalty"]) > 2: + data[self.settings["penalty"][2:].lower()] = [gamma] * len( + lambda_sequence + ) + + tables.append(pd.DataFrame(data)) + + return pd.concat(tables).reset_index(drop=True) + + def _repr_html_(self): + return self._characteristics_as_pandas_table()._repr_html_() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.settings})" + + def coeff( + self, + lambda_0: Optional[float] = None, + gamma: Optional[float] = None, + include_intercept: bool = True, + ) -> csc_matrix: + """Extracts the coefficient according to `lambda_0`, `gamma`, and `include_intercept` + + Values for lambda_0` and `gamma` do not need to be exact. The closest values, respectively, found in the + regularization path will be used + + If both `lambda_0` and `gamma` are not supplied, then a matrix of coefficients for all the solutions in the + regularization path is returned. + + If `lambda_0` is supplied but `gamma` is not, the smallest value of `gamma` is used. + + If `gamma` is supplied but not `lambda_0`, then a matrix of coefficients for all the solutions in the + regularization path for `gamma` is returned. + + Parameters + ---------- + lambda_0 : float, optional + If provided, designates which solutions will be returned based of `lambda_0` and `gamma` + + gamma : float, optional + If provided, designates which solutions will be returned based of `gamma` and `lambda_0` + + include_intercept : bool, default True + If provided, will include the intercepts as the first row of the coefficient matrix + + Returns + ------- + coeff: csc_matrix + Matrix of fit coefficients from the regularization path + Has shape (p, num_solutions) if `include_intercept` is True or (p+1, num_solutions) otherwise. + where num_solutions is defined by `lambda_0` and `gamma`. + + coeff[i, :] refers to features i's coefficient across the returned solutions + coeff[:, j] refers to solution j's coefficient + coeff[i, j] refers to features i's coefficient for solution j + + """ + if gamma is None: + if lambda_0 is None: + # Return all solutions + solutions = hstack(self.coeffs) + intercepts = [ + [ + intercept + for intercept_list in self.intercepts + for intercept in intercept_list + ] + ] + else: + # Return solution with closest lambda in first gamma + return self.coeff( + gamma=self.gamma[0], + lambda_0=lambda_0, + include_intercept=include_intercept, + ) + else: + gamma_index = int(np.argmin(np.abs(np.asarray(self.gamma) - gamma))) + if lambda_0 is None: + # Return all solutions for specific gamma + solutions = self.coeffs[gamma_index] + intercepts = [self.intercepts[gamma_index]] + else: + # Return solution with closest lambda in gamma_index + lambda_index = int( + np.argmin(np.abs(np.asarray(self.lambda_0[gamma_index]) - lambda_0)) + ) + solutions = self.coeffs[gamma_index][:, lambda_index] + intercepts = [self.intercepts[gamma_index][lambda_index]] + + if include_intercept: + solutions = csc_matrix(vstack([intercepts[0], solutions])) + + return solutions + + def characteristics( + self, lambda_0: Optional[float] = None, gamma: Optional[float] = None + ) -> pd.DataFrame: + """Formats the characteristics of the solutions that correspond to the specified `lambda_0` and `gamma` as a + pandas DataFrame where each row is a solution in the regularization path. + + Parameters + ---------- + lambda_0 : float, optional + If provided, designates which solutions will be returned based of `lambda_0` and `gamma` + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + gamma : float, optional + If provided, designates which solutions will be returned based of `gamma` and `lambda_0` + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + Returns + ------- + characteristics : pd.DataFrame: + Pandas DataFrame of characteristics of the solution path referenced by `lambda_0` and `gamma` + + The characteristics table has the following columns with the following interpretations: + gamma: The value of the L1 or L2 regularization parameter used in the specific solution of + regularization path referred to by the row of the characteristic table. + + Inspect FitModel.settings for specific of L1 vs L2 + lambda_0: The value of the L0 regularization parameter used in the specific solution of + regularization path referred to by the row of the characteristic table. + + support_size: The number of non-zero coefficients found in the specific solution of + regularization path referred to by the row of the characteristic table. + + Inspect FitModel.settings for the max support size was specified + + intercepts: The value of the intercept term found in the specific solution of + regularization path referred to by the row of the characteristic table. + + Inspect FitModel.settings if tunable intercept was used or not + + converged: Whether or not the specific solution of regularization path referred to by the row of the + characteristic table converged or not. + """ + + if gamma is None: + if lambda_0 is None: + # Return all solutions + intercepts = self.intercepts + lambda_0 = self.lambda_0 + gamma = self.gamma + support_size = self.support_size + converged = self.converged + else: + # Return solution with closest lambda in first gamma + return self.characteristics(gamma=self.gamma[0], lambda_0=lambda_0) + else: + gamma_index = int(np.argmin(np.abs(np.asarray(self.gamma) - gamma))) + if lambda_0 is None: + # Return all solutions for specific gamma + intercepts = [self.intercepts[gamma_index]] + lambda_0 = [self.lambda_0[gamma_index]] + gamma = [self.gamma[gamma_index]] + support_size = [self.support_size[gamma_index]] + converged = [self.converged[gamma_index]] + else: + # Return solution with closest lambda in gamma_index + lambda_index = int( + np.argmin(np.abs(np.asarray(self.lambda_0[gamma_index]) - lambda_0)) + ) + intercepts = [[self.intercepts[gamma_index][lambda_index]]] + lambda_0 = [[self.lambda_0[gamma_index][lambda_index]]] + gamma = [self.gamma[gamma_index]] + support_size = [[self.support_size[gamma_index][lambda_index]]] + converged = [[self.converged[gamma_index][lambda_index]]] + + return self._characteristics_as_pandas_table( + new_data=(gamma, lambda_0, support_size, intercepts, converged) + ) + + def plot( + self, + gamma: float = 0, + show_lines: bool = False, + include_legend: bool = True, + **kwargs, + ): + """Plots the regularization path for a given gamma. + + Parameters + ---------- + gamma : float + The value of gamma at which to plot + Values for `gamma` do not need to be exact. The closest values, respectively, found in the regularization + path will be used + + show_lines : bool + If True, the lines connecting the points in the plot are shown. + kwargs : dict of str to any + + Key Word arguments passed to matplotlib.pyplot + + Defaults for legends: + bbox_to_anchor -> (1.05, 1) + loc -> 2 + borderaxespad -> 0. + ncol -> Size of largest support divided by 10 + + Notes + ----- + If the solutions with the same support size exist in a regularization path, the first support size is plotted. + + Returns + ------- + ax : matplotlib.axes._subplots.AxesSubplot + """ + + gamma_to_plot = self.coeff(gamma=gamma, include_intercept=False) + p = gamma_to_plot.shape[1] + + # Find what coeffs are seen in regularization path. Skip later solutions + seen_supports = set() + seen_coeffs = set() + for col in range(p): + rows, cols, values = find(gamma_to_plot[:, col]) + support_size = len(rows) + if support_size in seen_supports: + warnings.warn( + f"Duplicate solution seen at support size {support_size}. Plotting only first solution" + ) + continue + seen_supports.add(support_size) + seen_coeffs.update(rows) + + # For each coefficient seen in regularization path, record value of each coefficient over + seen_coeffs = list(sorted(seen_coeffs)) + coef_value_over_path = gamma_to_plot[seen_coeffs, :].toarray() + path_support_size = (gamma_to_plot != 0).sum(axis=0).T + + f, ax = plt.subplots(**kwargs) + + if show_lines: + linestyle = kwargs.pop("linestyle", "-.") + else: + linestyle = kwargs.pop("linestyle", "None") + + marker = kwargs.pop("marker", "o") + + for i, coeff in enumerate(seen_coeffs): + ax.plot( + path_support_size, + coef_value_over_path[i, :], + label=coeff, + linestyle=linestyle, + marker=marker, + ) + + plt.ylabel("Coefficient Value") + plt.xlabel("Support Size") + + if include_legend: + plt.legend( + bbox_to_anchor=(1.05, 1), + loc=2, + borderaxespad=0.0, + ncol=len(seen_coeffs) // 10, + ) + + return ax + + def score( + self, + x: np.ndarray, + y: np.ndarray, + lambda_0: Optional[float] = None, + gamma: Optional[float] = None, + training: bool = False, + include_characteristics: bool = False, + ) -> Union[pd.DataFrame, np.ndarray]: + """Scores the performance of solutions in the regularization path at predicting `y_hat` based + on features in `x`. + + Scoring function used is specified by the FitModel.settings #TODO Add sphinx reference to seetings + + Parameters + ---------- + x : np.ndarray of shape (N, P) + Features or design matrix to predict `y_hat` from + Does not inclue an intercept term as the model will automatically add one if specified in + FitMode.settings + + y : np.ndarray of shape (N, ) + Observations or ground truths of `y` which will be compared with `y_hat` + + See FitModel.predict for details on predictions + :meth:`l0learn.models.FitModel.predict` + + lambda_0 : float, optional + If provided, designates which solutions will be returned based of `lambda_0` and `gamma` + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + gamma : float, optional + If provided, designates which solutions will be returned based of `gamma` and `lambda_0` + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + training: bool, default False + Whether or not to include regularization losses when calculating model performance. + + If `training` is True, all penalties according to FitModel.settings will be applied. + Otherwise, only the loss function will be calculated based on `y` and `y_hat` + + include_characteristics: bool, default False + Whether or not to include the characteristics of each solution. This will result in `score` beginning + returned as a Pandas DataFrame similar to FitModel.characteristics, but with an additional column for score + + See FitModel.characteristics for details on characteristics + :meth:`l0learn.models.FitModel.characteristics` + + Returns + ------- + score: np.ndarray of shape (num_solutions) or pd.Dataframe of length num_solutions + + Where score[i] is the loss function between y and y_hat based on the ith solution of the regularization path + specified by `gamma` and `lamda_0` values. + + If `include_characteristics` is True, score will be added as a column of the characteristics table + + """ + predictions = self.predict(x=x, lambda_0=lambda_0, gamma=gamma) + characteristics = self.characteristics(lambda_0=lambda_0, gamma=gamma) + + if training: + coeffs = self.coeff(lambda_0=lambda_0, gamma=gamma, include_intercept=False) + l0 = characteristics.get("l0", 0) + l1 = characteristics.get("l1", 0) + l2 = characteristics.get("l2", 0) + else: + coeffs = None + l0 = 0 + l1 = 0 + l2 = 0 + + if self.settings["loss"] == "SquaredError": + score = squared_error( + y_true=y, + y_pred=predictions, + coeffs=coeffs, + l0=l0, + l1=l1, + l2=l2, + ) + elif self.settings["loss"] == "Logistic": + score = logistic_loss( + y_true=y, + y_pred=predictions, + coeffs=coeffs, + l0=l0, + l1=l1, + l2=l2, + ) + else: + score = squared_hinge_loss( + y_true=y, + y_pred=predictions, + coeffs=coeffs, + l0=l0, + l1=l1, + l2=l2, + ) + + if include_characteristics: + characteristics[self.settings["loss"]] = score + return characteristics + else: + return score + + def predict( + self, + x: np.ndarray, + lambda_0: Optional[float] = None, + gamma: Optional[float] = None, + ) -> np.ndarray: + """Predicts the response for a given sample. + + Parameters + ---------- + x: array-like + An array of feature observations on which predictions are made. + X should have shape (N, P). + Intercept terms will be added by the predict function if specified during original training. + #TODO Sphinx Reference for Settings + + lambda_0 : float, optional + Which `lambda_0` value to use for predictions + + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + gamma : float, optional + Which gamma value to use for predictions + + See FitModel.coeff for details on `lambda_0` specifications + :meth:`l0learn.models.FitModel.coeff` + + Returns + ------- + predictions : np.ndarray + Predictions of the response vector given x. + + For logistic regression (loss specified as "Logistic"), values are non-threshold + + When multiple coeffs are specified due to the settings of `lambda_0` and `gamma` the prediction array is + formatted with standard convention: + predictions[i, j] refer to predicted response for observation i using coefficients from solution j. + """ + coeffs = self.coeff( + lambda_0=lambda_0, gamma=gamma, include_intercept=self.settings["intercept"] + ) + + n = x.shape[0] + if self.settings["intercept"]: + x = np.hstack([np.ones((n, 1)), x]) + + activations = x @ coeffs + + if self.settings["loss"] == "Logistic": + return 1 / (1 + np.exp(-activations)) + else: + return activations + + +@dataclass(frozen=True, repr=False, eq=False) +class CVFitModel(FitModel): + cv_means: List[np.ndarray] = field(repr=False) + cv_sds: List[np.ndarray] = field(repr=False) + + def cv_plot(self, gamma: float = 0, **kwargs): + """ + Plot the cross-validation errors for a given gamma. + + Parameters + ---------- + gamma : float + The value of gamma at which to plot + kwargs + Key Word arguments passed to matplotlib.pyplot + Returns + ------- + ax : matplotlib.axes._subplots.AxesSubplot + """ + gamma_index = int(np.argmin(np.abs(np.asarray(self.gamma) - gamma))) + f, ax = plt.subplots(**kwargs) + + plt.errorbar( + x=self.support_size[gamma_index], + y=self.cv_means[gamma_index], + yerr=self.cv_sds[gamma_index], + ) + + plt.ylabel("Cross-Validation Error") + plt.xlabel("Support Size") + + return ax + + +def gen_synthetic( + n: int, + p: int, + k: int, + seed: Optional[int] = None, + rho: float = 0, + b0: float = 0, + snr: float = 1, +) -> Dict[str, Union[float, np.ndarray]]: # pragma: no cover + """ + Generates a synthetic dataset as follows: + 1) Sample every element in data matrix X from N(0,1). + 2) Generate a vector B with the first k entries set to 1 and the rest are zeros. + 3) Sample every element in the noise vector e from N(0,A) where A is selected so y, X, B have snr as specified. + 4) Set y = XB + b0 + e. + + Parameters + ---------- + n : int + Number of samples + p : int + Number of features + k : int + Number of non-zeros in true vector of coefficients + seed : int, optional + The seed used for randomly generating the data + If None, numbers will be random + rho : float, default 0. + The threshold for setting X values to 0. if |X[i, j]| < rho => X[i, j] <- 0 + b0 : float, default 0 + The intercept value to translate y by. + snr : float, default 1 + Desired Signal-to-Noise ratio. This sets the magnitude of the error term 'e'. + + Note: SNR is defined as SNR = Var(XB)/Var(e) + + Returns + ------- + Data: Dict + A dict containing: + the data matrix X, + the response vector y, + the coefficients B, + the error vector e, + the intercept term b0. + + Examples + -------- + >>>data = gen_synthetic(n=500,p=1000,k=10,seed=1) + """ + + # TODO: Don't re-seed generator. https://numpy.org/doc/stable/reference/random/generated/numpy.random.seed.html + np.random.seed(seed) + + X = np.random.normal(size=(n, p)) + X[abs(X) < rho] = 0 + B = np.zeros(p) + B[0:k] = 1 + + if snr == float("Inf"): + sd_e = 0 + else: + sd_e = np.sqrt(np.var(X @ B) / snr) + + e = np.random.normal(size=n, scale=sd_e) + y = X @ B + e + b0 + return {"X": X, "y": y, "B": B, "e": e, "b0": b0} + + +def cor_matrix(p: int, base_cor: float): # pragma: no cover + if not (0 < base_cor < 1): + raise ValueError( + f"Expected base_cor to be a float between 0 and 1 exclusively, but got {base_cor}" + ) + + cor_mat = base_cor * np.ones((p, p)) + pow_mat = abs(np.arange(p) - np.arange(p).reshape(-1, 1)) + + return np.power(cor_mat, pow_mat) + + +def gen_synthetic_high_corr( + n: int, + p: int, + k: int, + seed: Optional[int] = None, + rho: float = 0, + b0: float = 0, + snr: float = 1, + mu: float = 0, + base_cor: float = 0.8, +) -> Dict[str, Union[float, np.ndarray]]: # pragma: no cover + """ + Generates a synthetic dataset as follows: + 1) Generate a correlation matrix, SIG, where item [i, j] = base_core^|i-j|. + 2) Draw from a Multivariate Normal Distribution using (mu and SIG) to generate X. + 3) Generate a vector B with every ~p/k entry set to 1 and the rest are zeros. + 4) Sample every element in the noise vector e from N(0,A) where A is selected so y, X, B have snr as specified. + 5) Set y = XB + b0 + e. + + Parameters + ---------- + n : int + Number of samples + p : int + Number of features + k : int + Number of non-zeros in true vector of coefficients + seed : int + The seed used for randomly generating the data + rho : float + The threshold for setting values to 0. if |X[i, j]| < rho => X[i, j] <- 0 + b0 : float + intercept value to scale y by. + snr : float + desired Signal-to-Noise ratio. This sets the magnitude of the error term 'e'. + SNR is defined as SNR = Var(XB)/Var(e) + mu : float + The mean for drawing from the Multivariate Normal Distribution. A scalar of vector of length p. + base_cor : float + The base correlation, A in [i, j] = A^|i-j|. + + Returns + ------- + data : dict + A dict containing: + the data matrix X, + the response vector y, + the coefficients B, + the error vector e, + the intercept term b0. + + Examples + -------- + >>>gen_synthetic_high_corr(n=500,p=1000,k=10,seed=1) + + """ + # TODO: Don't re-seed generator. https://numpy.org/doc/stable/reference/random/generated/numpy.random.seed.html + np.random.seed(seed) + + cor = cor_matrix(p, base_cor) + X = multivariate_normal(n, mu, cor) + + X[abs(X) < rho] = 0.0 + + B = np.zeros(p) + for i in np.round(np.linspace(start=0, stop=p, num=k)).astype(int): + B[i] = 1 + + if snr == float("inf"): + sd_e = 0 + else: + sd_e = np.sqrt(np.var(X @ B) / snr) + + e = np.random.normal(size=n, scale=sd_e) + y = X @ B + e + b0 + + return {"X": X, "y": y, "B": B, "e": e, "b0": b0} + + +def gen_synthetic_logistic( + n: int, + p: int, + k: int, + seed: Optional[int] = None, + rho: float = 0, + b0: float = 0, + s: float = 1, + mu: Optional[float] = None, + base_cor: Optional[float] = None, +) -> Dict[str, Union[float, np.ndarray]]: # pragma: no cover + """ + Generates a synthetic dataset as follows: + 1) Generate a data matrix, X, drawn from either N(0, 1) (see gen_synthetic) or a multivariate_normal(mu, sigma) + (See gen_synthetic_high_corr) + gen_synthetic_logistic delegates these caluclations to the respective functions. + 2) Generate a vector B with k entries set to 1 and the rest are zeros. + 3) Every coordinate yi of the outcome vector y exists in {0, 1}^n is sampled independently from a Bernoulli + distribution with success probability: + P(yi = 1|xi) = 1/(1 + exp(-s)) + Source https://arxiv.org/pdf/2001.06471.pdf Section 5.1 Data Generation + + Parameters + ---------- + n : int + Number of samples + + p : int + Number of features + + k : int + Number of non-zeros in true vector of coefficients + + seed : int + The seed used for randomly generating the data + + rho : float + The threshold for setting values to 0. if |X[i, j]| > rho => X[i, j] <- 0 + + b0 : float + The intercept value to scale the log odds of y by. + As b0 -> +inf, y will contain more 1s + As b0 -> -inf, y will contain more 0s + + s : float + Signal-to-noise parameter. As s -> +Inf, the data generated becomes linearly separable. + + mu : float, optional + The mean for drawing from the Multivariate Normal Distribution. A scalar of vector of length p. + If mu and base_cor are not specified, will be drawn from N(0, 1) using gen_synthetic. + If mu and base_cor are specified will be drawn from multivariate_normal(mu, sigma) see gen_synthetic_high_corr + + base_cor : float + The base correlation, A in [i, j] = A^|i-j|. + If mu and base_cor are not specified, will be drawn from N(0, 1) + If mu and base_cor are specified will be drawn from multivariate_normal(mu, sigma) see gen_synthetic_high_corr + + Returns + ------- + data : dict + A dict containing: + the data matrix X + the response vector y, + the coefficients B, + the intercept term b0. + + """ + if mu is None and base_cor is None: + data = gen_synthetic(n=n, p=p, k=k, seed=seed, rho=rho, b0=b0, snr=float("Inf")) + else: + data = gen_synthetic_high_corr( + n=n, + p=p, + k=k, + seed=seed, + rho=rho, + b0=b0, + snr=float("Inf"), + mu=mu, + base_cor=base_cor, + ) + + y = binom.rvs(1, 1 / (1 + np.exp(-s * data["X"] @ data["B"])), size=n) + + data["y"] = y + del data["e"] + + return data diff --git a/python/l0learn/pyl0learn.cpp b/python/l0learn/pyl0learn.cpp new file mode 100644 index 0000000..b4cf34b --- /dev/null +++ b/python/l0learn/pyl0learn.cpp @@ -0,0 +1,155 @@ +#include "pyl0learn.h" + +namespace py = pybind11; + + +arma::sp_mat to_sparse(const py::object& S){ + py::tuple shape = S.attr("shape").cast< py::tuple >(); + const auto nr = shape[0].cast< size_t >(), nc = shape[1].cast< size_t >(); + const arma::uvec ind = carma::arr_to_col(S.attr("indices").cast< py::array_t< arma::uword > >()); + const arma::uvec ind_ptr = carma::arr_to_col(S.attr("indptr").cast< py::array_t< arma::uword > >()); + const arma::vec data = carma::arr_to_col(S.attr("data").cast< py::array_t< double > >()); + return arma::sp_mat(ind, ind_ptr, data, nr, nc); +} + +py::object to_py_sparse(const arma::sp_mat& X){ + py::module_ scipy_sparse = py::module_::import("scipy.sparse"); + + py::array_t< double > values = carma::col_to_arr(arma::vec(X.values, X.n_elem)); + py::array_t< arma::uword > rowind = carma::col_to_arr(arma::uvec (X.row_indices, X.n_elem)); + py::array_t< arma::uword > colptr = carma::col_to_arr(arma::uvec (X.col_ptrs, X.n_cols + 1)); + const py::tuple shape = py::make_tuple(X.n_rows, X.n_cols); + + return scipy_sparse.attr("csc_matrix")(py::make_tuple(values.squeeze(), rowind.squeeze(), colptr.squeeze()), shape); +} + +py::list col_field_to_list(const arma::field &x) { + py::list lst(x.n_elem); + + for (auto i = 0; i < x.n_elem; i++) { + lst[i] = carma::col_to_arr(x[i]); + } + return lst; +} + + +py::list sparse_field_to_list(const arma::field &x) { + py::list lst(x.n_elem); + + for (auto i = 0; i < x.n_elem; i++) { + lst[i] = to_py_sparse(x[i]); + } + return lst; +} + +py_fitmodel L0LearnFit_sparse_wrapper( + const py::object &X, const arma::vec &y, const std::string &Loss, + const std::string &Penalty, const std::string &Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, const double atol, + const bool ActiveSet, const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, + const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + + return py_fitmodel(L0LearnFit( + to_sparse(X), y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, + G_nrows, Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, + ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, + LambdaU, Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs)); +} + +py_fitmodel L0LearnFit_dense_wrapper( + const arma::mat &X, const arma::vec &y, const std::string &Loss, + const std::string &Penalty, const std::string &Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, const double atol, + const bool ActiveSet, const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, + const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + return py_fitmodel(L0LearnFit( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, + Intercept, withBounds, Lows, Highs)); +} + +py_cvfitmodel L0LearnCV_dense_wrapper( + const arma::mat &X, const arma::vec &y, const std::string &Loss, + const std::string &Penalty, const std::string &Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, const double atol, + const bool ActiveSet, const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, const unsigned int nfolds, + const size_t seed, const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + return py_cvfitmodel(L0LearnCV( + X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, nfolds, seed, + ExcludeFirstK, Intercept, withBounds, Lows, Highs)); +} + +py_cvfitmodel L0LearnCV_sparse_wrapper( + const py::object &X, const arma::vec &y, const std::string &Loss, + const std::string &Penalty, const std::string &Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, const double atol, + const bool ActiveSet, const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, const unsigned int nfolds, + const size_t seed, const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + return py_cvfitmodel(L0LearnCV( + to_sparse(X), y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, + G_nrows, Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, + ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, + LambdaU, Lambdas, nfolds, seed, ExcludeFirstK, Intercept, withBounds, + Lows, Highs)); +} + +PYBIND11_MODULE(l0learn, m) { + m.def("_L0LearnFit_dense", &L0LearnFit_dense_wrapper); + + m.def("_L0LearnFit_sparse", &L0LearnFit_sparse_wrapper); + + m.def("_L0LearnCV_dense", &L0LearnCV_dense_wrapper); + + m.def("_L0LearnCV_sparse", &L0LearnCV_sparse_wrapper); + + py::class_(m, "_py_fitmodel") + .def(py::init()) + .def_readonly("Lambda0", &py_fitmodel::Lambda0) + .def_readonly("Lambda12", &py_fitmodel::Lambda12) + .def_readonly("NnzCount", &py_fitmodel::NnzCount) + .def_readonly("Beta", &py_fitmodel::Beta) + .def_readonly("Intercept", &py_fitmodel::Intercept) + .def_readonly("Converged", &py_fitmodel::Converged); + + py::class_(m, "_py_cvfitmodel") + .def(py::init()) + .def_readonly("Lambda0", &py_cvfitmodel::Lambda0) + .def_readonly("Lambda12", &py_cvfitmodel::Lambda12) + .def_readonly("NnzCount", &py_cvfitmodel::NnzCount) + .def_readonly("Beta", &py_cvfitmodel::Beta) + .def_readonly("Intercept", &py_cvfitmodel::Intercept) + .def_readonly("Converged", &py_cvfitmodel::Converged) + .def_readonly("CVMeans", &py_cvfitmodel::CVMeans) + .def_readonly("CVSDs", &py_cvfitmodel::CVSDs); +} diff --git a/python/l0learn/pyl0learn.h b/python/l0learn/pyl0learn.h new file mode 100644 index 0000000..1bdea20 --- /dev/null +++ b/python/l0learn/pyl0learn.h @@ -0,0 +1,119 @@ +#ifndef PYTHON_L0LEARN_CORE_H +#define PYTHON_L0LEARN_CORE_H + +#include +#include +#include + +#include "L0LearnCore.h" +#include "arma_includes.h" + +#include + +//struct sparse_mat { +// const arma::uvec rowind; +// const arma::uvec colptr; +// const arma::vec values; +// const arma::uword n_rows; +// const arma::uword n_cols; +// +// explicit sparse_mat(const arma::sp_mat &x) +// : rowind(arma::uvec(x.row_indices, x.n_elem)), +// colptr(arma::uvec(x.col_ptrs, x.n_cols + 1)), +// values(arma::vec(x.values, x.n_elem)), +// n_rows(x.n_rows), +// n_cols(x.n_cols) {} +//// sparse_mat(const sparse_mat &x) +//// : rowind(x.rowind), +//// colptr(x.colptr), +//// values(x.values), +//// n_rows(x.n_rows), +//// n_cols(x.n_cols) {} +// sparse_mat(arma::uvec rowind, arma::uvec colptr, +// arma::vec values, const arma::uword n_rows, +// const arma::uword n_cols) +// : rowind(std::move(rowind)), +// colptr(std::move(colptr)), +// values(std::move(values)), +// n_rows(n_rows), +// n_cols(n_cols) { +// COUT << "rowind/row_indices.size" << this->rowind.size() << " " << arma::sum(this->rowind) <<" \n"; +// COUT << "colptr/col_ptrs" << this->colptr.size() << " " << arma::sum(this->colptr) <<" \n"; +// COUT << "values/values" << this->values.size() << " " << arma::sum(this->values) <<" \n"; +// } +// +// arma::sp_mat to_arma_object() const { +// COUT << "to_arma_object begin\n"; +// COUT << "rowind/row_indices " << this->rowind << "\n"; +// COUT << "rowind/row_indices.size" << this->rowind.size() << " " << arma::sum(this->rowind) <<" \n"; +// COUT << "colptr/col_ptrs" << this->colptr.size() << " " << arma::sum(this->colptr) <<" \n"; +// COUT << "values/values" << this->values.size() << " " << arma::sum(this->values) <<" \n"; +// COUT << "n_rows/n_rows" << this->n_rows <<" \n"; +// COUT << "n_cols/n_cols" << this->n_cols <<" \n"; +// std::this_thread::sleep_for(std::chrono::milliseconds(1000)); +// return {this->rowind, this->colptr, this->values, this->n_rows, +// this->n_cols}; +// } +//}; + +py::list col_field_to_list(const arma::field &x); + +py::list sparse_field_to_list(const arma::field &x); + + + +struct py_fitmodel { + std::vector> Lambda0; + std::vector Lambda12; + std::vector> NnzCount; + py::list Beta; + std::vector> Intercept; + std::vector> Converged; + + py_fitmodel(const py_fitmodel &) = default; + py_fitmodel(std::vector> &lambda0, + std::vector &lambda12, + std::vector> &nnzCount, + py::list &beta, + std::vector> &intercept, + std::vector> &converged) + : Lambda0(lambda0), + Lambda12(lambda12), + NnzCount(nnzCount), + Beta(beta), + Intercept(intercept), + Converged(converged) {} + + explicit py_fitmodel(const fitmodel &f) + : Lambda0(f.Lambda0), + Lambda12(f.Lambda12), + NnzCount(f.NnzCount), + Beta(sparse_field_to_list(f.Beta)), + Intercept(f.Intercept), + Converged(f.Converged) {} +}; + +struct py_cvfitmodel : py_fitmodel { + py::list CVMeans; + py::list CVSDs; + + py_cvfitmodel(const py_cvfitmodel &) = default; + + py_cvfitmodel(std::vector> &lambda0, + std::vector &lambda12, + std::vector> &nnzCount, + py::list &beta, + std::vector> &intercept, + std::vector> &converged, + py::list &cVMeans, py::list &cVSDs) + : py_fitmodel(lambda0, lambda12, nnzCount, beta, intercept, converged), + CVMeans(cVMeans), + CVSDs(cVSDs) {} + + explicit py_cvfitmodel(const cvfitmodel &f) + : py_fitmodel(f), + CVMeans(col_field_to_list(f.CVMeans)), + CVSDs(col_field_to_list(f.CVSDs)) {} +}; + +#endif // PYTHON_L0LEARN_CORE_H diff --git a/python/l0learn/src/Normalize.cpp b/python/l0learn/src/Normalize.cpp new file mode 100644 index 0000000..e6f313f --- /dev/null +++ b/python/l0learn/src/Normalize.cpp @@ -0,0 +1,14 @@ +#include "Normalize.h" + +std::tuple DeNormalize(beta_vector &B_scaled, + arma::vec &BetaMultiplier, + arma::vec &meanX, double meany) { + beta_vector B_unscaled = B_scaled % BetaMultiplier; + double intercept = meany - arma::dot(B_unscaled, meanX); + // Matrix Type, Intercept + // Dense, True -> meanX = colMeans(X) + // Dense, False -> meanX = 0 Vector (meany = 0) + // Sparse, True -> meanX = 0 Vector + // Sparse, False -> meanX = 0 Vector + return std::make_tuple(B_unscaled, intercept); +} diff --git a/python/l0learn/src/include/BetaVector.h b/python/l0learn/src/include/BetaVector.h new file mode 100644 index 0000000..1851c60 --- /dev/null +++ b/python/l0learn/src/include/BetaVector.h @@ -0,0 +1,103 @@ +#ifndef BETA_VECTOR_H +#define BETA_VECTOR_H +#include + +#include "arma_includes.h" + +/* + * arma::vec implementation + */ + +using beta_vector = arma::vec; +// using beta_vector = arma::sp_mat; + +inline std::vector nnzIndicies(const arma::vec &B) { + // Returns a vector of the Non Zero Indicies of B + const arma::ucolvec nnzs_indicies = arma::find(B); + return arma::conv_to>::from(nnzs_indicies); +} + +// std::vector nnzIndicies(const arma::sp_mat& B){ +// // Returns a vector of the Non Zero Indicies of B +// std::vector S; +// arma::sp_mat::const_iterator it; +// const arma::sp_mat::const_iterator it_end = B.end(); +// for(it = B.begin(); it != it_end; ++it) +// { +// S.push_back(it.row()); +// } +// return S; +// } + +inline std::vector nnzIndicies(const arma::vec &B, + const std::size_t low) { + // Returns a vector of the Non Zero Indicies of a slice of B starting at low + // This is for NoSelectK situations + const arma::vec B_slice = B.subvec(low, B.n_rows - 1); + const arma::ucolvec nnzs_indicies = arma::find(B_slice); + return arma::conv_to>::from(nnzs_indicies); +} + +// std::vector nnzIndicies(const arma::sp_mat& B, const std::size_t +// low){ +// // Returns a vector of the Non Zero Indicies of B +// std::vector S; +// +// +// arma::sp_mat::const_iterator it; +// const arma::sp_mat::const_iterator it_end = B.end(); +// +// +// for(it = B.begin(); it != it_end; ++it) +// { +// if (it.row() >= low){ +// S.push_back(it.row()); +// } +// } +// return S; +// } + +inline std::size_t n_nonzero(const arma::vec &B) { + const arma::vec nnzs = arma::nonzeros(B); + return nnzs.n_rows; +} + +// std::size_t n_nonzero(const arma::sp_mat& B){ +// return B.n_nonzero; +// +// } + +inline bool has_same_support(const arma::vec &B1, const arma::vec &B2) { + if (B1.size() != B2.size()) { + return false; + } + std::size_t n = B1.n_rows; + + bool same_support = true; + for (std::size_t i = 0; i < n; i++) { + same_support = same_support && ((B1.at(i) != 0) == (B2.at(i) != 0)); + } + return same_support; +} + +// bool has_same_support(const arma::sp_mat& B1, const arma::sp_mat& B2){ +// +// if (B1.n_nonzero != B2.n_nonzero) { +// return false; +// } else { // same number of nnz and Supp is sorted +// arma::sp_mat::const_iterator i1, i2; +// const arma::sp_mat::const_iterator i1_end = B1.end(); +// +// +// for(i1 = B1.begin(), i2 = B2.begin(); i1 != i1_end; ++i1, ++i2) +// { +// if(i1.row() != i2.row()) +// { +// return false; +// } +// } +// return true; +// } +// } + +#endif // BETA_VECTOR_H diff --git a/python/l0learn/src/include/CD.h b/python/l0learn/src/include/CD.h new file mode 100644 index 0000000..68db5a2 --- /dev/null +++ b/python/l0learn/src/include/CD.h @@ -0,0 +1,543 @@ +#ifndef CD_H +#define CD_H +#include +#include + +#include "BetaVector.h" +#include "FitResult.h" +#include "Model.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +constexpr double lambda1_fudge_factor = 1e-15; + +template +class CDBase { + protected: + std::size_t NoSelectK; + std::vector *Xtr; + std::size_t n, p; + std::size_t Iter; + + beta_vector B; + beta_vector Bprev; + + std::size_t SameSuppCounter = 0; + double objective; + std::vector Order; // Cycling order + std::vector OldOrder; // Cycling order to be used after support + // stabilization + convergence. + FitResult result; + + /* Intercept and b0 are used for: + * 1. Classification as b0 is updated iteratively in the CD algorithm + * 2. Regression on Sparse Matrices as we cannot adjust the support of + * the columns of X and thus b0 must be updated iteraveily from the + * residuals + */ + double b0 = 0; + const double lambda0; + const double lambda1; + const double lambda2; + double thr; + double thr2; // threshold squared; + + bool isSparse; + const bool intercept; + const bool withBounds; + + public: + const T *X; + const arma::vec y; + std::vector ModelParams; + + char CyclingOrder; + std::size_t MaxIters; + std::size_t + CurrentIters; // current number of iterations - maintained by Converged() + const double rtol; + const double atol; + arma::vec Lows; + arma::vec Highs; + bool ActiveSet; + std::size_t ActiveSetNum; + bool Stabilized = false; + + CDBase(const T &Xi, const arma::vec &yi, const Params &P); + + FitResult Fit(); + + virtual ~CDBase() {} + + virtual inline double Objective(const arma::vec &, const beta_vector &) = 0; + + virtual inline double Objective() = 0; + + virtual FitResult _FitWithBounds() = 0; + + virtual FitResult _Fit() = 0; + + static CDBase *make_CD(const T &Xi, const arma::vec &yi, const Params &P); +}; + +template +class CDSwaps : public CDBase { + protected: + std::size_t MaxNumSwaps; + Params P; + + public: + CDSwaps(const T &Xi, const arma::vec &yi, const Params &P); + + virtual ~CDSwaps(){}; +}; + +class NotImplemented : public std::logic_error { + public: + NotImplemented() : std::logic_error("Function not yet implemented"){}; +}; + +template +class CD : public CDBase { + protected: + std::size_t ScreenSize; + std::vector Range1p; + + public: + CD(const T &Xi, const arma::vec &yi, const Params &P); + + virtual ~CD(){}; + + inline double GetBiGrad(const std::size_t i) { + // Must be implemented in Child Classes + throw NotImplemented(); + }; + + inline double GetBiValue(const double old_Bi, const double grd_Bi) { + // Must be implemented in Child Classes + throw NotImplemented(); + }; + + inline double GetBiReg(const double nrb_Bi) { + // Must be implemented in Child Classes + throw NotImplemented(); + }; + + inline void ApplyNewBi(const std::size_t i, const double old_Bi, + const double new_Bi) { + // Must be implemented in Child Classes + throw NotImplemented(); + }; + + inline void ApplyNewBiCWMinCheck(const std::size_t i, const double old_Bi, + const double new_Bi) { + // Must be implemented in Child Classes + throw NotImplemented(); + }; + + void UpdateBi(const std::size_t i); + + void UpdateBiWithBounds(const std::size_t i); + + bool UpdateBiCWMinCheck(const std::size_t i, const bool Cwmin); + + bool UpdateBiCWMinCheckWithBounds(const std::size_t i, const bool Cwmin); + + void RestrictSupport(); + + void UpdateSparse_b0(arma::vec &r); + + bool isConverged(); + + bool CWMinCheck(); + + bool CWMinCheckWithBounds(); +}; + +template +void CD::UpdateBiWithBounds(const std::size_t i) { + // Update a single coefficient of B for various CD Settings + // The following functions are virtual and must be defined for any CD + // implementation. + // GetBiValue + // GetBiValue + // GetBiReg + // ApplyNewBi + // ApplyNewBiCWMinCheck (found in UpdateBiCWMinCheck) + + const double grd_Bi = + static_cast(this)->GetBiGrad(i); // Gradient of Loss wrt to Bi + + (*this->Xtr)[i] = + std::abs(grd_Bi); // Store absolute value of gradient for later steps + + const double old_Bi = + this->B[i]; // copy of old Bi to adjust residuals if Bi updates + + const double nrb_Bi = + static_cast(this)->GetBiValue(old_Bi, grd_Bi); + // Update Step for New No regularization No Bounds Bi: + // n r b _Bi => nrb_Bi + // Example + // For CDL0: the update step is nrb_Bi = old_Bi + grd_Bi + + const double reg_Bi = static_cast(this)->GetBiReg(nrb_Bi); + // Ideal Bi with L1 and L2 regularization (no bounds) + // Does not account for L0 regularziaton + // Example + // For CDL0: reg_Bi = nrb_Bi as there is no L1, L2 parameters + + const double bnd_Bi = + clamp(std::copysign(reg_Bi, nrb_Bi), this->Lows[i], this->Highs[i]); + // Ideal Bi with regularization and bounds + + // Rcpp::Rcout << "reg_Bi: " << reg_Bi << "\n"; + // Rcpp::Rcout << "new_Bi: " << bnd_Bi << "\n"; + // Rcpp::Rcout << "this->thr: " << this->thr << "\n"; + + if (i < this->NoSelectK) { + // L0 penalty is not applied for NoSelectK Variables. + // Only L1 and L2 (if either are used) + if (std::abs(nrb_Bi) > this->lambda1) { + static_cast(this)->ApplyNewBi(i, old_Bi, bnd_Bi); + // Rcpp::Rcout << "No Select k, Old: " << old_Bi << ", New: " << bnd_Bi << + // "\n"; + } else if (old_Bi != 0) { + static_cast(this)->ApplyNewBi(i, old_Bi, 0); + // Rcpp::Rcout << "No Select k, Old: " << old_Bi << ", New: " << 0 << + // "\n"; + } + } else if (reg_Bi < this->thr) { + // If ideal non-bounded reg_Bi is less than threshold, coefficient is not + // worth setting. + if (old_Bi != 0) { + static_cast(this)->ApplyNewBi(i, old_Bi, 0); + // Rcpp::Rcout << "Below Thresh, Old: " << old_Bi << ", New: " << 0 << + // "\n"; + } + } else { + // Thus reg_Bi >= this->thr + + const double delta_tmp = std::sqrt(reg_Bi * reg_Bi - this->thr2); + // Due to numerical precision delta_tmp might be nan + // Turns nans to 0. + const double delta = (delta_tmp == delta_tmp) ? delta_tmp : 0; + + const double range_Bi = std::copysign(reg_Bi, nrb_Bi); + + if ((range_Bi - delta < bnd_Bi) && (bnd_Bi < range_Bi + delta)) { + // bnd_Bi exists in [bnd_Bi - delta, bnd_Bi + delta] + // Therefore accept bnd_Bi + static_cast(this)->ApplyNewBi(i, old_Bi, bnd_Bi); + // Rcpp::Rcout << "Old: " << old_Bi << ", New: " << bnd_Bi << "\n"; + } else if (old_Bi != 0) { + // Otherwise, reject bnd_Bi + static_cast(this)->ApplyNewBi(i, old_Bi, 0); + // Rcpp::Rcout << "Old: " << old_Bi << ", New: " << 0 << "\n"; + } + } +} + +template +void CD::UpdateBi(const std::size_t i) { + // Update a single coefficient of B for various CD Settings + // The following functions are virtual and must be defined for any CD + // implementation. + // GetBiValue + // GetBiValue + // GetBiReg + // ApplyNewBi + // ApplyNewBiCWMinCheck (found in UpdateBiCWMinCheck) + + const double grd_Bi = + static_cast(this)->GetBiGrad(i); // Gradient of Loss wrt to Bi + + (*this->Xtr)[i] = + std::abs(grd_Bi); // Store absolute value of gradient for later steps + + const double old_Bi = + this->B[i]; // copy of old Bi to adjust residuals if Bi updates + + const double nrb_Bi = + static_cast(this)->GetBiValue(old_Bi, grd_Bi); + // Update Step for New No regularization No Bounds Bi: + // n r b _Bi => nrb_Bi + // Example + // For CDL0: the update step is nrb_Bi = old_Bi + grd_Bi + + const double reg_Bi = static_cast(this)->GetBiReg(nrb_Bi); + // Ideal Bi with L1 and L2 regularization (no bounds) + // Does not account for L0 regularization + // Example + // For CDL0: reg_Bi = nrb_Bi as there is no L1, L2 parameters + + const double new_Bi = std::copysign(reg_Bi, nrb_Bi); + + if (i < this->NoSelectK) { + // L0 penalty is not applied for NoSelectK Variables. + // Only L1 and L2 (if either are used) + if (std::abs(nrb_Bi) > this->lambda1) { + static_cast(this)->ApplyNewBi(i, old_Bi, new_Bi); + } else if (old_Bi != 0) { + static_cast(this)->ApplyNewBi(i, old_Bi, 0); + } + } else if (reg_Bi < this->thr + lambda1_fudge_factor) { + // If ideal non-bounded reg_Bi is less than threshold, coefficient is not + // worth setting. + if (old_Bi != 0) { + static_cast(this)->ApplyNewBi(i, old_Bi, 0); + // Rcpp::Rcout << "Z" << i <<" "; + } + } else { + static_cast(this)->ApplyNewBi(i, old_Bi, new_Bi); + // Rcpp::Rcout << "NZ" << i <<" "; + } +} + +template +bool CD::UpdateBiCWMinCheck(const std::size_t i, const bool Cwmin) { + // See CD::UpdateBi for documentation + const double grd_Bi = static_cast(this)->GetBiGrad(i); + const double old_Bi = 0; + + (*this->Xtr)[i] = std::abs(grd_Bi); + + const double nrb_Bi = + static_cast(this)->GetBiValue(old_Bi, grd_Bi); + const double reg_Bi = static_cast(this)->GetBiReg(nrb_Bi); + const double new_Bi = std::copysign(reg_Bi, nrb_Bi); + + if (reg_Bi < this->thr + lambda1_fudge_factor) { + return Cwmin; + } else { + // Rcpp::Rcout << "Old B[" << i << "] = " << old_Bi << ", New B[" << i << "] + // = " << new_Bi << "\n"; + static_cast(this)->ApplyNewBiCWMinCheck(i, old_Bi, new_Bi); + return false; + } +} + +template +bool CD::UpdateBiCWMinCheckWithBounds(const std::size_t i, + const bool Cwmin) { + // See CD::UpdateBi for documentation + const double grd_Bi = static_cast(this)->GetBiGrad(i); + const double old_Bi = 0; + + (*this->Xtr)[i] = std::abs(grd_Bi); + + const double nrb_Bi = + static_cast(this)->GetBiValue(old_Bi, grd_Bi); + const double reg_Bi = static_cast(this)->GetBiReg(nrb_Bi); + const double bnd_Bi = + clamp(std::copysign(reg_Bi, nrb_Bi), this->Lows[i], this->Highs[i]); + + if (reg_Bi < this->thr) { + return Cwmin; + } else { + const double delta_tmp = std::sqrt(reg_Bi * reg_Bi - this->thr2); + const double delta = (delta_tmp == delta_tmp) ? delta_tmp : 0; + + const double range_Bi = std::copysign(reg_Bi, nrb_Bi); + if ((range_Bi - delta < bnd_Bi) && (bnd_Bi < range_Bi + delta)) { + static_cast(this)->ApplyNewBiCWMinCheck(i, old_Bi, bnd_Bi); + return false; + } else { + return Cwmin; + } + } +} + +/* + * + * CDBase + * + */ + +template +CDBase::CDBase(const T &Xi, const arma::vec &yi, const Params &P) + : lambda0{P.ModelParams[0]}, + lambda1{P.ModelParams[1]}, + lambda2{P.ModelParams[2]}, + intercept{P.intercept}, + withBounds{P.withBounds}, + y{yi}, + ModelParams{P.ModelParams}, + CyclingOrder{P.CyclingOrder}, + MaxIters{P.MaxIters}, + rtol{P.rtol}, + atol{P.atol}, + Lows{P.Lows}, + Highs{P.Highs}, + ActiveSet{P.ActiveSet}, + ActiveSetNum{P.ActiveSetNum} { + this->result.ModelParams = P.ModelParams; + this->NoSelectK = P.NoSelectK; + + this->Xtr = P.Xtr; + this->Iter = P.Iter; + + this->isSparse = std::is_same::value; + + this->b0 = P.b0; + + this->X = Ξ + + this->n = X->n_rows; + this->p = X->n_cols; + + if (P.Init == 'u') { + this->B = *(P.InitialSol); + } else { + // this->B = arma::zeros(p); + this->B = this->B.zeros(p); + } + + if (CyclingOrder == 'u') { + this->Order = P.Uorder; + } else if (CyclingOrder == 'c') { + std::vector cyclic(p); + std::iota(std::begin(cyclic), std::end(cyclic), 0); + this->Order = cyclic; + } + + this->CurrentIters = 0; +} + +template +FitResult CDBase::Fit() { + // arma::cout << "CDBase::Fit() Start\n"; + // std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if (this->withBounds) { + return this->_FitWithBounds(); + } else { + // arma::cout << "CDBase::Fit No Bounds() Start\n"; + // std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return this->_Fit(); + } +} + +template class CDBase; +template class CDBase; + +/* + * + * CD + * + */ + +template +void CD::UpdateSparse_b0(arma::vec &r) { + // Only run for regression when T is arma::sp_mat and intercept is True. + // r is this->r on outer scope; + const double new_b0 = arma::mean(r); + r -= new_b0; + this->b0 += new_b0; +} + +template +bool CD::isConverged() { + this->CurrentIters += 1; // keeps track of the number of calls to Converged + const double objectiveold = this->objective; + this->objective = this->Objective(); + + // Rcpp::Rcout << "Old: "<< objectiveold << ", New: " << this->objective << + // "\n"; Rcpp::Rcout << "Exit 1: " << (std::abs(objectiveold - + // this->objective) <= this->rtol*objectiveold) << ", Exit 2: " << + // (this->objective <= 1e-12) << "\n"; + return std::abs(objectiveold - this->objective) <= + this->rtol * objectiveold || + this->objective <= this->atol; +} + +template +void CD::RestrictSupport() { + if (has_same_support(this->B, this->Bprev)) { + this->SameSuppCounter += 1; + + if (this->SameSuppCounter == this->ActiveSetNum - 1) { + std::vector NewOrder = nnzIndicies(this->B); + + /// Map m of {Order[i] -> i}: + std::unordered_map m; + + std::size_t index = 0; + for (const auto &i : this->Order) { + m.insert(std::make_pair(i, index)); + index++; + } + + std::sort(NewOrder.begin(), NewOrder.end(), + [&m](std::size_t i, std::size_t j) { return m[i] < m[j]; }); + + this->OldOrder = this->Order; + this->Order = NewOrder; + this->ActiveSet = false; + this->Stabilized = true; + } + + } else { + this->SameSuppCounter = 0; + } +} + +template +bool CD::CWMinCheckWithBounds() { + std::vector S = nnzIndicies(this->B); + + std::vector Sc; + set_difference(this->Range1p.begin(), this->Range1p.end(), S.begin(), S.end(), + back_inserter(Sc)); + + bool Cwmin = true; + for (auto &i : Sc) { + Cwmin = this->UpdateBiCWMinCheckWithBounds(i, Cwmin); + } + return Cwmin; +} + +template +bool CD::CWMinCheck() { + std::vector S = nnzIndicies(this->B); + + std::vector Sc; + set_difference(this->Range1p.begin(), this->Range1p.end(), S.begin(), S.end(), + back_inserter(Sc)); + + bool Cwmin = true; + for (auto &i : Sc) { + Cwmin = this->UpdateBiCWMinCheck(i, Cwmin); + } + + return Cwmin; +} + +template +CD::CD(const T &Xi, const arma::vec &yi, const Params &P) + : CDBase(Xi, yi, P) { + Range1p.resize(this->p); + std::iota(std::begin(Range1p), std::end(Range1p), 0); + ScreenSize = P.ScreenSize; +} + +// template class CD; +// template class CD; + +/* + * + * CDSwaps + * + */ + +template +CDSwaps::CDSwaps(const T &Xi, const arma::vec &yi, const Params &Pi) + : CDBase(Xi, yi, Pi) { + MaxNumSwaps = Pi.MaxNumSwaps; + P = Pi; +} + +template class CDSwaps; +template class CDSwaps; + +#endif diff --git a/python/l0learn/src/include/CDL0.h b/python/l0learn/src/include/CDL0.h new file mode 100644 index 0000000..4b4fc6f --- /dev/null +++ b/python/l0learn/src/include/CDL0.h @@ -0,0 +1,191 @@ +#ifndef CDL0_H +#define CDL0_H +#include + +#include "BetaVector.h" +#include "CD.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL0 : public CD> { + private: + arma::vec r; // vector of residuals + + public: + CDL0(const T &Xi, const arma::vec &yi, const Params &P); + //~CDL0(){} + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &, const beta_vector &) final; + + inline double Objective() final; + + inline double GetBiGrad(const std::size_t i); + + inline double GetBiValue(const double old_Bi, const double grd_Bi); + + inline double GetBiReg(const double nrb_Bi); + + inline void ApplyNewBi(const std::size_t i, const double old_Bi, + const double new_Bi); + + inline void ApplyNewBiCWMinCheck(const std::size_t i, const double old_Bi, + const double new_Bi); +}; + +template +inline double CDL0::GetBiGrad(const std::size_t i) { + return matrix_column_dot(*(this->X), i, this->r); +} + +template +inline double CDL0::GetBiValue(const double old_Bi, const double grd_Bi) { + return grd_Bi + old_Bi; +} + +template +inline double CDL0::GetBiReg(const double nrb_Bi) { + return std::abs(nrb_Bi); +} + +template +inline void CDL0::ApplyNewBi(const std::size_t i, const double old_Bi, + const double new_Bi) { + this->r += matrix_column_mult(*(this->X), i, old_Bi - new_Bi); + this->B[i] = new_Bi; +} + +template +inline void CDL0::ApplyNewBiCWMinCheck(const std::size_t i, + const double old_Bi, + const double new_Bi) { + this->r += matrix_column_mult(*(this->X), i, old_Bi - new_Bi); + this->B[i] = new_Bi; + this->Order.push_back(i); +} + +template +inline double CDL0::Objective(const arma::vec &r, const beta_vector &B) { + return 0.5 * arma::dot(r, r) + this->lambda0 * n_nonzero(B); +} + +template +inline double CDL0::Objective() { + return 0.5 * arma::dot(this->r, this->r) + this->lambda0 * n_nonzero(this->B); +} + +template +CDL0::CDL0(const T &Xi, const arma::vec &yi, const Params &P) + : CD>(Xi, yi, P) { + this->thr2 = 2 * this->lambda0; + this->thr = sqrt(this->thr2); + this->r = *P.r; + this->result.r = P.r; +} + +template +FitResult CDL0::_Fit() { + this->objective = Objective(this->r, this->B); + + std::vector FullOrder = this->Order; + + if (this->ActiveSet) { + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); // std::min(1000,Order.size()) + } + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + // Rcpp::Rcout << "{" << this->Order.size() << "}"; + for (auto &i : this->Order) { + this->UpdateBi(i); + } + + this->RestrictSupport(); + + if (this->isConverged() && this->CWMinCheck()) { + // Rcpp::Rcout << " |Converged on iter:" << t << "CWMinCheck \n"; + break; + } + } + + // arma::cout << "CDL0::_Fit() Loop End\n"; + // std::this_thread::sleep_for(std::chrono::milliseconds(10)); + // Re-optimize b0 after convergence. + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + this->result.Objective = this->objective; + this->result.B = this->B; + *(this->result.r) = this->r; // change to pointer later + this->result.IterNum = this->CurrentIters; + this->result.b0 = this->b0; + + return this->result; +} + +template +FitResult CDL0::_FitWithBounds() { + // Rcpp::Rcout << "CDL0 Fit: "; + clamp_by_vector(this->B, this->Lows, this->Highs); + + this->objective = Objective(this->r, this->B); + + std::vector FullOrder = this->Order; + + if (this->ActiveSet) { + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); // std::min(1000,Order.size()) + } + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + for (auto &i : this->Order) { + this->UpdateBiWithBounds(i); + } + + this->RestrictSupport(); + + if (this->isConverged() && this->CWMinCheckWithBounds()) { + // Rcpp::Rcout << "Converged on iter:" << t << "\n"; + break; + } + } + + // Re-optimize b0 after convergence. + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + this->result.Objective = this->objective; + this->result.B = this->B; + *(this->result.r) = this->r; // change to pointer later + this->result.IterNum = this->CurrentIters; + this->result.b0 = this->b0; + + return this->result; +} + +template class CDL0; +template class CDL0; + +#endif diff --git a/python/l0learn/src/include/CDL012.h b/python/l0learn/src/include/CDL012.h new file mode 100644 index 0000000..d5c934d --- /dev/null +++ b/python/l0learn/src/include/CDL012.h @@ -0,0 +1,189 @@ +#ifndef CDL012_H +#define CDL012_H +#include "BetaVector.h" +#include "CD.h" +#include "FitResult.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012 : public CD> { + private: + double Onep2lamda2; + arma::vec r; // vector of residuals + + public: + CDL012(const T &Xi, const arma::vec &yi, const Params &P); + //~CDL012(){} + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &r, const beta_vector &B) final; + + inline double Objective() final; + + inline double GetBiGrad(const std::size_t i); + + inline double GetBiValue(const double old_Bi, const double grd_Bi); + + inline double GetBiReg(const double nrb_Bi); + + inline void ApplyNewBi(const std::size_t i, const double old_Bi, + const double new_Bi); + + inline void ApplyNewBiCWMinCheck(const std::size_t i, const double old_Bi, + const double new_Bi); +}; + +template +inline double CDL012::GetBiGrad(const std::size_t i) { + return matrix_column_dot(*(this->X), i, this->r); +} + +template +inline double CDL012::GetBiValue(const double old_Bi, const double grd_Bi) { + return grd_Bi + old_Bi; +} + +template +inline double CDL012::GetBiReg(const double nrb_Bi) { + // sign(nrb_Bi)*(|nrb_Bi| - lambda1)/(1 + 2*lambda2) + return (std::abs(nrb_Bi) - this->lambda1) / Onep2lamda2; +} + +template +inline void CDL012::ApplyNewBi(const std::size_t i, const double Bi_old, + const double Bi_new) { + this->r += matrix_column_mult(*(this->X), i, Bi_old - Bi_new); + this->B[i] = Bi_new; +} + +template +inline void CDL012::ApplyNewBiCWMinCheck(const std::size_t i, + const double Bi_old, + const double Bi_new) { + this->r += matrix_column_mult(*(this->X), i, Bi_old - Bi_new); + this->B[i] = Bi_new; + this->Order.push_back(i); +} + +template +inline double CDL012::Objective(const arma::vec &r, const beta_vector &B) { + auto l2norm = arma::norm(B, 2); + return 0.5 * arma::dot(r, r) + this->lambda0 * n_nonzero(this->B) + + this->lambda1 * arma::norm(B, 1) + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012::Objective() { + auto l2norm = arma::norm(this->B, 2); + return 0.5 * arma::dot(this->r, this->r) + + this->lambda0 * n_nonzero(this->B) + + this->lambda1 * arma::norm(this->B, 1) + + this->lambda2 * l2norm * l2norm; +} + +template +CDL012::CDL012(const T &Xi, const arma::vec &yi, const Params &P) + : CD>(Xi, yi, P) { + Onep2lamda2 = 1 + 2 * this->lambda2; + + this->thr2 = 2 * this->lambda0 / Onep2lamda2; + this->thr = std::sqrt(this->thr2); + this->r = *P.r; + this->result.r = P.r; +} + +template +FitResult CDL012::_Fit() { + this->objective = Objective(this->r, this->B); + + std::vector FullOrder = this->Order; + + if (this->ActiveSet) { + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + } + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + for (auto &i : this->Order) { + this->UpdateBi(i); + } + + this->RestrictSupport(); + + if (this->isConverged() && this->CWMinCheck()) { + break; + } + } + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + this->result.Objective = this->objective; + this->result.B = this->B; + *(this->result.r) = this->r; // change to pointer later + this->result.IterNum = this->CurrentIters; + this->result.b0 = this->b0; + return this->result; +} + +template +FitResult CDL012::_FitWithBounds() { + clamp_by_vector(this->B, this->Lows, this->Highs); + + this->objective = Objective(this->r, this->B); + + std::vector FullOrder = this->Order; + + if (this->ActiveSet) { + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + } + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + for (auto &i : this->Order) { + this->UpdateBiWithBounds(i); + } + + this->RestrictSupport(); + + // B.print(); + if (this->isConverged() && this->CWMinCheckWithBounds()) { + break; + } + } + + if (this->isSparse && this->intercept) { + this->UpdateSparse_b0(this->r); + } + + this->result.Objective = this->objective; + this->result.B = this->B; + *(this->result.r) = this->r; // change to pointer later + this->result.IterNum = this->CurrentIters; + this->result.b0 = this->b0; + return this->result; +} + +template class CDL012; +template class CDL012; + +#endif diff --git a/python/l0learn/src/include/CDL012Logistic.h b/python/l0learn/src/include/CDL012Logistic.h new file mode 100644 index 0000000..0d471c5 --- /dev/null +++ b/python/l0learn/src/include/CDL012Logistic.h @@ -0,0 +1,216 @@ +#ifndef CDL012Logistic_H +#define CDL012Logistic_H +#include "BetaVector.h" +#include "CD.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012Logistic : public CD> { + private: + const double LipschitzConst = 0.25; + double twolambda2; + double qp2lamda2; + double lambda1ol; + arma::vec ExpyXB; + // std::vector * Xtr; + T *Xy; + + public: + CDL012Logistic(const T &Xi, const arma::vec &yi, const Params &P); + //~CDL012Logistic(){} + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &r, const beta_vector &B) final; + + inline double Objective() final; + + inline double GetBiGrad(const std::size_t i); + + inline double GetBiValue(const double old_Bi, const double grd_Bi); + + inline double GetBiReg(const double Bi_step); + + inline void ApplyNewBi(const std::size_t i, const double Bi_old, + const double Bi_new); + + inline void ApplyNewBiCWMinCheck(const std::size_t i, const double old_Bi, + const double new_Bi); +}; + +template +inline double CDL012Logistic::GetBiGrad(const std::size_t i) { + /* + * Notes: + * When called in CWMinCheck, we know that this->B[i] is 0. + */ + return -arma::dot(matrix_column_get(*(this->Xy), i), 1 / (1 + ExpyXB)) + + twolambda2 * this->B[i]; + // return -arma::sum( matrix_column_get(*(this->Xy), i) / (1 + ExpyXB) ) + + // twolambda2 * this->B[i]; +} + +template +inline double CDL012Logistic::GetBiValue(const double old_Bi, + const double grd_Bi) { + return old_Bi - grd_Bi / qp2lamda2; +} + +template +inline double CDL012Logistic::GetBiReg(const double Bi_step) { + return std::abs(Bi_step) - lambda1ol; +} + +template +inline void CDL012Logistic::ApplyNewBi(const std::size_t i, + const double old_Bi, + const double new_Bi) { + ExpyXB %= arma::exp((new_Bi - old_Bi) * matrix_column_get(*(this->Xy), i)); + this->B[i] = new_Bi; +} + +template +inline void CDL012Logistic::ApplyNewBiCWMinCheck(const std::size_t i, + const double old_Bi, + const double new_Bi) { + ExpyXB %= arma::exp((new_Bi - old_Bi) * matrix_column_get(*(this->Xy), i)); + this->B[i] = new_Bi; + this->Order.push_back(i); +} + +template +inline double CDL012Logistic::Objective( + const arma::vec &expyXB, + const beta_vector &B) { // hint inline + const auto l2norm = arma::norm(B, 2); + // arma::sum(arma::log(1 + 1 / expyXB)) is the negative log-likelihood + return arma::sum(arma::log(1 + 1 / expyXB)) + this->lambda0 * n_nonzero(B) + + this->lambda1 * arma::norm(B, 1) + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012Logistic::Objective() { + return this->Objective(ExpyXB, this->B); +} + +template +CDL012Logistic::CDL012Logistic(const T &Xi, const arma::vec &yi, + const Params &P) + : CD>(Xi, yi, P) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = + (LipschitzConst + twolambda2); // this is the univariate lipschitz const + // of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + lambda1ol = this->lambda1 / qp2lamda2; + + ExpyXB = + arma::exp(this->y % (*(this->X) * this->B + + this->b0)); // Maintained throughout the algorithm + Xy = P.Xy; +} + +template +FitResult CDL012Logistic::_Fit() { + this->objective = Objective(); // Implicitly used ExpyXB + + std::vector FullOrder = this->Order; // never used in LR + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + // Update the intercept + if (this->intercept) { + const double b0old = this->b0; + // const double partial_b0 = - arma::sum( *(this->y) / (1 + ExpyXB) ); + const double partial_b0 = -arma::dot((this->y), 1 / (1 + ExpyXB)); + this->b0 -= partial_b0 / + (this->n * LipschitzConst); // intercept is not regularized + ExpyXB %= arma::exp((this->b0 - b0old) * (this->y)); + } + + for (auto &i : this->Order) { + this->UpdateBi(i); + } + + this->RestrictSupport(); + + // only way to terminate is by (i) converging on active set and (ii) + // CWMinCheck + if (this->isConverged() && this->CWMinCheck()) { + break; + } + } + + this->result.Objective = this->objective; + this->result.B = this->B; + this->result.Model = this; + this->result.b0 = this->b0; + this->result.ExpyXB = ExpyXB; + this->result.IterNum = this->CurrentIters; + + return this->result; +} + +template +FitResult CDL012Logistic::_FitWithBounds() { // always uses active sets + + // arma::sp_mat B2 = this->B; + clamp_by_vector(this->B, this->Lows, this->Highs); + + this->objective = Objective(); // Implicitly used ExpyXB + + std::vector FullOrder = this->Order; // never used in LR + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + + for (std::size_t t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + // Update the intercept + if (this->intercept) { + const double b0old = this->b0; + // const double partial_b0 = - arma::sum( *(this->y) / (1 + ExpyXB) ); + const double partial_b0 = -arma::dot((this->y), 1 / (1 + ExpyXB)); + this->b0 -= partial_b0 / + (this->n * LipschitzConst); // intercept is not regularized + ExpyXB %= arma::exp((this->b0 - b0old) * (this->y)); + } + + for (auto &i : this->Order) { + this->UpdateBiWithBounds(i); + } + + this->RestrictSupport(); + + // only way to terminate is by (i) converging on active set and (ii) + // CWMinCheck + if (this->isConverged() && this->CWMinCheckWithBounds()) { + break; + } + } + + this->result.Objective = this->objective; + this->result.B = this->B; + this->result.Model = this; + this->result.b0 = this->b0; + this->result.ExpyXB = ExpyXB; + this->result.IterNum = this->CurrentIters; + + return this->result; +} + +template class CDL012Logistic; +template class CDL012Logistic; + +#endif diff --git a/python/l0learn/src/include/CDL012LogisticSwaps.h b/python/l0learn/src/include/CDL012LogisticSwaps.h new file mode 100644 index 0000000..17e4205 --- /dev/null +++ b/python/l0learn/src/include/CDL012LogisticSwaps.h @@ -0,0 +1,226 @@ +#ifndef CDL012LogisticSwaps_H +#define CDL012LogisticSwaps_H +#include "BetaVector.h" +#include "CD.h" +#include "CDL012Logistic.h" +#include "CDSwaps.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012LogisticSwaps : public CDSwaps { + private: + const double LipschitzConst = 0.25; + double twolambda2; + double qp2lamda2; + double lambda1ol; + double stl0Lc; + arma::vec ExpyXB; + // std::vector * Xtr; + T *Xy; + + public: + CDL012LogisticSwaps(const T &Xi, const arma::vec &yi, const Params &P); + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &r, const beta_vector &B) final; + + inline double Objective() final; +}; + +template +inline double CDL012LogisticSwaps::Objective(const arma::vec &r, + const beta_vector &B) { + auto l2norm = arma::norm(B, 2); + return arma::sum(arma::log(1 + 1 / r)) + this->lambda0 * n_nonzero(B) + + this->lambda1 * arma::norm(B, 1) + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012LogisticSwaps::Objective() { + return this->Objective(ExpyXB, this->B); +} + +template +CDL012LogisticSwaps::CDL012LogisticSwaps(const T &Xi, const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = + (LipschitzConst + twolambda2); // this is the univariate lipschitz const + // of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); + lambda1ol = this->lambda1 / qp2lamda2; + Xy = Pi.Xy; +} + +template +FitResult CDL012LogisticSwaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template +FitResult CDL012LogisticSwaps::_Fit() { + auto result = CDL012Logistic(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->b0 = result.b0; // Initialize from previous later....! + this->B = result.B; + ExpyXB = result.ExpyXB; // Maintained throughout the algorithm + + double objective = result.Objective; + double Fmin = objective; + std::size_t maxindex; + double Bmaxindex; + + this->P.Init = 'u'; + + bool foundbetter = false; + bool foundbetter_i = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + // TODO: Add shuffle of Order + // std::shuffle(std::begin(Order), std::end(Order), engine); + + foundbetter = false; + + // TODO: Check if this should be Templated Operation + arma::mat ExpyXBnojs = arma::zeros(this->n, NnzIndices.size()); + + int j_index = -1; + for (auto &j : NnzIndices) { + // Remove NnzIndices[j] + ++j_index; + ExpyXBnojs.col(j_index) = + ExpyXB % + arma::exp(-this->B.at(j) * matrix_column_get(*(this->Xy), j)); + } + arma::mat gradients = -1 / (1 + ExpyXBnojs).t() * *Xy; + arma::mat abs_gradients = arma::abs(gradients); + + j_index = -1; + for (auto &j : NnzIndices) { + // Set B[j] = 0 + ++j_index; + arma::vec ExpyXBnoj = ExpyXBnojs.col(j_index); + arma::rowvec gradient = gradients.row(j_index); + arma::rowvec abs_gradient = abs_gradients.row(j_index); + + arma::uvec indices = arma::sort_index(arma::abs(gradient), "descend"); + foundbetter_i = false; + + // TODO: make sure this scans at least 100 coordinates from outside supp + // (now it does not) + for (std::size_t ll = 0; ll < std::min(50, (int)this->p); ++ll) { + std::size_t i = indices(ll); + + if (this->B[i] == 0 && i >= this->NoSelectK) { + // Do not swap B[i] if i between 0 and NoSelectK; + + arma::vec ExpyXBnoji = ExpyXBnoj; + + double Biold = 0; + double partial_i = gradient[i]; + bool converged = false; + + beta_vector Btemp = this->B; + Btemp[j] = 0; + double ObjTemp = Objective(ExpyXBnoji, Btemp); + std::size_t innerindex = 0; + + double x = Biold - partial_i / qp2lamda2; + double z = std::abs(x) - lambda1ol; + double Binew = std::copysign(z, x); + // double Binew = clamp(std::copysign(z, x), this->Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + + while (!converged && innerindex < 10 && + ObjTemp >= Fmin) { // ObjTemp >= Fmin + ExpyXBnoji %= + arma::exp((Binew - Biold) * matrix_column_get(*Xy, i)); + // partial_i = - arma::sum( matrix_column_get(*Xy, i) / (1 + + // ExpyXBnoji) ) + twolambda2 * Binew; + partial_i = + -arma::dot(matrix_column_get(*Xy, i), 1 / (1 + ExpyXBnoji)) + + twolambda2 * Binew; + + if (std::abs((Binew - Biold) / Biold) < 0.0001) { + converged = true; + // std::cout<<"swaps converged!!!"<Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + innerindex += 1; + } + + if (ObjTemp >= Fmin) { + ExpyXBnoji %= arma::exp( (Binew - Biold) * matrix_column_get(*Xy, i)); + Btemp[i] = Binew; + ObjTemp = Objective(ExpyXBnoji, Btemp); + } else { + Binew = 0; + Fmin = ObjTemp; + maxindex = i; + Bmaxindex = Binew; + foundbetter_i = true; + } + + // Can be made much faster (later) + Btemp[i] = Binew; + } + + if (foundbetter_i) { + this->B[j] = 0; + this->B[maxindex] = Bmaxindex; + this->P.InitialSol = &(this->B); + + // TODO: Check if this line is necessary. P should already have b0. + this->P.b0 = this->b0; + + result = CDL012Logistic(*(this->X), this->y, this->P).Fit(); + + ExpyXB = result.ExpyXB; + this->B = result.B; + this->b0 = result.b0; + objective = result.Objective; + Fmin = objective; + foundbetter = true; + break; + } + } + + // auto end2 = std::chrono::high_resolution_clock::now(); + // std::cout<<"restricted: + // "<(end2-start2).count() + // << " ms " << std::endl; + + if (foundbetter) { + break; + } + } + + if (!foundbetter) { + // Early exit to prevent looping + return result; + } + } + + // result.Model = this; + return result; +} + +#endif diff --git a/python/l0learn/src/include/CDL012SquaredHinge.h b/python/l0learn/src/include/CDL012SquaredHinge.h new file mode 100644 index 0000000..c28fcdf --- /dev/null +++ b/python/l0learn/src/include/CDL012SquaredHinge.h @@ -0,0 +1,225 @@ +#ifndef CDL012SquaredHinge_H +#define CDL012SquaredHinge_H +#include "BetaVector.h" +#include "CD.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012SquaredHinge : public CD> { + private: + const double LipschitzConst = 2; // for f (without regularization) + double twolambda2; + double qp2lamda2; + double lambda1ol; + // std::vector * Xtr; + arma::vec onemyxb; + arma::uvec indices; + T *Xy; + + public: + CDL012SquaredHinge(const T &Xi, const arma::vec &yi, const Params &P); + + //~CDL012SquaredHinge(){} + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &r, const beta_vector &B) final; + + inline double Objective() final; + + inline double GetBiGrad(const std::size_t i); + + inline double GetBiValue(const double old_Bi, const double grd_Bi); + + inline double GetBiReg(const double Bi_step); + + inline void ApplyNewBi(const std::size_t i, const double Bi_old, + const double Bi_new); + + inline void ApplyNewBiCWMinCheck(const std::size_t i, const double old_Bi, + const double new_Bi); +}; + +template +inline double CDL012SquaredHinge::GetBiGrad(const std::size_t i) { + // Rcpp::Rcout << "Grad stuff: " << arma::sum(2 * onemyxb.elem(indices) % (- + // matrix_column_get(*Xy, i).elem(indices)) ) << "\n"; + return arma::sum(2 * onemyxb.elem(indices) % + (-matrix_column_get(*Xy, i).elem(indices))) + + twolambda2 * this->B[i]; +} + +template +inline double CDL012SquaredHinge::GetBiValue(const double old_Bi, + const double grd_Bi) { + return old_Bi - grd_Bi / qp2lamda2; +} + +template +inline double CDL012SquaredHinge::GetBiReg(const double Bi_step) { + return std::abs(Bi_step) - lambda1ol; +} + +template +inline void CDL012SquaredHinge::ApplyNewBi(const std::size_t i, + const double Bi_old, + const double Bi_new) { + onemyxb += (Bi_old - Bi_new) * matrix_column_get(*(this->Xy), i); + this->B[i] = Bi_new; + indices = arma::find(onemyxb > 0); +} + +template +inline void CDL012SquaredHinge::ApplyNewBiCWMinCheck(const std::size_t i, + const double Bi_old, + const double Bi_new) { + onemyxb += (Bi_old - Bi_new) * matrix_column_get(*(this->Xy), i); + this->B[i] = Bi_new; + indices = arma::find(onemyxb > 0); + this->Order.push_back(i); +} + +template +inline double CDL012SquaredHinge::Objective(const arma::vec &onemyxb, + const beta_vector &B) { + auto l2norm = arma::norm(B, 2); + arma::uvec indices = arma::find(onemyxb > 0); + + return arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) + + this->lambda0 * n_nonzero(B) + this->lambda1 * arma::norm(B, 1) + + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012SquaredHinge::Objective() { + auto l2norm = arma::norm(this->B, 2); + return arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) + + this->lambda0 * n_nonzero(this->B) + + this->lambda1 * arma::norm(this->B, 1) + + this->lambda2 * l2norm * l2norm; +} + +template +CDL012SquaredHinge::CDL012SquaredHinge(const T &Xi, const arma::vec &yi, + const Params &P) + : CD>(Xi, yi, P) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = + (LipschitzConst + twolambda2); // this is the univariate lipschitz const + // of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + lambda1ol = this->lambda1 / qp2lamda2; + + // TODO: Review this line + // TODO: Pass work from previous solution. + onemyxb = 1 - this->y % (*(this->X) * this->B + this->b0); + + // TODO: Add comment for purpose of 'indices' + indices = arma::find(onemyxb > 0); + Xy = P.Xy; +} + +template +FitResult CDL012SquaredHinge::_Fit() { + this->objective = Objective(); // Implicitly uses onemyx + + std::vector FullOrder = this->Order; // never used in LR + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + + for (auto t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + // Update the intercept + if (this->intercept) { + const double b0old = this->b0; + const double partial_b0 = + arma::sum(2 * onemyxb.elem(indices) % -this->y.elem(indices)); + this->b0 -= partial_b0 / + (this->n * LipschitzConst); // intercept is not regularized + onemyxb += this->y * (b0old - this->b0); + indices = arma::find(onemyxb > 0); + } + + for (auto &i : this->Order) { + this->UpdateBi(i); + } + + this->RestrictSupport(); + + // only way to terminate is by (i) converging on active set and (ii) + // CWMinCheck + if ((this->isConverged()) && this->CWMinCheck()) { + break; + } + } + + this->result.Objective = this->objective; + this->result.B = this->B; + this->result.Model = this; + this->result.b0 = this->b0; + this->result.IterNum = this->CurrentIters; + this->result.onemyxb = this->onemyxb; + return this->result; +} + +template +FitResult CDL012SquaredHinge::_FitWithBounds() { + clamp_by_vector(this->B, this->Lows, this->Highs); + + this->objective = Objective(); // Implicitly uses onemyx + + std::vector FullOrder = this->Order; // never used in LR + this->Order.resize( + std::min((int)(n_nonzero(this->B) + this->ScreenSize + this->NoSelectK), + (int)(this->p))); + + for (auto t = 0; t < this->MaxIters; ++t) { + this->Bprev = this->B; + + // Update the intercept + if (this->intercept) { + const double b0old = this->b0; + const double partial_b0 = + arma::sum(2 * onemyxb.elem(indices) % -this->y.elem(indices)); + this->b0 -= partial_b0 / + (this->n * LipschitzConst); // intercept is not regularized + onemyxb += this->y * (b0old - this->b0); + indices = arma::find(onemyxb > 0); + } + + for (auto &i : this->Order) { + this->UpdateBiWithBounds(i); + } + + this->RestrictSupport(); + + // only way to terminate is by (i) converging on active set and (ii) + // CWMinCheck + if (this->isConverged()) { + if (this->CWMinCheckWithBounds()) { + break; + } + } + } + + this->result.Objective = this->objective; + this->result.B = this->B; + this->result.Model = this; + this->result.b0 = this->b0; + this->result.IterNum = this->CurrentIters; + this->result.onemyxb = this->onemyxb; + return this->result; +} + +template class CDL012SquaredHinge; +template class CDL012SquaredHinge; + +#endif diff --git a/python/l0learn/src/include/CDL012SquaredHingeSwaps.h b/python/l0learn/src/include/CDL012SquaredHingeSwaps.h new file mode 100644 index 0000000..1fbf1be --- /dev/null +++ b/python/l0learn/src/include/CDL012SquaredHingeSwaps.h @@ -0,0 +1,193 @@ +#ifndef CDL012SquredHingeSwaps_H +#define CDL012SquredHingeSwaps_H +#include "BetaVector.h" +#include "CD.h" +#include "CDL012SquaredHinge.h" +#include "CDSwaps.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012SquaredHingeSwaps : public CDSwaps { + private: + const double LipschitzConst = 2; + double twolambda2; + double qp2lamda2; + double lambda1ol; + double stl0Lc; + // std::vector * Xtr; + + public: + CDL012SquaredHingeSwaps(const T &Xi, const arma::vec &yi, const Params &P); + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + inline double Objective(const arma::vec &r, const beta_vector &B) final; + + inline double Objective() final; +}; + +template +inline double CDL012SquaredHingeSwaps::Objective(const arma::vec &onemyxb, + const beta_vector &B) { + auto l2norm = arma::norm(B, 2); + arma::uvec indices = arma::find(onemyxb > 0); + return arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) + + this->lambda0 * n_nonzero(B) + this->lambda1 * arma::norm(B, 1) + + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012SquaredHingeSwaps::Objective() { + throw std::runtime_error( + "CDL012SquaredHingeSwaps does not have this->onemyxb"); +} + +template +CDL012SquaredHingeSwaps::CDL012SquaredHingeSwaps(const T &Xi, + const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) { + twolambda2 = 2 * this->lambda2; + qp2lamda2 = (LipschitzConst + + twolambda2); // this is the univariate lipschitz + // constant of the differentiable objective + this->thr2 = (2 * this->lambda0) / qp2lamda2; + this->thr = std::sqrt(this->thr2); + stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); + lambda1ol = this->lambda1 / qp2lamda2; +} + +template +FitResult CDL012SquaredHingeSwaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template +FitResult CDL012SquaredHingeSwaps::_Fit() { + auto result = CDL012SquaredHinge(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->b0 = result.b0; // Initialize from previous later....! + this->B = result.B; + + arma::vec onemyxb = result.onemyxb; + + this->objective = result.Objective; + double Fmin = this->objective; + std::size_t maxindex; + double Bmaxindex; + + this->P.Init = 'u'; + + bool foundbetter = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + // Rcpp::Rcout << "Swap Number: " << t << "|mean(onemyxb): " << + // arma::mean(onemyxb) << "\n"; + + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + // TODO: Implement shuffle of NnzIndices Indicies + + foundbetter = false; + + for (auto &j : NnzIndices) { + arma::vec onemyxbnoj = + onemyxb + this->B[j] * this->y % matrix_column_get(*(this->X), j); + arma::uvec indices = arma::find(onemyxbnoj > 0); + + for (std::size_t i = 0; i < this->p; ++i) { + if (this->B[i] == 0 && i >= this->NoSelectK) { + double Biold = 0; + double Binew; + + double partial_i = + arma::sum(2 * onemyxbnoj.elem(indices) % + (-(this->y.elem(indices) % + matrix_column_get(*(this->X), i).elem(indices)))); + + bool converged = false; + if (std::abs(partial_i) >= this->lambda1 + stl0Lc) { + // std::cout<<"Adding: "<B; + Btemp[j] = 0; + // double ObjTemp = Objective(onemyxbnoj,Btemp); + // double Biolddescent = 0; + while (!converged) { + double x = Biold - partial_i / qp2lamda2; + double z = std::abs(x) - lambda1ol; + Binew = std::copysign(z, x); + + // Binew = clamp(std::copysign(z, x), this->Lows[i], + // this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) + onemyxbnoji += + (Biold - Binew) * this->y % matrix_column_get(*(this->X), i); + + arma::uvec indicesi = arma::find(onemyxbnoji > 0); + partial_i = + arma::sum(2 * onemyxbnoji.elem(indicesi) % + (-this->y.elem(indicesi) % + matrix_column_get(*(this->X), i).elem(indicesi))); + + if (std::abs((Binew - Biold) / Biold) < 0.0001) { + converged = true; + } + + Biold = Binew; + l += 1; + } + + Btemp[i] = Binew; + double Fnew = Objective(onemyxbnoji, Btemp); + + if (Fnew < Fmin) { + Fmin = Fnew; + maxindex = i; + Bmaxindex = Binew; + } + } + } + } + + if (Fmin < this->objective) { + this->B[j] = 0; + this->B[maxindex] = Bmaxindex; + + this->P.InitialSol = &(this->B); + + // TODO: Check if this line is needed. P should already have b0. + this->P.b0 = this->b0; + + result = CDL012SquaredHinge(*(this->X), this->y, this->P).Fit(); + + this->B = result.B; + this->b0 = result.b0; + + onemyxb = result.onemyxb; + this->objective = result.Objective; + Fmin = this->objective; + foundbetter = true; + break; + } + if (foundbetter) { + break; + } + } + + if (!foundbetter) { + return result; + } + } + + return result; +} + +#endif diff --git a/python/l0learn/src/include/CDL012Swaps.h b/python/l0learn/src/include/CDL012Swaps.h new file mode 100644 index 0000000..307f365 --- /dev/null +++ b/python/l0learn/src/include/CDL012Swaps.h @@ -0,0 +1,147 @@ +#ifndef CDL012SWAPS_H +#define CDL012SWAPS_H +#include + +#include "BetaVector.h" +#include "CD.h" +#include "CDL012.h" +#include "CDSwaps.h" +#include "FitResult.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class CDL012Swaps : public CDSwaps { + public: + CDL012Swaps(const T &Xi, const arma::vec &yi, const Params &Pi); + + FitResult _FitWithBounds() final; + + FitResult _Fit() final; + + double Objective(const arma::vec &r, const beta_vector &B) final; + + double Objective() final; +}; + +template +inline double CDL012Swaps::Objective(const arma::vec &r, + const beta_vector &B) { + auto l2norm = arma::norm(B, 2); + return 0.5 * arma::dot(r, r) + this->lambda0 * n_nonzero(B) + + this->lambda1 * arma::norm(B, 1) + this->lambda2 * l2norm * l2norm; +} + +template +inline double CDL012Swaps::Objective() { + throw std::runtime_error("CDL012Swaps does not have this->r."); +} + +template +CDL012Swaps::CDL012Swaps(const T &Xi, const arma::vec &yi, + const Params &Pi) + : CDSwaps(Xi, yi, Pi) {} + +template +FitResult CDL012Swaps::_FitWithBounds() { + throw "This Error should not happen. Please report it as an issue to " + "https://github.com/hazimehh/L0Learn "; +} + +template +FitResult CDL012Swaps::_Fit() { + auto result = CDL012(*(this->X), this->y, this->P) + .Fit(); // result will be maintained till the end + this->B = result.B; + this->b0 = result.b0; + double objective = result.Objective; + this->P.Init = 'u'; + + bool foundbetter = false; + + for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { + std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); + + foundbetter = false; + + // TODO: shuffle NNz Indices to prevent bias. + // std::shuffle(std::begin(Order), std::end(Order), engine); + + // TODO: This calculation is already preformed in a previous step + // Can be pulled/stored + arma::vec r = this->y - *(this->X) * this->B - this->b0; + + for (auto &i : NnzIndices) { + arma::rowvec riX = + (r + this->B[i] * matrix_column_get(*(this->X), i)).t() * *(this->X); + + double maxcorr = -1; + std::size_t maxindex = -1; + + for (std::size_t j = this->NoSelectK; j < this->p; ++j) { + // TODO: Account for bounds when determining best swap + // Loops through each column and finds the column with the highest + // correlation to residuals In non-constrained cases, the highest + // correlation will always be the best option However, if bounds + // restrict the value of B[j], it is possible that swapping column 'i' + // and column 'j' might be rejected as B[j], when constrained, is not + // able to take a value with sufficient magnitude to utilize the + // correlation. Therefore, we must ensure that 'j' was not already + // rejected. + if (std::fabs(riX[j]) > maxcorr && this->B[j] == 0) { + maxcorr = std::fabs(riX[j]); + maxindex = j; + } + } + + // Check if the correlation is sufficiently large to make up for + // regularization + if (maxcorr > (1 + 2 * this->ModelParams[2]) * std::fabs(this->B[i]) + + this->ModelParams[1]) { + // Rcpp::Rcout << t << ": Proposing Swap " << i << " => NNZ and " << + // maxindex << " => 0 \n"; Proposed new Swap Value (without considering + // bounds are solvable in closed form) Must be clamped to bounds + + this->B[i] = 0; + + // Bi with No Bounds (nb); + double Bi_nb = (riX[maxindex] - + std::copysign(this->ModelParams[1], riX[maxindex])) / + (1 + 2 * this->ModelParams[2]); + // double Bi_wb = clamp(Bi_nb, this->Lows[maxindex], + // this->Highs[maxindex]); // Bi With Bounds (wb) + this->B[maxindex] = Bi_nb; + + // Change initial solution to Swapped value to seed standard CD + // algorithm. + this->P.InitialSol = &(this->B); + *this->P.r = this->y - *(this->X) * (this->B) - this->b0; + // this->P already has access to b0. + + // proposed_result object. + // Keep tack of previous_best result object + // Only override previous_best if proposed_result has a better + // objective. + result = CDL012(*(this->X), this->y, this->P).Fit(); + + // Rcpp::Rcout << "Swap Objective " << result.Objective << " \n"; + // Rcpp::Rcout << "Old Objective " << objective << " \n"; + this->B = result.B; + objective = result.Objective; + foundbetter = true; + break; + } + } + + if (!foundbetter) { + // Early exit to prevent looping + return result; + } + } + + return result; +} + + +#endif diff --git a/python/l0learn/src/include/CDSwaps.h b/python/l0learn/src/include/CDSwaps.h new file mode 100644 index 0000000..752d0a1 --- /dev/null +++ b/python/l0learn/src/include/CDSwaps.h @@ -0,0 +1,5 @@ +#ifndef CDSWAPS_H +#define CDSWAPS_H +#include "CD.h" + +#endif diff --git a/python/l0learn/src/include/FitResult.h b/python/l0learn/src/include/FitResult.h new file mode 100644 index 0000000..e4b598f --- /dev/null +++ b/python/l0learn/src/include/FitResult.h @@ -0,0 +1,22 @@ +#ifndef FITRESULT_H +#define FITRESULT_H +#include "BetaVector.h" +#include "arma_includes.h" + +template // Forward Reference to prevent circular dependencies +class CDBase; + +template +struct FitResult { + double Objective; + beta_vector B; + CDBase *Model; + std::size_t IterNum; + arma::vec *r; + std::vector ModelParams; + double b0 = 0; // used by classification models and sparse regression models + arma::vec ExpyXB; // Used by Logistic regression + arma::vec onemyxb; // Used by SquaredHinge regression +}; + +#endif diff --git a/python/l0learn/src/include/Grid.h b/python/l0learn/src/include/Grid.h new file mode 100644 index 0000000..bb11d2f --- /dev/null +++ b/python/l0learn/src/include/Grid.h @@ -0,0 +1,102 @@ +#ifndef GRID_H +#define GRID_H +#include +#include +#include + +#include "FitResult.h" +#include "Grid1D.h" +#include "Grid2D.h" +#include "GridParams.h" +#include "Normalize.h" +#include "arma_includes.h" + +template +class Grid { + private: + T Xscaled; + arma::vec yscaled; + arma::vec BetaMultiplier; + arma::vec meanX; + double meany; + double scaley; + + public: + GridParams PG; + + std::vector> Lambda0; + std::vector Lambda12; + std::vector> NnzCount; + std::vector> Solutions; + std::vector> Intercepts; + std::vector> Converged; + + Grid(const T &X, const arma::vec &y, const GridParams &PG); + //~Grid(); + + void Fit(); +}; + +template +Grid::Grid(const T &X, const arma::vec &y, const GridParams &PGi) { + PG = PGi; + + std::tie(BetaMultiplier, meanX, meany, scaley) = Normalize( + X, y, Xscaled, yscaled, !PG.P.Specs.Classification, PG.intercept); + + // Must rescale bounds by BetaMultiplier in order for final result to conform + // to bounds + if (PG.P.withBounds) { + PG.P.Lows /= BetaMultiplier; + PG.P.Highs /= BetaMultiplier; + } +} + +template +void Grid::Fit() { + std::vector>>> G; + + if (PG.P.Specs.L0) { + G.push_back(std::move(Grid1D(Xscaled, yscaled, PG).Fit())); + Lambda12.push_back(0); + } else { + G = std::move(Grid2D(Xscaled, yscaled, PG).Fit()); + } + + Lambda0 = std::vector>(G.size()); + NnzCount = std::vector>(G.size()); + Solutions = std::vector>(G.size()); + Intercepts = std::vector>(G.size()); + Converged = std::vector>(G.size()); + + for (std::size_t i = 0; i < G.size(); ++i) { + if (PG.P.Specs.L0L1) { + Lambda12.push_back(G[i][0]->ModelParams[1]); + } else if (PG.P.Specs.L0L2) { + Lambda12.push_back(G[i][0]->ModelParams[2]); + } + + for (auto &g : G[i]) { + Lambda0[i].push_back(g->ModelParams[0]); + + NnzCount[i].push_back(n_nonzero(g->B)); + + Converged[i].push_back(g->IterNum != PG.P.MaxIters); + + beta_vector B_unscaled; + double b0; + + std::tie(B_unscaled, b0) = + DeNormalize(g->B, BetaMultiplier, meanX, meany); + Solutions[i].push_back(arma::sp_mat(B_unscaled)); + /* scaley is 1 for classification problems. + * g->intercept is 0 unless specifically optimized for in: + * classification + * sparse regression and intercept = true + */ + Intercepts[i].push_back(scaley * g->b0 + b0); + } + } +} + +#endif diff --git a/python/l0learn/src/include/Grid1D.h b/python/l0learn/src/include/Grid1D.h new file mode 100644 index 0000000..40140ea --- /dev/null +++ b/python/l0learn/src/include/Grid1D.h @@ -0,0 +1,283 @@ +#ifndef GRID1D_H +#define GRID1D_H +#include +#include +#include + +#include "FitResult.h" +#include "GridParams.h" +#include "MakeCD.h" +#include "Params.h" +#include "arma_includes.h" + +template +class Grid1D { + private: + std::size_t G_ncols; + Params P; + const T *X; + const arma::vec *y; + std::size_t p; + std::vector>> G; + arma::vec Lambdas; + bool LambdaU; + std::size_t NnzStopNum; + std::vector *Xtr; + arma::rowvec *ytX; + double LambdaMinFactor; + bool PartialSort; + bool XtrAvailable; + double ytXmax2d; + double ScaleDownFactor; + std::size_t NoSelectK; + + public: + Grid1D(const T &Xi, const arma::vec &yi, const GridParams &PG); + ~Grid1D(); + std::vector>> Fit(); +}; + +template +Grid1D::Grid1D(const T &Xi, const arma::vec &yi, const GridParams &PG) { + // automatically selects lambda_0 (but assumes other lambdas are given in + // PG.P.ModelParams) + + X = Ξ + y = &yi; + p = Xi.n_cols; + LambdaMinFactor = PG.LambdaMinFactor; + ScaleDownFactor = PG.ScaleDownFactor; + P = PG.P; + P.Xtr = new std::vector(X->n_cols); // needed! careful + P.ytX = new arma::rowvec(X->n_cols); + P.D = new std::map(); + P.r = new arma::vec(Xi.n_rows); + Xtr = P.Xtr; + ytX = P.ytX; + NoSelectK = P.NoSelectK; + + LambdaU = PG.LambdaU; + + if (!LambdaU) { + G_ncols = PG.G_ncols; + } else { + G_ncols = PG.Lambdas.n_rows; // override the user's ncols if LambdaU = 1 + } + + G.reserve(G_ncols); + if (LambdaU) { + Lambdas = PG.Lambdas; + } // user-defined lambda0 grid + /* + else { + Lambdas.reserve(G_ncols); + Lambdas.push_back((0.5*arma::square(y->t() * *X)).max()); + } + */ + NnzStopNum = PG.NnzStopNum; + PartialSort = PG.PartialSort; + XtrAvailable = PG.XtrAvailable; + if (XtrAvailable) { + ytXmax2d = PG.ytXmax; + Xtr = PG.Xtr; + } +} + +template +Grid1D::~Grid1D() { + // delete all dynamically allocated memory + delete P.Xtr; + delete P.ytX; + delete P.D; + delete P.r; +} + +template +std::vector>> Grid1D::Fit() { + if (P.Specs.L0 || P.Specs.L0L2 || P.Specs.L0L1) { + bool scaledown = false; + + double Lipconst; + arma::vec Xtrarma; + if (P.Specs.Logistic) { + if (!XtrAvailable) { + Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); + } // = gradient of logistic loss at zero} + Lipconst = 0.25 + 2 * P.ModelParams[2]; + } else if (P.Specs.SquaredHinge) { + if (!XtrAvailable) { + // gradient of loss function at zero + Xtrarma = 2 * arma::abs(y->t() * *X).t(); + } + Lipconst = 2 + 2 * P.ModelParams[2]; + } else { + if (!XtrAvailable) { + *ytX = y->t() * *X; + Xtrarma = arma::abs(*ytX).t(); // Least squares + } + Lipconst = 1 + 2 * P.ModelParams[2]; + *P.r = *y - P.b0; // B = 0 initially + } + + double ytXmax; + if (!XtrAvailable) { + *Xtr = arma::conv_to>::from(Xtrarma); + ytXmax = arma::max(Xtrarma); + } else { + ytXmax = ytXmax2d; + } + + double lambdamax = + ((ytXmax - P.ModelParams[1]) * (ytXmax - P.ModelParams[1])) / + (2 * (Lipconst)); + + // Rcpp::Rcout << "lambdamax: " << lambdamax << "\n"; + + if (!LambdaU) { + P.ModelParams[0] = lambdamax; + } else { + P.ModelParams[0] = Lambdas[0]; + } + + // Rcpp::Rcout << "P ModelParams: {" << P.ModelParams[0] << ", " << + // P.ModelParams[1] << ", " << P.ModelParams[2] << ", " << P.ModelParams[3] + // << "}\n"; + + P.Init = 'z'; + + // std::cout<< "Lambda max: "<< lambdamax << std::endl; + // double lambdamin = lambdamax*LambdaMinFactor; + // Lambdas = arma::logspace(std::log10(lambdamin), std::log10(lambdamax), + // G_ncols); Lambdas = arma::flipud(Lambdas); + + // std::size_t StopNum = (X->n_rows < NnzStopNum) ? X->n_rows : NnzStopNum; + std::size_t StopNum = NnzStopNum; + // std::vector* Xtr = P.Xtr; + std::vector idx(p); + double Xrmax; + bool prevskip = false; // previous grid point was skipped + bool currentskip = false; // current grid point should be skipped + + for (std::size_t i = 0; i < G_ncols; ++i) { + UserInterrupt(); + // Rcpp::Rcout << "Grid1D: " << i << "\n"; + FitResult *prevresult = + new FitResult; // prevresult is ptr to the prev result object + // std::unique_ptr prevresult; + if (i > 0) { + // prevresult = std::move(G.back()); + *prevresult = *(G.back()); + } + + currentskip = false; + + if (!prevskip) { + std::iota(idx.begin(), idx.end(), 0); // make global class var later + // Exclude the first NoSelectK features from sorting. + if (PartialSort && p > 5000 + NoSelectK) + std::partial_sort(idx.begin() + NoSelectK, + idx.begin() + 5000 + NoSelectK, idx.end(), + [this](std::size_t i1, std::size_t i2) { + return (*Xtr)[i1] > (*Xtr)[i2]; + }); + else + std::sort(idx.begin() + NoSelectK, idx.end(), + [this](std::size_t i1, std::size_t i2) { + return (*Xtr)[i1] > (*Xtr)[i2]; + }); + P.CyclingOrder = 'u'; + P.Uorder = idx; // can be made faster + + // + Xrmax = (*Xtr)[idx[NoSelectK]]; + + if (i > 0) { + std::vector Sp = nnzIndicies(prevresult->B); + + for (std::size_t l = NoSelectK; l < p; ++l) { + if (std::binary_search(Sp.begin(), Sp.end(), idx[l]) == false) { + Xrmax = (*Xtr)[idx[l]]; + // std::cout<<"Grid Iteration: "<> result(new FitResult); + *result = Model->Fit(); + + delete Model; + + scaledown = false; + if (i >= 1) { + std::vector Spold = nnzIndicies(prevresult->B); + + std::vector Spnew = nnzIndicies(result->B); + + bool samesupp = false; + + if (Spold == Spnew) { + samesupp = true; + scaledown = true; + } + + // // + // + // if (samesupp) { + // scaledown = true; + // } // got same solution + } + + // else {scaledown = false;} + G.push_back(std::move(result)); + + if (n_nonzero(G.back()->B) >= StopNum) { + break; + } + // result->B.t().print(); + P.InitialSol = &(G.back()->B); + P.b0 = G.back()->b0; + // Udate: After 1.1.0, P.r is automatically updated by the previous call + // to CD + //*P.r = G.back()->r; + } + + delete prevresult; + + P.Init = 'u'; + P.Iter += 1; + prevskip = currentskip; + } + } + + return std::move(G); +} + +#endif diff --git a/python/l0learn/src/include/Grid2D.h b/python/l0learn/src/include/Grid2D.h new file mode 100644 index 0000000..40fd563 --- /dev/null +++ b/python/l0learn/src/include/Grid2D.h @@ -0,0 +1,161 @@ +#ifndef GRID2D_H +#define GRID2D_H +#include + +#include "FitResult.h" +#include "Grid1D.h" +#include "GridParams.h" +#include "Params.h" +#include "arma_includes.h" +#include "utils.h" + +template +class Grid2D { + private: + std::size_t G_nrows; + std::size_t G_ncols; + GridParams PG; + const T *X; + const arma::vec *y; + std::size_t p; + std::vector>>> G; + // each inner vector corresponds to a single lambda_1/lambda_2 + + double Lambda2Max; + double Lambda2Min; + double LambdaMinFactor; + std::vector *Xtr; + Params P; + + public: + Grid2D(const T &Xi, const arma::vec &yi, const GridParams &PGi); + ~Grid2D(); + std::vector>>> Fit(); +}; + +template +Grid2D::Grid2D(const T &Xi, const arma::vec &yi, const GridParams &PGi) { + // automatically selects lambda_0 (but assumes other lambdas are given in + // PG.P.ModelParams) + X = Ξ + y = &yi; + p = Xi.n_cols; + PG = PGi; + G_nrows = PG.G_nrows; + G_ncols = PG.G_ncols; + G.reserve(G_nrows); + Lambda2Max = PG.Lambda2Max; + Lambda2Min = PG.Lambda2Min; + LambdaMinFactor = PG.LambdaMinFactor; + + P = PG.P; +} + +template +Grid2D::~Grid2D() { + delete Xtr; + if (PG.P.Specs.Logistic) delete PG.P.Xy; + if (PG.P.Specs.SquaredHinge) delete PG.P.Xy; +} + +template +std::vector>>> Grid2D::Fit() { + arma::vec Xtrarma; + + if (PG.P.Specs.Logistic) { + auto n = X->n_rows; + double b0 = 0; + arma::vec ExpyXB = arma::ones(n); + if (PG.intercept) { + for (std::size_t t = 0; t < 50; ++t) { + double partial_b0 = -arma::sum(*y / (1 + ExpyXB)); + b0 -= partial_b0 / (n * 0.25); // intercept is not regularized + ExpyXB = arma::exp(b0 * *y); + } + } + PG.P.b0 = b0; + Xtrarma = arma::abs(-arma::trans(*y / (1 + ExpyXB)) * *X) + .t(); // = gradient of logistic loss at zero + // Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); // = gradient of logistic + // loss at zero + + T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; + + PG.P.Xy = new T; + *PG.P.Xy = Xy; + } + + else if (PG.P.Specs.SquaredHinge) { + auto n = X->n_rows; + double b0 = 0; + arma::vec onemyxb = arma::ones(n); + arma::uvec indices = arma::find(onemyxb > 0); + if (PG.intercept) { + for (std::size_t t = 0; t < 50; ++t) { + double partial_b0 = + arma::sum(2 * onemyxb.elem(indices) % (-y->elem(indices))); + b0 -= partial_b0 / (n * 2); // intercept is not regularized + onemyxb = 1 - (*y * b0); + indices = arma::find(onemyxb > 0); + } + } + PG.P.b0 = b0; + T indices_rows = matrix_rows_get(*X, indices); + Xtrarma = + 2 * arma::abs(arma::trans(y->elem(indices) % onemyxb.elem(indices)) * + indices_rows) + .t(); // = gradient of loss function at zero + // Xtrarma = 2 * arma::abs(y->t() * *X).t(); // = gradient of loss function + // at zero + T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; + PG.P.Xy = new T; + *PG.P.Xy = Xy; + } else { + Xtrarma = arma::abs(y->t() * *X).t(); + } + + double ytXmax = arma::max(Xtrarma); + + std::size_t index; + if (PG.P.Specs.L0L1) { + index = 1; + if (G_nrows != 1) { + Lambda2Max = ytXmax; + Lambda2Min = Lambda2Max * LambdaMinFactor; + } + } else if (PG.P.Specs.L0L2) { + index = 2; + } + + arma::vec Lambdas2 = + arma::logspace(std::log10(Lambda2Min), std::log10(Lambda2Max), G_nrows); + Lambdas2 = arma::flipud(Lambdas2); + + std::vector Xtrvec = + arma::conv_to>::from(Xtrarma); + + Xtr = new std::vector(X->n_cols); // needed! careful + + PG.XtrAvailable = true; + // Rcpp::Rcout << "Grid2D Start\n"; + for (std::size_t i = 0; i < Lambdas2.size(); ++i) { // auto &l : Lambdas2 + // Rcpp::Rcout << "Grid1D Start: " << i << "\n"; + *Xtr = Xtrvec; + + PG.Xtr = Xtr; + PG.ytXmax = ytXmax; + + PG.P.ModelParams[index] = Lambdas2[i]; + + if (PG.LambdaU == true) PG.Lambdas = PG.LambdasGrid[i]; + + // std::vector> Gl(); + // auto Gl = Grid1D(*X, *y, PG).Fit(); + // Rcpp::Rcout << "Grid1D Start: " << i << "\n"; + G.push_back(std::move(Grid1D(*X, *y, PG).Fit())); + } + + return std::move(G); +} + +#endif diff --git a/python/l0learn/src/include/GridParams.h b/python/l0learn/src/include/GridParams.h new file mode 100644 index 0000000..420dc13 --- /dev/null +++ b/python/l0learn/src/include/GridParams.h @@ -0,0 +1,27 @@ +#ifndef GRIDPARAMS_H +#define GRIDPARAMS_H +#include "Params.h" +#include "arma_includes.h" + +template +struct GridParams { + Params P; + std::size_t G_ncols = 100; + std::size_t G_nrows = 10; + bool LambdaU = false; + std::size_t NnzStopNum = 200; + double LambdaMinFactor = 0.01; + arma::vec Lambdas; + std::vector> LambdasGrid; + double Lambda2Max = 0.1; + double Lambda2Min = 0.001; + std::string Type = "L0"; + bool PartialSort = true; + bool XtrAvailable = false; + double ytXmax; + std::vector *Xtr; + double ScaleDownFactor = 0.8; + bool intercept; +}; + +#endif diff --git a/python/l0learn/src/include/L0LearnCore.h b/python/l0learn/src/include/L0LearnCore.h new file mode 100644 index 0000000..cbb3af2 --- /dev/null +++ b/python/l0learn/src/include/L0LearnCore.h @@ -0,0 +1,349 @@ +#ifndef INTERFACE_H +#define INTERFACE_H + +#include +#include +#include +#include + +#include "FitResult.h" +#include "Grid.h" +#include "GridParams.h" +#include "arma_includes.h" + +// Make an external struct + +struct fitmodel { + std::vector> Lambda0; + std::vector Lambda12; + std::vector> NnzCount; + arma::field Beta; + std::vector> Intercept; + std::vector> Converged; + + fitmodel() = default; + fitmodel(const fitmodel &) = default; + fitmodel(std::vector> &lambda0, + std::vector &lambda12, + std::vector> &nnzCount, + arma::field &beta, + std::vector> &intercept, + std::vector> &converged) + : Lambda0(lambda0), + Lambda12(lambda12), + NnzCount(nnzCount), + Beta(beta), + Intercept(intercept), + Converged(converged) {} +}; + +struct cvfitmodel : fitmodel { + arma::field CVMeans; + arma::field CVSDs; + + cvfitmodel() = default; + cvfitmodel(const cvfitmodel &) = default; + + cvfitmodel(std::vector> &lambda0, + std::vector &lambda12, + std::vector> &nnzCount, + arma::field &beta, + std::vector> &intercept, + std::vector> &converged, + arma::field &cVMeans, arma::field &cVSDs) + : fitmodel(lambda0, lambda12, nnzCount, beta, intercept, converged), + CVMeans(cVMeans), + CVSDs(cVSDs) {} +}; + +template +GridParams makeGridParams( + const std::string Loss, const std::string Penalty, + const std::string Algorithm, const std::size_t NnzStopNum, + const std::size_t G_ncols, const std::size_t G_nrows, + const double Lambda2Max, const double Lambda2Min, const bool PartialSort, + const std::size_t MaxIters, const double rtol, const double atol, + const bool ActiveSet, const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, + const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + GridParams PG; + PG.NnzStopNum = NnzStopNum; + PG.G_ncols = G_ncols; + PG.G_nrows = G_nrows; + PG.Lambda2Max = Lambda2Max; + PG.Lambda2Min = Lambda2Min; + PG.LambdaMinFactor = Lambda2Min; // + PG.PartialSort = PartialSort; + PG.ScaleDownFactor = ScaleDownFactor; + PG.LambdaU = LambdaU; + PG.LambdasGrid = Lambdas; + PG.Lambdas = Lambdas[0]; // to handle the case of L0 (i.e., Grid1D) + PG.intercept = Intercept; + + Params P; + PG.P = P; + PG.P.MaxIters = MaxIters; + PG.P.rtol = rtol; + PG.P.atol = atol; + PG.P.ActiveSet = ActiveSet; + PG.P.ActiveSetNum = ActiveSetNum; + PG.P.MaxNumSwaps = MaxNumSwaps; + PG.P.ScreenSize = ScreenSize; + PG.P.NoSelectK = ExcludeFirstK; + PG.P.intercept = Intercept; + PG.P.withBounds = withBounds; + PG.P.Lows = Lows; + PG.P.Highs = Highs; + + if (Loss == "SquaredError") { + PG.P.Specs.SquaredError = true; + } else if (Loss == "Logistic") { + PG.P.Specs.Logistic = true; + PG.P.Specs.Classification = true; + } else if (Loss == "SquaredHinge") { + PG.P.Specs.SquaredHinge = true; + PG.P.Specs.Classification = true; + } + + if (Algorithm == "CD") { + PG.P.Specs.CD = true; + } else if (Algorithm == "CDPSI") { + PG.P.Specs.PSI = true; + } + + if (Penalty == "L0") { + PG.P.Specs.L0 = true; + } else if (Penalty == "L0L2") { + PG.P.Specs.L0L2 = true; + } else if (Penalty == "L0L1") { + PG.P.Specs.L0L1 = true; + } + return PG; +} + +template +fitmodel L0LearnFit(const T &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const std::size_t NnzStopNum, const std::size_t G_ncols, + const std::size_t G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const std::size_t MaxIters, const double rtol, + const double atol, const bool ActiveSet, + const std::size_t ActiveSetNum, + const std::size_t MaxNumSwaps, const double ScaleDownFactor, + const std::size_t ScreenSize, const bool LambdaU, + const std::vector> &Lambdas, + const std::size_t ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, + const arma::vec &Highs) { +// COUT << "L0LearnCore.h L0LearnFit Entered. \n"; +// COUT << "L0LearnCore.h X shape = " << X.n_rows << ", " << X.n_cols << "\n"; +// COUT << "L0LearnCore.h y shape = " << y.n_rows << "\n"; +// std::this_thread::sleep_for(std::chrono::milliseconds(1000)); +// +//// COUT << "L0LearnCore.h X = " << X << "\n"; +// std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // y.n_cols << "\n"; arma::cout << "L0LearnCore.h Lows shape = " << + // Lows.n_rows << ", " << Lows.n_cols << "\n"; arma::cout << "L0LearnCore.h + // Highs shape = " << Highs.n_rows << ", " << Highs.n_cols << "\n"; + + // arma::cout << "makeGridParams Entering"; + GridParams PG = makeGridParams( + Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, + Intercept, withBounds, Lows, Highs); + + // arma::cout << "PG.P.Specs.L0 " << PG.P.Specs.L0; + // arma::cout << "PG.P.Specs.CD " << PG.P.Specs.CD; + // arma::cout << "PG.P.Specs.CD " << PG.P.Specs.CD; + // arma::cout << "Grid G Entering"; + Grid G(X, y, PG); + // arma::cout << "Grid G.Fit Entering"; + G.Fit(); + + // Next Construct the list of Sparse Beta Matrices. + + auto p = X.n_cols; + arma::field Bs(G.Lambda12.size()); + + for (std::size_t i = 0; i < G.Lambda12.size(); ++i) { + // create the px(reg path size) sparse sparseMatrix + arma::sp_mat B(p, G.Solutions[i].size()); + for (unsigned int j = 0; j < G.Solutions[i].size(); ++j) { + B.col(j) = G.Solutions[i][j]; + } + + // append the sparse matrix + Bs[i] = B; + } +// COUT << "L0LearnCore.h L0LearnFit Finished. \n"; +// std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + return fitmodel(G.Lambda0, G.Lambda12, G.NnzCount, Bs, G.Intercepts, + G.Converged); +} + +template +cvfitmodel L0LearnCV( + const T &X, const arma::vec &y, const std::string Loss, + const std::string Penalty, const std::string Algorithm, + const unsigned int NnzStopNum, const unsigned int G_ncols, + const unsigned int G_nrows, const double Lambda2Max, + const double Lambda2Min, const bool PartialSort, + const unsigned int MaxIters, const double rtol, const double atol, + const bool ActiveSet, const unsigned int ActiveSetNum, + const unsigned int MaxNumSwaps, const double ScaleDownFactor, + const unsigned int ScreenSize, const bool LambdaU, + const std::vector> Lambdas, const unsigned int nfolds, + const size_t seed, const unsigned int ExcludeFirstK, const bool Intercept, + const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { + GridParams PG = makeGridParams( + Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, + Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, + MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, Lambdas, ExcludeFirstK, + Intercept, withBounds, Lows, Highs); + + Grid G(X, y, PG); + arma::arma_rng::set_seed(seed); + G.Fit(); + + // Next Construct the list of Sparse Beta Matrices. + + auto p = X.n_cols; + auto n = X.n_rows; + arma::field Bs(G.Lambda12.size()); + + for (std::size_t i = 0; i < G.Lambda12.size(); ++i) { + // create the px(reg path size) sparse sparseMatrix + arma::sp_mat B(p, G.Solutions[i].size()); + for (std::size_t j = 0; j < G.Solutions[i].size(); ++j) + B.col(j) = G.Solutions[i][j]; + + // append the sparse matrix + Bs[i] = B; + } + + // CV starts here + + // Solutions = std::vector< std::vector >(G.size()); + // Intercepts = std::vector< std::vector >(G.size()); + + std::size_t Ngamma = G.Lambda12.size(); + + // std::vector< arma::mat > CVError (G.Solutions.size()); + arma::field CVError(G.Solutions.size()); + + for (std::size_t i = 0; i < G.Solutions.size(); ++i) + CVError[i] = arma::mat(G.Lambda0[i].size(), nfolds, arma::fill::zeros); + + arma::uvec a = arma::linspace(0, X.n_rows - 1, X.n_rows); + + arma::uvec indices = arma::shuffle(a); + + int samplesperfold = std::ceil(n / double(nfolds)); + int samplesinlastfold = samplesperfold - (samplesperfold * nfolds - n); + + std::vector fullindices(X.n_rows); + std::iota(fullindices.begin(), fullindices.end(), 0); + + for (std::size_t j = 0; j < nfolds; ++j) { + std::vector validationindices; + if (j < nfolds - 1) + validationindices.resize(samplesperfold); + else + validationindices.resize(samplesinlastfold); + + std::iota(validationindices.begin(), validationindices.end(), + samplesperfold * j); + + std::vector trainingindices; + + std::set_difference( + fullindices.begin(), fullindices.end(), validationindices.begin(), + validationindices.end(), + std::inserter(trainingindices, trainingindices.begin())); + + // validationindicesarma contains the randomly permuted validation indices + // as a uvec + arma::uvec validationindicesarma; + arma::uvec validationindicestemp = + arma::conv_to::from(validationindices); + validationindicesarma = indices.elem(validationindicestemp); + + // trainingindicesarma is similar to validationindicesarma but for training + arma::uvec trainingindicesarma; + + arma::uvec trainingindicestemp = + arma::conv_to::from(trainingindices); + + trainingindicesarma = indices.elem(trainingindicestemp); + + T Xtraining = matrix_rows_get(X, trainingindicesarma); + + arma::mat ytraining = y.elem(trainingindicesarma); + + T Xvalidation = matrix_rows_get(X, validationindicesarma); + + arma::mat yvalidation = y.elem(validationindicesarma); + + PG.LambdaU = true; + PG.XtrAvailable = + false; // reset XtrAvailable since its changed upon every call + PG.LambdasGrid = G.Lambda0; + PG.NnzStopNum = + p + 1; // remove any constraints on the supp size when fitting over the + // cv folds // +1 is imp to avoid =p edge case + if (PG.P.Specs.L0 == true) { + PG.Lambdas = PG.LambdasGrid[0]; + } + Grid Gtraining(Xtraining, ytraining, PG); + Gtraining.Fit(); + + for (std::size_t i = 0; i < Ngamma; ++i) { + // i indexes the gamma parameter + for (std::size_t k = 0; k < Gtraining.Lambda0[i].size(); ++k) { + // k indexes the solutions for a specific gamma + + if (PG.P.Specs.SquaredError) { + arma::vec r = yvalidation - Xvalidation * Gtraining.Solutions[i][k] + + Gtraining.Intercepts[i][k]; + CVError[i](k, j) = arma::dot(r, r) / yvalidation.n_rows; + } else if (PG.P.Specs.Logistic) { + arma::sp_mat B = Gtraining.Solutions[i][k]; + double b0 = Gtraining.Intercepts[i][k]; + arma::vec ExpyXB = arma::exp(yvalidation % (Xvalidation * B + b0)); + CVError[i](k, j) = + arma::sum(arma::log(1 + 1 / ExpyXB)) / yvalidation.n_rows; + // std::cout<<"i, j, k"< 0); + CVError[i](k, j) = + arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) / + yvalidation.n_rows; + } + } + } + } + + arma::field CVMeans(Ngamma); + arma::field CVSDs(Ngamma); + + for (std::size_t i = 0; i < Ngamma; ++i) { + CVMeans[i] = arma::mean(CVError[i], 1); + CVSDs[i] = arma::stddev(CVError[i], 0, 1); + } + + return cvfitmodel(G.Lambda0, G.Lambda12, G.NnzCount, Bs, G.Intercepts, + G.Converged, CVMeans, CVSDs); +} + +#endif // INTERFACE_H diff --git a/python/l0learn/src/include/MakeCD.h b/python/l0learn/src/include/MakeCD.h new file mode 100644 index 0000000..bc6588d --- /dev/null +++ b/python/l0learn/src/include/MakeCD.h @@ -0,0 +1,42 @@ +#ifndef MAKECD_H +#define MAKECD_H +#include "CD.h" +#include "CDL0.h" +#include "CDL012.h" +#include "CDL012Logistic.h" +#include "CDL012LogisticSwaps.h" +#include "CDL012SquaredHinge.h" +#include "CDL012SquaredHingeSwaps.h" +#include "CDL012Swaps.h" +#include "Params.h" +#include "arma_includes.h" + +template +CDBase *make_CD(const T &Xi, const arma::vec &yi, const Params &P) { + if (P.Specs.SquaredError) { + if (P.Specs.CD) { + if (P.Specs.L0) { + return new CDL0(Xi, yi, P); + } else { + return new CDL012(Xi, yi, P); + } + } else if (P.Specs.PSI) { + return new CDL012Swaps(Xi, yi, P); + } + } else if (P.Specs.Logistic) { + if (P.Specs.CD) { + return new CDL012Logistic(Xi, yi, P); + } else if (P.Specs.PSI) { + return new CDL012LogisticSwaps(Xi, yi, P); + } + } else if (P.Specs.SquaredHinge) { + if (P.Specs.CD) { + return new CDL012SquaredHinge(Xi, yi, P); + } else if (P.Specs.PSI) { + return new CDL012SquaredHingeSwaps(Xi, yi, P); + } + } + return new CDL0(Xi, yi, P); // handle later +} + +#endif diff --git a/python/l0learn/src/include/Model.h b/python/l0learn/src/include/Model.h new file mode 100644 index 0000000..d6620d2 --- /dev/null +++ b/python/l0learn/src/include/Model.h @@ -0,0 +1,20 @@ +#ifndef MODEL_H +#define MODEL_H + +struct Model { + bool SquaredError = false; + bool Logistic = false; + bool SquaredHinge = false; + bool Classification = false; + + bool CD = false; + bool PSI = false; + + bool L0 = false; + bool L0L1 = false; + bool L0L2 = false; + bool L1 = false; + bool L1Relaxed = false; +}; + +#endif diff --git a/python/l0learn/src/include/Normalize.h b/python/l0learn/src/include/Normalize.h new file mode 100644 index 0000000..180b62f --- /dev/null +++ b/python/l0learn/src/include/Normalize.h @@ -0,0 +1,50 @@ +#ifndef NORMALIZE_H +#define NORMALIZE_H + +#include + +#include "BetaVector.h" +#include "arma_includes.h" +#include "utils.h" + +std::tuple DeNormalize(beta_vector &B_scaled, + arma::vec &BetaMultiplier, + arma::vec &meanX, double meany); + +template +std::tuple Normalize( + const T &X, const arma::vec &y, T &X_normalized, arma::vec &y_normalized, + bool Normalizey, bool intercept) { + arma::rowvec meanX = matrix_center(X, X_normalized, intercept); + arma::rowvec scaleX = matrix_normalize(X_normalized); + + arma::vec BetaMultiplier; + double meany = 0; + double scaley; + if (Normalizey) { + if (intercept) { + meany = arma::mean(y); + } + y_normalized = y - meany; + + // TODO: Use l2_norm + scaley = arma::norm(y_normalized, 2); + + // properly handle cases where y is constant + if (scaley == 0) { + scaley = 1; + } + + y_normalized = y_normalized / scaley; + BetaMultiplier = scaley / (scaleX.t()); // transpose scale to get a col vec + // Multiplying the learned Beta by BetaMultiplier gives the optimal Beta on + // the original scale + } else { + y_normalized = y; + BetaMultiplier = 1 / (scaleX.t()); // transpose scale to get a col vec + scaley = 1; + } + return std::make_tuple(BetaMultiplier, meanX.t(), meany, scaley); +} + +#endif // NORMALIZE_H diff --git a/python/l0learn/src/include/Params.h b/python/l0learn/src/include/Params.h new file mode 100644 index 0000000..17d8cc2 --- /dev/null +++ b/python/l0learn/src/include/Params.h @@ -0,0 +1,39 @@ +#ifndef PARAMS_H +#define PARAMS_H +#include + +#include "BetaVector.h" +#include "Model.h" +#include "arma_includes.h" + +template +struct Params { + Model Specs; + std::vector ModelParams{0, 0, 0, 2}; + std::size_t MaxIters = 500; + double rtol = 1e-8; + double atol = 1e-12; + char Init = 'z'; // 'z' => zeros + std::size_t RandomStartSize = 10; + beta_vector *InitialSol; + double b0 = 0; // intercept + char CyclingOrder = 'c'; + std::vector Uorder; + bool ActiveSet = true; + std::size_t ActiveSetNum = 6; + std::size_t MaxNumSwaps = 200; // Used by CDSwaps + std::vector *Xtr; + arma::rowvec *ytX; + std::map *D; + std::size_t Iter = 0; // Current iteration number in the grid + std::size_t ScreenSize = 1000; + arma::vec *r; + T *Xy; // used for classification. + std::size_t NoSelectK = 0; + bool intercept = false; + bool withBounds = false; + arma::vec Lows; + arma::vec Highs; +}; + +#endif diff --git a/python/l0learn/src/include/arma_includes.h b/python/l0learn/src/include/arma_includes.h new file mode 100644 index 0000000..065b710 --- /dev/null +++ b/python/l0learn/src/include/arma_includes.h @@ -0,0 +1,33 @@ +#ifndef L0LEARN_CORE_ARMA_INCLUDES_H +#define L0LEARN_CORE_ARMA_INCLUDES_H + +#ifndef INCLUDES_H +#define INCLUDES_H + +#include +#include +#include +#include +#include + +#define COUT std::cout + +// A type that should be translated to a standard Python exception +class PyException : public std::exception { + public: + explicit PyException(const char *m) : message{m} {} + const char *what() const noexcept override { return message.c_str(); } + + private: + std::string message; +}; + +void inline UserInterrupt() { + if (PyErr_CheckSignals() != 0) throw py::error_already_set(); +} + +#define STOP throw PyException + +#endif // INCLUDES_H + +#endif // L0LEARN_CORE_ARMA_INCLUDES_H diff --git a/python/l0learn/src/include/utils.h b/python/l0learn/src/include/utils.h new file mode 100644 index 0000000..2551784 --- /dev/null +++ b/python/l0learn/src/include/utils.h @@ -0,0 +1,221 @@ +#ifndef L0LEARN_UTILS_HPP +#define L0LEARN_UTILS_HPP +#include + +#include "BetaVector.h" +#include "arma_includes.h" + +template +inline T clamp(T x, T low, T high) { + // -O3 Compiler should remove branches + if (x < low) x = low; + if (x > high) x = high; + return x; +} + +template +arma::vec inline matrix_column_get(const arma::mat &mat, T1 col) { + return mat.unsafe_col(col); +} + +template +arma::vec inline matrix_column_get(const arma::sp_mat &mat, T1 col) { + return arma::vec(mat.col(col)); +} + +template +arma::mat inline matrix_rows_get(const arma::mat &mat, + const T1 vector_of_row_indices) { + return mat.rows(vector_of_row_indices); +} + +template +arma::sp_mat inline matrix_rows_get(const arma::sp_mat &mat, + const T1 vector_of_row_indices) { + // Option for CV for random splitting or contiguous splitting. + // 1 - N without permutations splitting at floor(N/n_folds) + arma::sp_mat row_mat = arma::sp_mat(vector_of_row_indices.n_elem, mat.n_cols); + + for (auto i = 0; i < vector_of_row_indices.n_elem; i++) { + auto row_index = vector_of_row_indices(i); + arma::sp_mat::const_row_iterator begin = mat.begin_row(row_index); + arma::sp_mat::const_row_iterator end = mat.end_row(row_index); + + for (; begin != end; ++begin) { + row_mat(i, begin.col()) = *begin; + } + } + return row_mat; +} + +template +arma::mat inline matrix_vector_schur_product(const arma::mat &mat, + const T1 &y) { + // return mat[i, j] * y[i] for each j + return mat.each_col() % *y; +} + +template +arma::sp_mat inline matrix_vector_schur_product(const arma::sp_mat &mat, + const T1 &y) { + arma::sp_mat Xy = arma::sp_mat(mat); + arma::sp_mat::iterator begin = Xy.begin(); + arma::sp_mat::iterator end = Xy.end(); + + auto yp = (*y); + for (; begin != end; ++begin) { + auto row = begin.row(); + *begin = (*begin) * yp(row); + } + return Xy; +} + +template +arma::sp_mat inline matrix_vector_divide(const arma::sp_mat &mat, const T1 &u) { + arma::sp_mat divided_mat = arma::sp_mat(mat); + + // auto up = (*u); + arma::sp_mat::iterator begin = divided_mat.begin(); + arma::sp_mat::iterator end = divided_mat.end(); + for (; begin != end; ++begin) { + *begin = (*begin) / u(begin.row()); + } + return divided_mat; +} + +template +arma::mat inline matrix_vector_divide(const arma::mat &mat, const T1 &u) { + return mat.each_col() / u; +} + +arma::rowvec inline matrix_column_sums(const arma::mat &mat) { + return arma::sum(mat, 0); +} + +arma::rowvec inline matrix_column_sums(const arma::sp_mat &mat) { + return arma::rowvec(arma::sum(mat, 0)); +} + +template +double inline matrix_column_dot(const arma::mat &mat, T1 col, const T2 &u) { + return arma::dot(matrix_column_get(mat, col), u); +} + +template +double inline matrix_column_dot(const arma::sp_mat &mat, T1 col, const T2 &u) { + return arma::dot(matrix_column_get(mat, col), u); +} + +template +arma::vec inline matrix_column_mult(const arma::mat &mat, T1 col, const T2 &u) { + return matrix_column_get(mat, col) * u; +} + +template +arma::vec inline matrix_column_mult(const arma::sp_mat &mat, T1 col, + const T2 &u) { + return matrix_column_get(mat, col) * u; +} + +void inline clamp_by_vector(arma::vec &B, const arma::vec &lows, + const arma::vec &highs) { + const std::size_t n = B.n_rows; + for (std::size_t i = 0; i < n; i++) { + B.at(i) = clamp(B.at(i), lows.at(i), highs.at(i)); + } +} + +// void clamp_by_vector(arma::sp_mat &B, const arma::vec& lows, const arma::vec& +// highs){ +// // See above implementation without filter for error. +// auto begin = B.begin(); +// auto end = B.end(); +// +// std::vector inds; +// for (; begin != end; ++begin) +// inds.push_back(begin.row()); +// +// auto n = B.size(); +// inds.erase(std::remove_if(inds.begin(), +// inds.end(), +// [n](size_t x){return (x > n) && (x < 0);}), +// inds.end()); +// for (auto& it : inds) { +// double B_item = B(it, 0); +// const double low = lows(it); +// const double high = highs(it); +// B(it, 0) = clamp(B_item, low, high); +// } +// } + +arma::rowvec inline matrix_normalize(arma::sp_mat &mat_norm) { + auto p = mat_norm.n_cols; + arma::rowvec scaleX = + arma::zeros(p); // will contain the l2norm of every col + + for (auto col = 0; col < p; col++) { + double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); + scaleX(col) = l2norm; + } + + scaleX.replace(0, -1); + + for (auto col = 0; col < p; col++) { + arma::sp_mat::col_iterator begin = mat_norm.begin_col(col); + arma::sp_mat::col_iterator end = mat_norm.end_col(col); + for (; begin != end; ++begin) (*begin) = (*begin) / scaleX(col); + } + + if (mat_norm.has_nan()) + mat_norm.replace(arma::datum::nan, + 0); // can handle numerical instabilities. + + return scaleX; +} + +arma::rowvec inline matrix_normalize(arma::mat &mat_norm) { + auto p = mat_norm.n_cols; + arma::rowvec scaleX = + arma::zeros(p); // will contain the l2norm of every col + + for (auto col = 0; col < p; col++) { + double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); + scaleX(col) = l2norm; + } + + scaleX.replace(0, -1); + mat_norm.each_row() /= scaleX; + + if (mat_norm.has_nan()) { + mat_norm.replace(arma::datum::nan, + 0); // can handle numerical instabilities. + } + + return scaleX; +} + +arma::rowvec inline matrix_center(const arma::mat &X, arma::mat &X_normalized, + bool intercept) { + auto p = X.n_cols; + arma::rowvec meanX; + + if (intercept) { + meanX = arma::mean(X, 0); + X_normalized = X.each_row() - meanX; + } else { + meanX = arma::zeros(p); + X_normalized = arma::mat(X); + } + + return meanX; +} + +arma::rowvec inline matrix_center(const arma::sp_mat &X, + arma::sp_mat &X_normalized, bool intercept) { + auto p = X.n_cols; + arma::rowvec meanX = arma::zeros(p); + X_normalized = arma::sp_mat(X); + return meanX; +} + +#endif // L0LEARN_UTILS_HPP diff --git a/python/l0learn/version.py b/python/l0learn/version.py new file mode 100644 index 0000000..764780a --- /dev/null +++ b/python/l0learn/version.py @@ -0,0 +1,5 @@ +"""THIS FILE IS AUTO-GENERATED BY l0learn setup.py.""" +name = 'l0learn' +version = '0.4.3' +full_version = '0.4.3' +release = True diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..1116cae --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "numpy>=1.19.0", + "ninja; platform_system!='Windows'", + "scikit-build>=0.14.1", + "cmake>=3.18", + "pybind11>=2.8.1", +] + +build-backend = "setuptools.build_meta" + + +[tool.cibuildwheel] +# Normal options, etc. +manylinux-x86_64-image = "manylinux2014" +manylinux-i686-image = "manylinux2014" +manylinux-aarch64-image = "manylinux2014" +manylinux-ppc64le-image = "manylinux2014" +manylinux-s390x-image = "manylinux2014" +manylinux-pypy_x86_64-image = "manylinux2014" +manylinux-pypy_i686-image = "manylinux2014" +manylinux-pypy_aarch64-image = "manylinux2014" \ No newline at end of file diff --git a/python/scripts/install_linux_libs.sh b/python/scripts/install_linux_libs.sh new file mode 100644 index 0000000..1a573d4 --- /dev/null +++ b/python/scripts/install_linux_libs.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Based on https://github.com/mlpack/mlpack-wheels +# Based on https://gitlab.com/jason-rumengan/pyarma + +basedir=$(python3 scripts/openblas_support.py) +$use_sudo cp -r $basedir/lib/* /usr/local/lib +$use_sudo cp $basedir/include/* /usr/local/include + +export DYLD_FALLBACK_LIBRARY_PATH=/usr/local/lib/ diff --git a/python/scripts/openblas_support.py b/python/scripts/openblas_support.py new file mode 100644 index 0000000..23ae6ce --- /dev/null +++ b/python/scripts/openblas_support.py @@ -0,0 +1,367 @@ +""" +Code from https://github.com/numpy/numpy/blob/main/tools/openblas_support.py + +The NumPy repository and source distributions bundle several libraries that are +compatibly licensed. We list these here. + +Name: lapack-lite +Files: numpy/linalg/lapack_lite/* +License: BSD-3-Clause + For details, see numpy/linalg/lapack_lite/LICENSE.txt + +Name: tempita +Files: tools/npy_tempita/* +License: MIT + For details, see tools/npy_tempita/license.txt + +Name: dragon4 +Files: numpy/core/src/multiarray/dragon4.c +License: MIT + For license text, see numpy/core/src/multiarray/dragon4.c + +Name: libdivide +Files: numpy/core/include/numpy/libdivide/* +License: Zlib + For license text, see numpy/core/include/numpy/libdivide/LICENSE.txt +""" +import glob +import hashlib +import os +import platform +import sysconfig +import sys +import shutil +import tarfile +import textwrap +import zipfile + +from tempfile import mkstemp, gettempdir +from urllib.request import urlopen, Request +from urllib.error import HTTPError + +OPENBLAS_V = '0.3.20' +OPENBLAS_LONG = 'v0.3.20' +BASE_LOC = 'https://anaconda.org/multibuild-wheels-staging/openblas-libs' +BASEURL = f'{BASE_LOC}/{OPENBLAS_LONG}/download' +SUPPORTED_PLATFORMS = [ + 'linux-aarch64', + 'linux-x86_64', + 'linux-i686', + 'linux-ppc64le', + 'linux-s390x', + 'win-amd64', + 'win-32', + 'macosx-x86_64', + 'macosx-arm64', +] +IS_32BIT = sys.maxsize < 2**32 + + +def get_plat(): + plat = sysconfig.get_platform() + plat_split = plat.split("-") + arch = plat_split[-1] + if arch == "win32": + plat = "win-32" + elif arch in ["universal2", "intel"]: + plat = f"macosx-{platform.uname().machine}" + elif len(plat_split) > 2: + plat = f"{plat_split[0]}-{arch}" + assert plat in SUPPORTED_PLATFORMS, f'invalid platform {plat}' + return plat + + +def get_ilp64(): + if os.environ.get("NPY_USE_BLAS_ILP64", "0") == "0": + return None + if IS_32BIT: + raise RuntimeError("NPY_USE_BLAS_ILP64 set on 32-bit arch") + return "64_" + + +def get_manylinux(arch): + if arch in ('x86_64', 'i686'): + default = '2010' + else: + default = '2014' + ret = os.environ.get("MB_ML_VER", default) + # XXX For PEP 600 this can be a glibc version + assert ret in ('1', '2010', '2014', '_2_24'), f'invalid MB_ML_VER {ret}' + return ret + + +def download_openblas(target, plat, ilp64): + osname, arch = plat.split("-") + fnsuffix = {None: "", "64_": "64_"}[ilp64] + filename = '' + headers = {'User-Agent': + ('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 ; ' + '(KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.3')} + suffix = None + if osname == "linux": + ml_ver = get_manylinux(arch) + suffix = f'manylinux{ml_ver}_{arch}.tar.gz' + typ = 'tar.gz' + elif plat == 'macosx-x86_64': + suffix = 'macosx_10_9_x86_64-gf_1becaaa.tar.gz' + typ = 'tar.gz' + elif plat == 'macosx-arm64': + suffix = 'macosx_11_0_arm64-gf_f26990f.tar.gz' + typ = 'tar.gz' + elif osname == 'win': + if plat == "win-32": + suffix = 'win32-gcc_8_1_0.zip' + else: + suffix = 'win_amd64-gcc_8_1_0.zip' + typ = 'zip' + + if not suffix: + return None + filename = f'{BASEURL}/openblas{fnsuffix}-{OPENBLAS_LONG}-{suffix}' + req = Request(url=filename, headers=headers) + try: + response = urlopen(req) + except HTTPError: + print(f'Could not download "{filename}"', file=sys.stderr) + raise + length = response.getheader('content-length') + if response.status != 200: + print(f'Could not download "{filename}"', file=sys.stderr) + return None + print(f"Downloading {length} from {filename}", file=sys.stderr) + data = response.read() + # Verify hash + key = os.path.basename(filename) + print("Saving to file", file=sys.stderr) + with open(target, 'wb') as fid: + fid.write(data) + return typ + + +def setup_openblas(plat=get_plat(), ilp64=get_ilp64()): + ''' + Download and setup an openblas library for building. If successful, + the configuration script will find it automatically. + + Returns + ------- + msg : str + path to extracted files on success, otherwise indicates what went wrong + To determine success, do ``os.path.exists(msg)`` + ''' + _, tmp = mkstemp() + if not plat: + raise ValueError('unknown platform') + typ = download_openblas(tmp, plat, ilp64) + if not typ: + return '' + osname, arch = plat.split("-") + if osname == 'win': + if not typ == 'zip': + return f'expecting to download zipfile on windows, not {typ}' + return unpack_windows_zip(tmp) + else: + if not typ == 'tar.gz': + return 'expecting to download tar.gz, not %s' % str(typ) + return unpack_targz(tmp) + + +def unpack_windows_zip(fname): + with zipfile.ZipFile(fname, 'r') as zf: + # Get the openblas.a file, but not openblas.dll.a nor openblas.dev.a + lib = [x for x in zf.namelist() if OPENBLAS_LONG in x and + x.endswith('a') and not x.endswith('dll.a') and + not x.endswith('dev.a')] + if not lib: + return 'could not find libopenblas_%s*.a ' \ + 'in downloaded zipfile' % OPENBLAS_LONG + if get_ilp64() is None: + target = os.path.join(gettempdir(), 'openblas.a') + else: + target = os.path.join(gettempdir(), 'openblas64_.a') + with open(target, 'wb') as fid: + fid.write(zf.read(lib[0])) + return target + + +def unpack_targz(fname): + target = os.path.join(gettempdir(), 'openblas') + if not os.path.exists(target): + os.mkdir(target) + with tarfile.open(fname, 'r') as zf: + # Strip common prefix from paths when unpacking + prefix = os.path.commonpath(zf.getnames()) + extract_tarfile_to(zf, target, prefix) + return target + + +def extract_tarfile_to(tarfileobj, target_path, archive_path): + """Extract TarFile contents under archive_path/ to target_path/""" + + target_path = os.path.abspath(target_path) + + def get_members(): + for member in tarfileobj.getmembers(): + if archive_path: + norm_path = os.path.normpath(member.name) + if norm_path.startswith(archive_path + os.path.sep): + member.name = norm_path[len(archive_path)+1:] + else: + continue + + dst_path = os.path.abspath(os.path.join(target_path, member.name)) + if os.path.commonpath([target_path, dst_path]) != target_path: + # Path not under target_path, probably contains ../ + continue + + yield member + + tarfileobj.extractall(target_path, members=get_members()) + + +def make_init(dirname): + ''' + Create a _distributor_init.py file for OpenBlas + ''' + with open(os.path.join(dirname, '_distributor_init.py'), 'wt') as fid: + fid.write(textwrap.dedent(""" + ''' + Helper to preload windows dlls to prevent dll not found errors. + Once a DLL is preloaded, its namespace is made available to any + subsequent DLL. This file originated in the numpy-wheels repo, + and is created as part of the scripts that build the wheel. + ''' + import os + import glob + if os.name == 'nt': + # convention for storing / loading the DLL from + # numpy/.libs/, if present + try: + from ctypes import WinDLL + basedir = os.path.dirname(__file__) + except: + pass + else: + libs_dir = os.path.abspath(os.path.join(basedir, '.libs')) + DLL_filenames = [] + if os.path.isdir(libs_dir): + for filename in glob.glob(os.path.join(libs_dir, + '*openblas*dll')): + # NOTE: would it change behavior to load ALL + # DLLs at this path vs. the name restriction? + WinDLL(os.path.abspath(filename)) + DLL_filenames.append(filename) + if len(DLL_filenames) > 1: + import warnings + warnings.warn("loaded more than 1 DLL from .libs:" + "\\n%s" % "\\n".join(DLL_filenames), + stacklevel=1) + """)) + + +def test_setup(plats): + ''' + Make sure all the downloadable files exist and can be opened + ''' + def items(): + """ yields all combinations of arch, ilp64 + """ + for plat in plats: + yield plat, None + osname, arch = plat.split("-") + if arch not in ('i686', 'arm64', '32'): + yield plat, '64_' + if osname == "linux" and arch in ('i686', 'x86_64'): + oldval = os.environ.get('MB_ML_VER', None) + os.environ['MB_ML_VER'] = '1' + yield plat, None + # Once we create x86_64 and i686 manylinux2014 wheels... + # os.environ['MB_ML_VER'] = '2014' + # yield arch, None, False + if oldval: + os.environ['MB_ML_VER'] = oldval + else: + os.environ.pop('MB_ML_VER') + + errs = [] + for plat, ilp64 in items(): + osname, _ = plat.split("-") + if plat not in plats: + continue + target = None + try: + try: + target = setup_openblas(plat, ilp64) + except Exception as e: + print(f'Could not setup {plat} with ilp64 {ilp64}, ') + print(e) + errs.append(e) + continue + if not target: + raise RuntimeError(f'Could not setup {plat}') + print(target) + if osname == 'win': + if not target.endswith('.a'): + raise RuntimeError("Not .a extracted!") + else: + files = glob.glob(os.path.join(target, "lib", "*.a")) + if not files: + raise RuntimeError("No lib/*.a unpacked!") + finally: + if target is not None: + if os.path.isfile(target): + os.unlink(target) + else: + shutil.rmtree(target) + if errs: + raise errs[0] + + +def test_version(expected_version, ilp64=get_ilp64()): + """ + Assert that expected OpenBLAS version is + actually available via NumPy + """ + import numpy + import ctypes + + dll = ctypes.CDLL(numpy.core._multiarray_umath.__file__) + if ilp64 == "64_": + get_config = dll.openblas_get_config64_ + else: + get_config = dll.openblas_get_config + get_config.restype = ctypes.c_char_p + res = get_config() + print('OpenBLAS get_config returned', str(res)) + if not expected_version: + expected_version = OPENBLAS_V + check_str = b'OpenBLAS %s' % expected_version.encode() + print(check_str) + assert check_str in res, f'{expected_version} not found in {res}' + if ilp64: + assert b"USE64BITINT" in res + else: + assert b"USE64BITINT" not in res + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser( + description='Download and expand an OpenBLAS archive for this ' + 'architecture') + parser.add_argument('--test', nargs='*', default=None, + help='Test different architectures. "all", or any of ' + f'{SUPPORTED_PLATFORMS}') + parser.add_argument('--check_version', nargs='?', default='', + help='Check provided OpenBLAS version string ' + 'against available OpenBLAS') + args = parser.parse_args() + if args.check_version != '': + test_version(args.check_version) + elif args.test is None: + print(setup_openblas()) + else: + if len(args.test) == 0 or 'all' in args.test: + test_setup(SUPPORTED_PLATFORMS) + else: + test_setup(args.test) \ No newline at end of file diff --git a/python/setup.cfg b/python/setup.cfg new file mode 100644 index 0000000..7b7540f --- /dev/null +++ b/python/setup.cfg @@ -0,0 +1,43 @@ +[metadata] +description = L0Learn is a highly efficient framework for solving L0-regularized learning problems. +author = Hussein Hazimeh +author_email = hazimeh@google.com +maintainer = Tim Nonet +maintainer_email = tim.nonet@gmail.com +license = MIT +url = https://github.com/hazimehh/L0Learn +project_urls = + Source = https://github.com/hazimehh/L0Learn + Tracker = https://github.com/hazimehh/L0Learn/issues +classifiers= + Intended Audience :: Science/Research + Intended Audience :: Developers + Topic :: Scientific/Engineering :: Mathematics + Topic :: Software Development :: Libraries :: Python Modules + Development Status :: 4 - Beta + Programming Language :: C++ + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Operating System :: POSIX + Operating System :: MacOS + License :: OSI Approved :: MIT License + +[options] +zip_safe = False +python_requires = >=3.7 +install_requires = + numpy>=1.19.0 + scipy>=1.0.0 + pandas>=1.0.0 + matplotlib + +[options.extras_require] +test = + hypothesis +all = + %(test)s +[tool:pytest] +addopts = -rsxX -v +testpaths = tests \ No newline at end of file diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..5447555 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# This was borrowed heavily form https://github.com/RUrlus/diptest/ +import sys + +try: + from skbuild import setup +except ImportError: + print( + "Please update pip, you need pip 10 or greater,\n" + " or you need to install the PEP 518 requirements in pyproject.toml yourself", + file=sys.stderr, + ) + raise + +from setuptools import find_packages +import io +import re +from os.path import dirname +from os.path import join +from distutils.sysconfig import get_python_inc, get_config_var + +PACKAGE_NAME = 'l0learn' + +MAJOR = 0 +MINOR = 4 +MICRO = 3 +DEVELOPMENT = False + +VERSION = f'{MAJOR}.{MINOR}.{MICRO}' +FULL_VERSION = VERSION +if DEVELOPMENT: + FULL_VERSION += '.dev' + + +def read(*names, **kwargs): + with io.open( + join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8") + ) as fh: + return fh.read() + + +def write_version_py(filename: str = f'{PACKAGE_NAME}/version.py') -> None: + """Write package version to version.py. + This will ensure that the version in version.py is in sync with us. + Parameters + ---------- + filename : str + the path the file to write the version.py + """ + # Do not modify the indentation of version_str! + version_str = """\"\"\"THIS FILE IS AUTO-GENERATED BY l0learn setup.py.\"\"\" +name = '{name!s}' +version = '{version!s}' +full_version = '{full_version!s}' +release = {is_release!s} +""" + + with open(filename, 'w') as version_file: + version_file.write( + version_str.format(name=PACKAGE_NAME.lower(), + version=VERSION, + full_version=FULL_VERSION, + is_release=not DEVELOPMENT) + ) + + +if __name__ == '__main__': + write_version_py() + + setup( + name=PACKAGE_NAME, + version=FULL_VERSION, + long_description_content_type="text/markdown", + long_description="%s\n%s" + % ( + re.compile("^.. start-badges.*^.. end-badges", re.M | re.S).sub( + "", read("README.md") + ), + re.sub(":[a-z]+:`~?(.*?)`", r"``\1``", read("CHANGELOG.md")), + ), + packages=find_packages(), + setup_requires=["setuptools", + "wheel", + "scikit-build", + "cmake", + "ninja"], + cmake_install_dir="l0learn", + cmake_args=[ + f"-DL0LEARN_VERSION_INFO:STRING={VERSION}", + f"-DPython3_EXECUTABLE={sys.executable}", + f"-DPYTHON3_INCLUDE_DIR:STRING={get_python_inc()}", + f"-DPYTHON3_LIBRARY:STRING={get_config_var('LIBDIR')}" + ] + ) diff --git a/python/sparse.ipynb b/python/sparse.ipynb new file mode 100644 index 0000000..0ec9055 --- /dev/null +++ b/python/sparse.ipynb @@ -0,0 +1,970 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.sparse import csc_matrix\n", + "import l0learn" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "np.random.seed(4) # fix the seed to get a reproducible result\n", + "n, p, k = 500, 1000, 10\n", + "X = np.random.normal(size=(n, p))\n", + "B = np.zeros(p)\n", + "B[:k] = 1\n", + "e = np.random.normal(size=(n,))/2\n", + "y = X@B + e" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 500)" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.unravel_index(np.argmin(np.abs(X + 1.7194)), X.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "not (X.flags.f_contiguous or X.flags.owndata)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
l0support_sizeinterceptconverged
00.0795460-0.156704True
10.0787501-0.147182True
20.0658622-0.161024True
30.0504643-0.002500True
40.0445175-0.041058True
50.0416727-0.058013True
60.0397058-0.061685True
70.032715100.002157True
80.00021211-0.000857True
90.00018712-0.002161True
100.00017813-0.001199True
110.00015915-0.007959True
120.00014116-0.009603True
130.00013318-0.015697True
140.00013221-0.012732True
\n", + "
" + ], + "text/plain": [ + "FitModel({'loss': 'SquaredError', 'intercept': True, 'penalty': 'L0'})" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fit_model" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "X at column 1 = 0.5000\n", + " -1.8889\n", + " -0.5859\n", + " 0.8286\n", + " -0.2622\n", + " -0.7380\n", + " 0.2591\n", + " 0.1908\n", + " 0.2588\n", + " -2.5680\n", + " 1.1428\n", + " -1.7766\n", + " -0.3098\n", + " 0.5666\n", + " 1.0566\n", + " 1.2307\n", + " 0.0474\n", + " -0.1998\n", + " -0.7337\n", + " 0.1452\n", + " 0.4838\n", + " 0.5324\n", + " 0.8432\n", + " 0.4499\n", + " -1.8477\n", + " -0.5379\n", + " -1.0892\n", + " 0.1596\n", + " 0.2531\n", + " 1.5462\n", + " 1.5877\n", + " 1.2362\n", + " 0.7071\n", + " 0.0398\n", + " -0.2925\n", + " 0.5104\n", + " -0.4558\n", + " 2.3242\n", + " 1.5085\n", + " 1.1496\n", + " 0.1538\n", + " 1.0296\n", + " 1.0332\n", + " -0.9911\n", + " -0.6222\n", + " 1.6301\n", + " -1.6879\n", + " 0.6994\n", + " 0.5250\n", + " -0.7482\n", + " 2.1634\n", + " -1.2182\n", + " -0.3143\n", + " -0.1359\n", + " 0.0358\n", + " 1.6373\n", + " 0.5623\n", + " -0.1503\n", + " 1.0821\n", + " 0.0880\n", + " -1.4195\n", + " -0.9476\n", + " 1.2908\n", + " -0.1857\n", + " -0.6444\n", + " 1.2578\n", + " 0.9470\n", + " -2.0210\n", + " -1.0287\n", + " 1.4844\n", + " -0.6175\n", + " 0.9521\n", + " 1.3697\n", + " -0.1475\n", + " 0.2178\n", + " 0.1435\n", + " 1.2622\n", + " -0.6379\n", + " 0.4418\n", + " 2.5668\n", + " -0.8739\n", + " -0.0836\n", + " 0.7807\n", + " -1.4666\n", + " 0.3183\n", + " 0.9187\n", + " -0.0080\n", + " 0.8404\n", + " 0.3050\n", + " -0.2181\n", + " -0.4265\n", + " -1.0178\n", + " -1.6794\n", + " 0.4758\n", + " -0.4668\n", + " 0.4159\n", + " 0.3365\n", + " 0.2947\n", + " 0.7806\n", + " -1.3214\n", + " 0.2648\n", + " -0.9103\n", + " 0.2739\n", + " -0.6863\n", + " 0.6278\n", + " -0.7779\n", + " 1.6019\n", + " -2.1517\n", + " 0.7428\n", + " -0.4263\n", + " 0.3456\n", + " -0.9314\n", + " -1.6319\n", + " 0.7414\n", + " -0.7167\n", + " -0.4755\n", + " -0.2003\n", + " 0.9763\n", + " 0.2781\n", + " -0.0792\n", + " -0.1558\n", + " 1.5288\n", + " -0.1590\n", + " -1.6623\n", + " -2.3574\n", + " -0.9667\n", + " -0.1357\n", + " 1.3221\n", + " -0.9303\n", + " 1.0448\n", + " 1.2577\n", + " -0.4005\n", + " 0.2230\n", + " 1.7486\n", + " -0.2355\n", + " 0.6986\n", + " -0.1379\n", + " -0.2243\n", + " 0.7646\n", + " 1.2902\n", + " 0.6031\n", + " 1.2509\n", + " -0.7641\n", + " 0.3928\n", + " -1.0471\n", + " 1.6325\n", + " -0.1716\n", + " -0.8323\n", + " -1.4190\n", + " 0.0444\n", + " 1.0159\n", + " -0.7294\n", + " -0.2728\n", + " -1.0811\n", + " 1.1715\n", + " 1.5101\n", + " -0.4544\n", + " 0.7426\n", + " -0.5534\n", + " 0.7377\n", + " 0.0630\n", + " 0.0929\n", + " 0.3204\n", + " 0.3148\n", + " -1.0520\n", + " -0.2410\n", + " 1.7144\n", + " -0.0958\n", + " -0.7845\n", + " 0.5091\n", + " -0.7293\n", + " -1.4166\n", + " -1.4303\n", + " -1.0545\n", + " 1.3686\n", + " 0.6150\n", + " -0.7779\n", + " -0.0787\n", + " 0.6675\n", + " -0.2162\n", + " -0.1038\n", + " 1.5616\n", + " -0.3338\n", + " -0.4212\n", + " -0.6214\n", + " -1.2782\n", + " 1.4457\n", + " -0.4670\n", + " 0.2427\n", + " -1.0694\n", + " 0.9614\n", + " 1.4707\n", + " -0.2979\n", + " 0.7881\n", + " 1.6846\n", + " 1.7765\n", + " -0.8942\n", + " -1.4393\n", + " 2.5469\n", + " 0.6866\n", + " 0.9250\n", + " -0.1550\n", + " -0.2935\n", + " -1.1473\n", + " -0.4607\n", + " 0.7399\n", + " 1.1199\n", + " 1.4736\n", + " 0.9349\n", + " -0.2786\n", + " -0.6606\n", + " -0.4544\n", + " 0.2700\n", + " 1.0996\n", + " -1.1453\n", + " -0.1624\n", + " -0.2379\n", + " -1.5148\n", + " -0.4477\n", + " 0.4786\n", + " 0.2882\n", + " -0.1786\n", + " 0.8847\n", + " -0.9459\n", + " 1.6703\n", + " -1.4216\n", + " 0.6008\n", + " 0.3946\n", + " -1.5668\n", + " 0.9338\n", + " -0.7770\n", + " 1.1549\n", + " 1.1032\n", + " -1.5046\n", + " -0.3785\n", + " -0.5927\n", + " -1.1602\n", + " 0.3997\n", + " 0.5879\n", + " -0.3723\n", + " -2.0234\n", + " -1.4964\n", + " 0.7040\n", + " -1.4460\n", + " -0.4611\n", + " -1.7708\n", + " -0.4558\n", + " -0.4998\n", + " 0.8435\n", + " 0.5242\n", + " -2.1870\n", + " 1.2325\n", + " 1.1520\n", + " -0.5316\n", + " -0.1897\n", + " 1.2699\n", + " -0.4812\n", + " 0.2390\n", + " -0.2685\n", + " 1.9363\n", + " 1.3044\n", + " 0.2275\n", + " 0.9217\n", + " -1.5408\n", + " -0.5082\n", + " 0.5022\n", + " 0.3041\n", + " -0.0455\n", + " -1.3367\n", + " 0.2646\n", + " -0.1962\n", + " -0.9274\n", + " 0.9223\n", + " 0.5121\n", + " 0.1100\n", + " 0.3363\n", + " -0.4371\n", + " 0.5857\n", + " -0.4663\n", + " -0.9203\n", + " 0.4568\n", + " 0.1511\n", + " 1.0236\n", + " 0.2822\n", + " 0.1780\n", + " -0.6013\n", + " -0.7013\n", + " -0.5320\n", + " 1.0928\n", + " 0.3758\n", + " 2.4104\n", + " -4.1451\n", + " -0.2888\n", + " -3.8551\n", + " 0.7568\n", + " 0.3756\n", + " 0.3861\n", + " 0.1014\n", + " -0.1255\n", + " 0.4443\n", + " -0.6165\n", + " -0.2394\n", + " -0.7020\n", + " -0.2927\n", + " 1.5096\n", + " 0.1202\n", + " -0.4189\n", + " -1.2244\n", + " 0.2963\n", + " 1.8693\n", + " -0.9340\n", + " 1.5663\n", + " -0.0747\n", + " 0.3265\n", + " -0.6769\n", + " -0.4813\n", + " -1.3764\n", + " -1.2413\n", + " -0.1362\n", + " -0.2609\n", + " 0.1758\n", + " -0.0325\n", + " -0.7772\n", + " -0.7084\n", + " -0.1339\n", + " -0.8516\n", + " -0.5234\n", + " -0.5122\n", + " 0.5784\n", + " -0.4658\n", + " -0.8313\n", + " -0.7572\n", + " 0.4142\n", + " 1.3081\n", + " -0.4885\n", + " -1.8294\n", + " 0.8008\n", + " -0.6806\n", + " 0.9877\n", + " -0.4658\n", + " -0.5152\n", + " -2.2519\n", + " -0.9184\n", + " 0.0452\n", + " 0.6234\n", + " 0.4417\n", + " -0.4270\n", + " -0.9116\n", + " 1.2123\n", + " 1.9425\n", + " 2.7714\n", + " -0.3023\n", + " -1.4019\n", + " -0.9243\n", + " -1.9179\n", + " 0.8465\n", + " 0.1831\n", + " 0.6253\n", + " 1.0873\n", + " -0.1138\n", + " 0.5737\n", + " -0.0160\n", + " -0.3136\n", + " 0.6287\n", + " 0.4997\n", + " -1.0553\n", + " 1.5128\n", + " -0.7724\n", + " -0.8117\n", + " 0.4409\n", + " 0.5041\n", + " -2.1216\n", + " -0.8234\n", + " -1.2071\n", + " 1.6990\n", + " 0.1539\n", + " -1.5476\n", + " 0.1917\n", + " 1.0442\n", + " -1.3003\n", + " -0.7363\n", + " -1.2590\n", + " 0.6875\n", + " 0.2634\n", + " 0.6787\n", + " 0.7348\n", + " 0.5719\n", + " 0.1611\n", + " 0.6663\n", + " 0.7529\n", + " -1.1562\n", + " -0.2304\n", + " 0.6477\n", + " -0.2201\n", + " 1.3055\n", + " -0.1141\n", + " -0.5299\n", + " -0.4522\n", + " -1.9043\n", + " 0.8776\n", + " 1.9302\n", + " 1.1516\n", + " -0.6075\n", + " 2.3531\n", + " -0.7080\n", + " 0.4949\n", + " 0.0472\n", + " -1.7398\n", + " 2.7106\n", + " 0.4589\n", + " -0.8962\n", + " -2.4618\n", + " 0.4810\n", + " 2.8438\n", + " -1.6133\n", + " 0.3117\n", + " 0.0361\n", + " -0.3038\n", + " 1.5661\n", + " -0.4900\n", + " -1.9978\n", + " 1.7319\n", + " 2.1458\n", + " -0.9193\n", + " 1.0363\n", + " 0.8575\n", + " 0.8833\n", + " 0.8168\n", + " -1.2229\n", + " 0.1438\n", + " -1.0943\n", + " 1.0756\n", + " -0.0651\n", + " -0.1466\n", + " -0.4549\n", + " 0.1315\n", + " 0.0567\n", + " -1.6938\n", + " -0.2960\n", + " -0.0285\n", + " -1.5097\n", + " -0.6940\n", + " -0.6508\n", + " -1.6149\n", + " -1.0484\n", + " 0.5908\n", + " -0.7661\n", + " 0.1657\n", + " -0.1332\n", + " -1.6022\n", + " -0.3147\n", + " -0.9328\n", + " -0.5750\n", + " 2.1529\n", + " 3.2324\n", + " 0.3846\n", + " -0.7117\n", + " -0.3512\n", + " -0.3957\n", + " 1.2097\n", + " 0.0667\n", + " 0.5044\n", + " 0.5186\n", + " 0.8504\n", + " 0.0150\n", + " -0.2528\n", + " 0.1668\n", + " 0.0921\n", + " 0.0315\n", + " -1.0027\n", + " 0.0170\n", + " 0.4593\n", + " -1.3572\n", + " 1.6892\n", + " 0.7206\n", + " -0.6786\n", + " 1.5432\n", + " 0.1526\n", + " 0.4065\n", + " -1.8494\n", + " 1.5476\n", + " -0.0046\n", + " -1.1916\n", + " 0.5081\n", + " 2.0424\n", + " 0.6191\n", + " -0.9728\n", + " -0.1111\n", + " 0.1431\n", + " -1.0241\n", + " -1.7796\n", + " -0.6147\n", + " 1.2153\n", + " -0.6740\n", + " 0.4614\n", + " 0.3792\n", + " -0.0326\n", + " 1.0495\n", + " -0.3696\n", + " 1.6860\n", + "X at row 1 = -0.1303 -1.8889 -1.8943 -0.7641 0.8248 0.8346 -0.1940 -1.2710 1.1092 -0.3537 -0.9102 -0.3909 -0.2594 1.6956 -0.0247 -0.5292 -0.4512 -1.5525 1.2297 -0.9600 0.1138 -0.9826 -0.7896 -2.1680 0.0735 2.1952 1.7877 0.4093 1.4068 -0.7727 -1.1346 1.1528 -0.6630 2.1216 1.3934 -1.1337 -0.3597 -0.3541 0.7632 -0.3796 -0.0305 -1.5087 1.6120 0.8809 0.0749 -1.5934 0.1615 2.9833 0.4106 1.2373 -2.1477 -0.3035 -0.2331 1.0815 -0.9824 0.8464 1.0854 0.0905 -0.1758 0.4472 -1.0333 -1.6160 -1.7218 0.1391 -0.3146 -0.1687 -0.5959 -0.1425 1.8204 -0.3562 -0.9599 -1.2752 -1.0133 0.4999 -0.2816 -0.4540 -0.2762 -1.5043 -0.9698 -0.4773 -0.4899 0.1741 0.8084 -0.7487 0.2317 0.3013 -0.6859 0.4594 0.8885 -1.7017 0.3351 0.3276 0.2349 -1.6038 0.9208 -0.2154 -0.8873 -1.6463 0.3400 -0.2532 -1.4465 1.5566 -1.3078 0.5931 -1.0350 -0.2642 0.6000 -0.3168 0.4864 -0.0245 1.4653 0.6982 0.4726 -0.7308 -0.4193 0.2034 1.7011 1.5083 0.1936 1.1821 0.1794 0.2560 0.1005 -0.2652 0.5479 3.1340 -0.2358 1.1982 0.9420 0.1006 0.8255 -0.3822 0.1978 1.5097 1.6373 0.0431 -0.1129 -1.2686 0.8054 -0.4961 -0.8063 0.2664 0.0069 -1.3997 1.2121 -1.6549 -1.6258 0.9492 -0.3033 -1.7251 1.0179 0.8703 -0.2136 -0.1070 0.1601 0.7922 -0.1611 -1.1424 -0.1301 -0.5462 -1.6649 -0.5373 -1.0936 0.0651 1.1558 0.4532 2.7863 0.4111 0.1179 -0.9391 0.8304 0.3314 -1.6710 -0.6781 0.6208 -0.4852 0.3932 0.1969 2.0448 -1.5095 -0.1868 -1.3593 -0.9128 1.9446 0.9201 0.5928 -0.0625 0.0299 2.0419 -1.0426 1.2962 1.1168 -0.9104 0.2406 -0.6990 0.0609 0.8988 0.3169 1.2566 0.7810 -2.2314 0.8412 -1.2644 -2.9319 -0.3245 -0.5170 2.1046 1.0606 0.8861 0.0856 0.9396 -0.6322 1.1533 -0.4309 -0.1291 0.3175 -0.6852 3.0213 0.2461 -0.3835 -0.9106 -0.6247 -0.0162 0.5834 -1.4655 -0.4383 1.8640 0.3467 0.0492 -0.3549 -0.1978 0.1766 1.4384 1.3914 -0.5123 -0.8472 0.5280 -0.5381 1.0141 1.1520 -0.7202 -2.0949 -1.3701 -0.0942 -1.1858 0.3153 0.2089 0.0400 -0.1543 -0.7056 0.5936 -1.9365 0.4321 2.3047 1.2961 0.0102 -0.2855 0.3889 -0.1387 1.6600 -0.5668 -0.1872 -1.1931 0.5835 -1.5910 0.8203 0.5441 -1.2831 0.4048 -0.2641 -0.5522 -1.3442 -1.6359 -1.4626 -1.6930 0.0279 -1.0272 2.1113 0.3575 1.1408 -0.6979 1.5549 -0.4504 0.9850 0.3416 0.7010 -1.1694 -0.9518 0.9355 0.4494 -0.5949 0.5880 0.0933 -0.5871 2.0428 -2.3030 -0.2917 0.8635 0.4134 1.9696 0.5869 0.5109 -0.6359 1.6277 -0.6014 -0.3479 -0.3488 0.6429 -0.5888 -0.0647 -0.5147 -1.7515 -0.6798 2.0559 -0.3951 0.1686 2.5886 -0.7805 0.7106 1.2175 -0.9543 1.5560 0.9034 -1.1940 0.0754 -1.2196 2.8129 2.1289 0.0210 -0.6777 0.4525 -1.1427 -0.2391 -1.1154 -0.2574 0.4185 0.3286 0.7284 -0.5076 0.8819 0.0942 0.3560 1.5036 0.0449 -0.9635 0.6887 -1.0330 0.0872 -0.0501 -2.1020 -1.4337 -0.9257 0.3425 0.9070 0.0722 2.3275 0.8198 -1.5429 0.6366 -1.0006 -0.7866 -0.2862 -0.2392 0.3370 0.7633 1.1114 -0.5599 0.0947 1.1797 -1.1000 1.0090 0.7367 -0.3539 1.2207 0.4190 0.7183 1.6049 -0.0837 -1.1266 -0.4496 -1.0850 1.0135 -0.3688 -0.4396 -0.6355 1.1079 0.3006 0.7692 0.2293 -0.7408 -0.9206 1.1963 -0.2899 0.6682 -1.1477 -0.3087 -0.5710 -0.8685 0.5535 0.8530 -0.2707 2.0816 -0.0427 -1.6252 0.5045 -1.3051 -0.3969 0.2542 1.1368 0.5439 -0.7348 1.2657 0.2403 1.2683 1.2973 0.0072 -0.7310 0.6890 0.0616 -0.0828 0.1312 -0.8594 -1.0277 -0.5403 0.1421 -0.3251 0.6137 0.7264 0.4142 -1.0199 -1.4757 -0.9888 -1.1946 0.7967 0.7680 -1.6643 -0.4378 -0.1735 -0.9022 -0.3338 -0.8859 -0.4636 -0.0393 0.3110 -0.8364 0.6814 -0.8818 -0.3938 -0.2424 -0.9607 0.4192 -0.6948 -0.9700 1.6247 -0.3068 -0.4554 -2.3321 -0.2029 1.4800 -0.3986 -0.3510 0.7297 0.8157 1.1313 0.6526 -0.2743 -0.7319 0.5434 0.4993 0.4788 -0.6852 -0.5029 1.5120 -0.3975 0.5953 -0.0040 -1.6946 0.5412 -1.0569 1.5897 -0.4021 -0.6620 -0.2189 -0.0788 -0.4391 -0.3756 1.0283 0.1207 0.7142 0.0609 0.5808 -0.6671 1.5082 -0.9241 -1.7482 -1.3749 -1.0201 -0.3169 1.6518 -1.2610 0.6662 -0.9701 1.3008 0.5260 1.7987 -0.3011 0.7496 0.8225 -0.4573 -0.1228 -0.6626 -0.8160 -0.6457 -1.2092 0.7843 -0.1486 -0.9408 -0.0216 0.2489 -0.3920 0.9855 -0.2958 0.0864 2.1242 -0.6019 -0.0168 0.2337 -1.2911 -0.4708 0.7310 1.5578 -0.7311 0.4221 -0.3300 -0.4875 -1.2506 0.3472 -2.1284 0.5914 0.1862 0.2549 -0.2563 -1.3353 -1.5943 0.0710 0.4695 0.1217 -0.9617 0.8348 2.2675 -0.4620 -0.9975 -0.4232 1.7423 -0.8411 -0.5181 -0.1982 -0.3315 0.6473 -0.4850 -1.2316 0.1319 0.0068 1.9731 1.1936 1.7843 0.0455 0.4089 0.4818 0.0050 -1.6032 0.5682 0.2087 1.6447 1.0798 -0.5055 0.3588 -1.0928 1.1815 -0.1790 -1.3245 -2.1736 0.7826 0.5969 -0.9311 2.3017 -0.5934 1.5801 -0.7191 0.2348 -0.5762 2.0757 0.9789 0.2594 0.3832 0.1516 0.1950 -1.3627 0.4390 1.3800 -0.7068 -1.3057 -1.4113 -0.9749 0.9718 0.4244 -1.3514 1.4114 -0.3652 0.6211 -0.3572 -1.0582 0.1297 -0.4077 0.2258 0.0350 -1.1996 0.4340 -0.4507 0.4769 0.8807 0.4588 0.9047 1.6949 -0.3244 -0.6571 -1.0580 1.0078 -0.9006 0.6270 1.4200 -0.8754 1.4353 -1.0162 0.7245 0.0132 -1.5419 -0.0709 1.5211 -0.2002 -1.2329 0.9319 0.9392 -1.1274 0.1109 -1.3233 -1.4660 0.6924 0.7260 1.5430 0.7486 -0.2156 1.1025 -0.8867 -0.4322 0.1090 -0.4704 -0.1856 -0.8473 0.9206 1.4661 1.1225 1.8906 0.9805 0.8283 -1.3156 0.1768 -1.4570 1.0890 0.2154 -0.1140 -0.4136 -0.6786 -0.1035 -0.3162 0.3441 -1.2909 -0.4467 1.4900 -1.9820 -0.4664 -1.2786 0.3676 0.7329 -0.3929 -0.7365 -0.2747 0.9056 -0.7859 -0.2956 1.0680 -1.0719 -1.3234 0.6934 1.2849 0.7435 2.3218 -1.0324 -0.6139 0.3953 -2.2905 -0.5012 2.0438 1.2503 -1.0526 2.1789 1.5080 0.2720 -0.5432 0.7212 1.4434 0.1203 -0.7942 0.5545 0.4358 -1.4950 -1.3790 -1.0028 0.7541 -1.1814 -0.2463 0.2507 -1.0173 -1.0405 -0.8867 -0.3166 -1.0032 1.7837 0.4660 1.0115 -1.0576 -1.0503 -1.2691 -0.9969 0.6017 1.1066 1.0629 0.8304 -2.1412 -0.4744 0.3403 0.6981 -0.2230 0.3195 -0.1354 -0.2387 0.5058 -0.8588 -0.6456 0.2905 0.2384 0.0323 0.7997 -0.5590 0.2237 1.0920 -0.7261 -0.3966 1.2628 -0.3463 0.6499 -0.8623 -0.6873 -1.2582 -1.3649 1.1135 -0.1239 -0.6870 0.9076 0.1912 0.4151 0.0974 0.7719 -1.4241 -1.4873 -0.4365 0.0903 0.0109 -0.6919 0.4479 0.2771 -0.1120 -0.1478 1.2252 -0.2829 -1.2783 -1.1165 0.9661 -0.2791 0.2517 0.3617 -0.0298 -0.7508 -0.1293 0.6322 -0.9279 0.0107 0.0431 -1.2497 -0.3207 0.3729 0.0378 -0.8449 1.2685 -0.6131 0.8045 0.5319 0.0397 1.3639 2.4420 0.9601 0.5330 0.2731 0.1203 -1.3857 0.8220 -0.7880 -2.3870 0.1022 -0.3642 -1.4525 0.8016 0.2126 1.0889 -0.3641 -0.3100 0.3088 0.9449 0.2051 1.6871 -0.3313 -0.2810 -0.6349 -0.8021 -1.1781 -1.4312 -0.3652 0.4600 1.5668 -0.0133 -1.6548 0.8254 -0.8026 0.7557 0.1951 -0.5060 -0.3632 -0.5389 0.6426 -0.7309 -0.2953 -0.6089 -0.6251 0.7391 1.1973 -0.3344 0.1649 0.9641 0.7885 -0.4885 0.8268 0.1011 -1.2578 -0.3759 -0.9399 0.5553 0.7497 0.9535 -0.2177 -0.5243 -0.2782 0.0437 -0.6773 0.3488 0.9226 -1.7426 0.8475 0.8574 -1.2473 4.2163 0.8636 0.3285 -1.5353 0.1917 0.0582 0.5318 -0.4769 -1.0342 -1.1038 0.2288 -0.7653 0.2579 1.6202 -1.6433 -0.2785 2.1554 0.4466 1.7524 0.1536 1.5707 1.0376 1.1222 -1.1420 1.3743 0.0680 -0.4519 0.8247 -0.5629 -1.1361 0.1096 1.0499 -0.3424 0.6436 1.4140 -0.0269 -0.0982 -0.7024 -0.5683 -0.1053 -2.5014 0.1795 1.8151 0.1059 0.0383 -0.6801 0.6451 -0.3206 1.8594 0.4789 0.5867 0.3746 -0.3948 -2.5526 -0.4541 0.7067 -1.9336 0.4456 0.4308 0.5824 0.1896 0.2485 -0.3686 1.1640 -1.3761 -1.1453 -0.9871 -0.9182 0.6855 -0.2783 0.8357 -1.3133 -0.4625 -0.8105 0.0411 0.1895 0.2088 -0.6791 1.2506 -0.3479 0.7949 1.6892 1.3242 -1.3077 1.7444 -0.1910 0.6751 0.1102 -0.0098 0.0822 0.5126 0.8483 -0.9459 -1.6989 -0.4744 -1.3666 -0.9707 1.1305 0.5723 -0.8086 -0.6741 -0.6032 -0.4959 -0.2727 0.0663 0.8952 -0.2796 -1.8718 -0.0125 -0.2311 -0.5582 0.7608 -1.9581 -1.1854 -0.8694 1.5049 -0.9910 -0.4829 0.3583 0.0323 -2.2564\n", + "soluton at column 0 = [matrix size: 1000x1; n_nonzero: 0; density: 0%]\n", + "\n", + "soluton at column 1 = [matrix size: 1000x1; n_nonzero: 1; density: 0.10%]\n", + "\n", + " (2, 0) 1.3478\n", + "\n", + "soluton at column 2 = [matrix size: 1000x1; n_nonzero: 2; density: 0.20%]\n", + "\n", + " (2, 0) 1.4386\n", + " (4, 0) 1.1724\n", + "\n", + "soluton at column 3 = [matrix size: 1000x1; n_nonzero: 3; density: 0.30%]\n", + "\n", + " (2, 0) 1.3930\n", + " (4, 0) 1.1254\n", + " (9, 0) 1.0608\n", + "\n", + "soluton at column 4 = [matrix size: 1000x1; n_nonzero: 5; density: 0.50%]\n", + "\n", + " (1, 0) 0.9976\n", + " (2, 0) 1.3135\n", + " (4, 0) 1.1826\n", + " (7, 0) 0.9941\n", + " (9, 0) 1.0887\n", + "\n", + "soluton at column 5 = [matrix size: 1000x1; n_nonzero: 7; density: 0.70%]\n", + "\n", + " (1, 0) 1.0195\n", + " (2, 0) 1.2185\n", + " (4, 0) 1.0848\n", + " (5, 0) 1.0042\n", + " (6, 0) 1.0029\n", + " (7, 0) 1.0397\n", + " (9, 0) 0.9992\n", + "\n", + "soluton at column 6 = [matrix size: 1000x1; n_nonzero: 8; density: 0.80%]\n", + "\n", + " (0, 0) 0.8908\n", + " (1, 0) 1.0186\n", + " (2, 0) 1.1519\n", + " (4, 0) 1.0353\n", + " (5, 0) 1.0185\n", + " (6, 0) 1.0207\n", + " (7, 0) 1.0407\n", + " (9, 0) 0.9614\n", + "\n", + "soluton at column 7 = [matrix size: 1000x1; n_nonzero: 10; density: 1.00%]\n", + "\n", + " (0, 0) 1.0201\n", + " (1, 0) 0.9734\n", + " (2, 0) 0.9983\n", + " (3, 0) 0.9969\n", + " (4, 0) 1.0115\n", + " (5, 0) 1.0021\n", + " (6, 0) 1.0129\n", + " (7, 0) 0.9923\n", + " (8, 0) 0.9962\n", + " (9, 0) 1.0268\n", + "\n", + "soluton at column 8 = [matrix size: 1000x1; n_nonzero: 11; density: 1.10%]\n", + "\n", + " (0, 0) 1.0168\n", + " (1, 0) 0.9820\n", + " (2, 0) 0.9996\n", + " (3, 0) 0.9966\n", + " (4, 0) 1.0130\n", + " (5, 0) 1.0038\n", + " (6, 0) 1.0150\n", + " (7, 0) 1.0024\n", + " (8, 0) 0.9947\n", + " (9, 0) 1.0246\n", + " (530, 0) -0.0691\n", + "\n", + "soluton at column 9 = [matrix size: 1000x1; n_nonzero: 12; density: 1.20%]\n", + "\n", + " (0, 0) 1.0185\n", + " (1, 0) 0.9864\n", + " (2, 0) 0.9972\n", + " (3, 0) 0.9934\n", + " (4, 0) 1.0187\n", + " (5, 0) 1.0021\n", + " (6, 0) 1.0153\n", + " (7, 0) 1.0063\n", + " (8, 0) 0.9939\n", + " (9, 0) 1.0224\n", + " (380, 0) -0.0631\n", + " (530, 0) -0.0693\n", + "\n", + "soluton at column 10 = [matrix size: 1000x1; n_nonzero: 13; density: 1.30%]\n", + "\n", + " (0, 0) 1.0198\n", + " (1, 0) 0.9869\n", + " (2, 0) 0.9942\n", + " (3, 0) 0.9968\n", + " (4, 0) 1.0195\n", + " (5, 0) 0.9979\n", + " (6, 0) 1.0123\n", + " (7, 0) 1.0077\n", + " (8, 0) 0.9934\n", + " (9, 0) 1.0228\n", + " (380, 0) -0.0622\n", + " (530, 0) -0.0701\n", + " (676, 0) -0.0629\n", + "\n", + "soluton at column 11 = [matrix size: 1000x1; n_nonzero: 15; density: 1.50%]\n", + "\n", + " (0, 0) 1.0201\n", + " (1, 0) 0.9840\n", + " (2, 0) 0.9957\n", + " (3, 0) 0.9897\n", + " (4, 0) 1.0237\n", + " (5, 0) 0.9964\n", + " (6, 0) 1.0112\n", + " (7, 0) 1.0112\n", + " (8, 0) 0.9934\n", + " (9, 0) 1.0165\n", + " (91, 0) 0.0606\n", + " (380, 0) -0.0625\n", + " (530, 0) -0.0732\n", + " (676, 0) -0.0606\n", + " (974, 0) 0.0608\n", + "\n", + "soluton at column 12 = [matrix size: 1000x1; n_nonzero: 16; density: 1.60%]\n", + "\n", + " (0, 0) 1.0180\n", + " (1, 0) 0.9807\n", + " (2, 0) 0.9936\n", + " (3, 0) 0.9918\n", + " (4, 0) 1.0215\n", + " (5, 0) 0.9941\n", + " (6, 0) 1.0092\n", + " (7, 0) 1.0097\n", + " (8, 0) 0.9958\n", + " (9, 0) 1.0156\n", + " (20, 0) -0.0552\n", + " (91, 0) 0.0626\n", + " (380, 0) -0.0638\n", + " (530, 0) -0.0733\n", + " (676, 0) -0.0608\n", + " (974, 0) 0.0596\n", + "\n", + "soluton at column 13 = [matrix size: 1000x1; n_nonzero: 18; density: 1.80%]\n", + "\n", + " (0, 0) 1.0152\n", + " (1, 0) 0.9813\n", + " (2, 0) 0.9909\n", + " (3, 0) 0.9925\n", + " (4, 0) 1.0160\n", + " (5, 0) 0.9951\n", + " (6, 0) 1.0128\n", + " (7, 0) 1.0086\n", + " (8, 0) 0.9992\n", + " (9, 0) 1.0188\n", + " (20, 0) -0.0580\n", + " (91, 0) 0.0648\n", + " (304, 0) -0.0548\n", + " (380, 0) -0.0666\n", + " (530, 0) -0.0664\n", + " (676, 0) -0.0627\n", + " (779, 0) 0.0650\n", + " (974, 0) 0.0546\n", + "\n", + "soluton at column 14 = [matrix size: 1000x1; n_nonzero: 21; density: 2.10%]\n", + "\n", + " (0, 0) 1.0074\n", + " (1, 0) 0.9804\n", + " (2, 0) 0.9900\n", + " (3, 0) 0.9884\n", + " (4, 0) 1.0201\n", + " (5, 0) 0.9867\n", + " (6, 0) 1.0184\n", + " (7, 0) 1.0056\n", + " (8, 0) 1.0003\n", + " (9, 0) 1.0254\n", + " (20, 0) -0.0627\n", + " (91, 0) 0.0709\n", + " (304, 0) -0.0554\n", + " (346, 0) -0.0606\n", + " (380, 0) -0.0666\n", + " (429, 0) 0.0608\n", + " (530, 0) -0.0634\n", + " (676, 0) -0.0669\n", + " (779, 0) 0.0686\n", + " (972, 0) 0.0584\n", + " (974, 0) 0.0536\n", + "\n" + ] + } + ], + "source": [ + "fit_model = l0learn.fit(X, y, max_support_size=20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/tests/test_fit.py b/python/tests/test_fit.py new file mode 100644 index 0000000..532e321 --- /dev/null +++ b/python/tests/test_fit.py @@ -0,0 +1,473 @@ +import pytest +import numpy as np +from scipy.sparse import csc_matrix + +import l0learn + +N = 50 + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_X_sparse_support(f): + x = np.random.random(size=(N, N)) + x_sparse = csc_matrix(x) + y = np.random.random(size=(N,)) + model_fit = f(x_sparse, y, intercept=False) + assert max(model_fit.support_size[0]) == N + + +@pytest.mark.parametrize( + "x", + [ + np.random.random(size=(N, N, N)), # Wrong Size + "A String", # Wrong Type + np.random.random(size=(N, N)).astype(complex), # Wrong dtype + np.random.random(size=(N, N)).astype(int), # Wrong dtype + np.random.random(size=(0, N)), # degenerate 2D array + ], +) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_X_dense_bad_checks(f, x): + # Check size of matrix X + y = np.random.random(size=(N,)) + with pytest.raises(ValueError): + f(x, y) + + +@pytest.mark.parametrize( + "y", + [ + np.random.random(size=(N, N, N)), # wrong dimensions + "A String", # wrong type + np.random.random(size=(N,)).astype(complex), # wrong dtype + np.random.random(size=(N + 1)), + ], +) # wrong size +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_y_bad_checks(f, y): + # Check size of matrix X + x = np.random.random(size=(N, N)) + with pytest.raises(ValueError): + f(x, y) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_loss_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + with pytest.raises(ValueError): + f(x, y, loss="NOT A LOSS") + + +@pytest.mark.parametrize("loss", l0learn.interface.SUPPORTED_LOSS) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_loss_good_checks(f, loss): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.randint(low=0, high=2, size=(N,)).astype(float) + _ = f(x, y, loss=loss) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_penalty_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + with pytest.raises(ValueError): + f(x, y, penalty="L0LX") + + +@pytest.mark.parametrize("penalty", l0learn.interface.SUPPORTED_PENALTY) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_penalty_good_checks(f, penalty): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + _ = f(x, y, penalty=penalty) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_algorithm_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + with pytest.raises(ValueError): + f(x, y, algorithm="NOT CD or CDPSI") + + +@pytest.mark.parametrize("algorithm", l0learn.interface.SUPPORTED_ALGORITHM) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_algorithm_good_checks(f, algorithm): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + _ = f(x, y, algorithm=algorithm) + + +@pytest.mark.parametrize("max_support_size", [-1, 2.0]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_max_support_size_bad_checks(f, max_support_size): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, max_support_size=max_support_size) + + +@pytest.mark.parametrize("max_support_size", [N, N - 1, N + 1]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_max_support_size_good_checks(f, max_support_size): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + _ = f(x, y, max_support_size=max_support_size) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_gamma_max_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, gamma_max=-1) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_gamma_min_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, gamma_min=1, gamma_max=0.5) + + with pytest.raises(ValueError): + _ = f(x, y, gamma_min=-1) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_paritial_sort_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, partial_sort="NOT A BOOL") + + +@pytest.mark.parametrize("max_iter", [1.0, 0]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_max_iter_bad_checks(f, max_iter): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, max_iter=max_iter) + + +@pytest.mark.parametrize("rtol", [1.0, -0.1]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_rtol_bad_checks(f, rtol): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, rtol=rtol) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_atol_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, atol=-1) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_active_set_sort_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, active_set="NOT A BOOL") + + +@pytest.mark.parametrize("active_set_num", [1.3, 0]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_active_set_num_bad_checks(f, active_set_num): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, active_set_num=active_set_num) + + +@pytest.mark.parametrize("max_swaps", [0, 4.5]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_max_swaps_bad_checks(f, max_swaps): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, max_swaps=max_swaps) + + +@pytest.mark.parametrize("scale_down_factor", [-1, 2]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_scale_down_factor_bad_checks(f, scale_down_factor): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, scale_down_factor=scale_down_factor) + + +@pytest.mark.parametrize("screen_size", [-1, 2.0]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_screen_size_bad_checks(f, screen_size): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, screen_size=screen_size) + + +@pytest.mark.parametrize("exclude_first_k", [-1, 2.0, N + 1]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_exclude_first_k_bad_checks(f, exclude_first_k): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, exclude_first_k=exclude_first_k) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_intercept_bad_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=(N,)) + + with pytest.raises(ValueError): + _ = f(x, y, intercept="NOT A BOOL") + + +@pytest.mark.parametrize("loss", l0learn.interface.CLASSIFICATION_LOSS) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_classification_loss_bad_y_checks(f, loss): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.zeros(N) + y[0] = 1 + y[1] = 2 + + with pytest.raises(ValueError): + _ = f(x, y, loss=loss) + + +@pytest.mark.parametrize("loss", l0learn.interface.CLASSIFICATION_LOSS) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_classification_loss_bad_lambda_grid_L0_checks(f, loss): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.randint(0, 2, size=N) + lambda_grid = [[10], [10]] + + with pytest.raises(ValueError): + _ = f( + x, + y, + loss=loss, + penalty="L0", + lambda_grid=lambda_grid, + num_gamma=None, + num_lambda=None, + ) + + _ = f( + x, + y, + loss=loss, + penalty="L0", + lambda_grid=[[10]], + num_gamma=None, + num_lambda=None, + ) + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_bad_lambda_grid_L0_checks(f): + # Check size of matrix X + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + lambda_grid = [[10], [10]] + + with pytest.raises(ValueError): + _ = f( + x, y, penalty="L0", lambda_grid=lambda_grid, num_gamma=None, num_lambda=None + ) + + _ = f(x, y, penalty="L0", lambda_grid=[[10]], num_gamma=None, num_lambda=None) + + +@pytest.mark.parametrize("penalty", ["L0L1", "L0L2"]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_classification_loss_bad_num_gamma_L0_checks(f, penalty): + x = np.random.random(size=(N, N)) + y = np.random.randint(2, size=N) + + with pytest.warns(None) as wrn: + _ = f(x, y, loss="SquaredHinge", penalty=penalty, num_gamma=1, num_lambda=10) + + assert len(wrn) == 1 + + with pytest.warns(None) as wrn: + _ = f(x, y, loss="SquaredHinge", penalty=penalty, num_gamma=2, num_lambda=10) + + assert len(wrn) == 0 + + +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_auto_lambda_bad_checks(f): + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + + with pytest.raises(ValueError): + _ = f(x, y, penalty="L0L1", num_gamma=3.0, num_lambda=5) + + with pytest.raises(ValueError): + _ = f(x, y, penalty="L0L1", num_gamma=-2, num_lambda=5) + + with pytest.raises(ValueError): + _ = f(x, y, penalty="L0L1", num_gamma=5, num_lambda=5.0) + + with pytest.raises(ValueError): + _ = f(x, y, penalty="L0L1", num_gamma=5, num_lambda=-2) + + with pytest.raises(ValueError): + _ = f(x, y, penalty="L0", num_gamma=2, num_lambda=10) + + _ = f(x, y, penalty="L0", num_gamma=1, num_lambda=10) + + +@pytest.mark.parametrize("penalty", ["L0L1", "L0L2"]) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_lambda_grid_bad_over_defined_checks(f, penalty): + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + + with pytest.raises(ValueError): + _ = f( + x, + y, + penalty=penalty, + lambda_grid=[[10], [10]], + num_lambda=1, + num_gamma=None, + ) + + with pytest.raises(ValueError): + _ = f( + x, + y, + penalty=penalty, + lambda_grid=[[10], [10]], + num_lambda=None, + num_gamma=2, + ) + + _ = f( + x, y, penalty=penalty, lambda_grid=[[10], [10]], num_lambda=None, num_gamma=None + ) + + +@pytest.mark.parametrize( + "penalty_lambda_grid", + [ + ("L0L1", [[-1]]), + ("L0", [[10, 11]]), + ], +) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_lambda_grid_bad_checks(f, penalty_lambda_grid): + penalty, lambda_grid = penalty_lambda_grid + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + + with pytest.raises(ValueError): + _ = f( + x, + y, + penalty=penalty, + lambda_grid=lambda_grid, + num_gamma=None, + num_lambda=None, + ) + + +@pytest.mark.parametrize( + "bounds", + [ + ("NOT A FLOAT", 1.0), + (1.0, "NOT A FLOAT"), + (1.0, 1.0), + (-np.ones((N, 2)), 1.0), + (-np.ones(N + 1), 1.0), + (np.ones(N), 1.0), + (-1.0, -1.0), + (-1.0, np.ones((N, 2))), + (-1.0, np.ones(N + 1)), + (-1.0, -1.0 * np.ones(N)), + (0.0, 0.0), + (np.zeros(N), np.zeros(N)), + ], +) +@pytest.mark.parametrize("f", [l0learn.fit, l0learn.cvfit]) +def test_with_bounds_bad_checks(f, bounds): + lows, highs = bounds + + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + + with pytest.raises(ValueError): + _ = f(x, y, lows=lows, highs=highs) + + +@pytest.mark.parametrize("num_folds", [-1, 0, 1, N + 1, 2.0]) +def test_cvfit_num_folds_bad_check(num_folds): + x = np.random.random(size=(N, N)) + y = np.random.random(size=N) + + with pytest.raises(ValueError): + _ = l0learn.cvfit(x, y, num_folds=num_folds) + + +def test_L0_classification_is_actually_L0L2(): + x = np.random.random(size=(N, N)) + y = np.random.randint(0, 2, size=N) + + result = l0learn.fit(x, y, loss="Logistic") + + assert len(result.gamma) == 1 + assert result.gamma[0] == 1e-7 + + +def test_L0L2_classification_is_actually_L0L2(): + x = np.random.random(size=(N, N)) + y = np.random.randint(0, 2, size=N) + + result = l0learn.fit(x, y, loss="Logistic", penalty="L0L2") + + assert len(result.gamma) >= 1 \ No newline at end of file diff --git a/python/tests/test_models.py b/python/tests/test_models.py new file mode 100644 index 0000000..6956903 --- /dev/null +++ b/python/tests/test_models.py @@ -0,0 +1,569 @@ +import numpy as np +import pandas as pd +import pytest +from hypothesis.strategies import floats +from scipy.sparse import csc_matrix, rand + +from l0learn.models import ( + FitModel, + CVFitModel, + regularization_loss, + squared_error, + logistic_loss, + squared_hinge_loss, +) + +from pytest import fixture +from hypothesis import given +from hypothesis.extra import numpy as npst + + +def _sample_FitModel( + loss: str = "SquaredError", intercept: bool = True, penalty: str = "L0L1" +): + i = intercept + beta1 = csc_matrix([[0, 0], [1, 2]]) + beta2 = csc_matrix([[0], [3]]) + beta3 = csc_matrix([[0, 0, 0], [4, 5, 6]]) + + return FitModel( + settings={"loss": loss, "intercept": intercept, "penalty": penalty}, + lambda_0=[[10, 5], [10], [10, 5, 0]], + gamma=[2, 1, 0], + support_size=[[0, 1], [3], [4, 5, 6]], + coeffs=[beta1, beta2, beta3], + intercepts=[[-1 * i, -2 * i], [-3 * i], [-4 * i, -5 * i, -6 * i]], + converged=[[True, True], [False], [True, False, True]], + ) + + +@fixture +def sample_FitModel(): + return _sample_FitModel() + + +@fixture +def sample_CVFitModel(num_folds: int = 2): + fit_model = _sample_FitModel() + cvMeans = np.random.random(num_folds) + 2 # Ensure positive + cvSTDs = np.random.random(num_folds) + + settings = fit_model.settings + settings["num_folds"] = num_folds + + return CVFitModel( + settings=fit_model.settings, + lambda_0=fit_model.lambda_0, + gamma=fit_model.gamma, + support_size=fit_model.support_size, + coeffs=fit_model.coeffs, + intercepts=fit_model.intercepts, + converged=fit_model.converged, + cv_means=cvMeans, + cv_sds=cvSTDs, + ) + + +def test_CVFitModel_plot(sample_CVFitModel): + sample_CVFitModel.cv_plot(gamma=1) + + +def test_FitModel_plot(sample_FitModel): + sample_FitModel.plot(gamma=1, include_legend=False) + sample_FitModel.plot(gamma=1, show_lines=True, include_legend=False) + + +def test_FitModel_coeff(sample_FitModel): + np.testing.assert_array_equal( + sample_FitModel.coeff().toarray(), + np.array([[-1, -2, -3, -4, -5, -6], [0, 0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6]]), + ) + np.testing.assert_array_equal( + sample_FitModel.coeff(include_intercept=False).toarray(), + np.array([[0, 0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6]]), + ) + + np.testing.assert_array_equal( + sample_FitModel.coeff(gamma=1).toarray(), + np.array( + [ + [ + -3, + ], + [ + 0, + ], + [ + 3, + ], + ] + ), + ) + np.testing.assert_array_equal( + sample_FitModel.coeff(gamma=1, include_intercept=False).toarray(), + np.array( + [ + [ + 0, + ], + [ + 3, + ], + ] + ), + ) + + np.testing.assert_array_equal( + sample_FitModel.coeff(lambda_0=6).toarray(), + np.array( + [ + [ + -2, + ], + [ + 0, + ], + [ + 2, + ], + ] + ), + ) + + np.testing.assert_array_equal( + sample_FitModel.coeff(lambda_0=6, include_intercept=False).toarray(), + np.array( + [ + [ + 0, + ], + [ + 2, + ], + ] + ), + ) + + np.testing.assert_array_equal( + sample_FitModel.coeff(gamma=0, lambda_0=6).toarray(), + np.array( + [ + [ + -5, + ], + [ + 0, + ], + [ + 5, + ], + ] + ), + ) + + np.testing.assert_array_equal( + sample_FitModel.coeff(gamma=0, lambda_0=6, include_intercept=False).toarray(), + np.array( + [ + [ + 0, + ], + [ + 5, + ], + ] + ), + ) + + +def test_characteristics(sample_FitModel): + pd.testing.assert_frame_equal( + sample_FitModel.characteristics(), + pd.DataFrame( + { + "l0": [10, 5, 10, 10, 5, 0], + "support_size": [0, 1, 3, 4, 5, 6], + "intercept": [-1, -2, -3, -4, -5, -6], + "converged": [True, True, False, True, False, True], + "l1": [2, 2, 1, 0, 0, 0], + } + ), + ) + + pd.testing.assert_frame_equal( + sample_FitModel.characteristics(gamma=1), + pd.DataFrame( + { + "l0": [10], + "support_size": [3], + "intercept": [-3], + "converged": [False], + "l1": [1], + } + ), + ) + + pd.testing.assert_frame_equal( + sample_FitModel.characteristics(lambda_0=6), + pd.DataFrame( + { + "l0": [5], + "support_size": [1], + "intercept": [-2], + "converged": [True], + "l1": [2], + } + ), + ) + + pd.testing.assert_frame_equal( + sample_FitModel.characteristics(gamma=0, lambda_0=5), + pd.DataFrame( + { + "l0": [5], + "support_size": [5], + "intercept": [-5], + "converged": [False], + "l1": [0], + } + ), + ) + + +@given( + coeffs=npst.arrays( + dtype=np.float64, + elements=floats( + allow_nan=False, allow_infinity=False, max_value=1e100, min_value=-1e100 + ), + shape=npst.array_shapes(min_dims=2, max_dims=2), + ) +) +def test_regularization_loss(coeffs): + coeffs_csc = csc_matrix(coeffs) + if coeffs.shape[1] == 1: + np.testing.assert_equal( + (coeffs != 0).sum(), regularization_loss(coeffs_csc, l0=1) + ) + np.testing.assert_equal( + (coeffs != 0).sum() + sum(abs(coeffs)) + sum(coeffs**2), + regularization_loss(coeffs_csc, l0=1, l1=1, l2=1), + ) + else: + num_solutions = coeffs.shape[1] + np.testing.assert_equal( + (coeffs != 0).sum(axis=0), + regularization_loss(coeffs_csc, l0=[1] * num_solutions), + ) + np.testing.assert_equal( + (coeffs != 0).sum(axis=0) + + abs(coeffs).sum(axis=0) + + (coeffs**2).sum(axis=0), + regularization_loss(coeffs_csc, l0=[1] * num_solutions, l1=1, l2=1), + ) + np.testing.assert_equal( + np.arange(num_solutions) * ((coeffs != 0).sum(axis=0)) + + abs(coeffs).sum(axis=0) + + (coeffs**2).sum(axis=0), + regularization_loss(coeffs_csc, l0=range(num_solutions), l1=1, l2=1), + ) + + +def test_regularization_loss_bad_inputs(): + N = 10 + a = rand(N, N, 0.2, format="csc") + l0 = np.ones(N) + l1 = np.ones((1, N)) + l2 = 5 + with pytest.raises(ValueError): + regularization_loss(a, l0, l1, l2) + + l0 = np.ones(N + 1) + l1 = np.ones(N + 1) + l2 = 5 + with pytest.raises(ValueError): + regularization_loss(a, l0, l1, l2) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + ( + np.arange(10), + np.arange(10), + 0, + ), + ( + np.arange(10).reshape(10, 1), + np.arange(10), + np.array([142.5, 102.5, 72.5, 52.5, 42.5, 42.5, 52.5, 72.5, 102.5, 142.5]), + ), + ], +) +def test_squared_error_testing(y_y_hat_error): + y, y_hat, error = y_y_hat_error + np.testing.assert_equal(squared_error(y, y_hat), error) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + (np.arange(10), np.arange(10), 0), + ( + np.arange(10).reshape(10, 1), + np.arange(10), + np.array([142.5, 102.5, 72.5, 52.5, 42.5, 42.5, 52.5, 72.5, 102.5, 142.5]), + ), + ], +) +@given( + coeffs=npst.arrays( + dtype=np.float64, + elements=floats( + allow_nan=False, allow_infinity=False, max_value=100, min_value=-100 + ), + shape=(10, 10), + ) +) +def test_squared_error_testing_reg(y_y_hat_error, coeffs): + coeffs_csc = csc_matrix(coeffs) + y, y_hat, error = y_y_hat_error + np.testing.assert_array_almost_equal( + squared_error(y, y_hat, coeffs_csc, l0=0, l1=0, l2=0), error + ) + + reg_error = ( + (coeffs != 0).sum(axis=0) + + abs(coeffs).sum(axis=0) + + np.square(coeffs).sum(axis=0) + ) + np.testing.assert_array_almost_equal( + squared_error(y, y_hat, coeffs_csc, l0=np.ones(10), l1=1, l2=1), + error + reg_error, + ) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + ( + np.ones(10), + np.ones(10), + 0, + ), + (np.ones(10).reshape(10, 1), np.ones(10), np.zeros(10)), + (np.ones(10), 0.5 * np.ones(10), np.log(2) * 10), + ], +) +def test_logistic_loss(y_y_hat_error): + y, y_hat, error = y_y_hat_error + np.testing.assert_almost_equal(logistic_loss(y, y_hat), error) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + ( + np.ones(10), + np.ones(10), + 0, + ), + (np.ones(10).reshape(10, 1), np.ones(10), np.zeros(10)), + (np.ones(10), 0.5 * np.ones(10), np.log(2) * 10), + ], +) +@given( + coeffs=npst.arrays( + dtype=np.float64, + elements=floats( + allow_nan=False, allow_infinity=False, max_value=100, min_value=-100 + ), + shape=(10, 10), + ) +) +def test_logistic_loss_testing_reg(y_y_hat_error, coeffs): + coeffs_csc = csc_matrix(coeffs) + y, y_hat, error = y_y_hat_error + np.testing.assert_almost_equal( + logistic_loss(y, y_hat, coeffs_csc, l0=0, l1=0, l2=0), error + ) + + reg_error = ( + (coeffs != 0).sum(axis=0) + + abs(coeffs).sum(axis=0) + + np.square(coeffs).sum(axis=0) + ) + np.testing.assert_array_almost_equal( + logistic_loss(y, y_hat, coeffs_csc, l0=np.ones(10), l1=1, l2=1), + error + reg_error, + ) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + ( + np.ones(10), + np.ones(10), + 0, + ), + (np.ones(10).reshape(10, 1), np.ones(10), np.zeros(10)), + (np.ones(10), 0.5 * np.ones(10), 0.25), + (np.ones(10), np.zeros(10), 1), + ], +) +def test_squared_hinge_loss(y_y_hat_error): + y, y_hat, error = y_y_hat_error + np.testing.assert_almost_equal(squared_hinge_loss(y, y_hat), error) + + +@pytest.mark.parametrize( + "y_y_hat_error", + [ + ( + np.ones(10), + np.ones(10), + 0, + ), + (np.ones(10).reshape(10, 1), np.ones(10), np.zeros(10)), + (np.ones(10), 0.5 * np.ones(10), 0.25), + (np.ones(10), np.zeros(10), 1), + ], +) +@given( + coeffs=npst.arrays( + dtype=np.float64, + elements=floats( + allow_nan=False, allow_infinity=False, max_value=100, min_value=-100 + ), + shape=(10, 10), + ) +) +def test_squared_hinge_loss_reg(y_y_hat_error, coeffs): + coeffs_csc = csc_matrix(coeffs) + y, y_hat, error = y_y_hat_error + np.testing.assert_almost_equal( + squared_hinge_loss(y, y_hat, coeffs_csc, l0=0, l1=0, l2=0), error + ) + + reg_error = ( + (coeffs != 0).sum(axis=0) + + abs(coeffs).sum(axis=0) + + np.square(coeffs).sum(axis=0) + ) + np.testing.assert_array_almost_equal( + squared_hinge_loss(y, y_hat, coeffs_csc, l0=np.ones(10), l1=1, l2=1), + error + reg_error, + ) + + +@pytest.mark.parametrize("training", [True, False]) +@pytest.mark.parametrize("loss", ["SquaredHinge", "Logistic", "SquaredError"]) +def test_score(training, loss): + fit_model = _sample_FitModel(loss=loss) + + x_training = np.identity(2) + y_training = np.array([1, 1]) + + scored = fit_model.score( + x_training, y_training, lambda_0=10, gamma=2, training=training + ) + + if training: + exra_args = { + "coeffs": fit_model.coeff(lambda_0=10, gamma=2, include_intercept=False), + "l0": fit_model.characteristics(lambda_0=10, gamma=2)["l0"], + "l1": fit_model.characteristics(lambda_0=10, gamma=2)["l1"], + } + else: + exra_args = {} + + print(exra_args) + + if loss == "Logistic": + expected_value = logistic_loss( + y_true=y_training, + y_pred=fit_model.predict(x_training, lambda_0=10, gamma=2), + **exra_args + ) + elif loss == "SquaredHinge": + expected_value = squared_hinge_loss( + y_true=y_training, + y_pred=fit_model.predict(x_training, lambda_0=10, gamma=2), + **exra_args + ) + + else: + expected_value = squared_error( + y_true=y_training, + y_pred=fit_model.predict(x_training, lambda_0=10, gamma=2), + **exra_args + ) + + np.testing.assert_array_almost_equal(scored, expected_value) + + +@pytest.mark.parametrize("training", [True, False]) +@pytest.mark.parametrize("loss", ["SquaredHinge", "Logistic", "SquaredError"]) +def test_score_characteristics(training, loss, sample_FitModel): + pass + + +def test_predict(sample_FitModel): + ones_1D = np.ones([1, 2]) + ones_2D = np.ones([2, 2]) + + np.testing.assert_array_equal(sample_FitModel.predict(ones_1D), np.zeros([1, 6])) + np.testing.assert_array_equal(sample_FitModel.predict(ones_2D), np.zeros([2, 6])) + + np.testing.assert_array_equal( + sample_FitModel.predict(ones_1D, gamma=1), np.zeros([1, 1]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(ones_2D, gamma=1), np.zeros([2, 1]) + ) + + np.testing.assert_array_equal( + sample_FitModel.predict(ones_1D, lambda_0=1), np.zeros([1, 1]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(ones_2D, lambda_0=1), np.zeros([2, 1]) + ) + + np.testing.assert_array_equal( + sample_FitModel.predict(ones_1D, lambda_0=1, gamma=1), np.zeros([1, 1]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(ones_2D, lambda_0=1, gamma=1), np.zeros([2, 1]) + ) + + x_1D = np.array([[1, 2]]) + x_2D = np.array([[1, 2], [1, 2]]) + + np.testing.assert_array_equal( + sample_FitModel.predict(x_1D), np.arange(1, 7)[np.newaxis, :] + ) + np.testing.assert_array_equal( + sample_FitModel.predict(x_2D), np.tile(np.arange(1, 7), (2, 1)) + ) + + np.testing.assert_array_equal( + sample_FitModel.predict(x_1D, gamma=1), np.array([[3]]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(x_2D, gamma=1), np.array([[3], [3]]) + ) + + np.testing.assert_array_equal( + sample_FitModel.predict(x_1D, lambda_0=1), np.array([[2]]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(x_2D, lambda_0=1), np.array([[2], [2]]) + ) + + np.testing.assert_array_equal( + sample_FitModel.predict(x_1D, lambda_0=1, gamma=1), np.array([[3]]) + ) + np.testing.assert_array_equal( + sample_FitModel.predict(x_2D, lambda_0=1, gamma=1), np.array([[3], [3]]) + ) diff --git a/scripts/dirs_check.py b/scripts/dirs_check.py new file mode 100644 index 0000000..60608a4 --- /dev/null +++ b/scripts/dirs_check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import argparse +import filecmp +import pathlib +import sys +from typing import Sequence, Dict, List + + +class onlydiffdircmp(filecmp.dircmp): + def report_dict(self) -> Dict[str, List[str]]: + print("report_dict", self.left, self.right) + + results = {} + results["left"] = self.left + results["right"] = self.right + results["left_only"] = self.left_only.sort() + results["right_only"] = self.right_only + results["diff_files"] = self.diff_files + results["funny_files"] = self.funny_files + return results + + +def dirs_check(dirs: Sequence[pathlib.Path], ignore: Sequence[pathlib.Path]): + first_dir, *other_dirs = dirs + + bad_diffs = [] + + for other_dir in other_dirs: + d = onlydiffdircmp(first_dir, other_dir, ignore=ignore) + diff_results = d.report_dict() + + if any( + [ + diff_results["left_only"], + diff_results["right_only"], + diff_results["diff_files"], + diff_results["funny_files"], + ] + ): + d.report() + bad_diffs.append(other_dir) + + if bad_diffs: + print(f"dirs {bad_diffs} are mis-matched from '{first_dir}'") + sys.exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Check directories to ensure equivalence" + ) + parser.add_argument( + "dirs", + metavar="paths/to/dirs", + type=str, + nargs="*", + help="paths to check for equivalence between dirs", + ) + parser.add_argument( + "--ignore", + metavar="paths/to/ignore", + nargs="*", + help="paths exclude from equivalence between dirs", + ) + + args = parser.parse_args() + + dirs_check(dirs=args.dirs, ignore=args.ignore) + sys.exit() diff --git a/src/CDL012LogisticSwaps.cpp b/src/CDL012LogisticSwaps.cpp deleted file mode 100644 index 5f1c755..0000000 --- a/src/CDL012LogisticSwaps.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "CDL012LogisticSwaps.h" - -template -CDL012LogisticSwaps::CDL012LogisticSwaps(const T& Xi, const arma::vec& yi, const Params& Pi) : CDSwaps(Xi, yi, Pi) { - twolambda2 = 2 * this->lambda2; - qp2lamda2 = (LipschitzConst + twolambda2); // this is the univariate lipschitz const of the differentiable objective - this->thr2 = (2 * this->lambda0) / qp2lamda2; - this->thr = std::sqrt(this->thr2); - stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); - lambda1ol = this->lambda1 / qp2lamda2; - Xy = Pi.Xy; -} - -template -FitResult CDL012LogisticSwaps::_FitWithBounds() { - throw "This Error should not happen. Please report it as an issue to https://github.com/hazimehh/L0Learn "; -} - -template -FitResult CDL012LogisticSwaps::_Fit() { - auto result = CDL012Logistic(*(this->X), this->y, this->P).Fit(); // result will be maintained till the end - this->b0 = result.b0; // Initialize from previous later....! - this->B = result.B; - ExpyXB = result.ExpyXB; // Maintained throughout the algorithm - - double objective = result.Objective; - double Fmin = objective; - std::size_t maxindex; - double Bmaxindex; - - this->P.Init = 'u'; - - bool foundbetter = false; - bool foundbetter_i = false; - - for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { - - std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); - - // TODO: Add shuffle of Order - //std::shuffle(std::begin(Order), std::end(Order), engine); - - foundbetter = false; - - // TODO: Check if this should be Templated Operation - arma::mat ExpyXBnojs = arma::zeros(this->n, NnzIndices.size()); - - int j_index = -1; - for (auto& j : NnzIndices) - { - // Remove NnzIndices[j] - ++j_index; - ExpyXBnojs.col(j_index) = ExpyXB % arma::exp( - this->B.at(j) * matrix_column_get(*(this->Xy), j)); - - } - arma::mat gradients = - 1/(1 + ExpyXBnojs).t() * *Xy; - arma::mat abs_gradients = arma::abs(gradients); - - - j_index = -1; - for (auto& j : NnzIndices) { - // Set B[j] = 0 - ++j_index; - arma::vec ExpyXBnoj = ExpyXBnojs.col(j_index); - arma::rowvec gradient = gradients.row(j_index); - arma::rowvec abs_gradient = abs_gradients.row(j_index); - - arma::uvec indices = arma::sort_index(arma::abs(gradient), "descend"); - foundbetter_i = false; - - // TODO: make sure this scans at least 100 coordinates from outside supp (now it does not) - for(std::size_t ll = 0; ll < std::min(50, (int) this->p); ++ll) { - std::size_t i = indices(ll); - - if(this->B[i] == 0 && i >= this->NoSelectK) { - // Do not swap B[i] if i between 0 and NoSelectK; - - arma::vec ExpyXBnoji = ExpyXBnoj; - - double Biold = 0; - double partial_i = gradient[i]; - bool converged = false; - - beta_vector Btemp = this->B; - Btemp[j] = 0; - double ObjTemp = Objective(ExpyXBnoji, Btemp); - std::size_t innerindex = 0; - - double x = Biold - partial_i/qp2lamda2; - double z = std::abs(x) - lambda1ol; - double Binew = std::copysign(z, x); - // double Binew = clamp(std::copysign(z, x), this->Lows[i], this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) - - while(!converged && innerindex < 10 && ObjTemp >= Fmin) { // ObjTemp >= Fmin - ExpyXBnoji %= arma::exp( (Binew - Biold) * matrix_column_get(*Xy, i)); - //partial_i = - arma::sum( matrix_column_get(*Xy, i) / (1 + ExpyXBnoji) ) + twolambda2 * Binew; - partial_i = - arma::dot( matrix_column_get(*Xy, i), 1/(1 + ExpyXBnoji) ) + twolambda2 * Binew; - - if (std::abs((Binew - Biold)/Biold) < 0.0001) { - converged = true; - //std::cout<<"swaps converged!!!"<Lows[i], this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) - innerindex += 1; - } - - - if (ObjTemp >= Fmin) { - ExpyXBnoji %= arma::exp( (Binew - Biold) * matrix_column_get(*Xy, i)); - Btemp[i] = Binew; - ObjTemp = Objective(ExpyXBnoji, Btemp); - } else { - Binew = 0; - } - - if (ObjTemp < Fmin) { - Fmin = ObjTemp; - maxindex = i; - Bmaxindex = Binew; - foundbetter_i = true; - } - - // Can be made much faster (later) - Btemp[i] = Binew; - - } - - if (foundbetter_i) { - this->B[j] = 0; - this->B[maxindex] = Bmaxindex; - this->P.InitialSol = &(this->B); - - // TODO: Check if this line is necessary. P should already have b0. - this->P.b0 = this->b0; - - result = CDL012Logistic(*(this->X), this->y, this->P).Fit(); - - ExpyXB = result.ExpyXB; - this->B = result.B; - this->b0 = result.b0; - objective = result.Objective; - Fmin = objective; - foundbetter = true; - break; - } - } - - //auto end2 = std::chrono::high_resolution_clock::now(); - //std::cout<<"restricted: "<(end2-start2).count() << " ms " << std::endl; - - if (foundbetter){ - break; - } - - } - - if(!foundbetter) { - // Early exit to prevent looping - return result; - } - } - - //result.Model = this; - return result; -} - -template class CDL012LogisticSwaps; -template class CDL012LogisticSwaps; diff --git a/src/CDL012SquaredHingeSwaps.cpp b/src/CDL012SquaredHingeSwaps.cpp deleted file mode 100644 index c24ba82..0000000 --- a/src/CDL012SquaredHingeSwaps.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include "CDL012SquaredHingeSwaps.h" - -template -CDL012SquaredHingeSwaps::CDL012SquaredHingeSwaps(const T& Xi, const arma::vec& yi, const Params& Pi) : CDSwaps(Xi, yi, Pi) { - twolambda2 = 2 * this->lambda2; - qp2lamda2 = (LipschitzConst + twolambda2); // this is the univariate lipschitz constant of the differentiable objective - this->thr2 = (2 * this->lambda0) / qp2lamda2; - this->thr = std::sqrt(this->thr2); - stl0Lc = std::sqrt((2 * this->lambda0) * qp2lamda2); - lambda1ol = this->lambda1 / qp2lamda2; -} - -template -FitResult CDL012SquaredHingeSwaps::_FitWithBounds() { - throw "This Error should not happen. Please report it as an issue to https://github.com/hazimehh/L0Learn "; -} - -template -FitResult CDL012SquaredHingeSwaps::_Fit() { - auto result = CDL012SquaredHinge(*(this->X), this->y, this->P).Fit(); // result will be maintained till the end - this->b0 = result.b0; // Initialize from previous later....! - this->B = result.B; - - arma::vec onemyxb = result.onemyxb; - - this->objective = result.Objective; - double Fmin = this->objective; - std::size_t maxindex; - double Bmaxindex; - - this->P.Init = 'u'; - - bool foundbetter = false; - - for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { - // Rcpp::Rcout << "Swap Number: " << t << "|mean(onemyxb): " << arma::mean(onemyxb) << "\n"; - - std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); - - // TODO: Implement shuffle of NnzIndices Indicies - - foundbetter = false; - - for (auto& j : NnzIndices) { - - arma::vec onemyxbnoj = onemyxb + this->B[j] * this->y % matrix_column_get(*(this->X), j); - arma::uvec indices = arma::find(onemyxbnoj > 0); - - - for(std::size_t i = 0; i < this->p; ++i) { - if(this->B[i] == 0 && i>=this->NoSelectK) { - - double Biold = 0; - double Binew; - - - double partial_i = arma::sum(2 * onemyxbnoj.elem(indices) % (- (this->y.elem(indices) % matrix_column_get(*(this->X), i).elem(indices)))); - - bool converged = false; - if (std::abs(partial_i) >= this->lambda1 + stl0Lc){ - - //std::cout<<"Adding: "<B; - Btemp[j] = 0; - //double ObjTemp = Objective(onemyxbnoj,Btemp); - //double Biolddescent = 0; - while(!converged) { - - double x = Biold - partial_i / qp2lamda2; - double z = std::abs(x) - lambda1ol; - Binew = std::copysign(z, x); - - // Binew = clamp(std::copysign(z, x), this->Lows[i], this->Highs[i]); // no need to check if >= sqrt(2lambda_0/Lc) - onemyxbnoji += (Biold - Binew) * this->y % matrix_column_get(*(this->X), i); - - arma::uvec indicesi = arma::find(onemyxbnoji > 0); - partial_i = arma::sum(2 * onemyxbnoji.elem(indicesi) % (- this->y.elem(indicesi) % matrix_column_get(*(this->X), i).elem(indicesi))); - - if (std::abs((Binew - Biold) / Biold) < 0.0001){ - converged = true; - } - - Biold = Binew; - l += 1; - - } - - Btemp[i] = Binew; - double Fnew = Objective(onemyxbnoji, Btemp); - - if (Fnew < Fmin) { - Fmin = Fnew; - maxindex = i; - Bmaxindex = Binew; - } - } - } - } - - if (Fmin < this->objective) { - this->B[j] = 0; - this->B[maxindex] = Bmaxindex; - - this->P.InitialSol = &(this->B); - - // TODO: Check if this line is needed. P should already have b0. - this->P.b0 = this->b0; - - result = CDL012SquaredHinge(*(this->X), this->y, this->P).Fit(); - - this->B = result.B; - this->b0 = result.b0; - - onemyxb = result.onemyxb; - this->objective = result.Objective; - Fmin = this->objective; - foundbetter = true; - break; - } - if (foundbetter){break;} - } - - if(!foundbetter) { - return result; - } - } - - return result; -} - -template class CDL012SquaredHingeSwaps; -template class CDL012SquaredHingeSwaps; diff --git a/src/CDL012Swaps.cpp b/src/CDL012Swaps.cpp deleted file mode 100644 index 246ec4c..0000000 --- a/src/CDL012Swaps.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "CDL012Swaps.h" - -template -CDL012Swaps::CDL012Swaps(const T& Xi, const arma::vec& yi, const Params& Pi) : CDSwaps(Xi, yi, Pi) {} - - -template -FitResult CDL012Swaps::_FitWithBounds() { - throw "This Error should not happen. Please report it as an issue to https://github.com/hazimehh/L0Learn "; -} - -template -FitResult CDL012Swaps::_Fit() { - auto result = CDL012(*(this->X), this->y, this->P).Fit(); // result will be maintained till the end - this->B = result.B; - this->b0 = result.b0; - double objective = result.Objective; - this->P.Init = 'u'; - - bool foundbetter = false; - - for (std::size_t t = 0; t < this->MaxNumSwaps; ++t) { - - std::vector NnzIndices = nnzIndicies(this->B, this->NoSelectK); - - foundbetter = false; - - // TODO: shuffle NNz Indices to prevent bias. - //std::shuffle(std::begin(Order), std::end(Order), engine); - - // TODO: This calculation is already preformed in a previous step - // Can be pulled/stored - arma::vec r = this->y - *(this->X) * this->B - this->b0; - - for (auto& i : NnzIndices) { - arma::rowvec riX = (r + this->B[i] * matrix_column_get(*(this->X), i)).t() * *(this->X); - - double maxcorr = -1; - std::size_t maxindex = -1; - - for(std::size_t j = this->NoSelectK; j < this->p; ++j) { - // TODO: Account for bounds when determining best swap - // Loops through each column and finds the column with the highest correlation to residuals - // In non-constrained cases, the highest correlation will always be the best option - // However, if bounds restrict the value of B[j], it is possible that swapping column 'i' - // and column 'j' might be rejected as B[j], when constrained, is not able to take a value - // with sufficient magnitude to utilize the correlation. - // Therefore, we must ensure that 'j' was not already rejected. - if (std::fabs(riX[j]) > maxcorr && this->B[j] == 0) { - maxcorr = std::fabs(riX[j]); - maxindex = j; - } - } - - // Check if the correlation is sufficiently large to make up for regularization - if(maxcorr > (1 + 2 * this->ModelParams[2])*std::fabs(this->B[i]) + this->ModelParams[1]) { - // Rcpp::Rcout << t << ": Proposing Swap " << i << " => NNZ and " << maxindex << " => 0 \n"; - // Proposed new Swap - // Value (without considering bounds are solvable in closed form) - // Must be clamped to bounds - - this->B[i] = 0; - - // Bi with No Bounds (nb); - double Bi_nb = (riX[maxindex] - std::copysign(this->ModelParams[1],riX[maxindex])) / (1 + 2 * this->ModelParams[2]); - //double Bi_wb = clamp(Bi_nb, this->Lows[maxindex], this->Highs[maxindex]); // Bi With Bounds (wb) - this->B[maxindex] = Bi_nb; - - // Change initial solution to Swapped value to seed standard CD algorithm. - this->P.InitialSol = &(this->B); - *this->P.r = this->y - *(this->X) * (this->B) - this->b0; - // this->P already has access to b0. - - // proposed_result object. - // Keep tack of previous_best result object - // Only override previous_best if proposed_result has a better objective. - result = CDL012(*(this->X), this->y, this->P).Fit(); - - // Rcpp::Rcout << "Swap Objective " << result.Objective << " \n"; - // Rcpp::Rcout << "Old Objective " << objective << " \n"; - this->B = result.B; - objective = result.Objective; - foundbetter = true; - break; - } - } - - if(!foundbetter) { - // Early exit to prevent looping - return result; - } - } - - return result; -} - -template class CDL012Swaps; -template class CDL012Swaps; - \ No newline at end of file diff --git a/src/Grid.cpp b/src/Grid.cpp deleted file mode 100644 index 79d430b..0000000 --- a/src/Grid.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "Grid.h" - -// Assumes PG.P.Specs have been already set -template -Grid::Grid(const T& X, const arma::vec& y, const GridParams& PGi) { - PG = PGi; - - std::tie(BetaMultiplier, meanX, meany, scaley) = Normalize(X, - y, Xscaled, yscaled, !PG.P.Specs.Classification, PG.intercept); - - // Must rescale bounds by BetaMultiplier in order for final result to conform to bounds - if (PG.P.withBounds){ - PG.P.Lows /= BetaMultiplier; - PG.P.Highs /= BetaMultiplier; - } -} - -template -void Grid::Fit() { - - std::vector>>> G; - - if (PG.P.Specs.L0) { - G.push_back(std::move(Grid1D(Xscaled, yscaled, PG).Fit())); - Lambda12.push_back(0); - } else { - G = std::move(Grid2D(Xscaled, yscaled, PG).Fit()); - } - - Lambda0 = std::vector< std::vector >(G.size()); - NnzCount = std::vector< std::vector >(G.size()); - Solutions = std::vector< std::vector >(G.size()); - Intercepts = std::vector< std::vector >(G.size()); - Converged = std::vector< std::vector >(G.size()); - - for (std::size_t i=0; iModelParams[1]); - } else if (PG.P.Specs.L0L2) { - Lambda12.push_back(G[i][0]->ModelParams[2]); - } - - for (auto &g : G[i]) { - Lambda0[i].push_back(g->ModelParams[0]); - - NnzCount[i].push_back(n_nonzero(g->B)); - - if (g->IterNum != PG.P.MaxIters){ - Converged[i].push_back(true); - } else { - Converged[i].push_back(false); - } - - beta_vector B_unscaled; - double b0; - - std::tie(B_unscaled, b0) = DeNormalize(g->B, BetaMultiplier, meanX, meany); - Solutions[i].push_back(arma::sp_mat(B_unscaled)); - /* scaley is 1 for classification problems. - * g->intercept is 0 unless specifically optimized for in: - * classification - * sparse regression and intercept = true - */ - Intercepts[i].push_back(scaley*g->b0 + b0); - } - } -} - -template class Grid; -template class Grid; diff --git a/src/Grid1D.cpp b/src/Grid1D.cpp deleted file mode 100644 index 59555b4..0000000 --- a/src/Grid1D.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include "Grid1D.h" - -template -Grid1D::Grid1D(const T& Xi, const arma::vec& yi, const GridParams& PG) { - // automatically selects lambda_0 (but assumes other lambdas are given in PG.P.ModelParams) - - X = Ξ - y = &yi; - p = Xi.n_cols; - LambdaMinFactor = PG.LambdaMinFactor; - ScaleDownFactor = PG.ScaleDownFactor; - P = PG.P; - P.Xtr = new std::vector(X->n_cols); // needed! careful - P.ytX = new arma::rowvec(X->n_cols); - P.D = new std::map(); - P.r = new arma::vec(Xi.n_rows); - Xtr = P.Xtr; - ytX = P.ytX; - NoSelectK = P.NoSelectK; - - LambdaU = PG.LambdaU; - - if (!LambdaU) { - G_ncols = PG.G_ncols; - } else { - G_ncols = PG.Lambdas.n_rows; // override the user's ncols if LambdaU = 1 - } - - G.reserve(G_ncols); - if (LambdaU) { - Lambdas = PG.Lambdas; - } // user-defined lambda0 grid - /* - else { - Lambdas.reserve(G_ncols); - Lambdas.push_back((0.5*arma::square(y->t() * *X)).max()); - } - */ - NnzStopNum = PG.NnzStopNum; - PartialSort = PG.PartialSort; - XtrAvailable = PG.XtrAvailable; - if (XtrAvailable) { - ytXmax2d = PG.ytXmax; - Xtr = PG.Xtr; - } -} - -template -Grid1D::~Grid1D() { - // delete all dynamically allocated memory - delete P.Xtr; - delete P.ytX; - delete P.D; - delete P.r; -} - - -template -std::vector>> Grid1D::Fit() { - - if (P.Specs.L0 || P.Specs.L0L2 || P.Specs.L0L1) { - bool scaledown = false; - - double Lipconst; - arma::vec Xtrarma; - if (P.Specs.Logistic) { - if (!XtrAvailable) { - Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); - } // = gradient of logistic loss at zero} - Lipconst = 0.25 + 2 * P.ModelParams[2]; - } else if (P.Specs.SquaredHinge) { - if (!XtrAvailable) { - // gradient of loss function at zero - Xtrarma = 2 * arma::abs(y->t() * *X).t(); - } - Lipconst = 2 + 2 * P.ModelParams[2]; - } else { - if (!XtrAvailable) { - *ytX = y->t() * *X; - Xtrarma = arma::abs(*ytX).t(); // Least squares - } - Lipconst = 1 + 2 * P.ModelParams[2]; - *P.r = *y - P.b0; // B = 0 initially - } - - double ytXmax; - if (!XtrAvailable) { - *Xtr = arma::conv_to< std::vector >::from(Xtrarma); - ytXmax = arma::max(Xtrarma); - } else { - ytXmax = ytXmax2d; - } - - double lambdamax = ((ytXmax - P.ModelParams[1]) * (ytXmax - P.ModelParams[1])) / (2 * (Lipconst)); - - // Rcpp::Rcout << "lambdamax: " << lambdamax << "\n"; - - if (!LambdaU) { - P.ModelParams[0] = lambdamax; - } else { - P.ModelParams[0] = Lambdas[0]; - } - - // Rcpp::Rcout << "P ModelParams: {" << P.ModelParams[0] << ", " << P.ModelParams[1] << ", " << P.ModelParams[2] << ", " << P.ModelParams[3] << "}\n"; - - P.Init = 'z'; - - - //std::cout<< "Lambda max: "<< lambdamax << std::endl; - //double lambdamin = lambdamax*LambdaMinFactor; - //Lambdas = arma::logspace(std::log10(lambdamin), std::log10(lambdamax), G_ncols); - //Lambdas = arma::flipud(Lambdas); - - - //std::size_t StopNum = (X->n_rows < NnzStopNum) ? X->n_rows : NnzStopNum; - std::size_t StopNum = NnzStopNum; - //std::vector* Xtr = P.Xtr; - std::vector idx(p); - double Xrmax; - bool prevskip = false; //previous grid point was skipped - bool currentskip = false; // current grid point should be skipped - - for (std::size_t i = 0; i < G_ncols; ++i) { - Rcpp::checkUserInterrupt(); - // Rcpp::Rcout << "Grid1D: " << i << "\n"; - FitResult * prevresult = new FitResult; // prevresult is ptr to the prev result object - //std::unique_ptr prevresult; - if (i > 0) { - //prevresult = std::move(G.back()); - *prevresult = *(G.back()); - - } - - currentskip = false; - - if (!prevskip) { - - std::iota(idx.begin(), idx.end(), 0); // make global class var later - // Exclude the first NoSelectK features from sorting. - if (PartialSort && p > 5000 + NoSelectK) - std::partial_sort(idx.begin() + NoSelectK, idx.begin() + 5000 + NoSelectK, idx.end(), [this](std::size_t i1, std::size_t i2) {return (*Xtr)[i1] > (*Xtr)[i2] ;}); - else - std::sort(idx.begin() + NoSelectK, idx.end(), [this](std::size_t i1, std::size_t i2) {return (*Xtr)[i1] > (*Xtr)[i2] ;}); - P.CyclingOrder = 'u'; - P.Uorder = idx; // can be made faster - - // - Xrmax = (*Xtr)[idx[NoSelectK]]; - - if (i > 0) { - std::vector Sp = nnzIndicies(prevresult->B); - - for(std::size_t l = NoSelectK; l < p; ++l) { - if ( std::binary_search(Sp.begin(), Sp.end(), idx[l]) == false ) { - Xrmax = (*Xtr)[idx[l]]; - //std::cout<<"Grid Iteration: "<> result(new FitResult); - *result = Model->Fit(); - - delete Model; - - scaledown = false; - if (i >= 1) { - std::vector Spold = nnzIndicies(prevresult->B); - - std::vector Spnew = nnzIndicies(result->B); - - bool samesupp = false; - - if (Spold == Spnew) { - samesupp = true; - scaledown = true; - } - - // // - // - // if (samesupp) { - // scaledown = true; - // } // got same solution - } - - //else {scaledown = false;} - G.push_back(std::move(result)); - - - if(n_nonzero(G.back()->B) >= StopNum) { - break; - } - //result->B.t().print(); - P.InitialSol = &(G.back()->B); - P.b0 = G.back()->b0; - // Udate: After 1.1.0, P.r is automatically updated by the previous call to CD - //*P.r = G.back()->r; - - } - - delete prevresult; - - - P.Init = 'u'; - P.Iter += 1; - prevskip = currentskip; - } - } - - return std::move(G); -} - - -template class Grid1D; -template class Grid1D; diff --git a/src/Grid2D.cpp b/src/Grid2D.cpp deleted file mode 100644 index ec2c55e..0000000 --- a/src/Grid2D.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include "Grid2D.h" - -template -Grid2D::Grid2D(const T& Xi, const arma::vec& yi, const GridParams& PGi) -{ - // automatically selects lambda_0 (but assumes other lambdas are given in PG.P.ModelParams) - X = Ξ - y = &yi; - p = Xi.n_cols; - PG = PGi; - G_nrows = PG.G_nrows; - G_ncols = PG.G_ncols; - G.reserve(G_nrows); - Lambda2Max = PG.Lambda2Max; - Lambda2Min = PG.Lambda2Min; - LambdaMinFactor = PG.LambdaMinFactor; - - P = PG.P; -} - -template -Grid2D::~Grid2D(){ - delete Xtr; - if (PG.P.Specs.Logistic) - delete PG.P.Xy; - if (PG.P.Specs.SquaredHinge) - delete PG.P.Xy; -} - -template -std::vector< std::vector> > > Grid2D::Fit() { - arma::vec Xtrarma; - - if (PG.P.Specs.Logistic) { - auto n = X->n_rows; - double b0 = 0; - arma::vec ExpyXB = arma::ones(n); - if (PG.intercept) { - for (std::size_t t = 0; t < 50; ++t) { - double partial_b0 = - arma::sum( *y / (1 + ExpyXB) ); - b0 -= partial_b0 / (n * 0.25); // intercept is not regularized - ExpyXB = arma::exp(b0 * *y); - } - } - PG.P.b0 = b0; - Xtrarma = arma::abs(- arma::trans(*y /(1+ExpyXB)) * *X).t(); // = gradient of logistic loss at zero - //Xtrarma = 0.5 * arma::abs(y->t() * *X).t(); // = gradient of logistic loss at zero - - T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; - - PG.P.Xy = new T; - *PG.P.Xy = Xy; - } - - else if (PG.P.Specs.SquaredHinge) { - auto n = X->n_rows; - double b0 = 0; - arma::vec onemyxb = arma::ones(n); - arma::uvec indices = arma::find(onemyxb > 0); - if (PG.intercept){ - for (std::size_t t = 0; t < 50; ++t){ - double partial_b0 = arma::sum(2 * onemyxb.elem(indices) % (- y->elem(indices) ) ); - b0 -= partial_b0 / (n * 2); // intercept is not regularized - onemyxb = 1 - (*y * b0); - indices = arma::find(onemyxb > 0); - } - } - PG.P.b0 = b0; - T indices_rows = matrix_rows_get(*X, indices); - Xtrarma = 2 * arma::abs(arma::trans(y->elem(indices) % onemyxb.elem(indices))* indices_rows).t(); // = gradient of loss function at zero - //Xtrarma = 2 * arma::abs(y->t() * *X).t(); // = gradient of loss function at zero - T Xy = matrix_vector_schur_product(*X, y); // X->each_col() % *y; - PG.P.Xy = new T; - *PG.P.Xy = Xy; - } else { - Xtrarma = arma::abs(y->t() * *X).t(); - } - - - double ytXmax = arma::max(Xtrarma); - - std::size_t index; - if (PG.P.Specs.L0L1) { - index = 1; - if(G_nrows != 1) { - Lambda2Max = ytXmax; - Lambda2Min = Lambda2Max * LambdaMinFactor; - } - } else if (PG.P.Specs.L0L2) { - index = 2; - } - - arma::vec Lambdas2 = arma::logspace(std::log10(Lambda2Min), std::log10(Lambda2Max), G_nrows); - Lambdas2 = arma::flipud(Lambdas2); - - std::vector Xtrvec = arma::conv_to< std::vector >::from(Xtrarma); - - Xtr = new std::vector(X->n_cols); // needed! careful - - - PG.XtrAvailable = true; - // Rcpp::Rcout << "Grid2D Start\n"; - for(std::size_t i=0; i> Gl(); - //auto Gl = Grid1D(*X, *y, PG).Fit(); - // Rcpp::Rcout << "Grid1D Start: " << i << "\n"; - G.push_back(std::move(Grid1D(*X, *y, PG).Fit())); - } - - return std::move(G); - -} - -template class Grid2D; -template class Grid2D; diff --git a/src/Interface.cpp b/src/Interface.cpp deleted file mode 100644 index 07edc03..0000000 --- a/src/Interface.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "Interface.h" -#include "GridParams.h" -#include "RcppArmadillo.h" -#include "Grid.h" -#include "utils.h" -// [[Rcpp::depends(RcppArmadillo)]] - -template -GridParams makeGridParams(const std::string Loss, const std::string Penalty, - const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, - const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, - const bool PartialSort, const std::size_t MaxIters, const double rtol, - const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, - const std::size_t MaxNumSwaps, const double ScaleDownFactor, - const std::size_t ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, - const std::size_t ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, const arma::vec &Highs){ - GridParams PG; - PG.NnzStopNum = NnzStopNum; - PG.G_ncols = G_ncols; - PG.G_nrows = G_nrows; - PG.Lambda2Max = Lambda2Max; - PG.Lambda2Min = Lambda2Min; - PG.LambdaMinFactor = Lambda2Min; // - PG.PartialSort = PartialSort; - PG.ScaleDownFactor = ScaleDownFactor; - PG.LambdaU = LambdaU; - PG.LambdasGrid = Lambdas; - PG.Lambdas = Lambdas[0]; // to handle the case of L0 (i.e., Grid1D) - PG.intercept = Intercept; - - Params P; - PG.P = P; - PG.P.MaxIters = MaxIters; - PG.P.rtol = rtol; - PG.P.atol = atol; - PG.P.ActiveSet = ActiveSet; - PG.P.ActiveSetNum = ActiveSetNum; - PG.P.MaxNumSwaps = MaxNumSwaps; - PG.P.ScreenSize = ScreenSize; - PG.P.NoSelectK = ExcludeFirstK; - PG.P.intercept = Intercept; - PG.P.withBounds = withBounds; - PG.P.Lows = Lows; - PG.P.Highs = Highs; - - if (Loss == "SquaredError") { - PG.P.Specs.SquaredError = true; - } else if (Loss == "Logistic") { - PG.P.Specs.Logistic = true; - PG.P.Specs.Classification = true; - } else if (Loss == "SquaredHinge") { - PG.P.Specs.SquaredHinge = true; - PG.P.Specs.Classification = true; - } - - if (Algorithm == "CD") { - PG.P.Specs.CD = true; - } else if (Algorithm == "CDPSI") { - PG.P.Specs.PSI = true; - } - - if (Penalty == "L0") { - PG.P.Specs.L0 = true; - } else if (Penalty == "L0L2") { - PG.P.Specs.L0L2 = true; - } else if (Penalty == "L0L1") { - PG.P.Specs.L0L1 = true; - } - return PG; -} - - -template -Rcpp::List _L0LearnFit(const T& X, const arma::vec& y, const std::string Loss, const std::string Penalty, - const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, - const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, - const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, - const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, - const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, const std::size_t ExcludeFirstK, - const bool Intercept, const bool withBounds, const arma::vec &Lows, - const arma::vec &Highs){ - - GridParams PG = makeGridParams(Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, - Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, ActiveSet, - ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, - LambdaU, Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs); - - Grid G(X, y, PG); - G.Fit(); - - std::string FirstParameter = "lambda"; - std::string SecondParameter = "gamma"; - - // Next Construct the list of Sparse Beta Matrices. - - auto p = X.n_cols; - arma::field Bs(G.Lambda12.size()); - - for (std::size_t i=0; i -Rcpp::List _L0LearnCV(const T& X, const arma::vec& y, const std::string Loss, - const std::string Penalty, const std::string Algorithm, - const unsigned int NnzStopNum, const unsigned int G_ncols, - const unsigned int G_nrows, const double Lambda2Max, - const double Lambda2Min, const bool PartialSort, - const unsigned int MaxIters, const double rtol, - const double atol, const bool ActiveSet, - const unsigned int ActiveSetNum, - const unsigned int MaxNumSwaps, const double ScaleDownFactor, - const unsigned int ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, - const unsigned int nfolds, const double seed, - const unsigned int ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, - const arma::vec &Highs){ - - GridParams PG = makeGridParams(Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, - Lambda2Max, Lambda2Min, PartialSort, MaxIters, rtol, atol, - ActiveSet,ActiveSetNum, MaxNumSwaps, ScaleDownFactor, - ScreenSize,LambdaU, Lambdas, ExcludeFirstK, Intercept, - withBounds, Lows, Highs); - - Grid G(X, y, PG); - G.Fit(); - - std::string FirstParameter = "lambda"; - std::string SecondParameter = "gamma"; - - // Next Construct the list of Sparse Beta Matrices. - - auto p = X.n_cols; - auto n = X.n_rows; - arma::field Bs(G.Lambda12.size()); - - for (std::size_t i=0; i >(G.size()); - //Intercepts = std::vector< std::vector >(G.size()); - - std::size_t Ngamma = G.Lambda12.size(); - - //std::vector< arma::mat > CVError (G.Solutions.size()); - arma::field< arma::mat > CVError (G.Solutions.size()); - - for (std::size_t i=0; i(0, X.n_rows-1, X.n_rows); - - arma::uvec indices = arma::shuffle(a); - - int samplesperfold = std::ceil(n/double(nfolds)); - int samplesinlastfold = samplesperfold - (samplesperfold*nfolds - n); - - std::vector fullindices(X.n_rows); - std::iota(fullindices.begin(), fullindices.end(), 0); - - - for (std::size_t j=0; j validationindices; - if (j < nfolds-1) - validationindices.resize(samplesperfold); - else - validationindices.resize(samplesinlastfold); - - std::iota(validationindices.begin(), validationindices.end(), samplesperfold*j); - - std::vector trainingindices; - - std::set_difference(fullindices.begin(), fullindices.end(), validationindices.begin(), validationindices.end(), - std::inserter(trainingindices, trainingindices.begin())); - - - // validationindicesarma contains the randomly permuted validation indices as a uvec - arma::uvec validationindicesarma; - arma::uvec validationindicestemp = arma::conv_to< arma::uvec >::from(validationindices); - validationindicesarma = indices.elem(validationindicestemp); - - // trainingindicesarma is similar to validationindicesarma but for training - arma::uvec trainingindicesarma; - - arma::uvec trainingindicestemp = arma::conv_to< arma::uvec >::from(trainingindices); - - - trainingindicesarma = indices.elem(trainingindicestemp); - - - T Xtraining = matrix_rows_get(X, trainingindicesarma); - - arma::mat ytraining = y.elem(trainingindicesarma); - - T Xvalidation = matrix_rows_get(X, validationindicesarma); - - arma::mat yvalidation = y.elem(validationindicesarma); - - PG.LambdaU = true; - PG.XtrAvailable = false; // reset XtrAvailable since its changed upon every call - PG.LambdasGrid = G.Lambda0; - PG.NnzStopNum = p+1; // remove any constraints on the supp size when fitting over the cv folds // +1 is imp to avoid =p edge case - if (PG.P.Specs.L0 == true){ - PG.Lambdas = PG.LambdasGrid[0]; - } - Grid Gtraining(Xtraining, ytraining, PG); - Gtraining.Fit(); - - for (std::size_t i=0; i 0); - CVError[i](k,j) = arma::sum(onemyxb.elem(indices) % onemyxb.elem(indices)) / yvalidation.n_rows; - } - } - } - } - - arma::field CVMeans(Ngamma); - arma::field CVSDs(Ngamma); - - for (std::size_t i=0; i > Lambdas, - const std::size_t ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { - - return _L0LearnFit(X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, Lambda2Min, - PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, - Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs); -} - - -// [[Rcpp::export]] -Rcpp::List L0LearnFit_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, - const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, - const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, - const bool PartialSort, const std::size_t MaxIters, const double rtol, - const double atol, const bool ActiveSet, const std::size_t ActiveSetNum, - const std::size_t MaxNumSwaps, const double ScaleDownFactor, - const std::size_t ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, - const std::size_t ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, const arma::vec &Highs) { - - return _L0LearnFit(X, y, Loss, Penalty, Algorithm, NnzStopNum, G_ncols, G_nrows, Lambda2Max, Lambda2Min, - PartialSort, MaxIters, rtol, atol, ActiveSet, ActiveSetNum, MaxNumSwaps, ScaleDownFactor, ScreenSize, LambdaU, - Lambdas, ExcludeFirstK, Intercept, withBounds, Lows, Highs); -} - - -// [[Rcpp::export]] -Rcpp::List L0LearnCV_sparse(const arma::sp_mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, - const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, - const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, - const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, - const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, - const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, const std::size_t nfolds, - const double seed, const std::size_t ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, const arma::vec &Highs){ - - return _L0LearnCV(X, y, Loss, Penalty, - Algorithm, NnzStopNum, G_ncols, G_nrows, - Lambda2Max, Lambda2Min, PartialSort, - MaxIters, rtol,atol, ActiveSet, - ActiveSetNum, MaxNumSwaps, - ScaleDownFactor, ScreenSize, LambdaU, Lambdas, - nfolds, seed, ExcludeFirstK, Intercept, withBounds, Lows, Highs); -} - -// [[Rcpp::export]] -Rcpp::List L0LearnCV_dense(const arma::mat& X, const arma::vec& y, const std::string Loss, const std::string Penalty, - const std::string Algorithm, const std::size_t NnzStopNum, const std::size_t G_ncols, - const std::size_t G_nrows, const double Lambda2Max, const double Lambda2Min, - const bool PartialSort, const std::size_t MaxIters, const double rtol, const double atol, - const bool ActiveSet, const std::size_t ActiveSetNum, const std::size_t MaxNumSwaps, - const double ScaleDownFactor, const std::size_t ScreenSize, const bool LambdaU, - const std::vector< std::vector > Lambdas, const std::size_t nfolds, - const double seed, const std::size_t ExcludeFirstK, const bool Intercept, - const bool withBounds, const arma::vec &Lows, const arma::vec &Highs){ - - return _L0LearnCV(X, y, Loss, Penalty, - Algorithm, NnzStopNum, G_ncols, G_nrows, - Lambda2Max, Lambda2Min, PartialSort, - MaxIters, rtol,atol, ActiveSet, - ActiveSetNum, MaxNumSwaps, - ScaleDownFactor, ScreenSize, LambdaU, Lambdas, - nfolds, seed, ExcludeFirstK, Intercept, withBounds, Lows, Highs); -} - -// [[Rcpp::export]] -Rcpp::NumericMatrix cor_matrix(const int p, const double base_cor) { - Rcpp::NumericMatrix cor(p, p); - for (int i = 0; i < p; i++){ - for (int j = 0; j < p; j++){ - cor(i, j) = std::pow(base_cor, std::abs(i - j)); - } - } - return cor; -} - diff --git a/src/Makevars b/src/Makevars deleted file mode 100644 index 990fa39..0000000 --- a/src/Makevars +++ /dev/null @@ -1,3 +0,0 @@ -CXX_STD = CXX11 -PKG_CXXFLAGS = "-Iinclude" -PKG_LIBS= $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) diff --git a/src/Makevars.in b/src/Makevars.in deleted file mode 100644 index 32eb0f5..0000000 --- a/src/Makevars.in +++ /dev/null @@ -1,3 +0,0 @@ -CXX_STD = CXX11 -PKG_CXXFLAGS = "-Iinclude" @OPENMP_FLAG@ -PKG_LIBS= @OPENMP_FLAG@ $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) diff --git a/src/Makevars.win b/src/Makevars.win deleted file mode 100644 index 4931af6..0000000 --- a/src/Makevars.win +++ /dev/null @@ -1,3 +0,0 @@ -CXX_STD = CXX11 -PKG_CXXFLAGS = "-Iinclude" $(SHLIB_OPENMP_CXXFLAGS) -PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS) $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) diff --git a/src/Normalize.cpp b/src/Normalize.cpp deleted file mode 100644 index ac9a055..0000000 --- a/src/Normalize.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "Normalize.h" - -std::tuple DeNormalize(beta_vector & B_scaled, - arma::vec & BetaMultiplier, - arma::vec & meanX, double meany) { - beta_vector B_unscaled = B_scaled % BetaMultiplier; - double intercept = meany - arma::dot(B_unscaled, meanX); - // Matrix Type, Intercept - // Dense, True -> meanX = colMeans(X) - // Dense, False -> meanX = 0 Vector (meany = 0) - // Sparse, True -> meanX = 0 Vector - // Sparse, False -> meanX = 0 Vector - return std::make_tuple(B_unscaled, intercept); -} diff --git a/src/Test_Interface.cpp b/src/Test_Interface.cpp deleted file mode 100644 index a6e95da..0000000 --- a/src/Test_Interface.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "Test_Interface.h" -// [[Rcpp::depends(RcppArmadillo)]] - - -// [[Rcpp::export]] -arma::vec R_matrix_column_get_dense(const arma::mat &mat, int col) { - return matrix_column_get(mat, col); -} - -// [[Rcpp::export]] -arma::vec R_matrix_column_get_sparse(const arma::sp_mat &mat, int col) { - return matrix_column_get(mat, col); -} - -// [[Rcpp::export]] -arma::mat R_matrix_rows_get_dense(const arma::mat &mat, const arma::ucolvec rows){ - return matrix_rows_get(mat, rows); -} - -// [[Rcpp::export]] -arma::sp_mat R_matrix_rows_get_sparse(const arma::sp_mat &mat, const arma::ucolvec rows){ - return matrix_rows_get(mat, rows); -} - -// [[Rcpp::export]] -arma::mat R_matrix_vector_schur_product_dense(const arma::mat &mat, const arma::vec &u){ - return matrix_vector_schur_product(mat, &u); -} - -// [[Rcpp::export]] -arma::sp_mat R_matrix_vector_schur_product_sparse(const arma::sp_mat &mat, const arma::vec &u){ - return matrix_vector_schur_product(mat, &u); -} - - -// [[Rcpp::export]] -arma::mat R_matrix_vector_divide_dense(const arma::mat &mat, const arma::vec &u){ - return matrix_vector_divide(mat, u); -} - -// [[Rcpp::export]] -arma::sp_mat R_matrix_vector_divide_sparse(const arma::sp_mat &mat, const arma::vec &u){ - return matrix_vector_divide(mat, u); -} - -// [[Rcpp::export]] -arma::rowvec R_matrix_column_sums_dense(const arma::mat &mat){ - return matrix_column_sums(mat); -} - -// [[Rcpp::export]] -arma::rowvec R_matrix_column_sums_sparse(const arma::sp_mat &mat){ - return matrix_column_sums(mat); -} - - -// [[Rcpp::export]] -double R_matrix_column_dot_dense(const arma::mat &mat, int col, const arma::vec u){ - return matrix_column_dot(mat, col, u); -} - -// [[Rcpp::export]] -double R_matrix_column_dot_sparse(const arma::sp_mat &mat, int col, const arma::vec u){ - return matrix_column_dot(mat, col, u); -} - -// [[Rcpp::export]] -arma::vec R_matrix_column_mult_dense(const arma::mat &mat, int col, double u){ - return matrix_column_mult(mat, col, u); -} - -// [[Rcpp::export]] -arma::vec R_matrix_column_mult_sparse(const arma::sp_mat &mat, int col, double u){ - return matrix_column_mult(mat, col, u); -} - -// [[Rcpp::export]] -Rcpp::List R_matrix_normalize_dense(arma::mat mat_norm){ - arma::rowvec ScaleX = matrix_normalize(mat_norm); - return Rcpp::List::create(Rcpp::Named("mat_norm") = mat_norm, - Rcpp::Named("ScaleX") = ScaleX); -}; - -// [[Rcpp::export]] -Rcpp::List R_matrix_normalize_sparse(arma::sp_mat mat_norm){ - arma::rowvec ScaleX = matrix_normalize(mat_norm); - return Rcpp::List::create(Rcpp::Named("mat_norm") = mat_norm, - Rcpp::Named("ScaleX") = ScaleX); -}; - -// [[Rcpp::export]] -Rcpp::List R_matrix_center_dense(const arma::mat mat, arma::mat X_normalized, bool intercept){ - arma::rowvec meanX = matrix_center(mat, X_normalized, intercept); - return Rcpp::List::create(Rcpp::Named("mat_norm") = X_normalized, - Rcpp::Named("MeanX") = meanX); -}; - -// [[Rcpp::export]] -Rcpp::List R_matrix_center_sparse(const arma::sp_mat mat, arma::sp_mat X_normalized,bool intercept){ - arma::rowvec meanX = matrix_center(mat, X_normalized, intercept); - return Rcpp::List::create(Rcpp::Named("mat_norm") = X_normalized, - Rcpp::Named("MeanX") = meanX); -}; \ No newline at end of file diff --git a/src/include/BetaVector.h b/src/include/BetaVector.h deleted file mode 100644 index efc5d92..0000000 --- a/src/include/BetaVector.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef BETA_VECTOR_H -#define BETA_VECTOR_H -#include -#include "RcppArmadillo.h" - -/* - * arma::vec implementation - */ - - -using beta_vector = arma::vec; -//using beta_vector = arma::sp_mat; - -std::vector nnzIndicies(const arma::vec& B); - -std::vector nnzIndicies(const arma::sp_mat& B); - -std::vector nnzIndicies(const arma::vec& B, const std::size_t low); - -std::vector nnzIndicies(const arma::sp_mat& B, const std::size_t low); - -std::size_t n_nonzero(const arma::vec& B); - -std::size_t n_nonzero(const arma::sp_mat& B); - -bool has_same_support(const arma::vec& B1, const arma::vec& B2); - -bool has_same_support(const arma::sp_mat& B1, const arma::sp_mat& B2); - - -#endif // BETA_VECTOR_H \ No newline at end of file diff --git a/src/include/Interface.h b/src/include/Interface.h deleted file mode 100644 index 4f33daf..0000000 --- a/src/include/Interface.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef RINTERFACE_H -#define RINTERFACE_H - -#include -#include -#include "RcppArmadillo.h" -#include "Grid.h" -#include "GridParams.h" -#include "FitResult.h" - - -inline void to_arma_error() { - Rcpp::stop("L0Learn.fit only supports sparse matricies (dgCMatrix), 2D arrays (Dense Matricies)"); -} - -#endif // RINTERFACE_H diff --git a/src/profile.cpp b/src/profile.cpp deleted file mode 100644 index 40490e9..0000000 --- a/src/profile.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// #include "RcppArmadillo.h" -// #include "gperftools/profiler.h" -// -// using namespace Rcpp; -// -// // [[Rcpp::export]] -// SEXP start_profiler(SEXP str) { -// ProfilerStart(as(str)); -// return R_NilValue; -// } -// -// // [[Rcpp::export]] -// SEXP stop_profiler() { -// ProfilerStop(); -// return R_NilValue; -// } \ No newline at end of file diff --git a/src/utils.cpp b/src/utils.cpp deleted file mode 100644 index eadecb5..0000000 --- a/src/utils.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "utils.h" - -void clamp_by_vector(arma::vec &B, const arma::vec& lows, const arma::vec& highs){ - const std::size_t n = B.n_rows; - for (std::size_t i = 0; i < n; i++){ - B.at(i) = clamp(B.at(i), lows.at(i), highs.at(i)); - } -} - -// void clamp_by_vector(arma::sp_mat &B, const arma::vec& lows, const arma::vec& highs){ -// // See above implementation without filter for error. -// auto begin = B.begin(); -// auto end = B.end(); -// -// std::vector inds; -// for (; begin != end; ++begin) -// inds.push_back(begin.row()); -// -// auto n = B.size(); -// inds.erase(std::remove_if(inds.begin(), -// inds.end(), -// [n](size_t x){return (x > n) && (x < 0);}), -// inds.end()); -// for (auto& it : inds) { -// double B_item = B(it, 0); -// const double low = lows(it); -// const double high = highs(it); -// B(it, 0) = clamp(B_item, low, high); -// } -// } - -arma::rowvec matrix_normalize(arma::sp_mat &mat_norm){ - auto p = mat_norm.n_cols; - arma::rowvec scaleX = arma::zeros(p); // will contain the l2norm of every col - - for (auto col = 0; col < p; col++){ - double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); - scaleX(col) = l2norm; - } - - scaleX.replace(0, -1); - - for (auto col = 0; col < p; col++){ - arma::sp_mat::col_iterator begin = mat_norm.begin_col(col); - arma::sp_mat::col_iterator end = mat_norm.end_col(col); - for (; begin != end; ++begin) - (*begin) = (*begin)/scaleX(col); - } - - if (mat_norm.has_nan()) - mat_norm.replace(arma::datum::nan, 0); // can handle numerical instabilities. - - return scaleX; -} - -arma::rowvec matrix_normalize(arma::mat& mat_norm){ - - auto p = mat_norm.n_cols; - arma::rowvec scaleX = arma::zeros(p); // will contain the l2norm of every col - - for (auto col = 0; col < p; col++) { - double l2norm = arma::norm(matrix_column_get(mat_norm, col), 2); - scaleX(col) = l2norm; - } - - scaleX.replace(0, -1); - mat_norm.each_row() /= scaleX; - - if (mat_norm.has_nan()){ - mat_norm.replace(arma::datum::nan, 0); // can handle numerical instabilities. - } - - return scaleX; -} - -arma::rowvec matrix_center(const arma::mat& X, arma::mat& X_normalized, - bool intercept){ - auto p = X.n_cols; - arma::rowvec meanX; - - if (intercept){ - meanX = arma::mean(X, 0); - X_normalized = X.each_row() - meanX; - } else { - meanX = arma::zeros(p); - X_normalized = arma::mat(X); - } - - return meanX; -} - -arma::rowvec matrix_center(const arma::sp_mat& X, arma::sp_mat& X_normalized, - bool intercept){ - auto p = X.n_cols; - arma::rowvec meanX = arma::zeros(p); - X_normalized = arma::sp_mat(X); - return meanX; -} diff --git a/tests/testthat/test-L0Learn_accuracy.R b/tests/testthat/test-L0Learn_accuracy.R deleted file mode 100644 index ecea2a3..0000000 --- a/tests/testthat/test-L0Learn_accuracy.R +++ /dev/null @@ -1,103 +0,0 @@ -library("Matrix") -library("testthat") -library("L0Learn") -library("pracma") - -K = 10 - -tmp <- L0Learn::GenSynthetic(n=100, p=1000, k=K, seed=1, rho=.5, snr=+Inf) -X <- tmp[[1]] -y <- tmp[[2]] -tol = 1e-4 - -if (norm(X%*%tmp$B + tmp$b0 - y) >= 1e-9){ - stop() -} - -if(0 %in% y){ - stop() -} - - -norm_vec <- function(x) {Norm(as.matrix(x), p = Inf)} - -test_that('L0Learn recovers coefficients with no error for L0', { - skip_on_cran() - fit <- L0Learn.fit(X, y, loss="SquaredError", penalty = "L0") - - for (j in 1:length(fit$suppSize[[1]])){ - # With only L0 penalty, therefore, once the support size is 10, all coefficients should be 1. - if (fit$suppSize[[1]][[j]] >= 10){ - expect_equal(norm_vec(fit$beta[[1]][,j] - tmp$B), 0, tolerance=1e-3, info=j) - } - } -}) - -test_that('L0Learn seperates data with no error for L0', { - skip_on_cran() - for (l in c("Logisitic", "SquaredHinge")){ - fit <- L0Learn.fit(X, sign(y), loss="Logistic", penalty = "L0") - - predict_ <- function(index){ - sign(X %*% fit$beta[[1]][,index] + fit$a0[[1]][index]) - } - - for (j in 1:length(fit$suppSize[[1]])){ - if (fit$suppSize[[1]][[j]] >= 10){ - expect_equal(predict_(j), sign(y)) - } - } - } -}) - - -test_that('L0Learn recovers coefficients with no error for L0L1/L0L2', { - skip_on_cran() - for (p in c("L0L1", "L0L2")){ - fit <- L0Learn.fit(X, y, loss="SquaredError", penalty = p) - - for (i in 1:length(fit$suppSize)){ - past_K_support_error = Inf - for (j in 1:length(fit$suppSize[[i]])){ - # With L0 and L1/L2 penalty, once the support size is 10 (dictated by L0 and L1 together), the coefficients - # will most likely not be 1 due the L1/L2 penalty. Therefore, as the L1/L2 penalty decreases, the coefficients - # should approach 1. - # Each iteration, the norm should decrease - if (fit$suppSize[[i]][[j]] >= K){ - new_K_support_error = norm_vec(fit$beta[[i]][,j] - tmp$B) - expect_lte(new_K_support_error, past_K_support_error) - new_K_support_error = past_K_support_error - } - } - } - } -}) - - -test_that('L0Learn seperates data with no error for L0L1/L0L2', { - skip_on_cran() - for (l in c("Logistic", "SquaredHinge")){ - for (p in c("L0L1", "L0L2")){ - fit <- L0Learn.fit(X, sign(y), loss=l, penalty = p) - - predict_ <- function(index1, index2){ - sign(X %*% fit$beta[[index1]][,index2] + fit$a0[[index1]][index2]) - } - - for (i in 1:length(fit$suppSize)){ - past_K_support_error = Inf - for (j in 1:length(fit$suppSize[[i]])){ - # With L0 and L1/L2 penalty, once the support size is 10 (dictated by L0 and L1 together), the coefficients - # will most likely not be 1 due the L1/L2 penalty. Therefore, as the L1/L2 penalty decreases, the coefficients - # should approach 1. - # Each iteration, the norm should decrease - if (fit$suppSize[[i]][[j]] >= K){ - new_K_support_error = Norm(predict_(i, j) - sign(y)) - expect_lte(new_K_support_error, past_K_support_error) - new_K_support_error = past_K_support_error - } - } - } - } - } -}) \ No newline at end of file diff --git a/tests/testthat/test-L0Learn_gen.R b/tests/testthat/test-L0Learn_gen.R deleted file mode 100644 index 92720cd..0000000 --- a/tests/testthat/test-L0Learn_gen.R +++ /dev/null @@ -1,25 +0,0 @@ -library("testthat") -library("L0Learn") - - -test_that("L0Learn GenSyntheticLogistic fails for improper s", { - expect_error(L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, s=-1)) -}) - -test_that("L0Learn GenSyntheticLogistic accepts Null and Diagonal Sigma", { - L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, sigma=NULL) - L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, - sigma=diag(1:1000)) - - expect_error(L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, - sigma=diag(1:999))) - - succeed() -}) - -test_that("L0Learn GenSyntheticLogistic shuffles B", { - L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, shuffle_B = TRUE) - L0Learn:::GenSyntheticLogistic(n=1000, p=1000, k=10, seed=1, shuffle_B = FALSE) - - succeed() -}) \ No newline at end of file diff --git a/tests/testthat/test_L0Learn.R b/tests/testthat/test_L0Learn.R deleted file mode 100644 index 94c6bab..0000000 --- a/tests/testthat/test_L0Learn.R +++ /dev/null @@ -1,524 +0,0 @@ -library("Matrix") -library("testthat") -library("L0Learn") - -tmp <- L0Learn::GenSynthetic(n=100, p=1000, k=20, seed=1, snr = 10, rho=.5) -X <- tmp[[1]] -y <- tmp[[2]] -tol = 1e-4 - -if (sum(apply(X, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") -} - -X_sparse <- as(X, "dgCMatrix") - -test_that('L0Learn Accepts Proper Matricies', { - skip_on_cran() - ignore <- L0Learn.fit(X, y) - ignore <- L0Learn.cvfit(X, y) - ignore <- L0Learn.fit(X_sparse, y, intercept = FALSE) - ignore <- L0Learn.cvfit(X_sparse, y, intercept = FALSE) - succeed() -}) - -test_that("L0Learn V2+ raises warning on autolambda usage", { - fit_user_grid = list() - fit_user_grid[[1]] = c(10:1) - expect_warning(L0Learn.fit(X, y, lambdaGrid=fit_user_grid, autoLambda = FALSE)) - - expect_warning(L0Learn.cvfit(X, y, lambdaGrid=fit_user_grid, autoLambda = FALSE)) - - expect_silent(L0Learn.fit(X, y, lambdaGrid=fit_user_grid, penalty = "L0L2", nGamma=1)) - expect_silent(L0Learn.cvfit(X, y, lambdaGrid=fit_user_grid, penalty = "L0L2", nGamma=1)) -}) - -test_that("L0Learn V2+ raises error on negative user_grid values", { - fit_user_grid = list() - fit_user_grid[[1]] = c(-2:-10) - - for (p in c("L0", "L0L1", "L0L2")){ - expect_error(L0Learn.fit(X, y, lambdaGrid=fit_user_grid, penalty = p)) - expect_error(L0Learn.cvfit(X, y, lambdaGrid=fit_user_grid, penalty = p)) - } -}) - -test_that("L0Learn respect colnames on X data matrix", { - X_with_names <- matrix(X, nrow=nrow(X), ncol=ncol(X)) - names = c() - for (i in 1:1000){ - names[i] = paste("F", i) - } - colnames(X_with_names) <- names - fit <- L0Learn.fit(X_with_names, y) - - # TODO: Add colnames to beta - expect_equal(colnames(X_with_names), fit$varnames) - fit <- L0Learn.cvfit(X_with_names, y) - expect_equal(colnames(X_with_names), fit$fit$varnames) -}) - -test_that("L0Learn raises error when classification has 3 or more values in y.", { - - y_bin_bad = sign(y) - y_bin_bad[[1]] = 2 - - for (loss in c("Logistic", "SquaredHinge")){ - expect_error(L0Learn.fit(X, y_bin_bad, loss=loss)) - expect_error(L0Learn.cvfit(X, y_bin_bad, loss=loss)) - - } -}) - -test_that("L0Learn raises error when L0 classification has too large of a lambda grid", { - # This is tricky. See implementation of L0 classification penalty in fit.R and cvfit.R - - lambda_grid = list() - lambda_grid[[1]] = c(10e-8, 10e-9) - lambda_grid[[2]] = c(10e-8, 10e-9) - - for (loss in c("Logistic", "SquaredHinge")){ - expect_error(L0Learn.fit(X, sign(y), loss=loss, lambdaGrid=lambda_grid)) - expect_error(L0Learn.cvfit(X, sign(y), loss=loss, lambdaGrid=lambda_grid)) - - } -}) - -test_that("L0Learn raises warning on degenerate solution path", { - lambda_grid = list() - lambda_grid[[1]] = c(10e-8, 10e-9) - - expect_warning(L0Learn.fit(X, y, lambdaGrid=lambda_grid)) - expect_warning(L0Learn.cvfit(X, y,lambdaGrid=lambda_grid)) - -}) - -test_that("L0Learn respects excludeFirstK for large L0", { - skip_on_cran() - BIGuserLambda = list() - BIGuserLambda[[1]] <- c(10) - for (k in c(0, 1, 10)){ - x1 <- L0Learn.fit(X, y, penalty = "L0", autoLambda=FALSE, - lambdaGrid=BIGuserLambda, excludeFirstK = k) - - expect_equal(x1$suppSize[[1]][1], k) - } -}) - -test_that("L0Learn excludeFirstK is still subject to L1 norms", { - skip_on_cran() - K = p = 10 - n = 100 - - tmp <- L0Learn::GenSynthetic(n=n, p=p, k=5, seed=1) - X_real <- tmp[[1]] - - tmp <- L0Learn::GenSynthetic(n=n, p=p, k=5, seed=2) - y_fake <- tmp[[2]] - - # X_real has little to do with generation of y_fake. - # Therefore, as L1 grows we can expect that the columns go to 0. - - - x1 <- L0Learn.fit(X_real, y_fake, penalty = "L0", excludeFirstK = K, maxSuppSize = p) - - expect_equal(length(x1$suppSize[[1]]), 1) - expect_equal(x1$suppSize[[1]][1], 10) - - # TODO: Fix Crash when excludeFirstK >= p - # x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K, maxSuppSize = 10) - - # TODO: Fix issue when support is not maximized in first iteration for - # x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K-1, maxSuppSize = 10) - # All coefficients should only be regularized by L1, the x2$suppSize is strange. - - - x2 <- L0Learn.fit(X_real, y_fake, penalty = "L0L1", excludeFirstK = K-1, maxSuppSize = p) - for (s in x2$suppSize[[1]]){ - expect_lt(s, p) - } -}) - - -test_that("L0Learn fit are deterministic for Dense fit", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - - -test_that("L0Learn cvfit are deterministic for Dense cvfit", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.cvfit(X, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.cvfit(X, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - -test_that("L0Learn fit and cvfit are deterministic for Dense fit", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - -test_that("L0Learn fit and cvfit are deterministic for Dense cvfit", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.cvfit(X_sparse, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.cvfit(X_sparse, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - - -test_that("L0Learn fit find same solution for different matrix representations", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - -test_that("L0Learn fit find same solution for different matrix representations", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=paste(p, lows)) - } -}) - -# test_that("L0Learn fit find similar solution for different matrix representations with bounds", { -# skip_on_cran() -# for (p in c("L0", "L0L2", "L0L1")){ -# for (lows in (c(0, -10000, -.1))){ -# set.seed(1) -# x1 <- L0Learn.fit(X_sparse, y, penalty=p, intercept = FALSE, lows=lows) -# set.seed(1) -# x2 <- L0Learn.fit(X, y, penalty=p, intercept = FALSE, lows=lows) -# # TODO: Investigate why X_sparse is missing a solution -# for (i in 1:length(x1$beta)){ -# expect_equal(x1$beta[[i]], x2$beta[[i]][, 2:ncol(x2$beta[[i]])], info=paste(p, lows, i)) -# } -# -# } -# } -# }) - -test_that("L0Learn cvfit find same solution for different matrix representations", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - set.seed(1) - x1 <- L0Learn.cvfit(X_sparse, y, penalty=p, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.cvfit(X, y, penalty=p, intercept = FALSE) - expect_equal(x1, x2, info=p) - } -}) - - -test_that("L0Learn fit and cvfit run with sparse X and intercepts", { - skip_on_cran() - L0Learn.fit(X_sparse, y, intercept = TRUE) - L0Learn.cvfit(X_sparse, y, intercept = TRUE) - succeed() -}) - -test_that("L0Learn fit and cvfit run with sparse X and intercepts and CDPSI", { - skip_on_cran() - L0Learn.fit(X_sparse, y, intercept = TRUE, algorithm = "CDPSI", maxSwaps = 2); - L0Learn.cvfit(X_sparse, y, intercept = TRUE, algorithm = "CDPSI", maxSwaps = 2); - succeed() -}) - - -test_that("L0Learn matches for all penalty for Sparse and Dense Matrices", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - for (f in c(L0Learn.cvfit, L0Learn.fit)){ - set.seed(1) - x1 <- f(X, y, penalty = p, intercept = FALSE) - set.seed(1) - x2 <- f(X_sparse, y, penalty = p, intercept = FALSE) - expect_equal(x1, x2) - } - } -}) - - - -test_that("L0Learn.Fit runs for all Loss for Sparse and Dense Matrices", { - skip_on_cran() - y_bin = matrix(rbinom(dim(y)[1], 1, 0.5)) - for (l in c("Logistic", "SquaredHinge")){ - set.seed(1) - x1 <- L0Learn.fit(X, y_bin, loss=l, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.fit(X_sparse, y_bin, loss=l, intercept = FALSE) - expect_equal(x1, x2, info = paste("fit", l)) - - set.seed(1) - x1 <- L0Learn.cvfit(X, y_bin, loss=l, intercept = FALSE) - set.seed(1) - x2 <- L0Learn.cvfit(X_sparse, y_bin, loss=l, intercept = FALSE) - expect_equal(x1, x2, info = paste("fit", l)) - } -}) - - -test_that("L0Learn.Fit runs for all algorithm for Sparse and Dense Matrices", { - skip_on_cran() - for (p in c("L0", "L0L2", "L0L1")){ - for (intercept in c(TRUE, FALSE)){ - set.seed(1) - x1 <- L0Learn.fit(X, y, penalty=p, algorithm='CDPSI', intercept = intercept) - set.seed(1) - x2 <- L0Learn.fit(X, y, penalty=p, algorithm='CDPSI', intercept = intercept) - expect_equal(x1, x2, info = paste(p, intercept)) - } - } -}) - - - -test_that('Utilities for processing regression fit and cv objects run', { - skip_on_cran() - # Test utils for L0Learn.fit - fit <- L0Learn.fit(X, y) - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - - # Test utils for L0Learn.cvfit - fit <- L0Learn.cvfit(X, y) - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - succeed() -}) - -test_that('Utilities for processing logistic fit and cv objects run', { - skip_on_cran() - # Test utils for L0Learn.fit - fit <- L0Learn.fit(X, sign(y), loss="Logistic") - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - - # Test utils for L0Learn.cvfit - fit <- L0Learn.cvfit(X, sign(y), loss="Logistic") - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - succeed() -}) - -test_that('Utilities for processing non-intercept fit and cv objects run', { - skip_on_cran() - # Test utils for L0Learn.fit - fit <- L0Learn.fit(X, y, intercept=FALSE) - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - - # Test utils for L0Learn.cvfit - fit <- L0Learn.cvfit(X, y, intercept=FALSE) - print(fit) - coef(fit, lambda=0.01); - coef(fit, lambda=0.01, gamma=0); - coef(fit); - plot(fit) - plot(fit, showlines=FALSE) - predict(fit, newx=X, lambda=0.01); - predict(fit, newx=X, lambda=0.01, gamma=0); - succeed() -}) - - -test_that('The CDPSI algorithm runs for different losses.', { - skip_on_cran() - # Test utils for L0Learn.fit - L0Learn.fit(X, y, algorithm = "CDPSI", loss = "SquaredError", maxSuppSize=5); - L0Learn.fit(X, sign(y), algorithm = "CDPSI", loss = "Logistic", maxSuppSize=5); - L0Learn.fit(X, sign(y), algorithm = "CDPSI", loss = "SquaredHinge", maxSuppSize=5); - succeed() -}) - -test_that('The fit and cvfit gracefully error on bad rtol.', { - skip_on_cran() - - f1 <- function(){L0Learn.fit(X, y, rtol=1.1);} - f2 <- function(){L0Learn.fit(X, y, rtol=-.1);} - f3 <- function(){L0Learn.fit(X, y, atol=-.1);} - f4 <- function(){L0Learn.cvfit(X, y, rtol=1.1);} - f5 <- function(){L0Learn.cvfit(X, y, rtol=-.1);} - f6 <- function(){L0Learn.cvfit(X, y, atol=-.1);} - - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) - expect_error(f4()) - expect_error(f5()) - expect_error(f6()) - -}) - -test_that('The fit and cvfit gracefully error on bad loss specifications', { - skip_on_cran() - - f1 <- function(){L0Learn.fit(X, y, loss="NOT A LOSS");} - f2 <- function(){L0Learn.cvfit(X, y, loss="NOT A LOSS");} - - expect_error(f1()) - expect_error(f2()) -}) - -test_that('The fit and cvfit gracefully error on bad penalty specifications', { - skip_on_cran() - - f1 <- function(){L0Learn.fit(X, y, penalty="NOT A PENALTY");} - f2 <- function(){L0Learn.cvfit(X, y, penalty="NOT A PENALTY");} - - expect_error(f1()) - expect_error(f2()) -}) - -test_that('The fit and cvfit gracefully error on bad algorithim specifications', { - skip_on_cran() - - f1 <- function(){L0Learn.fit(X, y, algorithm="NOT A ALGO");} - f2 <- function(){L0Learn.cvfit(X, y, algorithm="NOT A ALGO");} - - expect_error(f1()) - expect_error(f2()) -}) - -test_that('The fit and cvfit gracefully error on non classifcation y when for classicaiton', { - skip_on_cran() - - f1 <- function(){L0Learn.fit(X, y, loss="Logistic");} - f2 <- function(){L0Learn.fit(X, y, loss="SquaredHinge");} - f1 <- function(){L0Learn.cvfit(X, y, loss="Logistic");} - f2 <- function(){L0Learn.cvfit(X, y, loss="SquaredHinge");} - - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) - expect_error(f4()) -}) - - -test_that('The fit and cvfit gracefully error on L0 classifcation when lambdagrid is the wrong size', { - skip_on_cran() - - lambda_grid <- list() - lambda_grid[[1]] <- c(10:1) - lambda_grid[[2]] <- c(10:1) - f1 <- function(){L0Learn.fit(X, sign(y), loss="Logistic", penalty="L0", lambdaGrid=lambda_grid);} - f2 <- function(){L0Learn.fit(X, sign(y), loss="SquaredHinge", penalty="L0", lambdaGrid=lambda_grid);} - f1 <- function(){L0Learn.cvfit(X, sign(y), loss="Logistic", penalty="L0", lambdaGrid=lambda_grid);} - f2 <- function(){L0Learn.cvfit(X, sign(y), loss="SquaredHinge", penalty="L0", lambdaGrid=lambda_grid);} - - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) - expect_error(f4()) -}) - -test_that('The fit and cvfit gracefully error on L0 when lambdagrid is the wrong size', { - skip_on_cran() - - lambda_grid <- list() - lambda_grid[[1]] <- c(10:1) - lambda_grid[[2]] <- c(10:1) - f1 <- function(){L0Learn.fit(X, y, penalty="L0", lambdaGrid=lambda_grid);} - f2 <- function(){L0Learn.cvfit(X, y, penalty="L0", lambdaGrid=lambda_grid);} - - expect_error(f1()) - expect_error(f2()) -}) - -test_that('The fit and cvfit gracefully error on L0 when lambdagrid has not decreasing values', { - skip_on_cran() - - lambda_grid <- list() - lambda_grid[[1]] <- c(1:10) - f1 <- function(){L0Learn.fit(X, y, penalty="L0", lambdaGrid=lambda_grid);} - f2 <- function(){L0Learn.cvfit(X, y, penalty="L0", lambdaGrid=lambda_grid);} - - expect_error(f1()) - expect_error(f2()) -}) - -test_that('The fit and cvfit gracefully error on L0LX when lambdagrid has not decreasing values', { - skip_on_cran() - - lambda_grid <- list() - lambda_grid[[1]] <- c(10:1) - lambda_grid[[1]] <- c(1:10) - f1 <- function(){L0Learn.fit(X, y, penalty="L0L1", lambdaGrid=lambda_grid);} - f2 <- function(){L0Learn.cvfit(X, y, penalty="L0L1", lambdaGrid=lambda_grid);} - f3 <- function(){L0Learn.fit(X, y, penalty="L0L2", lambdaGrid=lambda_grid);} - f4 <- function(){L0Learn.cvfit(X, y, penalty="L0L2", lambdaGrid=lambda_grid);} - - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) - expect_error(f4()) -}) - -test_that('The fit and cvfit gracefully error on CDPSI when bounds are supplied', { - skip_on_cran() - - - f1 <- function(){L0Learn.fit(X, y, algorithm="CDPSI", lows=0);} - f2 <- function(){L0Learn.cvfit(X, y, algorithm="CDPSI", lows=0);} - - expect_error(f1()) - expect_error(f2()) -}) diff --git a/tests/testthat/test_L0Learn_bounds.R b/tests/testthat/test_L0Learn_bounds.R deleted file mode 100644 index 70335bb..0000000 --- a/tests/testthat/test_L0Learn_bounds.R +++ /dev/null @@ -1,221 +0,0 @@ -library("Matrix") -library("testthat") -library("L0Learn") -library("raster") - -tmp <- L0Learn::GenSynthetic(n=100, p=5000, k=10, seed=1, rho=1.5) -X <- tmp[[1]] -y <- tmp[[2]] -y_bin <- sign(y + rnorm(100)) -tol = 1e-4 -epsilon = 1e-12 - -if (sum(apply(X, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") -} - -X_sparse <- as(X, "dgCMatrix") - -#test_that("L0Learn finds the same solution with LOOSE bounds", { -# x1 <- L0Learn.fit(X, y, lows=-10000, highs=10000) -# x2 <- L0Learn.fit(X, y) -# expect_equal(x1, x2) -#}) - - -test_that('L0Learn Fails on in-proper Bounds', { - skip_on_cran() - for (f in c(L0Learn.fit, L0Learn.cvfit)){ - for (m in list(X, X_sparse)){ - f1 <- function(){ - f(m, y, intercept = FALSE, lows=NaN) - } - f2 <- function(){ - f(m, y, intercept = FALSE, highs=NaN) - } - f3 <- function(){ - f(m, y, intercept = FALSE, lows=1, highs=0) - } - f4 <- function(){ - f(m, y, intercept = FALSE, lows=0, highs=0) - } - f5 <- function(){ - f(m, y, intercept = FALSE, lows=rep(1, dim(m)[[2]]), highs=0) - } - f6 <- function(){ - f(m, y, intercept = FALSE, lows=rep(0, dim(m)[[2]]), highs=0) - } - f7 <- function(){ - f(m, y, intercept = FALSE, lows=1, highs=rep(1, dim(m)[[2]])) - } - f8 <- function(){ - f(m, y, intercept = FALSE, lows=1, highs=rep(0, dim(m)[[2]])) - } - f9 <- function(){ - f(m, y, intercept = FALSE, lows=1, highs=2) - } - f10 <- function(){ - f(m, y, intercept = FALSE, lows=-2, highs=-1) - } - f11 <- function(){ - f(m, y, intercept = FALSE, lows=c(1, rep(0, dim(m)[[2]]-1)), highs=rep(1, dim(m)[[2]])) - } - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) - expect_error(f4()) - expect_error(f5()) - expect_error(f6()) - expect_error(f7()) - expect_error(f8()) - expect_error(f9()) - expect_error(f10()) - expect_error(f11()) - - } - } -}) - -test_that("L0Learn fit fails on CDPSI with bound", { - skip_on_cran() - f1 <- function(){ - L0Learn.fit(X, y, algorithm = "CDPSI", lows=0) - } - f2 <- function(){ - L0Learn.fit(X, y, algorithm = "CDPSI", highs=0) - } - f3 <- function(){ - L0Learn.fit(X, y, algorithm = "CDPSI", lows=rep(0, 5000), highs=rep(1, 5000)) - } - expect_error(f1()) - expect_error(f2()) - expect_error(f3()) -}) - -test_that("L0Learn fit respect bounds", { - skip_on_cran() - low = -.04 - high = .05 - for (m in list(X, X_sparse)){ - for (p in c("L0", "L0L1", "L0L2")){ - fit <- L0Learn.fit(m, y, intercept = FALSE, penalty=p, lows=low, highs=high) - for(i in 1:length(fit$beta)){ - expect_gte(min(fit$beta[[i]]), low-epsilon) - expect_lte(max(fit$beta[[i]]), high+epsilon) - } - } - } -}) - -test_that("L0Learn cvfit respect bounds", { - skip_on_cran() - low = -.04 - high = .05 - for (m in list(X, X_sparse)){ - for (p in c("L0", "L0L1", "L0L2")){ - fit <- L0Learn.cvfit(m, y, intercept = FALSE, penalty=p, lows=low, highs=high) - for (i in 1:length(fit$fit$beta)){ - expect_gte(min(fit$fit$beta[[i]]), low-epsilon) - expect_lte(max(fit$fit$beta[[i]]), high+epsilon) - } - } - } -}) - -test_that("L0Learn respects bounds for all Losses", { - skip_on_cran() - low = -.04 - high = .05 - maxIters = 2 - maxSwaps = 2 - for (a in c("CD")){ #for (a in c("CD", "CDPSI")){ - for (m in list(X, X_sparse)){ - for (p in c("L0", "L0L1", "L0L2")){ - for (l in c("Logistic", "SquaredHinge")){ - fit <- L0Learn.fit(m, y_bin, loss=l, intercept = FALSE, - penalty=p, algorithm = a, lows=low, - highs=high, maxIters = maxIters, maxSwaps = maxSwaps) - for (i in 1:length(fit$beta)){ - expect_gte(min(fit$beta[[i]]), low-epsilon) - expect_lte(max(fit$beta[[i]]), high+epsilon) - } - } - - fit <- L0Learn.fit(m, y, loss='SquaredError', intercept = FALSE, - penalty=p, algorithm = a, lows=low, - highs=high, maxIters = maxIters, maxSwaps = maxSwaps) - for (i in 1:length(fit$beta)){ - expect_gte(min(fit$beta[[i]]), low-epsilon) - expect_lte(max(fit$beta[[i]]), high+epsilon) - } - } - } - } -}) - - -test_that("L0Learn respects vector bounds", { - p = dim(X)[[2]] - bounds = rnorm(p, 0, .5) - lows = -(bounds^2) - .01 - highs = (bounds^2) + .01 - for (m in list(X, X_sparse)){ - fit <- L0Learn.fit(m, y, intercept = FALSE, lows=lows, highs=highs) - for (i in 1:ncol(fit$beta[[1]])){ - expect_true(all(lows - 1e-9 <= fit$beta[[1]][,i ])) - expect_true(all(fit$beta[[1]][,i ] <= highs + 1e-9)) - } - } -}) - -find <- function(x, inside){ - which(sapply(inside, FUN=function(X) x %in% X), arr.ind = TRUE) -} - -test_that("L0Learn with bounds is better than no-bounds", { - skip_on_cran() - lows = -.02 - highs = .02 - fit_wb <- L0Learn.fit(X, y, intercept = FALSE, lows=lows, highs=highs) - fit_nb <- L0Learn.fit(X, y, intercept = FALSE, scaleDownFactor = .8, nLambda = 300) - - for (i in 1:length(fit_wb$suppSize[[1]])){ - nnz_wb = fit_wb$suppSize[[1]][i] - if (nnz_wb > 10){ #Don't look at NoSelectK - solution_with_same_nnz = find(nnz_wb, fit_nb$suppSize[[1]])[1] - if (is.finite(solution_with_same_nnz)){ - # If there is a solution in fit_nb that has the same number of nnz. - beta_wb = fit_wb$beta[[1]][, i] - beta_nb = clamp(fit_nb$beta[[1]][, solution_with_same_nnz], lows, highs) - - beta_wb = fit_wb$beta[[1]][, i] - beta_nb = clamp(fit_nb$beta[[1]][, i], lows, highs) - - r_wb = y - X %*% beta_wb - r_nb = y - X %*% beta_nb - - expect_gte(norm(r_nb, "2"), norm(r_wb, "2")) - } - } - } -}) - -# test_that("L0Learn and glmnet find similar solutions", { -# lows = -0.02 -# highs = 0.02 -# for (i in 1:2){ -# if (i == 1){ -# p = "L0L1" -# alpha = 1 -# } else{ -# p = "L0L2" -# alpha = 0 -# } -# fit_L0 = L0Learn.fit(X, y, penalty = p, autoLambda= FALSE, lambdaGrid = list(c(1e-6)), lows=lows,highs=highs) -# fit_glmnet = glmnet(X, y, alpha = alpha, lower=lows,upper=highs) -# } -# -# } -# fit_L0 = L0Learn.fit(X, y, penalty = "L0L1", lows=-.02,highs=.02) -# fit_glmnet = glmnet(X,y,lower=-.02,upper=.02) -# }) diff --git a/tests/testthat/test_L0Learn_intercept.R b/tests/testthat/test_L0Learn_intercept.R deleted file mode 100644 index db19d01..0000000 --- a/tests/testthat/test_L0Learn_intercept.R +++ /dev/null @@ -1,219 +0,0 @@ -library("Matrix") -library("testthat") -library("L0Learn") -library("pracma") - -# quad <- function(n, p, k=10, thr=.9){ -# means = runif(p) -# X = matrix(runif(n*p),nrow=n,ncol=p) -# m = matrix(runif(n*p),nrow=n,ncol=p) <= thr -# X[m] <- 0.0 -# B = c(rep(1,k),rep(0,p-k)) -# e = rnorm(n)/100 -# y = ((X - means)**2)%*%B + e -# list(X=X, y = y) -# } - -tmp <- L0Learn::GenSynthetic(n=50, p=200, k=10, seed=1, rho=0.5, b0=0, snr=+Inf) -Xsmall <- tmp[[1]] -ysmall <- tmp[[2]] -tol = 1e-4 -if (sum(apply(Xsmall, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") -} - -Xsmall_sparse <- as(Xsmall, "dgCMatrix") - -userLambda <- list() -userLambda[[1]] <- c(logspace(-1, -10, 100)) - -test_that("Intercepts are supported for all losses, algorithims, penalites, and matrix types", { - skip_on_cran() - # Try all losses - for (p in c("L0", "L0L1", "L0L2")){ - L0Learn.fit(Xsmall_sparse, ysmall, penalty=p, intercept = TRUE) - L0Learn.cvfit(Xsmall_sparse, ysmall, penalty=p, nFolds=2, intercept = TRUE) - } - - for (a in c("CD", "CDPSI")){ - L0Learn.fit(Xsmall_sparse, ysmall, algorithm=a, intercept = TRUE) - L0Learn.cvfit(Xsmall_sparse, ysmall, algorithm=a, nFolds=2, intercept = TRUE) - } - - for (l in c("Logistic", "SquaredHinge")){ - L0Learn.fit(Xsmall_sparse, sign(ysmall), loss=l, intercept = TRUE) - L0Learn.cvfit(Xsmall_sparse, ysmall, algorithm=a, nFolds=2, intercept = TRUE) - } - succeed() -}) - -test_that("Intercepts for Sparse Matricies are deterministic", { - skip_on_cran() - # Try all losses - for (p in c("L0", "L0L1", "L0L2")){ - set.seed(1) - x1 <- L0Learn.fit(Xsmall_sparse, ysmall, penalty=p) - set.seed(1) - x2 <- L0Learn.fit(Xsmall_sparse, ysmall, penalty=p) - expect_equal(x1$a0, x2$a0, info=p) - } - - for (a in c("CD", "CDPSI")){ - set.seed(1) - x1 <- L0Learn.fit(Xsmall_sparse, ysmall, algorithm=a) - set.seed(1) - x2 <- L0Learn.fit(Xsmall_sparse, ysmall, algorithm=a) - expect_equal(x1$a0, x2$a0, info=a) - } - - for (l in c("Logistic", "SquaredHinge")){ - set.seed(1) - x1 <- L0Learn.fit(Xsmall_sparse, sign(ysmall), loss=l) - set.seed(1) - x2 <- L0Learn.fit(Xsmall_sparse, sign(ysmall), loss=l) - expect_equal(x1$a0, x2$a0, info=l) - - } -}) - -test_that("Intercepts are passed between Swap iterations", { - skip_on_cran() - # TODO : Implement test case -}) - -tmp <- L0Learn::GenSynthetic(n=100, p=1000, k=10, seed=1, rho=1.5, b0=0) -X <- tmp[[1]] -y <- tmp[[2]] -tol = 1e-4 - -if (sum(apply(X, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") -} - -X_sparse <- as(X, "dgCMatrix") - -test_that("When lambda0 is large, intecepts should be found similar for both sparse and dense methods", { - skip_on_cran() - BIGuserLambda <- list() - BIGuserLambda[[1]] <- c(logspace(2, -2, 10)) - - # TODO: Prevent crash if lambdaGrid is not "acceptable. - for (a in c("CD", "CDPSI")){ - set.seed(1) - x1 <- L0Learn.fit(X_sparse, y, penalty="L0", intercept = TRUE, algorithm = a, - autoLambda=FALSE, lambdaGrid=BIGuserLambda, maxSuppSize=100) - set.seed(1) - x2 <- L0Learn.fit(X, y, penalty="L0", intercept = TRUE, algorithm = a, - autoLambda=FALSE, lambdaGrid=BIGuserLambda, maxSuppSize=100) - - for (i in 1:length(x1$a0)){ - if ((x1$suppSize[[1]][i] == 0) && (x2$suppSize[[1]][i] == 0)){ - expect_equal(x1$a0[[1]][i], x2$a0[[1]][i]) - } else if (x1$suppSize[[1]][i] == x2$suppSize[[1]][i]){ - expect_equal(x1$a0[[1]][i], x2$a0[[1]][i], tolerance=1e-6, scale=x1$a0[[1]][i]) - } - } - } -}) - -# test_that("Intercepts achieve a lower insample-error", { -# skip_on_cran() -# -# for (a in c("CD", "CDPSI")){ -# y_scaled = y*2 + 10 -# set.seed(1) -# x1 <- L0Learn.fit(X_sparse, y_scaled, penalty="L0", intercept = TRUE, -# algorithm = a, -# autoLambda=FALSE, lambdaGrid=userLambda, maxSuppSize=100) -# set.seed(1) -# x2 <- L0Learn.fit(X_sparse, y_scaled, penalty="L0", intercept = FALSE, -# algorithm = a, -# autoLambda=FALSE, lambdaGrid=userLambda, maxSuppSize=100) -# -# min_length = min(length(x1$a0[[1]]), length(x1$a0[[1]])) -# for (i in 1:min_length){ -# if (TRUE){ # x1$suppSize[[1]][i] >= x2$suppSize[[1]][i] -# x1_loss = norm(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i] - y_scaled, '2') -# x2_loss = norm(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i] - y_scaled, '2') -# expect_lte(x1_loss, x2_loss) -# } -# } -# - # logistic <- function(x){1/(1+exp(-x))}; - # logit <- sum(log(logistic)) - # - # x1 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = TRUE, - # algorithm = a, - # loss = "Logistic", autoLambda=FALSE, lambdaGrid=userLambda, - # maxSuppSize=1000) - # x2 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = FALSE, - # algorithm = a, - # loss = "Logistic", autoLambda=FALSE, lambdaGrid=userLambda, - # maxSuppSize=1000) - # - # for (i in 1:min_length){ - # - # x1_loss = sum(sign(y)*logistic(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i])) # more 1s - # x2_loss = sum(sign(y)*logistic(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i])) # more -1s - # print(paste(i, x1_loss - x2_loss)) - # #expect_lt(x1_loss, x2_loss) - # } - # - # squaredHinge <- function(y, yhat){max(0, 1-y*yhat)**2} - # - # x1 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = TRUE, - # algorithm = a, - # loss = "SquaredHinge", autoLambda=FALSE, lambdaGrid=userLambda, - # maxSuppSize=1000) - # x2 <- L0Learn.fit(X_sparse, sign(y), penalty="L0", intercept = FALSE, - # algorithm = a, - # loss = "SquaredHinge", autoLambda=FALSE, lambdaGrid=userLambda, - # maxSuppSize=1000) - # - # for (i in 1:min_length){ - # x1_loss = sum(squaredHinge(sign(X%*%x1$beta[[1]][,i] + x1$a0[[1]][i]), sign(y))) - # x2_loss = sum(squaredHinge(sign(X%*%x2$beta[[1]][,i] + x2$a0[[1]][i]), sign(y))) - # #print(paste(i, x1_loss - x2_loss)) - # expect_lt(x1_loss, x2_loss) - # } -# } -# }) - -test_that("Intercepts are learned close to real values", { - skip_on_cran() - fineuserLambda <- list() - fineuserLambda[[1]] <- c(logspace(-1, -10, 100)) - - k = 10 - for (a in c("CD", "CDPSI")){ - for (b0 in c(-100, -10, -2, 2, 10, 100)){ - tmp <- L0Learn::GenSynthetic(n=500, p=200, k=k, seed=1, rho=1, b0=b0) - X2 <- tmp[[1]] - y2 <- tmp[[2]] - - tol = 1e-4 - if (sum(apply(X2, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") - } - X2_sparse <- as(X2, "dgCMatrix") - - x1 <- L0Learn.fit(X2_sparse, y2, penalty="L0", intercept = TRUE, algorithm = a, - autoLambda=FALSE, lambdaGrid=fineuserLambda, maxSuppSize=1000) - - x2 <- L0Learn.fit(X2, y2, penalty="L0", intercept = TRUE, algorithm = a, - autoLambda=FALSE, lambdaGrid=fineuserLambda, maxSuppSize=1000) - - for (i in 1:length(x1$suppSize[[1]])){ - if (x1$suppSize[[1]][i] == k){ - expect_lt(abs(x1$a0[[1]][i] - b0), abs(.01*b0)) - } - } - - for (i in 1:length(x2$suppSize[[1]])){ - if (x2$suppSize[[1]][i] == k){ - expect_lt(abs(x2$a0[[1]][i] - b0), abs(.01*b0)) - } - } - } - } -}) \ No newline at end of file diff --git a/tests/testthat/test_L0Learn_usergrids.R b/tests/testthat/test_L0Learn_usergrids.R deleted file mode 100644 index 3269d5a..0000000 --- a/tests/testthat/test_L0Learn_usergrids.R +++ /dev/null @@ -1,183 +0,0 @@ -library("testthat") -library("L0Learn") - -tmp <- L0Learn::GenSynthetic(n=100, p=1000, k=10, seed=1) -X <- tmp[[1]] -y <- tmp[[2]] - -test_that("L0Learn L0 grid works", { - skip_on_cran() - userLambda = list() - userLambda[[1]] <- c(10, 1, 0.1, 0.01) - x1 <- L0Learn.fit(X, y, penalty = "L0", - lambdaGrid=userLambda) - - expect_equal(length(x1$lambda[[1]]), 4) - for (l in c("SquaredError", "SquaredHinge")){ - x1 <- L0Learn.fit(X, sign(y), penalty = "L0", loss=l, - lambdaGrid=userLambda) - expect_equal(length(x1$lambda[[1]]), 4) - - x1 <- L0Learn.fit(X, sign(y), penalty = "L0", loss=l, - lambdaGrid=userLambda) - expect_equal(length(x1$lambda[[1]]), 4) - - } -}) - -test_that("L0Learn L0 fails on bad userLambda", { - skip_on_cran() - userLambda = list() - userLambda[[1]] <- c(10, 11, 0.1, 0.01) - f1 <- function(){ - L0Learn.fit(X, y, penalty = "L0", - lambdaGrid=userLambda) - } - expect_error(f1()) - - for (l in c("SquaredError", "SquaredHinge")){ - f2 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0", loss=l, - lambdaGrid=userLambda) - } - expect_error(f2()) - - f3 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0", loss=l, - lambdaGrid=userLambda) - } - expect_error(f3()) - - - } -}) - -test_that("L0Learn L0 grid ignores nGamma ", { - skip_on_cran() - userLambda = list() - userLambda[[1]] <- c(10, 1, 0.1, 0.01) - x1 <- L0Learn.fit(X, y, penalty = "L0", nGamma = 1, - lambdaGrid=userLambda) - - expect_equal(length(x1$lambda), 1) - expect_equal(length(x1$lambda[[1]]), 4) -}) - - -test_that("L0Learn L0L1/2 grid works", { - skip_on_cran() - userLambda = list() - userLambda[[1]] <- c(10, 1, 0.1, 0.01) - userLambda[[2]] <- c(11, 1.1, 0.11, 0.011, 0.0011) - userLambda[[3]] <- c(12, 1.2, 0.12) - x1 <- L0Learn.fit(X, y, penalty = "L0L1", - lambdaGrid=userLambda, nGamma=3) - - expect_equal(length(x1$lambda), 3) - expect_equal(length(x1$lambda[[1]]), 4) - expect_equal(length(x1$lambda[[2]]), 5) - expect_equal(length(x1$lambda[[3]]), 3) - - x1 <- L0Learn.fit(X, y, penalty = "L0L2", - lambdaGrid=userLambda, nGamma=3) - - expect_equal(length(x1$lambda), 3) - expect_equal(length(x1$lambda[[1]]), 4) - expect_equal(length(x1$lambda[[2]]), 5) - expect_equal(length(x1$lambda[[3]]), 3) - - for (l in c("SquaredError", "SquaredHinge")){ - x1 <- L0Learn.fit(X, sign(y), penalty = "L0L1", loss=l, - lambdaGrid=userLambda, nGamma=3) - expect_equal(length(x1$lambda), 3) - expect_equal(length(x1$lambda[[1]]), 4) - expect_equal(length(x1$lambda[[2]]), 5) - expect_equal(length(x1$lambda[[3]]), 3) - - x1 <- L0Learn.fit(X, sign(y), penalty = "L0L2", loss=l, - lambdaGrid=userLambda, nGamma=3, maxSuppSize = 1000) - expect_equal(length(x1$lambda), 3) - expect_equal(length(x1$lambda[[1]]), 4) - expect_equal(length(x1$lambda[[2]]), 5) - expect_equal(length(x1$lambda[[3]]), 3) - - } - - - succeed() -}) - -test_that("L0Learn L0L1/2 ignores with wrong nGamma in v2.0.0", { - skip_on_cran() - # This changed between v1.2.0 and v2.0.0 - userLambda = list() - userLambda[[1]] <- c(10, 1, 0.1, 0.01) - userLambda[[2]] <- c(11, 1.1, 0.11, 0.011, 0.0011) - userLambda[[3]] <- c(12, 1.2, 0.12) - - f1 <- function(){ - L0Learn.fit(X, y, penalty = "L0L1", lambdaGrid=userLambda, nGamma=4) - } - f2 <- function(){ - L0Learn.fit(X, y, penalty = "L0L2", lambdaGrid=userLambda, nGamma=4) - } - - if (packageVersion("L0Learn") >= '2.0.0'){ - f1() - f2() - succeed() - } else{ - expect_error(f1()) - expect_error(f2()) - } - - for (l in c("SquaredError", "SquaredHinge")){ - f1 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0L1", loss=l, lambdaGrid=userLambda, nGamma=4) - } - f2 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0L2", loss=l, lambdaGrid=userLambda, nGamma=4) - } - - if (packageVersion("L0Learn") >= '2.0.0'){ - f1() - f2() - succeed() - } else { - expect_error(f1()) - expect_error(f2()) - } - - } -}) - -test_that("L0Learn L0L1/2 grid fails with bad userLambda", { - skip_on_cran() - userLambda = list() - userLambda[[1]] <- c(10, 1, 0.1, 0.01) - userLambda[[2]] <- c(11, 12, 0.11, 0.011, 0.0011) - userLambda[[3]] <- c(12, 1.2, 0.12) - - f1 <- function(){ - L0Learn.fit(X, y, penalty = "L0L1", lambdaGrid=userLambda, nGamma=3) - } - expect_error(f1()) - - f2 <- function(){ - L0Learn.fit(X, y, penalty = "L0L2", lambdaGrid=userLambda, nGamma=3) - } - expect_error(f2()) - - for (l in c("SquaredError", "SquaredHinge")){ - f1 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0L1", loss=l, lambdaGrid=userLambda, nGamma=3) - } - expect_error(f1()) - - f2 <- function(){ - L0Learn.fit(X, sign(y), penalty = "L0L2", loss=l, lambdaGrid=userLambda, nGamma=3) - } - expect_error(f2()) - } -}) - \ No newline at end of file diff --git a/tests/testthat/test_L0Learn_utils.R b/tests/testthat/test_L0Learn_utils.R deleted file mode 100644 index 07e72c1..0000000 --- a/tests/testthat/test_L0Learn_utils.R +++ /dev/null @@ -1,188 +0,0 @@ -library("Matrix") -library("testthat") -library("L0Learn") - -tmp <- L0Learn::GenSynthetic(n=1000, p=500, k=10, seed=1, rho=1) -X <- tmp[[1]] -y <- tmp[[2]] + 1 -tol = 1e-4 - -if (sum(apply(X, 2, sd) == 0)) { - stop("X needs to have non-zero std for each column") -} - -X_sparse <- as(X, "dgCMatrix") - -test_that('matrix_column_get dense', { - skip_on_cran() - x1 <- as.matrix(X[, 1]) - x2 <- .Call('_L0Learn_R_matrix_column_get_dense', X, 0) # C++ and R use different indexes - expect_equal(x1, x2) -}) - -test_that('matrix_column_get sparse', { - skip_on_cran() - x1 <- as.matrix(X_sparse[, 1]) - x2 <- .Call('_L0Learn_R_matrix_column_get_sparse', X_sparse, 0) # C++ and R use different indexes - expect_equal(x1, x2) -}) - -test_that('matrix_rows_get dense', { - skip_on_cran() - x1 <- X[1:4, ] - x2 <- .Call("_L0Learn_R_matrix_rows_get_dense", X, 0:3) - expect_equal(x1, x2) -}) - -test_that('matrix_rows_get sparse', { - skip_on_cran() - x1 <- X_sparse[1:4, ] - x2 <- .Call("_L0Learn_R_matrix_rows_get_sparse", X_sparse, 0:3) - expect_equal(x1, x2) -}) - -test_that('matrix_vector_schur_produce dense', { - skip_on_cran() - x1 <- X*as.vector(y) - x2 <- .Call('_L0Learn_R_matrix_vector_schur_product_dense', X, y) - expect_equal(x1, x2) -}) - -test_that('matrix_vector_schur_produce sparse', { - skip_on_cran() - x1 <- X_sparse*as.vector(y) - x2 <- .Call('_L0Learn_R_matrix_vector_schur_product_sparse', X_sparse, y) - expect_equal(x1, x2) -}) - - -test_that('matrix_vector_divide dense', { - skip_on_cran() - x1 <- X/as.vector(y) - x2 <- .Call('_L0Learn_R_matrix_vector_divide_dense', X, y) - expect_equal(x1, x2) -}) - -test_that('matrix_vector_divide sparse', { - skip_on_cran() - x1 <- X_sparse/as.vector(y) - x2 <- .Call('_L0Learn_R_matrix_vector_divide_sparse', X_sparse, y) - expect_equal(x1, x2) -}) - -test_that('matrix_column_sums dense', { - skip_on_cran() - x1 <- colSums(X) - x2 <- as.vector(.Call('_L0Learn_R_matrix_column_sums_dense', X)) - expect_equal(x1, x2) -}) - -test_that('matrix_column_sums sparse', { - skip_on_cran() - x1 <- colSums(X_sparse) - x2 <- as.vector(.Call('_L0Learn_R_matrix_column_sums_sparse', X_sparse)) - expect_equal(x1, x2) -}) - -test_that('matrix_column_dot dense', { - skip_on_cran() - x1 <- X[,1]%*%y - x2 <- .Call('_L0Learn_R_matrix_column_dot_dense', X, 0, y) - expect_equal(as.double(x1), as.double(x2)) -}) - -test_that('matrix_column_dot sparse', { - skip_on_cran() - x1 <- X_sparse[,1]%*%y - x2 <- .Call('_L0Learn_R_matrix_column_dot_sparse', X_sparse, 0, y) - expect_equal(as.double(x1), as.double(x2)) -}) - -test_that('matrix_column_mult dense', { - skip_on_cran() - c = 3.14 - x1 <- X[,1]*c - x2 <- .Call('_L0Learn_R_matrix_column_mult_dense', X, 0, c) - expect_equal(as.double(x1), as.double(x2)) -}) - -test_that('matrix_column_mult sparse', { - skip_on_cran() - c = 3.14 - x1 <- X_sparse[,1]*c - x2 <- .Call('_L0Learn_R_matrix_column_mult_sparse', X_sparse, 0, c) - expect_equal(as.double(x1), as.double(x2)) -}) - -center_colmeans <- function(x) { - skip_on_cran() - xcenter = colMeans(x) - x - rep(xcenter, rep.int(nrow(x), ncol(x))) -} - -colNorms <- function(x){ - apply(x, 2, function(x){sqrt(sum(x^2))}) -} - -test_that('matrix_normalize dense', { - skip_on_cran() - for (norm in c(TRUE, FALSE)){ - if (norm){ - X_norm <- center_colmeans(X) - expect_equal(colMeans(X_norm), 0*colMeans(X_norm)) - } else { - X_norm <- as.matrix(X) - } - X_norm_copy = as.matrix(X_norm) - expect_equal(X_norm, X_norm_copy) - - x1 <- .Call("_L0Learn_R_matrix_normalize_dense", X_norm) - - expect_equal(X_norm, X_norm_copy) # R should not modify X_norm - - expect_equal(colNorms(X_norm), as.vector(x1$ScaleX)) - expect_equal(X_norm %*% diag(1/colNorms(X_norm)), x1$mat_norm) - } -}) - -test_that('matrix_normalize sparse', { - skip_on_cran() - X_norm <- as(X, "dgCMatrix") - X_norm_copy = as(X, "dgCMatrix") - - expect_equal(X_norm, X_norm_copy) - - x1 <- .Call("_L0Learn_R_matrix_normalize_sparse", X_norm) - - expect_equal(X_norm, X_norm_copy) # R should not modify X_norm - - expect_equal(colNorms(X_sparse), as.vector(x1$ScaleX)) - expect_equal(as.matrix(X_norm %*% diag(1/colNorms(X_sparse))), as.matrix(x1$mat_norm)) -}) - -test_that('matrix_center dense', { - skip_on_cran() - for (intercept in c(TRUE, FALSE)){ - x_norm <- 0*X - x1 <- .Call("_L0Learn_R_matrix_center_dense", X, x_norm, intercept) - - if (intercept){ - expect_equal(as.vector(x1$MeanX), colMeans(X)) - expect_equal(x1$mat_norm, center_colmeans(X)) - } else { - expect_equal(as.vector(x1$MeanX), 0*colMeans(X)) - expect_equal(x1$mat_norm, X) - } - } -}) - - -test_that('matrix_center sparse', { - skip_on_cran() - for (intercept in c(TRUE, FALSE)){ - x_norm = 0*X_sparse - x1 <- .Call("_L0Learn_R_matrix_center_sparse", X_sparse, x_norm, intercept) - expect_equal(as.vector(x1$MeanX), 0*colMeans(X)) - expect_equal(x1$mat_norm, X_sparse) - } -}) \ No newline at end of file