diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..d343cf8f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +# Title Formatting +Please format your Pull Request title according to the Conventional Commits specification. This is crucial for automating our release process. + +Based on the changes in your PR, please use one of the following prefixes in your title: + +- fix: for a bug fix. + +Example: fix: correct user authentication flow + +- feat: for a new feature. + +Example: feat: add user profile page + +- For breaking changes, you must signify this in one of two ways: + + - Append a ! after the type in the title. + + Example: feat!: remove user endpoint + + - Include a footer in your PR description below that starts with BREAKING CHANGE:. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..700707ce --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..e1e71c33 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,41 @@ +# name: CI +# on: +# push: +# branches: +# - main +# tags: ['*'] +# pull_request: +# workflow_dispatch: +# concurrency: +# # Skip intermediate builds: always. +# # Cancel intermediate builds: only if it is a pull request build. +# group: ${{ github.workflow }}-${{ github.ref }} +# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} +# jobs: +# test: +# name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} +# runs-on: ${{ matrix.os }} +# timeout-minutes: 60 +# permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created +# actions: write +# contents: read +# strategy: +# fail-fast: false +# matrix: +# version: +# - '1.11' +# - '1.6' +# - 'pre' +# os: +# - ubuntu-latest +# arch: +# - x64 +# steps: +# - uses: actions/checkout@v4 +# - uses: julia-actions/setup-julia@v2 +# with: +# version: ${{ matrix.version }} +# arch: ${{ matrix.arch }} +# - uses: julia-actions/cache@v2 +# - uses: julia-actions/julia-buildpkg@v1 +# - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 00000000..cba9134c --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,16 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/Documenter.yml b/.github/workflows/Documenter.yml index 6882936c..09114f19 100644 --- a/.github/workflows/Documenter.yml +++ b/.github/workflows/Documenter.yml @@ -2,10 +2,9 @@ name: Documenter on: push: branches: - - master - main - - v0.2-main - tags: '*' + tags: + - 'v*' pull_request: branches: - master diff --git a/.github/workflows/HM_Bump_version.yml b/.github/workflows/HM_Bump_version.yml new file mode 100644 index 00000000..eff43fb3 --- /dev/null +++ b/.github/workflows/HM_Bump_version.yml @@ -0,0 +1,117 @@ +name: 'Propose Version Bump' + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + propose-bump: + # This job only runs when a PR is successfully merged. + if: github.event.pull_request.merged == true + + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout the repository code. + - name: 'Checkout Repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 # We need history to find the commit by its SHA + token: ${{ secrets.PAT_FOR_BUMP_PR }} + + # Step 2: Get the merge commit message + - name: 'Get Merge Commit Message' + id: commit_message + run: | + # Use git log to get the full message of the exact merge commit SHA. + # The -n 1 flag ensures we only get one commit. + # The output is set for the next step to use. + MSG=$(git log --format=%B -n 1 ${{ github.event.pull_request.merge_commit_sha }}) + echo "message<> $GITHUB_OUTPUT + echo "$MSG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Step 3: Determine bump type and update the Project.toml file. + - name: 'Calculate Bump and Update Version File' + id: bump_logic + shell: python + run: | + import os + import re + import sys + + # Read the commit message from the previous step's output + commit_message = os.environ['COMMIT_MESSAGE'] + + # Determine bump type based on the Angular Conventional Commit spec + bump_type = '' + first_line = commit_message.splitlines()[0] + + if re.search(r'BREAKING CHANGE:', commit_message) or re.match(r'^\w+(\([\w-]+\))?!:', first_line): + bump_type = 'major' + elif re.match(r'^feat(\([\w-]+\))?:', first_line): + bump_type = 'minor' + elif re.match(r'^fix(\([\w-]+\))?:', first_line): + bump_type = 'patch' + + if not bump_type: + print('No version bump required for this commit.') + print('::set-output name=bumped::false') + sys.exit(0) + + print(f'Determined bump type: {bump_type}') + + try: + with open('Project.toml', 'r+') as f: + content = f.read() + version_match = re.search(r'^version\s*=\s*\"(\d+)\.(\d+)\.(\d+)\"', content, re.M) + + if not version_match: + print('Error: Could not find version in Project.toml') + sys.exit(1) + + major, minor, patch = map(int, version_match.groups()) + + if bump_type == 'major': major, minor, patch = major + 1, 0, 0 + elif bump_type == 'minor': minor, patch = minor + 1, 0 + elif bump_type == 'patch': patch += 1 + + new_version = f'{major}.{minor}.{patch}' + print(f'Bumping version to {new_version}') + + new_content = re.sub(r'^version\s*=\s*\".*\"', f'version = "{new_version}"', content, count=1, flags=re.M) + + f.seek(0) + f.write(new_content) + f.truncate() + + print(f'::set-output name=new_version::{new_version}') + print('::set-output name=bumped::true') + + except FileNotFoundError: + print('Error: Project.toml not found.') + sys.exit(1) + env: + # Pass the message from the "Get Merge Commit Message" step + COMMIT_MESSAGE: ${{ steps.commit_message.outputs.message }} + + # Step 4: Create a new pull request with the version bump. + - name: 'Create Version Bump PR' + if: steps.bump_logic.outputs.bumped == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.PAT_FOR_BUMP_PR }} + commit-message: "chore: bump version to ${{ steps.bump_logic.outputs.new_version }}" + title: "chore: Propose version bump to ${{ steps.bump_logic.outputs.new_version }}" + body: | + This PR was automatically generated to propose the next version for the Julia package. + + The recommended version bump is to **${{ steps.bump_logic.outputs.new_version }}**. This was determined based on the conventional commit message of the last merged PR. + + Please review and merge to apply the version update. + branch: "chore/version-bump-${{ steps.bump_logic.outputs.new_version }}" + labels: 'automated-pr, version_update' + delete-branch: true \ No newline at end of file diff --git a/.github/workflows/HM_Changelog_update.yml b/.github/workflows/HM_Changelog_update.yml new file mode 100644 index 00000000..fb632ee0 --- /dev/null +++ b/.github/workflows/HM_Changelog_update.yml @@ -0,0 +1,51 @@ +name: Propose Changelog Update + +on: + # JuliaTagBot activated + issue_comment: + types: + - created + workflow_dispatch: + +jobs: + propose-changelog: + # Only when TagBot is activated, change the CHANGELOG + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + # Step 1 + - name: Checkout repository + uses: actions/checkout@v4 + with: + # All git history + fetch-depth: 0 + + # Step 2 + - name: Generate cumulative CHANGELOG.md + id: changelog_generator + uses: TriPSs/conventional-changelog-action@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + # output-file: "CHANGELOG.md" + input-file: "CHANGELOG.md" + version-file: "./Project.toml" + skip-commit: true + skip-tag: true + git-push: false + + # Step 3 + - name: Create Pull Request with updated CHANGELOG.md + if: steps.changelog_generator.outputs.skipped == 'false' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.PAT_FOR_CHANGELOG }} + commit-message: "docs(changelog): update CHANGELOG.md" + title: "Docs: Update CHANGELOG.md for new release" + body: | + This PR was automatically generated to update the `CHANGELOG.md` file for the upcoming release. + + Please review the changes and merge to trigger the final release. + # Branch for changelog + branch: "chore/update-changelog" + # If exist, update + delete-branch: true diff --git a/.github/workflows/HM_TagBot.yml b/.github/workflows/HM_TagBot.yml new file mode 100644 index 00000000..d9c0ddea --- /dev/null +++ b/.github/workflows/HM_TagBot.yml @@ -0,0 +1,25 @@ +name: Run TagBot on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tagbot: + # When pr is merged and the branch's name is 'chore/update-changelog' + if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'chore/update-changelog' + runs-on: ubuntu-latest + steps: + # Step 1 + - name: Checkout repository + uses: actions/checkout@v4 + + # Step 2 + - name: Run TagBot + uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 00000000..0cd3114e --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,31 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + default: "3" +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/docs/make.jl b/docs/make.jl index 2a8d2030..2aa2f2a0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,11 +10,19 @@ makedocs( sitename = "RLinearAlgebra", format = Documenter.HTML( collapselevel=1, + assets = String["custom_html.css"], ), plugins=[bib], modules = [RLinearAlgebra], pages = [ "Home" => "index.md", + "Tutorials" => [ + "Introduction" => "tutorials/introduction.md", + "Getting started" => "tutorials/getting_started.md" + ], + "Manual" => [ + "Introduction" => "manual/introduction.md", + ], "API Reference" => [ "Compressors" => [ "Compressors Overview" => "api/compressors.md", @@ -37,6 +45,16 @@ makedocs( ], ], ], + "Contributing" => [ + "Contributing Overview" => "dev/contributing.md", + "Design of Library" => "dev/design.md", + "Checklists" => [ + "dev/checklists.md", + "Compressors" => "dev/checklists/compressors.md", + "Loggers" => "dev/checklists/loggers.md" + ], + "Style Guide" => "dev/style_guide.md", + ], "References" => "references.md", ] ) @@ -45,5 +63,7 @@ makedocs( # See "Hosting Documentation" and deploydocs() in the Documenter manual # for more information. deploydocs( - repo = "github.com/numlinalg/RLinearAlgebra.jl" + repo = "github.com/numlinalg/RLinearAlgebra.jl", + devbranch = "master", # master's newest commit will become dev + push_preview = true # pull requests to the master will become available ) diff --git a/docs/src/custom_html.css b/docs/src/custom_html.css new file mode 100644 index 00000000..43a97745 --- /dev/null +++ b/docs/src/custom_html.css @@ -0,0 +1,7 @@ +/* This file is aim to customize html format */ + +/* Add margin at the beginning and the end of code blocks */ +li pre { + margin-top: 1em; + margin-bottom: 1em; +} \ No newline at end of file diff --git a/docs/src/dev/checklists.md b/docs/src/dev/checklists.md new file mode 100644 index 00000000..b9bdd4d4 --- /dev/null +++ b/docs/src/dev/checklists.md @@ -0,0 +1,55 @@ +# Overview +The purpose of this page is to offer checklists of tasks, for everyone who help to improve the package. These checklists are organized by method. + +## Create issue and use it! +Please add a brief introduction when you create an issue. After the introduction, copy and paste the corresponding checklist. +You can check the +boxes after you finish each steps, to help you contribute smoothly! + +For example, If I want to add a `moving_average` logging method to the package, I will create an issue as follows: +``` +# Introduction +Add the moving average method for both full and sketching residual. For more details, please see [the paper](https://arxiv.org/abs/2208.04989). +# Checklist +## Implementation +1. Method's core codes (`src/Solvers/Loggers`): + - [ ] Create a file in the directory `src/Solvers/Loggers`. For example, `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `Logger` struct with `max_it`, `collection_rate`, `threshold_info`, `stopping_criterion`, any other method-needed parameters, and argument validations to check invalid inputs. For example, `BasicLogger<:Logger`. + - [ ] Create a constructor with keyword default values for your `Logger` struct. For example, + ``` + BasicLogger(; + max_it = 0, + collection_rate = 1, + threshold = 0.0, + stopping_criterion = threshold_stop + ) = BasicLogger(max_it, collection_rate, threshold, stopping_criterion) + ``` + - [ ] Add documentation for this `Logger` struct, with mainly 5 parts: brief introduction, fields introduction, constructor and its keywords, what the constructor returns, and what the argument validations throw. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `LoggerRecipe` struct that uses the parameters from the `Logger` struct to preallocate memory. For example, `BasicLoggerRecipe{F<:Function} <: LoggerRecipe`. + - [ ] Create a `complete_logger` function that takes the `logger` struct as an input, and returns the `LoggerRecipe` struct you defined last step. For example, `complete_logger(logger::BasicLogger)`. + - [ ] Add documentation for this `LoggerRecipe` struct, with mainly 2 parts: brief introduction, fields introduction. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `update_logger!` function to log the errors as the iteration goes on, and stop the logging with convergence status or the maximum iteration limit. For example, + ``` + update_logger!(logger::BasicLoggerRecipe, error::Float64, iteration::Int64) + ``` + - [ ] Create a `reset_logger!` function to clean the history log information after convergence or exceed the maximum iteration. For example, `reset_logger!(logger::BasicLoggerRecipe)`. + - [ ] Create a `threshold_stop` function as the convergent stopping criterion designed for your `Logger` struct. For example, `threshold_stop(log::BasicLoggerRecipe)` + - [ ] Add documentation for this `threshold_stop` function, with mainly 3 parts: brief introduction, arguments introduction, and returns. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] **Optional**: If you have any helper functions that needed for your implementation, please implement them in a folder at `src/Solvers/Loggers`. +2. Package structure cooperation (`src/Solvers/Loggers.jl`, `src/RLinearAlgebra.jl`, `src/refs.bib`): + - [ ] Add an include("Loggers/[YOURFILE]") at bottom of the page, `src/Solvers/Loggers.jl`. + - [ ] Add import statements to `src/RLinearAlgebra.jl` with any functions from other packages that you use. + - [ ] Export your `Logger`, `LoggerRecipe`, any structs and functions you needed for your logger method to work in `src/RLinearAlgebra.jl`. + - [ ] Add your `Logger`, `LoggerRecipe`, needed structs and functions to `docs/src/api.loggers.md`, under the appropriate heading. + - [ ] If there are any new-added references, please add in `src/refs.bib`. +3. Tests (`test/Solvers/Loggers`): + - [ ] Add a procedural test to `test/Solvers/Loggers`. Be sure to check that the functions work as intended and all warnings/assertions are displayed. For example, `test/Solvers/Loggers/basic_logger.jl`. + - [ ] After finish implementing, you can goes to the julia's package environment by type `]` in the julia command line and run `test` to test whether you can pass all the tests. + +## Pull request +- [ ] Give a specific title to pull request. +- [ ] Lay out the features added to the pull request. +- [ ] Tag two people to review your pull request. +- [ ] **Optional**: If possible, please also add Copilot as a reviewer and choose to adopt its suggestions if reasonable. +``` +Note that, the checklist is copied from [Loggers](@ref "Loggers checklist") diff --git a/docs/src/dev/checklists/compressors.md b/docs/src/dev/checklists/compressors.md new file mode 100644 index 00000000..1447a857 --- /dev/null +++ b/docs/src/dev/checklists/compressors.md @@ -0,0 +1,33 @@ +# Compressors checklist +If you are implementing a compression method for the library, make sure you have completed +the following steps before making a pull request. + +``` +1. Implementation +- [ ] Create a file in the directory `src/Compressors`. +- [ ] Create a Compressor structure with n_rows and n_cols as well as other user-controlled +parameters. +- [ ] Create a constructor with keyword default values for your struct. +- [ ] Create a CompressorRecipe structure that uses the parameters from the Compressor +structure to preallocate memory. +- [ ] Create a `complete_compressor` function that takes as inputs of the Compressor, A, x,b +and returns a CompressorRecipe +- [ ] Create a `update_compressor!` function that generates new random values for a random +components of the Compressor +- [ ] a 5 input `mul!` function for applying a compressor to a matrix from the left +- [ ] a 5 input `mul!` function for applying a compressor to a matrix from the right +- [ ] a 5 input `mul!` function for applying a compressor to a vector +- [ ] a 5 input `mul!` function for applying the adjoint of the compressor to a vector +- [ ] Add an include("Compressors/[YOURFILE]") at bottom of page +- [ ] Add import statements to src/RLinearAlgebra.jl with any functions from other packages +that you use. +- [ ] Add your Compressor and CompressorRecipe to src/RLinearAlgebra.jl. +- [ ] Add your Compressor and CompressorRecipe to docs/src/api/compressors.md +under the appropiate heading. +- [ ] Add a procedural test to test/linear_samplers. Be sure to check that the functions +work as intended and all warnings/assertions are displayed. +2. Pull request +- [ ] Give a specific title to pull request. +- [ ] Lay out the features added to the pull request. +- [ ] Tag two people to review your pull request. +``` \ No newline at end of file diff --git a/docs/src/dev/checklists/loggers.md b/docs/src/dev/checklists/loggers.md new file mode 100644 index 00000000..6995faa9 --- /dev/null +++ b/docs/src/dev/checklists/loggers.md @@ -0,0 +1,47 @@ +# Loggers checklist +If you are implementing a logging method for the library, make sure you have completed +the following steps before making a pull request. In the following guides, `BasicLogger` +method is used as an example. + +``` +## Implementation +1. Method's core codes (`src/Solvers/Loggers`): + - [ ] Create a file in the directory `src/Solvers/Loggers`. For example, `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `Logger` struct with `max_it`, `collection_rate`, `threshold_info`, `stopping_criterion`, any other method-needed parameters, and argument validations to check invalid inputs. For example, `BasicLogger<:Logger`. + - [ ] Create a constructor with keyword default values for your `Logger` struct. For example, + ``` + BasicLogger(; + max_it = 0, + collection_rate = 1, + threshold = 0.0, + stopping_criterion = threshold_stop + ) = BasicLogger(max_it, collection_rate, threshold, stopping_criterion) + ``` + - [ ] Add documentation for this `Logger` struct, with mainly 5 parts: brief introduction, fields introduction, constructor and its keywords, what the constructor returns, and what the argument validations throw. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `LoggerRecipe` struct that uses the parameters from the `Logger` struct to preallocate memory. For example, `BasicLoggerRecipe{F<:Function} <: LoggerRecipe`. + - [ ] Create a `complete_logger` function that takes the `logger` struct as an input, and returns the `LoggerRecipe` struct you defined last step. For example, `complete_logger(logger::BasicLogger)`. + - [ ] Add documentation for this `LoggerRecipe` struct, with mainly 2 parts: brief introduction, fields introduction. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] Create a `update_logger!` function to log the errors as the iteration goes on, and stop the logging with convergence status or the maximum iteration limit. For example, + ``` + update_logger!(logger::BasicLoggerRecipe, error::Float64, iteration::Int64) + ``` + - [ ] Create a `reset_logger!` function to clean the history log information after convergence or exceed the maximum iteration. For example, `reset_logger!(logger::BasicLoggerRecipe)`. + - [ ] Create a `threshold_stop` function as the convergent stopping criterion designed for your `Logger` struct. For example, `threshold_stop(log::BasicLoggerRecipe)` + - [ ] Add documentation for this `threshold_stop` function, with mainly 3 parts: brief introduction, arguments introduction, and returns. For more details, you can check `src/Solvers/Loggers/basic_logger.jl`. + - [ ] **Optional**: If you have any helper functions that needed for your implementation, please implement them in a folder at `src/Solvers/Loggers`. +2. Package structure cooperation (`src/Solvers/Loggers.jl`, `src/RLinearAlgebra.jl`, `src/refs.bib`): + - [ ] Add an include("Loggers/[YOURFILE]") at bottom of the page, `src/Solvers/Loggers.jl`. + - [ ] Add import statements to `src/RLinearAlgebra.jl` with any functions from other packages that you use. + - [ ] Export your `Logger`, `LoggerRecipe`, any structs and functions you needed for your logger method to work in `src/RLinearAlgebra.jl`. + - [ ] Add your `Logger`, `LoggerRecipe`, needed structs and functions to `docs/src/api.loggers.md`, under the appropriate heading. + - [ ] If there are any new-added references, please add in `src/refs.bib`. +3. Tests (`test/Solvers/Loggers`): + - [ ] Add a procedural test to `test/Solvers/Loggers`. Be sure to check that the functions work as intended and all warnings/assertions are displayed. For example, `test/Solvers/Loggers/basic_logger.jl`. + - [ ] After finish implementing, you can goes to the julia's package environment by type `]` in the julia command line and run `test` to test whether you can pass all the tests. + +## Pull request +- [ ] Give a specific title to pull request. +- [ ] Lay out the features added to the pull request. +- [ ] Tag two people to review your pull request. +- [ ] **Optional**: If possible, please also add Copilot as a reviewer and choose to adopt its suggestions if reasonable. +``` \ No newline at end of file diff --git a/docs/src/dev/contributing.md b/docs/src/dev/contributing.md new file mode 100644 index 00000000..93fdcc01 --- /dev/null +++ b/docs/src/dev/contributing.md @@ -0,0 +1,37 @@ +# Contributing + +```@contents +Pages=["contributing.md"] +``` +## Improve the documentation +If you have ever been reading the documentation and been unsure about a description or + instruction, the documentation can be improved. Since you are the person finding this + gap in the documentation, you are also the person in the best position to fix it. + +The documentation is written in Markdown and built using + [Documenter.jl](https://documenter.juliadocs.org/stable/man/guide/). + The source code for all the docs is + [here](https://github.com/numlinalg/RLinearAlgebra.jl/tree/master/docs). + + +## Bug Reports +If you find a bug in our software we would love to know about it. You can make an issue +relating to a bug report [here](https://github.com/numlinalg/RLinearAlgebra.jl/issues/new?assignees=dmaldona%2C+npritch928%2C+vp314&labels=bug&projects=&template=bug_report.md&title=). + +## Feature Requests +Have a new method that you think would be a valuable addition to the package? + Make a [feature request](https://github.com/numlinalg/RLinearAlgebra.jl/issues/new?assignees=dmaldona%2C+npritch928%2C+vp314&labels=enhancement&projects=&template=feature_request.md&title=) + and let us know about it. Please provide details of why this feature would be valuable + and if possible point us to resources to help us better understand the feature. + +## Contribute code + +You can also contribute code to `RLinearAlgebra.jl`. Before contributing make sure that you + are familiar with [Git](https://git-scm.com/book/en/v2), + [GitHub](https://docs.github.com/en/get-started/start-your-journey/hello-world), + [Julia package development](https://docs.julialang.org/en/v1/stdlib/Pkg/#Developing-packages-1), + and the [Design](@ref) of the RLinearAlgebra.jl. + Once you are familiar with these items you can contribute to `RLinearAlgebra.jl` by + following the steps laid out in the + [JUMP](https://jump.dev/JuMP.jl/stable/developers/contributing/) guide. + diff --git a/docs/src/dev/design.md b/docs/src/dev/design.md new file mode 100644 index 00000000..2b8f0dbc --- /dev/null +++ b/docs/src/dev/design.md @@ -0,0 +1,757 @@ +# Design +## Overview Library Goals +RLinearAlgebra.jl implements randomized numerical linear algebra (RNLA) routines for +two tasks: (1) solving a matrix equations and (2) forming a low-rank approximation +to matrices. The primary tool Randomized Linear Algebra uses to accomplish these tasks is +multiplying the large matrix system by a smaller randomized matrix to compress the +large matrix. In the literature this process if often referred to as sampling or sketching, +RLinearAlgebra.jl refers to this process as compression. + +The library is organized with main techniques falling into one of three types: +Approximators, Compressors, and Solvers. Solvers feature their own set of +sub-techniques: Loggers, SolverErrors, and SubSolvers that facilitate solving. Approximators +have only one set of sub-techniques known as ApproximatorErrors. + +RLinearAlgebra.jl is designed so that the codebase has a good balance between efficiency +and modularity. RLinearAlgebra.jl tries to achieve these goals by introducing two +structures, one that contains user-controlled parameters which takes the form of +`[Technique]` and a second that is used by the technique to execute the techniques because it +contains the necessary preallocated memory and is known as a `[Technique]Recipe`. + +We can see an example of the difference between the two structures when considering an +implementation of compression with Gaussian matrices. In this implementation we wish to +have the user specify a compression dimension and size without having to know the dimension +of the matrix the sketch is applied to. The `Gaussian` structure facilitates this by having +two fields `n_rows` and `n_cols` with the default for both being zero. When the user then +constructs this structure the can specify the dimension and direction of the compression +by specifying the number of rows or columns they wish for the compression matrix to have. +If the user wanted a Gaussian matrix with 3 rows they would call +`Gaussian(n_rows = 3)`. We then turn this dimensional information into an usable compression +matrix by using the `complete_compressor` function to form a `GaussianRecipe`. This +`GaussianRecipe` contains Fields `n_rows = 3`, `n_cols` set to be the number of rows in the +compressor, and a Gaussian matrix of the size specified by `n_rows` and `n_cols`. + +Once a `[Technique]Recipe` has been created, this data structure can then be used to +execute a particular technique. The command to execute each technique varies by the class +of techniques, as such we lay out the specifics for each type of techniques in the following +section. + +## Technique Types +Overall, there are three top-level technique types: (1) Compressors, (2) Solvers, and +(3) Approximators, with the latter two also having additional sets of technique types +used in the execution of the top-level techniques. We group the discussions of the technique +classes by top-level technique. + +### Compressors +When implementing a Compressor, RLinearAlgebra requires an mutable `Compressor` +structure, a mutable `CompressorRecipe` structure, a `complete_compressor` function, a +`update_compressor!` function, and for five input mul! functions (one for applying the +compressor to vectors, one for applying the adjoint of a compressor to a vector, +and two for applying the compressor to matrices). + +#### Compressor Structure +Every compression technique needs a place to store user-controlled parameters. +This will be accomplished by the immutable Compressor structure. +We present an example structure used for the Sparse Sign technique. + +``` +struct SparseSign <: Compressor + n_rows::Int64 + n_cols::Int64 + nnz::Int64 +end +``` +You will first notice that `n_rows` and `n_cols` are fields present in the Compressor, +these fields allow for the user to specify either the number of rows or the number of +columns they wish the compressor to have. **Both `n_rows` and `n_cols` are required for +every Compressor structure.** Beyond those fields the technique will dictate the other +parameters that should be made available to the user. In addition to the structure, there +should be a **constructor for the structure that accepts keyword inputs for each +field of the Compressor structure.** For example in the `SparseSign` case we define, +``` +function SparseSign(;n_rows::Int64 = 0, n_cols::Int64 = 0, nnz::Int64 = 8) + # Partially construct the sparse sign datatype + return SparseSign(n_rows, n_cols, nnz) +end +``` +#### CompressorRecipe Structure +To form the compressor from the user-inputted information, we need information about the +linear system. Once this information is attained preallocations of the necessary memory can +be done. These preallocations are then stored in the `CompressorRecipe` structure. Because +this structure has all of the preallocated memory for applying the compression technique it +is this structure that can be applied to matrices and vectors. + +As example, we have included the CompressorRecipe for the sparse sign compressor. This +structure importantly includes the size of the compressor in the `n_rows` and `n_cols` +fields +``` +mutable struct SparseSignRecipe <: CompressorRecipe + n_rows::Int64 + n_cols::Int64 + max_idx::Int64 + nnz::Int64 + scale::Float64 + idxs::Vector{Int64} + signs::Vector{Bool} +end +``` +Here we have the **required `n_rows` and `n_cols`** fields for all compressors. The +remaining fields are specific to the sparse sign compression technique. + +#### complete_compressor +To create the CompressorRecipe from linear system information and the user-controlled +parameters, we use the function `complete_compressor(::Compressor, ::AbsractMatrix)`, +if vector information is required we can also define +`complete_compressor(::Compressor, ::AbsractMatrix, ::AbstractVector)`. +An example of how this is done for the sparse sign case can be seen below. +``` +function complete_compressor(sparse_info::SparseSign, A::AbstractMatrix) + n_rows = sparse_info.n_rows + n_cols = sparse_info.n_cols + # FInd the zero dimension and set it to be the dimension of A + if n_rows == 0 && n_cols == 0 + # by default we will compress the row dimension to size 2 + n_cols = size(A, 1) + n_rows = 2 + # correct these sizes + initial_size = max(n_rows, n_cols) + sample_size = min(n_rows, n_cols) + elseif n_rows == 0 && n_cols > 0 + # Assuming that if n_rows is not specified we compress column dimension + n_rows = size(A, 2) + # If the user specifies one size as nonzero that is the sample size + sample_size = n_cols + initial_size = n_rows + elseif n_rows > 0 && n_cols == 0 + n_cols = size(A, 1) + sample_size = n_rows + initial_size = n_cols + else + if n_rows == size(A, 2) + initial_size = n_rows + sample_size = n_cols + elseif n_cols == size(A, 2) + initial_size = n_cols + sample_size == n_rows + else + @assert false "Either you inputted row or column dimension must match \\ + the column or row dimension of the matrix." + end + end + + nnz = (sparse_info.nnz == 8) ? min(8, sample_size) : sparse_info.nnz + @assert nnz <= sample_size "Number of non-zero indices, $nnz, must be less than \\ + compression dimension, $sample_size." + idxs = Vector{Int64}(undef, nnz * initial_size) + start = 1 + for i in 1:initial_size + # every grouping of nnz entries corresponds to each row/column in sample + stop = start + nnz - 1 + # Sample indices from the intial_size + @views sample!( + 1:sample_size, + idxs[start:stop], + replace = false, + ordered = true + ) + start = stop + 1 + end + + # Store signs as a boolean to save memory + signs = bitrand(nnz * initial_size) + scale = 1 / sqrt(nnz) + + return SparseSignRecipe(n_rows, n_cols, sample_size, nnz, scale, idxs, signs) +end +``` +The `complete_compressor` function assumes that if the user inputs only `n_rows` or `n_cols` +in the Compressor structure this is the desired compression dimension. If they input +neither, it creates a compressor with a compression dimension of two and +if the input both and neither is consistent with +a dimension of the inputted linear system it returns an error. Otherwise, it assumes the +inconsistent dimension is the compression dimension. Once the sizes of the compressor have +been determined it next allocates the memory necessary for storing the initial compressor +and packages these allocations with the size information into the `CompressorRecipe`. + +#### update_compressor! +To generate a new version of the compressor we can call the function `update_compressor!`, +this function simply changes the random components of the CompressorRecipe. In the sparse +sign case this means updating the nonzero indices and the signs as can be seen in the +following example code. +``` +function update_compressor!( + S::SparseSignRecipe, + A::AbstractMatrix, + b::AbstractVector, + x::AbstractVector + ) + # Sample_size will be the minimum of the two size dimensions of `S` + sample_size = min(S.n_rows, S.n_cols) + initial_size = max(S.n_rows, S.n_cols) + start = 1 + for i in 1:sample_size + # every grouping of nnz entries corresponds to each row/column in sample + stop = start + S.nnz - 1 + # Sample indices from the intial_size + @views sample!( + 1:sample_size, + S.idxs[start:stop], + replace = false, + ordered = true + ) + start = stop + 1 + end + # There is no inplace update of bitrand and using sample is slower + S.signs .= bitrand(S.nnz * initial_size) + return +end +``` + +#### mul! +The last pieces of code that every compression technique requires are the `mul!` functions. +For these functions we follow the conventions laid out in the LinearAlgebra library where +there are five inputs (C, A, S, alpha, beta) and it outputs `C = beta * C + alpha * A * S`. +The `mul!` functions that should be implemented are two for applying the compression matrix +to vectors, one in standard orientation and one for when the adjoint of the compressor is +applied to the vector. Additionally, two `mul!` functions should be implemented for when +the compression matrix is applied to a matrix, one for when the compression matrix, S, is +applied from the left, i.e. AS, and one for when the compression matrix is applied from the +right, i.e. SA. + +### Solvers +A Solver technique is any technique that aims to find a vector ``x`` such that either +``Ax = b`` or ``x = \\min_u \\|A u - b\\|_2^2``. Solvers rely on compression techniques, +logging techniques, error techniques, and sub-solver techniques. We first discuss +implementation requirements for the sub-techniques and then discuss how we can use these +when creating a solver structure. + +#### Loggers +Loggers are structures with two goals (1) log a progress value produced by an error metric +and (2) evaluate whether that error is sufficient for stopping. The user controlled inputs +for a logging technique are contained in the Logger structure. + +##### Logger +The `Logger` structure is where the user inputs any information required +to logging progress and stopping the method. **The Logger is required to have a field for +`max_it`, `threshold_info`, and `stopping_criterion`.** The `max_it` field is a field +for the maximum number of iterations of the method. The `stopping_criterion` is a field that +contains a function that returns a stopping decision based on the information in the +`LoggerRecipe` and the `Tuple` of information supplied by the user in the `threshold_info` +field. It is important to note that constructors for these techniques should have keyword +inputs with predefined defaults. We present an example of the Logger structure for a +`BasicLogger` below +``` +struct BasicLogger <: Logger + max_it::Int64 + collection_rate::Int64 + threshold_info::Union{Float64, Tuple} + stopping_criterion::Function +end +``` + +Aside from the required parameters the `BasicLogger` also features a `collection rate` +parameter to allow the user to specify how often they wish for the `LoggerRecipe` to log +progress. + +##### LoggerRecipe +The `LoggerRecipe` will contain the user-controlled parameters from the `Logger` as well as +memory for storing the logged information. All `LoggerRecipes` +**must contain a `max_it` field and a `converged` field,** where the `converged` field is a +boolean indicating if the method has converged. An example of a `LoggerRecipe` is presented +below for the `BasicLoggerRecipe`. This Logger has a vector for the history of the progress +metric, a field whose inclusion is strongly suggested. It also has `record_location` field +to keep track of where the next observed progress estimate should be placed depending +on the `collection_rate`. +``` +mutable struct BasicLoggerRecipe{F} <: LoggerRecipe where F<:Function + max_it::Int64 + err::Float64 + threshold_info::Union{Float64, Tuple} + iteration::Int64 + record_location::Int64 + collection_rate::Int64 + converged::Bool + stopping_criterion::F + hist::Vector{Float64} +end +``` + +##### complete_logger +As with the other techniques, `complete_logger` takes a `Logger` data structure and +performs the appropriate allocations to generate a `LoggerRecipe`. An example of this +function for BasicLogger is presented below. +``` +function complete_logger(logger::BasicLogger, A::AbstractMatrix) + # We will run for a number of iterations equal to 3 itmes the number of rows if maxit is + # not set + max_it = logger.max_it == 0 ? 3 * size(A, 1) : logger.max_it + + max_collection = Int(ceil(max_it / logger.collection_rate)) + # use one more than max it form collection + hist = zeros(max_collection + 1) + return BasicLoggerRecipe{typeof(logger.stopping_criterion)}(max_it, + 0.0, + logger.threshold_info, + 1, + 1, + logger.collection_rate, + false, + logger.stopping_criterion, + hist + ) +end +``` + +##### update_logger! +As with the compressors `update_logger!` performs an in-place update of the +LoggerRecipe using the inputted progress metric and iteration of the method. An example of +the `update_logger!` function for the BasicLoggerRecipe is included below. +``` +function update_logger!(logger::BasicLoggerRecipe, err::Float64, iteration::Int64) + logger.iteration = iteration + logger.err = err + if rem(iteration, logger.collection_rate) == 0 + logger.hist[logger.record_location] = err + logger.record_location += 1 + end + # Always check max_it stopping criterion + # Compute in this way to avoid bounds error from searching in the max_it + 1 location + logger.converged = iteration <= logger.max_it ? logger.stopping_criterion(logger) : + false + return + +end +``` + +##### Stopping Functions +As was noted in the description of the required fields for the `Logger` the user should +have the opportunity to input a stopping function that should take the input of a +LoggerRecipe to which it updates the value of the `converged` field if stopping should +occur. An example implementation of function for threshold stopping, stop when progress +the metric falls below a particular threshold is presented below. +``` +function threshold_stop(log::LoggerRecipe) + return log.err < log.threshold_info +end +``` +#### SolverErrors +For computing the progress of a solver it is important to include implementations of +particular error techniques. These typically will be techniques like the residual or +compressed residual, but could be more complicated techniques like an estimate of backwards +stability. + +##### SolverError +This is a structure that holds user-controlled parameters for a progress estimation +technique. For basic techniques like the residual where no user-controlled parameters are +required this will simply be an empty structure. We have included an example of a +`SolverError` structure for the residual computations. It is important to note that +constructors for these techniques should have keyword inputs with predefined defaults. +``` +struct FullResidual <: SolverError + +end +``` + +##### SolverErrorRecipe +This structure contains the user-controlled parameters from the `SolverError` as well +memory allocations of a size determined based on the linear system. An example for a +residual technique has been included below. + +``` +mutable struct FullResidualRecipe{V<:AbstractVector} <: SolverErrorRecipe + residual::V +end +``` +##### complete_error +To generate the `SolverErrorRecipe` from the information in the linear system and +`SolverError` we use the function `complete_error`. This function should be implemented to +take the inputs of the SolverError`, a matrix `A` representing the linear system, and a +vector `b` representing the constant vector of the linear system. An example of this +function for the residual error technique has been included below. + +``` +function complete_error(error::FullResidual, A::AbstractMatrix, b::AbstractVector) + return FullResidualRecipe{typeof(b)}(zeros(size(b,1))) +end +``` +##### compute_error +To excute the technique we call the function `compute_error` with the inputs of the +`SolverErrorRecipe`, `Solver`, coefficient matrix `A`, and constant vector `b`. This +function then performs the necessary computations to return a single value indication of the +progress of the solver. An example of this for the residual technique that returns the norm- +squared of the residual is included below. +``` +function compute_error( + error::FullResidualRecipe, + solver::KaczmarzRecipe, + A::AbstractMatrix, + b::AbstractVector + )::Float64 + copyto!(error.residual, b) + mul!(error.residual, A, solver.solution_vec, -1.0, 1.0) + return dot(error.residual, error.residual) +end +``` + +#### SubSolvers +Although, randomized solvers are used to solve a larger linear system. They typically +rely on using compressors to generate a compressed linear system that can be easily solved +using standard techniques. The specifics of the 'standard' techniques is typically not +specified. For instance, if the compressed system is a least squares problem one could solve +this system with a QR algorithm or LSQR and potentially get vastly different performance +results. To allow the user to experiment with different techniques for solving the +compressed linear system, we introduce the SubSolver data structures. + +##### SubSolver +This is a data structure that allows the user to specify how they wish to solve the +compressed linear systems generated in the solving process. When the solver type is a direct +method it is possible for there to be no user inputs in this data structure. For iterative +methods there could be extensive user-controlled parameters included in this structure. For +example, for a LSQR SubSolver the user could input the maximum of iterations, a +preconditioner type, or stopping thresholds. We have included an example of the `SubSolver` +structure for the `LQSolver`, which is an approach for solving undetermined linear systems +and does not have any user-controlled parameters associated with it. It is important to note +that constructors for these techniques should have keyword inputs with predefined defaults. +``` +struct LQSolver <: SubSolver + +end +``` + +##### SubSolverRecipe +This is a data structure that contains the preallocated memory necessary for solving the +linear system. + +##### complete_solver +This is a function that takes a `SubSolver` and the linear system as input and uses these +inputs to output a SubSolverRecipe. + +##### update_sub_solver +This is a function that updates the preallocated memory in the SubSolverRecipe with +the relevant information for the new compressed linear system. + +##### ldiv! +A function that uses the SubSolverRecipe to solve the compressed linear system. + +#### Solvers +With an understanding of all of these sub techniques, we can discuss how to use these +methods to implement a Solver technique. The first data structure required for a solver is +the `Solver` structure. + +##### Solver +The Solver data structure is a structure where the user can input values of user-controlled +parameters specific to a particular type of solver. This typically involves the user +inputting the structures associated with their desired Compressor, Logger, Error, and +SubSolver, as well as any parameters like step-sizes associated with the particular +randomized solver they are using. As an example, we have included the Solver structure +associated with the Kaczmarz solver. It is important to note that constructors for these +techniques should have keyword inputs with predefined defaults. +``` +mutable struct Kaczmarz <: Solver + alpha::Float64 + S::Compressor + log::Logger + error::SolverError + sub_solver::SubSolver +end +``` + +##### SolverRecipe +The SolverRecipe will contain all the preallocated memory associate with the solver, the +solver specific user-controlled parameters, and all recipes associated with the +sub-techniques included in the `Solver` structure. We have included an example for the +`KaczmarzRecipe` below. +``` +mutable struct KaczmarzRecipe{T<:Number, + V<:AbstractVector, + M<:AbstractMatrix, + VV<:SubArray, + MV<:SubArray, + C<:CompressorRecipe, + L<:LoggerRecipe, + E<:SolverErrorRecipe, + B<:SubSolverRecipe + } <: SolverRecipe + S::C + log::L + error::E + sub_solver::B + alpha::Float64 + compressed_mat::M + compressed_vec::V + solution_vec::V + update_vec::V + mat_view::MV + vec_view::VV +end +``` +The first four fields are associated with the sub-techniques for the solver. The alpha +field is a user defined value and the remaining fields are preallocated space for storing +the result of the compression and the solution vector. + +##### complete_solver +The `complete_solver` function performs the necessary computations and allocations to change +a `Solver` structure into a `SolverRecipe`. In the example code below for a Kaczmarz solver +these computations include running `complete_[technique]` for the compression, logging, +error, and sub solver techniques, as well as allocating memory for storing the compressed +matrix and compressed vector, the solution vector, and update vector. **The `views` +allocated by this function should be replicated in other multi-compression solver structures +to allow for varying sizes of the compression matrix.** +``` +function complete_solver( + solver::Kaczmarz, + x::AbstractVector, + A::AbstractMatrix, + b::AbstractVector + ) + # Dimension checking will be performed in the complete_compressor + compressor = complete_compressor(solver.S, A, b) + logger = complete_logger(solver.log, A, b) + error = complete_error(solver.error, A, b) + # Check that required fields are in the types + @assert isdefined(error, :residual) "ErrorRecipe $(typeof(error)) does not contain the\ +field 'residual' and is not valid for a kaczmarz solver." + @assert isdefined(logger, :converged) "LoggerRecipe $(typeof(logger)) does not contain\ + the field 'converged' and is not valid for a kaczmarz solver." + # Assuming that max_it is defined in the logger + alpha::Float64 = solver.alpha + # We assume the user is using compressors to only decrease dimension + n_rows::Int64 = compressor.n_rows + n_cols::Int64 = compressor.n_cols + sample_size = n_rows + initial_size = n_cols + rows_a, cols_a = size(A) + # Allocate the information in the buffer using the types of A and b + compressed_mat = typeof(A)(undef, sample_size, cols_a) + compressed_vec = typeof(b)(undef, sample_size) + # Since sub_solver is applied to compressed matrices use here + sub_solver = complete_sub_solver(solver.sub_solver, compressed_mat, compressed_vec) + mat_view = view(compressed_mat, 1:sample_size, :) + vec_view = view(compressed_vec, 1:sample_size) + solution_vec = x + update_vec = typeof(x)(undef, cols_a) + return KaczmarzRecipe{eltype(A), + typeof(b), + typeof(A), + typeof(vec_view), + typeof(mat_view), + typeof(compressor), + typeof(logger), + typeof(error), + typeof(sub_solver) + }(compressor, + logger, + error, + sub_solver, + alpha, + compressed_mat, + compressed_vec, + solution_vec, + update_vec, + mat_view, + vec_view + ) +end +``` + +##### rsolve! +Every implementation of a Solver technique should include a `rsolve!` function that performs +in-place updates to a solution vector and `SolverRecipe`. An example of such an +implementation for a Kaczmarz solver is included below. To the greatest extent possible +the implementation should be written in a way that avoids new memory allocations. This means +making use in-place update functions like `mul!` or `ldiv!` rather than `*` or `\`. +``` +function rsolve!( + solver::KaczmarzRecipe, + x::AbstractVector, + A::AbstractMatrix, + b::AbstractVector + ) + solver.solution_vec = x + err = 0.0 + for i in 1:solver.log.max_it + err = compute_error(solver.error, solver, A, b) + # Update log adds value of err to log and checks stopping + update_logger!(solver.log, err, i) + if solver.log.converged + return solver.solution_vec, solver.log + end + + # generate a new version of the compression matrix + update_compressor!(solver.S, A, b, x) + # based on size of new compressor update views of matrix + # this should not result in new allocations + rows_s, cols_s = size(solver.S) + solver.mat_view = view(solver.compressed_mat, 1:rows_s, :) + solver.vec_view = view(solver.compressed_vec, 1:rows_s) + # compress the matrix and constant vector + mul!(solver.mat_view, solver.S, A) + mul!(solver.vec_view, solver.S, b) + # Compute the block residual + mul!(solver.vec_view, solver.mat_view, solver.solution_vec, -1.0, 1.0) + # sub-solver needs to designed for new compressed matrix + update_sub_solver!(solver.sub_solver, solver.mat_view) + # use sub-solver to find update the solution + sub_solve!(solver.update_vec, solver.sub_solver, solver.vec_view) + # Using over-relaxation parameter, alpha, to update solution + solver.solution_vec .+= solver.alpha .* solver.update_vec + end + + return solver.solution_vec, solver.log +end +``` + +### Approximators +Aside from solving linear systems, Randomized Linear Algebra has also been proven to be +extremely useful for generating low rank approximations to linear systems. These low-rank +approximations can then be used to solve linear systems or perform more efficient +matrix-matrix multiplications. The main types of low rank approximation methods implemented +in this version of the library are random range finder techniques like random SVD, CUR +type methods, and Nystrom Methods. Low rank approximations can be formed simply by calling +the `rapproximate` function. Once a Low-rank approximation has been formed it can then be +applied either as a preconditioner by calling the `ldiv!` function or multiplied by calling +the `mul!` function. Each of the low rank approximation technique requires the +implementation of the following data structures and functions. + +#### Approximator +This is a data structure that contains the user defined parameters for an approximator. An +example of this structure for the RangeFinder decomposition is included below. In the case +of the randomized range finder the only real user controlled parameter is the sketch size, +which is controlled by the `Compressor`. It should be noted that constructors for these +structures should be based around keyword inputs with preset defaults. +``` +mutable struct RangeFinder <: Approximator + S::Compressor + error::ErrorMethod +end +``` + +#### ApproximatorRecipe +This a data structure that contains preallocated memory and the user-controlled parameters +for a specific approximation method. An example of this data structure for a RangeFinder +decomposition is included below. +``` +mutable struct RangeFinderRecipe <: ApproximatorRecipe + S::CompressorRecipe + error::ErrorMethodRecipe + compressed_mat::AbstractMatrix + approx_range::AbstractMatrix +end +``` + +#### complete_approximator +The `complete_approximator` function takes the matrix `A` and the +`Approximator` data structure to output an `ApproximatorRecipe` with properly allocated +storage for the low-rank approximation. An example of this function for the +`RangeFinderRecipe` is included below. +``` +function complete_approximator(approx::RangeFinder, A::AbstractMatrix) + S = complete_compressor(approx.S, A) + err = complete_error(approx.error, A) + s_rows, s_cols = size(S) + a_rows, a_cols = size(A) + compressed_mat = Matrix{eltype(A)}(undef, a_rows, s_cols) + approx_range = Matrix{eltype(A)}(undef, a_rows, s_cols) + return RangeFinderRecipe(S, err, compressed_mat, approx_range) +end +``` + +#### rapproximate! +A function that returns an `ApproximatorRecipe` and approximation error value for a +particular approximation method. The returned `ApproximatorRecipe` can then be used for +matrix multiplication or preconditioning through the use of the `mul!` and `ldiv!` functions +respectively. An example of this function for the `RangeFinderRecipe` is included below. +``` +function r_approximate!( + approximator::RangeFinderRecipe + A::AbstractMatrix +) + m, n = size(A) + update_compressor!(aproximator.S) + # compuress the matrix + mul!(compressed_mat, A, aproximator.S) + # Array is required to compute the skinny qr + approximator.approx_range .= Array(qr(compressed_mat).Q) + err = compute_error(aproximator.error, A) + return approximator, error +end +``` + +#### ldiv! +A function that solves the system `Mx = b` for `x` where M is a low rank approximation +matrix. This is useful for preconditioning linear systems. When there is no obvious way to +use the low rank approximation to solve this system the implementation will be the same as +the implementation for `mul!`. + +#### mul! +A function that multiplies a low rank approximation with a matrix. This should be +implemented as the five input `mul!` function. For these functions we follow the conventions +laid out in the LinearAlgebra library where there are five inputs (C, A, S, alpha, beta) and +it outputs `C = beta * C + alpha * A * S`. + +### Approximation Error +For the `Approximator`s an important sub-techniques are those that verify the accuracy of a +particular approximation. These methods can be exact, as in the case of computing +``\|A - QQ'A\|_F``, where ``Q`` is a row-space approximator or approximate such as the +``\|AS - QQ'AS\|_F`` where ``S`` is a Gaussian matrix with 10 column vectors. + +#### ApproximatorError +The `ApproximatorError` data structure is a data structure that takes the user controlled +parameters for a method that computes the approximation error, e.g ``A - QQ'A`` for a +particular approximation method. In cases where this error is an exact approximation no +user-controlled parameters may be needed and the `ApproximatorError` can be implemented as +an empty data structure. If user-controlled parameters are necessary then constructors +should be implemented that take in keyword arguments with defaults. We have included an +example data structure for a method that computes the projected error, ``A - QQ'A``, below. +``` +mutable struct ProjectedError <: ApproximatorError + +end + +``` + +#### ApproximatorErrorRecipe +An `ApproximatorErrorRecipe` contains the user-controlled parameters and preallocated memory +for a method that computes the approximation error, e.g `A - QQ'A` for a particular +approximation method. +``` +mutable struct ProjectedErrorRecipe{T, M{T}} <: ApproximatorErrorRecipe + where M <: AbstractMatrix + error::Float64 + large_buff_mat::M + small_buffer_mat::M +end +``` +#### complete_error +The `complete_error` function takes the information from a `ApproximatorError` and an +`AbstractMatrix` to create an `ApproximatorErrorRecipe`. An example for the +`ProjectedError` structure is included below. +``` +function complete_error(error::ProjectedError, S::CompressorRecipe, A::AbstractMatrix) + row_s, col_s = size(S) + row_a, col_a = size(A) + T = eltype(A) + M = Matrix{T} + small_buffer_mat = M(undef, col_s, col_a) + large_buffer_mat = M(undef, row, col_a) + return ProjectedErrorRecipe{T, M}(0.0, large_buffer_mat, small_buffer_mat) +end +``` + +#### compute_error +A function that computes the error of a particular approximation method with respect to the +matrix `A` for a particular approximation technique. An example for `ProjectedError` is +included below. +``` +function compute_error( + error::ProjectedErrorRecipe, + approximator::RandRangeFinderRecipe, + A::AbstractMatrix + ) + mul!(error.small_buffer_mat, approximator.row_space', A) + mul!(error.large_buffer_mat, approximator.row_space, error.small_buffer_mat) + error.large_buffer_mat .-= A + error.error = norm(error.large_buffer_mat) + return error.error +end +``` diff --git a/docs/src/dev/style_guide.md b/docs/src/dev/style_guide.md new file mode 100644 index 00000000..2742fafd --- /dev/null +++ b/docs/src/dev/style_guide.md @@ -0,0 +1,60 @@ +# Style Guide +```@contents +Pages=["styleguide.md"] +``` +When writing code for `RLinearAlgebra.jl` we expect the code to be written in accordance +with the [BLUE](https://github.com/JuliaDiff/BlueStyle) style. + +## Documentation +This section describes the writing style that should be used when writing documentation for +`RLinearAlgebra.jl.` Many of these ideas for these suggestions +come from [JUMP](https://jump.dev/JuMP.jl/stable/developers/style/). +Overall when documenting the code one should follow these recommendations: + - Be concise + - Prefer lists over long sentences + - Use numbers when describing an ordered set of ideas + - Use bullets when these is no specific order + +### Docstrings + - Every new **function** and **data structure** needs to have a docstring + - Use properly punctuated complete sentences + +Below, we provide an example of a function docstring and a data structure docstring. + +#### Function Docstring +``` +""" + myFunction(args; kwargs...) + +A couple of sentences describing the function. These sentences should describe what inputs +are required and what is output by the function. + +### Arguments +- `arg1`, description of arg 1 + +### Outputs +The result of calling the function. This should be either the data structure that is +modified or what is returned. + +A citation from the package DocumenterCitations. +""" + +``` + +#### Data Structure Docstring +``` +""" + YourStructure <: YourStructuresSuperType + +A brief sentence describing the purpose of the structure. + +A citation in MLA format if the function comes from another author's work. + +### Fields +- `S::FieldType`, brief description of field purpose + +Include a sentence or two describing how the constructors work. Please be sure to include +the default values of the constructor. +""" + +``` diff --git a/docs/src/manual/introduction.md b/docs/src/manual/introduction.md new file mode 100644 index 00000000..27bb6f50 --- /dev/null +++ b/docs/src/manual/introduction.md @@ -0,0 +1,202 @@ +# A library for exploring Randomized Linear Algebra +If you are here, you probably know that Linear Algebra is foundational to data science and +scientific computing. You also probably know Linear Algebra routines dominate the +computational cost of many of the algorithms in these fields. Thus, improving the +scalability of these algorithms requires more scalable Linear Algebra techniques. + +An exciting new set of techniques that offers such improved scalability of +Linear Algebra techniques are Randomized Linear Algebra techniques. +In general, Randomized Linear Algebra techniques aim to achieve this improved +scalability by forming a representative sample of a matrix and performing +operations on that sample. In some circumstances operating on this sample +can offer profound speed-ups as can see in the following example +where a technique known as the RandomizedSVD (see [halko2011finding](@cite)) +is used to compute a rank-20 approximation to ``3000 \times 3000`` matrix +``A`` in place of a truncated SVD. Compared to computing the SVD and +truncating it, the RandomizedSVD is 100 times faster and just as accurate +as the truncated SVD. + +```julia +using RLinearAlgebra +using LinearAlgebra + +# Generate a rank-20 matrix +A = randn(3000, 20) * randn(20, 3000); + +@time U,S,V = svd(A); +# 4.566639 seconds (13 allocations: 412.354 MiB, 0.92% gc time) + +# Form the RandomizedSVD data structure +technique = RandomizedSVD( + compressor = Gaussian(compression_dim= 22, cardinality=Right()), + orthogonalize=false, + power_its = 0 +) + +# Take the RandomizedSVD of A +@time rec = rapproximate(technique, A); +# 0.050950 seconds (39 allocations: 5.069 MiB) + +# Take the norm of the difference between the RandomizedSVD and TrunctatedSVD at rank 22 +norm(rec.U * Diagonal(rec.S) * rec.V' - U[:,1:22] * Diagonal(S[1:22]) * (V[:, 1:22])') +# 6.914995919005829e-11 +``` + +Over the years, numerous Randomized Linear Algebra approaches have been proposed not only +for basic linear tasks such as computing a low-rank approximation to a matrix, solving +a linear system, or solving a least squares problem, but also for how obtain a +representative sample of the matrix itself. To this point, a single easy to prototype +library has not been developed to bring these techniques to the masses. RLinearAlgebra.jl is designed to be exactly such an easy-to-use library. + +In particular, RLinearAlgebra.jl leverages a modular design to allow you +to easily test Randomized Linear Algebra routines under a wide-range of parameter choices. RLinearAlgebra.jl provides routines for two core Linear Algebra tasks: finding a solution to +a linear system via ``Ax=b`` or ``\min_x \|Ax - b\|`` and forming a low rank +approximation to a matrix, ``\hat A`` where ``\hat A \approx A``. The solution to a linear +system appears everywhere: Optimization, Tomography, Statistics, Scientific Computing, Machine +Learning, etc. The low-rank approximation problem has only become more relevant in recent years +owing to the drastic increase in matrix sizes. It has been widely used in Statistics via PCA, but +also has become increasingly more relevant in all the fields where solving a linear system is +relevant. + +This manual will walk you through the use of the RLinearAlgebra.jl library. The remainder of this +section will be focused on providing an overview of the common design elements in the library, +and information about how to get started using the library. + +## Overview of the Library +The library is based on two data structure types: **techniques** that contain the parameters +that define a particular method and **technique recipes** that contain these parameters and +the necessary preallocations for the desired technique to be executed efficiently. As the +user you only need to define the techniques and the library will do all the work to form +the recipes for you. If you wish to convert a technique into a technique recipe you can use +the `complete_[technique type]` function. + +### The Technique Types +With an understanding of the basic structures in the library, one may wonder, what +types of techniques are there? First, there are the techniques for solving the linear +system, `Solvers` and techniques for forming a low-rank approximation to a matrix, +`Approximators`. Both `Solvers` and `Approximators` achieve speedups by working on +compressed forms (often known as sketched or sampled) of the linear system, techniques that +compress the linear system are known as `Compressors`. Aside from these global techniques, +there are also techniques that are specific to `Solvers`, which include: + +1. `SubSolvers`, techniques that solve the inner (compressed) linear system. +2. `Loggers`, techniques that log information and determine whether a stopping criterion has + been met. +3. `SolverError`, a technique that computes the error of a current iterate of a solver. + +Similarly, `Approximators` have their own specific techniques, which include: + +1. `ApproximorError`, a technique that computes the error of an `Approximator`. + +With all these technique structures, you may be wondering, what functions +can I call on these structures? Well, the answer is not many. As is +summarized in the following table. + +| Technique | Parent Technique | Function Calls | +| ---------- | ------------------ | ----------------------------------- | +|`Approximator` | None | `complete_approximator`,`rapproximate`| +|`Compressor` | None | `complete_compressor` | +|`Solver` | None | `complete_solver`, `rsolve` | +|`ApproximatorError`| `Approximator` | `complete_approximator_error` | +|`Logger` | `Solver` | `complete_logger` | +|`SolverError` | `Solver` | `complete_solver_error` | +|`SubSolver` | `Solver` | `complete_sub_solver` | + + +From the above table we can see that essentially all you are able to do unless you are using +an `Approximator` or a `Solver` is complete the technique. The reason being that all the +technique structures contain only information about algorithmic parameters that require no +information about the linear system. The recipes on the other hand have all the information +required to use a technique including the required pre-allocated memory. We determine the +preallocations for the Recipes by merging the parameter information of the technique +structures with the matrix and linear system information via the `complete_[technique]` +functions, which is the only function that you can call when you have a technique structure. +There is a special exception for `rsolve` and `rapproximate` because they implicitly call +all the necessary completes to form the appropriate recipe. The bottom line is that do +anything useful you will need a recipe. + +### The Recipe Types +Every technique can be transformed into a recipe. As has been stated before, what makes the +recipes different is that they contain all the required memory allocations. These allocations can +only be determined from once the matrix is known. As a user, +all you have to know is that as soon as you have a recipe you can do a lot. As can be seen +in the following table. + +| Technique Recipe | Parent Recipe | Function Calls | +|----------------- |------------------| ---------------------------------| +|`Approximator` | None | `mul!`, `rapproximate!` | +|`Compressor` | None | `mul!`,`update_compressor!` | +|`Solver` | None | `rsolve!` | +|`ApproximatorError`| `Approximator` | `compute_approximator_error` | +|`Logger` | `Solver` | `reset_logger!`, `update_logger!`| +|`SolverError` | `Solver` | `compute_error` | +|`SubSolver` | `Solver` | `update_sub_solver!`,`ldiv!` | + +Instead of providing +a different function for each method associated with these tasks, RLinearAlgebra.jl +leverages the multiple-dispatch functionality of Julia to allow all linear systems and +least squares problems to be solved calling the function +`rsolve(solver::Solver, x::AbstractVector, A::AbstractMatrix, b::AbstractVector)` +and all matrices to be approximated by calling the function +`rapproximate(approximator::Approximator, A::AbstractMatrix)`. Under this design, changing +the routine for solving your linear system or approximate your matrix is as +simple as changing the`solver` or `approximator` arguments. + +## Installing RLinearAlgebra +Currently, RLinearAlgebra.jl is not registered in Julia's official package registry. +It can be installed by writing in the REPL: +```julia +] add https://github.com/numlinalg/RLinearAlgebra.jl.git +``` +It can also be cloned into a local directory and installed by: +1. `cd` into the local project directory +2. Call `git clone https://github.com/numlinalg/RLinearAlgebra.jl.git` +3. Run Julia +4. Call `using Pkg` +5. Call `Pkg.activate(RLinearAlgebra.jl)` +6. Call `Pkg.instantiate()` + +For more information see [Using someone else's project](https://pkgdocs.julialang.org/v1/environments/#Using-someone-else's-project). + + + +## Using RLinearAlgebra.jl +For this example let's assume that we have a vector that we wish to compress +using one the RLinearAlgebra.jl `SparseSign` compressor. To do this: + +```@raw html +
    +
  1. + Load RLinearAlgebra.jl and generate your vector +
    using RLinearAlgebra
    +using LinearAlgebra
    +# Specify the size of the vector
    +n = 10000
    +x = rand(n)
    +
  2. +
  3. + Define the SparseSign technique. This requires us to specify a cardinality, + the direction we intend to apply the compressor from, and a compression_dim, + the number of entries we want in the compressed vector. In this instance we + want the cardinality to be Left() and the compression_dim = 20. +
    comp = SparseSign(compression_dim = 20, Cardinality = Left())
    +
  4. +
  5. + Use the complete_compressor function to generate the SparseSignRecipe. +
    S = complete_compressor(comp, A)
    +
  6. +
  7. + Apply the compressor to the vector using the multiplication function +
    Sx = S * x
    +    
    +norm(Sx)
    +
    +norm(x)
    +
  8. +
+``` + + + + + diff --git a/docs/src/tutorials/getting_started.md b/docs/src/tutorials/getting_started.md new file mode 100644 index 00000000..e4862752 --- /dev/null +++ b/docs/src/tutorials/getting_started.md @@ -0,0 +1,195 @@ +# Use Case: Solving a Least-Squares Problem with the Sparse Sign Method + +This guide demonstrates how to use the `SparseSign` compression method from the `RLinearAlgebra.jl` package to solve an overdetermined linear system (i.e., a least-squares problem) of the form: + +$$\min_{x} \|Ax - b\|_2^2$$ + +We will follow the design philosophy of `RLinearAlgebra.jl` by composing different modules (`Solver`, `Compressor`, `Logger`, etc.) to build and solve the problem. + +--- +## 1. Problem Setup + +Let's define a specific linear system $Ax = b$. + +To verify the accuracy of the final result, suppose that we know the true solution of the system, $x_{\text{true}}$, and then use it and a random generated matrix $A$ to generate the vector $b$. + +* **Matrix `A`**: A random $1000 \times 20$ matrix. +* **Vector `b`**: Calculated as $b = A x_{\text{true}}$, with dimensions $1000 \times 1$. +* **Goal**: Find a solution $x$ that is as close as possible to $x_{\text{true}}$. + +To achieve this, we need to import the required libraries and create the matrix `A` and vector `b` as defined above. We will also set an initial guess, `x_init`, for the solver. + +```@example SparseSignExample +# Import relevant libraries +using RLinearAlgebra, Random, LinearAlgebra + + +# Define the dimensions of the linear system +num_rows, num_cols = 1000, 20 + +# Create the matrix A and a known true solution x_true +A = randn(Float64, num_rows, num_cols); +x_true = randn(Float64, num_cols); + +# Calculate the right-hand side vector b from A and x_true +b = A * x_true; + +# Set an initial guess for the solution vector x (typically a zero vector) +x_init = zeros(Float64, num_cols); + +println("Dimensions:") +println(" - Matrix A: ", size(A)) +println(" - Vector b: ", size(b)) +println(" - True solution x_true: ", size(x_true)) +println(" - Initial guess x_init: ", size(x_true)) +``` + +--- +## 2. Create compressors + +In practice, we may encounter a much larger $A$ matrix than what we have here. Solving the problem with such a large matrix can slow down the performance of iterative algorithms that we will use to solve the least square problem. Therefore, we will use a randomized sketching technique to compress the matrix A and the corresponding vector b to a lower dimension, while preserving the essential geometric information of the system. + +Here, we will use the sparse sign method. + +### (a) Configure the `SparseSign` Compressor + +The idea of randomized methods is to reduce the scale of the original problem when the dimention of matrix $A$ is too big, using a random "sketch" or "compression" matrix, $S$. Here, we choose `SparseSign` as our `Compressor`. This compressor generates a sparse matrix whose non-zero elements are +1 or -1 (with scaling). More information can be found [here](@ref SparseSign). + +We will configure a compression matrix `S` that compresses the 100 rows of the original system down to 30 rows. + +```@example SparseSignExample +# The goal is to compress the 100 rows of A to 300 rows +compression_dim = 300 +# We want each row of the compression matrix S to have 5 non-zero elements +non_zeros = 5 + +# Configure the SparseSign compressor +# - cardinality=Left(): Indicates the compression matrix S will be +# left-multiplied with A (SAx = Sb). +# - compression_dim: The compressed dimension (number of rows). +# - nnz: The number of non-zero elements per column (left)/row (right) in S. +# - type: The element type for the compression matrix. +sparse_compressor = SparseSign( + cardinality=Left(), + compression_dim=compression_dim, + nnz=non_zeros, + type=Float64 +) +``` + +--- +### (b) Build the `SparseSign` recipe + +After configuring the compressor, we need to combine it with our specific matrix `A` to create a `SparseSignRecipe`. This recipe contains the generated sparse matrix and all necessary information to perform the compression efficiently. + +```@example SparseSignExample +# Pass the compressor configuration and the original matrix A to +# create the final compression recipe. +S = complete_compressor(sparse_compressor, A) + +# You can closely look at the compression recipe you created. +println(" - Compression matrix is applied to left or right: ", S.cardinality) +println(" - Compression matrix's number of rows: ", S.n_rows) +println(" - Compression matrix's number of columns: ", S.n_cols) +println(" - The number of nonzeros in each column (left)/row (right) of compression matrix: ", S.nnz) +println(" - Compression matrix's nonzero entry values: ", S.scale) +println(" - Compression matrix: ", S.op) +``` +Oops, I suddenly felt `300` rows is not a small enough size, and want to change the dimension to `10`. Then I need to +respecify the `SparseSign` compressor and build the recipe again: + +```@example SparseSignExample +# Change the dimension of the compressor. Similarly, you can use the idea +# for other configurations' changes. +sparse_compressor.compression_dim = 10 + +# Rebuild the compressor recipe +S = complete_compressor(sparse_compressor, A) +println("Compression matrix's number of rows: ", S.n_rows) +``` + +### (c) Apply the sparse sign matrix to the system + +While the solver can use the `S` recipe to perform multiplications on-the-fly, it can sometimes be useful to form the compressed system explicitly. We can use `*` for this. + +```@example SparseSignExample +# Form the compressed system SAx = Sb +SA = S * A +Sb = S * b + +println("Dimensions of the compressed system:") +println(" - Matrix SA: ", size(SA)) +println(" - Vector Sb: ", size(Sb)) +``` + +--- +## 3. Create solver + +With the problem and compressor defined, the next step is to choose and configure a solver. Here, we choose to use the +[Kaczmarz solver](@ref Kaczmarz). We configure it by passing in "ingredient" objects for each of its main functions: compressing the system (already done), logging progress, and checking for errors. + +### (a) Configure the logger and stopping rules + +To monitor the solver, we will use a `BasicLogger`. This object will serve two purposes: record the error history, and tell the solver when to stop. + +We will configure it to stop after a maximum of `50` iterations or if the calculated error drops below a tolerance of `1e-6`. + +```@example SparseSignExample +# Configure the logger to control the solver's execution +logger = BasicLogger( + max_it = 50, + threshold = 1e-6, + collection_rate = 5 +) +``` + +--- +### (b) Build the Kaczmarz Solver +Now, we assemble our configured components (compressor `S`, logger `L`) into the main Kaczmarz solver object. We will use the default methods for error checking and the sub-solver to be LQ factorization ([LQSolver](@ref LQSolver)). + +```@example SparseSignExample +# Create the Kaczmarz solver object by passing in the ingredients +kaczmarz_solver = Kaczmarz( + compressor = sparse_compressor, + log = logger, + sub_solver = LQSolver() +) +``` +Before we can run the solver, we must call `complete_solver`. This function takes the solver configurations and the specific problem data `A, b, x_init` and creates a `KaczmarzRecipe`. The recipe pre-allocates all the necessary memory buffers for efficient computation. + +```@example SparseSignExample +# Create the solver recipe by combining the solver and the problem data +solver_recipe = complete_solver(kaczmarz_solver, x_init, A, b) +``` + +--- +## 4. Solve the Compressed System + +With the recipe fully prepared, we can now call `rsolve!` to run the Kaczmarz algorithm. The function will iterate until the `stopping criterion` in the `logger` is met. + +The `rsolve!` function will modify `x_init` in-place, updating it with the calculated solution. + +```@example SparseSignExample +# Run the solver! +rsolve!(solver_recipe, x_init, A, b) + +# The solution is now stored in the updated x_init vector +solution = x_init; +``` + +--- +## 5. Verify the result + +Finally, let's check how close our calculated solution is to the known `x_true`. We can do this by calculating the Euclidean norm of the difference between the two vectors. A small error norm indicates a successful approximation. + +```@example SparseSignExample +# We can inspect the logger's history to see the convergence +error_history = solver_recipe.log.hist; +println(" - Solver stopped at iteration: ", solver_recipe.log.iteration) +println(" - Final error: ", error_history[solver_recipe.log.record_location - 1]) + +# Calculate the norm of the error +error_norm = norm(solution - x_true) +println(" - Norm of the error between the solution and x_true: ", error_norm) +``` +As you can see, by using the modular Kaczmarz solver, we were able to configure a randomized block-based method and find a solution vector that is very close to the true solution. \ No newline at end of file diff --git a/docs/src/tutorials/introduction.md b/docs/src/tutorials/introduction.md new file mode 100644 index 00000000..1b219eaa --- /dev/null +++ b/docs/src/tutorials/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +The purpose of these tutorials is to use examples help + new users quickly get hands on the usage of RLinearAlgebra. + +## How these tutorials are structured + +- Start from the solving $$Ax = b$$ + + + + diff --git a/readme.md b/readme.md index 715a145d..ba9a37e7 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,7 @@ [![Runtests](https://github.com/numlinalg/RLinearAlgebra.jl/actions/workflows/Runtests.yml/badge.svg)](https://github.com/numlinalg/RLinearAlgebra.jl/actions/workflows/Runtests.yml) [![codecov](https://codecov.io/github/numlinalg/RLinearAlgebra.jl/branch/v0.2-main/graph/badge.svg?token=GI7YUNM4YO)](https://codecov.io/github/numlinalg/RLinearAlgebra.jl) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) + RLinearAlgebra is a Julia package that implements standard Randomized Linear Algebra algorithms and provides means for performance comparison. diff --git a/src/Compressors/sparse_sign.jl b/src/Compressors/sparse_sign.jl index 93de96b2..46568abe 100644 --- a/src/Compressors/sparse_sign.jl +++ b/src/Compressors/sparse_sign.jl @@ -26,7 +26,7 @@ with dimension ``n \\times s``, where ``s`` is the compression dimension that is supplied by the user. In this case, each row of ``S`` is generated independently by the following steps: -1. Randomly choose `nnz` components fo the ``s`` components of the row. Note, `nnz` +1. Randomly choose `nnz` components of the ``s`` components of the row. Note, `nnz` is supplied by the user. 2. For each selected component, randomly set it either to ``-1/\\sqrt{\\text{nnz}}`` or ``1/\\sqrt{\\text{nnz}}`` with equal probability.