├── .gitignore ├── test ├── testgroups ├── runtests.jl ├── configuration.jl ├── patch.jl ├── reporting.jl ├── opt.jl ├── data.jl └── domain.jl ├── .JuliaFormatter.toml ├── docs ├── src │ ├── api.md │ ├── bugs.md │ ├── contributing.md │ ├── provided-binary.md │ ├── index.md │ ├── call-julia.md │ ├── build-a-binary.md │ ├── output.md │ ├── calling-another.md │ └── configuration.md ├── Project.toml └── make.jl ├── .github └── workflows │ ├── CompatHelper.yml │ ├── Release.yml │ └── CI.yml ├── config.toml ├── LICENSE ├── Project.toml ├── .releaserc ├── src ├── AllocationOpt.jl ├── configuration.jl ├── opt.jl ├── data.jl ├── domain.jl └── reporting.jl ├── README.md └── Manifest.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /data 3 | -------------------------------------------------------------------------------- /test/testgroups: -------------------------------------------------------------------------------- 1 | configuration 2 | data 3 | domain 4 | opt 5 | reporting 6 | -------------------------------------------------------------------------------- /.JuliaFormatter.toml: -------------------------------------------------------------------------------- 1 | style = "blue" 2 | margin = 92 3 | pipe_to_function_call = false 4 | -------------------------------------------------------------------------------- /docs/src/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ```@autodocs 4 | Modules = [AllocationOpt] 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | AllocationOpt = "e2472b13-1eb9-46f1-ac4b-64ea04136a8a" 3 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 4 | SemioticOpt = "a0c0fc10-8635-40d2-82ff-39f738735ee1" 5 | TheGraphData = "871720c8-5dfb-4fa2-998e-3fe6ebd08819" 6 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | using CSV 5 | using TheGraphData 6 | using JSON 7 | using Mocking 8 | using SemioticOpt 9 | using AllocationOpt 10 | using Test 11 | 12 | Mocking.activate() 13 | 14 | include("patch.jl") 15 | for f in readlines(joinpath(@__DIR__, "testgroups")) 16 | include(f * ".jl") 17 | end 18 | -------------------------------------------------------------------------------- /docs/src/bugs.md: -------------------------------------------------------------------------------- 1 | # Reporting Bugs 2 | 3 | If something breaks, we hope you'll submit a bug report! 4 | There are a few things we'll need from you to reproduce the failure mode you're experiencing. 5 | 6 | 1. Your config file 7 | 2. The CSVs (if any) that the Optimizer saved. 8 | 3. The stacktrace displaying the error message. 9 | 4. The version of the Allocation Optimizer you are running. 10 | 11 | These are a good place to start. 12 | We may ask you for more information as we investigate the bug. 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | workflow_dispatch: 6 | jobs: 7 | CompatHelper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 16 | run: julia -e 'using CompatHelper; CompatHelper.main()' 17 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" 2 | writedir = "data" 3 | readdir = "data" 4 | max_allocations = 10 5 | whitelist = [] 6 | blacklist = [] 7 | frozenlist = [] 8 | pinnedlist = [] 9 | allocation_lifetime = 28 10 | gas = 100 11 | min_signal = 100 12 | verbose = true 13 | num_reported_options = 2 14 | execution_mode = "none" 15 | opt_mode = "optimal" 16 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 17 | protocol_network = "arbitrum" 18 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 19 | -------------------------------------------------------------------------------- /docs/src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We're excited you are interested in contributing! 4 | Please follow the standards put forth in [ColPrac](https://github.com/SciML/ColPrac) to contribute to the Allocation Optimizer. 5 | 6 | Most importantly, we'd like to emphasise a few points. 7 | 8 | * PRs should do a single thing. 9 | * We'd prefer that you submit ten small PRs that are easy to understand as compared with one massive PR that requires a full day of review. 10 | * PRs should add tests which cover the new or fixed functionality. 11 | * PRs introducing breaking changes should make this clear when opening the PR. 12 | -------------------------------------------------------------------------------- /docs/src/provided-binary.md: -------------------------------------------------------------------------------- 1 | # Using The Provided Binary 2 | 3 | Using the provided binary has the advantage of being that only method that doesn't require that you use the Julia runtime. 4 | If you want to get running as quickly as possible, this is the path for you. 5 | 6 | !!! note 7 | We only release binaries for x86 Linux. We will not support any other architectures or operating systems. 8 | 9 | All you have to do is download the binary for the platform and release you want to use, unzip it, set up your configuration ([Configuration](@ref)) file, and run the binary from your terminal. 10 | 11 | ``` sh 12 | ./path/to/unzipped/folder/bin/AllocationOpt /path/to/your_config.toml 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using AllocationOpt 2 | using Documenter 3 | 4 | DocMeta.setdocmeta!(AllocationOpt, :DocTestSetup, :(using AllocationOpt); recursive=true) 5 | 6 | makedocs(; 7 | modules=[AllocationOpt], 8 | authors="The Graph Foundation", 9 | sitename="The Allocation Optimizer", 10 | format=Documenter.HTML(; 11 | prettyurls=get(ENV, "CI", "false") == "true", 12 | canonical="https://graphprotocol.github.io/allocation-optimizer", 13 | edit_link="main", 14 | assets=String[], 15 | ), 16 | pages=[ 17 | "Home" => "index.md", 18 | "Configuration" => "configuration.md", 19 | "Usage" => [ 20 | "provided-binary.md", 21 | "build-a-binary.md", 22 | "call-julia.md", 23 | "calling-another.md", 24 | ], 25 | "Understanding The Output" => "output.md", 26 | "API Reference" => "api.md", 27 | "Reporting Bugs" => "bugs.md", 28 | "Contributing" => "contributing.md", 29 | ], 30 | ) 31 | 32 | deploydocs(; 33 | repo="github.com/graphprotocol/allocation-optimizer", devbranch="main", devurl="latest" 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 The Graph Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: [completed] 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Semantic Release 17 | uses: cycjimmy/semantic-release-action@v3 18 | env: 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | semantic_version: 19.0.5 22 | extra_plugins: | 23 | @semantic-release/changelog 24 | @semantic-release/git 25 | @google/semantic-release-replace-plugin@1.2.0 26 | 27 | no-release: 28 | runs-on: ubuntu-latest 29 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 30 | steps: 31 | - run: | 32 | echo "Release skipped as previous stage failed." 33 | curl \ 34 | -X POST \ 35 | -H "Accept: application/vnd.github.v3+json" \ 36 | https://api.github.com/repos/octocat/hello-world/actions/runs/42/cancel 37 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "AllocationOpt" 2 | uuid = "e2472b13-1eb9-46f1-ac4b-64ea04136a8a" 3 | authors = ["The Graph Foundation"] 4 | version = "2.7.0" 5 | 6 | [deps] 7 | CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" 8 | Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" 9 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 10 | LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 11 | Mocking = "78c3b35d-d492-501b-9361-3d52fe80e533" 12 | PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" 13 | Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" 14 | SemioticOpt = "a0c0fc10-8635-40d2-82ff-39f738735ee1" 15 | SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" 16 | TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" 17 | TheGraphData = "871720c8-5dfb-4fa2-998e-3fe6ebd08819" 18 | TypedTables = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9" 19 | 20 | [compat] 21 | CSV = "0.10" 22 | Formatting = "0.4" 23 | JSON = "0.21" 24 | Mocking = "0.7" 25 | PackageCompiler = "2.1" 26 | Roots = "2.0" 27 | SemioticOpt = "^0.7" 28 | SplitApplyCombine = "1.2" 29 | TOML = "1.0" 30 | TheGraphData = "^0.2" 31 | TypedTables = "1.4" 32 | julia = "1.10" 33 | 34 | [extras] 35 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 36 | 37 | [targets] 38 | test = ["Test"] 39 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@google/semantic-release-replace-plugin", { 8 | "replacements": [ 9 | { 10 | "files": ["Project.toml"], 11 | "from": "version = \".*\"", 12 | "to": "version = \"${nextRelease.version}\"", 13 | "results": [ 14 | { 15 | "file": "Project.toml", 16 | "hasChanged": true, 17 | "numMatches": 1, 18 | "numReplacements": 1 19 | } 20 | ], 21 | "countMatches": true 22 | } 23 | ] 24 | } 25 | ], 26 | [ 27 | "@semantic-release/changelog", { 28 | "changelogTitle": "# Changelog" 29 | } 30 | ], 31 | [ 32 | "@semantic-release/git", { 33 | "assets": ["CHANGELOG.md", "Project.toml"], 34 | "message": "chore: release v${nextRelease.version}\n\n${nextRelease.notes}" 35 | } 36 | ], 37 | "@semantic-release/github" 38 | ], 39 | "repositoryUrl": "git@github.com:graphprotocol/allocation-optimizer.git" 40 | } 41 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = AllocationOpt 3 | ``` 4 | 5 | # The Allocation Optimizer 6 | 7 | This repository contains the code for the Indexer Allocation Optimizer. 8 | The goal of this project is to enable indexers to quickly determine how to allocate so as to maximise their indexing rewards. 9 | 10 | !!! warning 11 | We do *not* optimise for query fees directly, as we expect signal on a subgraph to be proportional to the query fees, as was the intention behind curation. 12 | Query fee information is also not public. 13 | It is local to each gateway. 14 | As a result, we will never be able to optimise with respect to query fees unless this changes. 15 | 16 | !!! note 17 | By default, `opt_mode="optimal"`. 18 | Because of our algorithm, `optimal` mode may take a long time to converge. 19 | If this is the case for you, you have two options. 20 | You can use `opt_mode=fastgas`, which runs a different algorithm. 21 | This algorithm is not guaranteed to find the optimal value, and may fail to ever converge (it could hang). 22 | However, it still considers gas unlike the third option `opt_mode=fastnogas`. 23 | This is your fastest option, but it won't take into account gas costs or your preferences for max allocations. 24 | This mode is appropriate when you have negligible gas fees and are okay with allocating to a large number of subgraphs. 25 | 26 | We will focus on usage of the code in this documentation. 27 | We refer you to these [blog posts](https://semiotic.ai/articles/indexer-allocation-optimisation/) for more technical details. 28 | If interested in how the code works, take a peek [Under The Hood](@ref)! 29 | 30 | There are a few different ways you can run the allocation optimizer. 31 | 32 | * [Using The Provided Binary](@ref) 33 | * [Calling From Julia](@ref) 34 | * [Build Your Own Binary](@ref) 35 | * [Calling From Another Language](@ref) 36 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | main 6 | pull_request: 7 | concurrency: 8 | # Skip intermediate builds: always. 9 | # Cancel intermediate builds: only if it is a pull request build. 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} 12 | jobs: 13 | test: 14 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | version: 20 | - '1.10' 21 | os: 22 | - ubuntu-latest 23 | arch: 24 | - x64 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: julia-actions/setup-julia@v1 28 | with: 29 | version: ${{ matrix.version }} 30 | arch: ${{ matrix.arch }} 31 | - uses: julia-actions/cache@v1 32 | - uses: julia-actions/julia-buildpkg@v1 33 | - uses: julia-actions/julia-runtest@v1 34 | - uses: julia-actions/julia-processcoverage@v1 35 | - uses: codecov/codecov-action@v2 36 | with: 37 | files: lcov.info 38 | docs: 39 | name: Documentation 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: julia-actions/setup-julia@v1 44 | with: 45 | version: '1.10' 46 | - uses: julia-actions/julia-buildpkg@v1 47 | - uses: julia-actions/julia-docdeploy@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} 51 | - run: | 52 | julia --project=docs -e ' 53 | using Documenter: DocMeta, doctest 54 | using AllocationOpt 55 | DocMeta.setdocmeta!(AllocationOpt, :DocTestSetup, :(using AllocationOpt); recursive=true) 56 | doctest(AllocationOpt)' 57 | -------------------------------------------------------------------------------- /test/configuration.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | @testset "configuration" begin 5 | @testset "configuredefaults!" begin 6 | config = Dict{String,Any}() 7 | @test_throws AssertionError AllocationOpt.configuredefaults!(config) 8 | 9 | config = Dict{String,Any}("id" => "a") 10 | config = AllocationOpt.configuredefaults!(config) 11 | @test isnothing(config["readdir"]) 12 | @test config["writedir"] == "." 13 | @test config["network_subgraph_endpoint"] == 14 | "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet" 15 | @test config["whitelist"] == String[] 16 | @test config["blacklist"] == String[] 17 | @test config["frozenlist"] == String[] 18 | @test config["pinnedlist"] == String[] 19 | @test config["allocation_lifetime"] == 28 20 | @test config["gas"] == 100 21 | @test config["min_signal"] == 100 22 | @test config["max_allocations"] == 10 23 | @test config["num_reported_options"] == 1 24 | @test config["execution_mode"] == "none" 25 | @test isnothing(config["indexer_url"]) 26 | @test !config["verbose"] 27 | @test config["opt_mode"] == "optimal" 28 | @test config["protocol_network"] == "mainnet" 29 | @test config["syncing_networks"] == ["mainnet"] 30 | 31 | config = Dict{String,Any}("id" => "a", "gas" => 0) 32 | config = AllocationOpt.configuredefaults!(config) 33 | @test config["gas"] == 0 34 | @test !config["verbose"] 35 | end 36 | 37 | @testset "readconfig" begin 38 | # Test roughly equivalent from test of TOML.jl parsefile 39 | dict = Dict{String,Any}("a" => 1) 40 | info = "a = 1" 41 | path, io = mktemp() 42 | write(io, info) 43 | close(io) 44 | val = AllocationOpt.readconfig(path) 45 | @test val == dict 46 | end 47 | 48 | @testset "formatconfig!" begin 49 | config = Dict("id" => "0xA") 50 | @test AllocationOpt.formatconfig!(config)["id"] == "0xa" 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /docs/src/call-julia.md: -------------------------------------------------------------------------------- 1 | # Calling From Julia 2 | 3 | 4 | If you're okay with having the Julia runtime on your computer, and you don't mind the precompilation time (around 3 seconds), this is the preferred way to run the Allocation Optimizer. 5 | In part, this is because access the to Julia runtime will enable you to add your own features if you'd like. 6 | You can even (hopefully) even submit some PRs with features to help other indexers! 7 | Compiling on your machine also allows Julia to specialise on idiosyncrasies of your hardware. 8 | For example, if your CPU supports [AVX-512](https://en.wikipedia.org/wiki/AVX-512), Julia will use those instructions to speed up your code beyond what the generic binary might give you. 9 | Again, the trade-off here is that you'll have to eat the precompilation time the first time you run the code after you start a new Julia session. 10 | 11 | That said, here's how to install and use the Allocation Optimizer from Julia. 12 | 13 | Install Julia! 14 | We prefer to use `juliaup`. 15 | You can install this via: 16 | 17 | ```bash 18 | curl -fsSL https://install.julialang.org | sh 19 | ``` 20 | 21 | !!! note 22 | As of writing this documentation, the latest version of Julia is v1.10. 23 | This the version the Allocation Optimizer currently uses, and the version `juliaup` will install by default. 24 | If `juliaup` begins to use v1.11, then you may need to use `juliaup` to manually install v1.10 via `juliaup add 1.10`. Then, you can either set the default to be v1.10 using `juliaup default 1.10`, or you can replace every time you see `julia` with `julia +1.10` below. 25 | 26 | 27 | Clone this repository and `cd` into it. 28 | 29 | ```bash 30 | git clone https://github.com/graphprotocol/allocation-optimizer.git 31 | cd allocation-optimizer 32 | ``` 33 | 34 | Start Julia. 35 | 36 | ```bash 37 | julia --project 38 | ``` 39 | 40 | Install the dependencies 41 | 42 | ```julia 43 | julia> ] 44 | pkg> instantiate 45 | ``` 46 | 47 | Set up your configuration file. 48 | See [Configuration](@ref) for details. 49 | 50 | From the Julia REPL (the TUI that comes up when you use `julia --project`), run the `main` function with the path to your config. 51 | 52 | ```julia 53 | julia> using AllocationOpt 54 | julia> path = "config.toml" # path to your config file 55 | julia> AllocationOpt.main(path) 56 | ``` 57 | 58 | !!! note 59 | If you are still in Pkg mode, `pkg>`, hitting *backspace* will bring you back into the REPL. 60 | -------------------------------------------------------------------------------- /docs/src/build-a-binary.md: -------------------------------------------------------------------------------- 1 | # Build Your Own Binary 2 | 3 | Compiling the binary yourself is an excellent way to use the Allocation Optimizer. 4 | You should use this route if a couple of reasons speak to you. 5 | 6 | 1. You'd prefer not to trust a random binary off the internet. 7 | 2. You have added your own features to the Allocation Optimizer, but would prefer the large, one-time cost of AOT compilation as opposed the small, every-time cost of JIT compilation. 8 | 9 | In this documentation, we'll take you through the process of generating an app binary. 10 | 11 | !!! note 12 | You can instead compile to a sysimage or to a library, but we don't natively support those ourselves. 13 | Look through the [PackageCompiler](https://julialang.github.io/PackageCompiler.jl/stable/index.html) documentation for steps if you would prefer one of those options.. 14 | 15 | 16 | Install Julia! 17 | We prefer to use `juliaup`. 18 | You can install this via: 19 | 20 | ```bash 21 | curl -fsSL https://install.julialang.org | sh 22 | ``` 23 | 24 | !!! note 25 | As of writing this documentation, the latest version of Julia is v1.10. 26 | This the version the Allocation Optimizer currently uses, and the version `juliaup` will install by default. 27 | If `juliaup` begins to use v1.11, then you may need to use `juliaup` to manually install v1.11 via `juliaup add 1.10`. Then, you can either set the default to be v1.10 using `juliaup default 1.10`, or you can replace every time you see `julia` with `julia +1.10` below. 28 | 29 | Clone this repository and `cd` into it. 30 | 31 | ```bash 32 | git clone https://github.com/graphprotocol/allocation-optimizer.git 33 | cd allocation-optimizer 34 | ``` 35 | 36 | Start Julia. 37 | 38 | ```bash 39 | julia --project 40 | ``` 41 | 42 | Install the dependencies 43 | 44 | ```julia 45 | julia> ] 46 | pkg> instantiate 47 | ``` 48 | 49 | Install a C-compiler such as [GCC](https://gcc.gnu.org/) or [Clang](https://clang.llvm.org/). 50 | 51 | From the Julia REPL (the TUI that comes up when you use `julia --project`), compile your app. 52 | 53 | ```julia 54 | julia> using PackageCompiler 55 | julia> create_app(".", "app") 56 | ``` 57 | !!! note 58 | If you are still in Pkg mode, `pkg>`, hitting *backspace* will bring you back into the REPL. 59 | 60 | Set up your configuration file. 61 | See [Configuration](@ref) for details. 62 | 63 | Run the binary pointing at the configuration TOML that you would like to use. 64 | 65 | ``` sh 66 | ./app/bin/AllocationOpt /path/to/your_config.toml 67 | ``` 68 | -------------------------------------------------------------------------------- /test/patch.jl: -------------------------------------------------------------------------------- 1 | read_csv_success_patch = @patch function TheGraphData.read(f; kwargs...) 2 | if f ∈ ("indexer.csv", "allocation.csv", "subgraph.csv", "network.csv") 3 | @info "TheGraphData.read stub => simulating success" 4 | return CSV.File(IOBuffer("X\nb\nc\na\nc")) 5 | else 6 | @info "TheGraphData.read stub => simulating failure" 7 | throw(ArgumentError("TheGraphData.read stub => simulating failure")) 8 | end 9 | end 10 | 11 | paginated_query_success_patch = @patch function paginated_query(v, a, f) 12 | if v == "subgraphDeployments" 13 | @info "paginated query stub ==> simulating subgraphs" 14 | return [ 15 | Dict( 16 | "ipfsHash" => "Qma", 17 | "signalledTokens" => "1", 18 | "stakedTokens" => "1", 19 | "deniedAt" => 0, 20 | ), 21 | Dict( 22 | "ipfsHash" => "Qmb", 23 | "signalledTokens" => "2", 24 | "stakedTokens" => "2", 25 | "deniedAt" => 0, 26 | ), 27 | ] 28 | end 29 | if v == "allocations" 30 | @info "paginated query stub ==> simulating allocations" 31 | return [ 32 | Dict( 33 | "subgraphDeployment" => Dict("ipfsHash" => "Qma"), 34 | "id" => "0xa", 35 | "allocatedTokens" => "1", 36 | ), 37 | ] 38 | end 39 | end 40 | 41 | query_success_patch = @patch function query(v, a, f) 42 | if v == "indexer" 43 | @info "query stub ==> simulating indexers" 44 | return [ 45 | Dict("delegatedTokens" => "1", "stakedTokens" => "10", "lockedTokens" => "100") 46 | ] 47 | end 48 | if v == "graphNetwork" 49 | @info "query stub ==> network parameters" 50 | return [ 51 | Dict( 52 | "totalTokensSignalled" => "100", 53 | "currentEpoch" => 1, 54 | "id" => "1", 55 | "networkGRTIssuancePerBlock" => "100", 56 | "epochLength" => 1, 57 | ), 58 | ] 59 | end 60 | end 61 | 62 | write_success_patch = @patch function TheGraphData.write(p, d) 63 | println("TheGraphData.write stub => simulating success") 64 | return p 65 | end 66 | 67 | writejson_success_patch = @patch function JSON.print(io, s) 68 | println("JSON.print stub => simulating success") 69 | return s 70 | end 71 | 72 | mutate_success_patch = @patch function mutate(v, a; kwargs...) 73 | println("mutate stub ==> simulating queueActions") 74 | return [Dict(v => a["actions"])] 75 | end 76 | -------------------------------------------------------------------------------- /src/AllocationOpt.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | module AllocationOpt 5 | 6 | using JSON 7 | using LinearAlgebra 8 | using Mocking 9 | using Roots 10 | using SemioticOpt 11 | using TheGraphData 12 | using TOML 13 | using TypedTables 14 | using Formatting 15 | 16 | import SplitApplyCombine as SAC 17 | 18 | include("configuration.jl") 19 | include("data.jl") 20 | include("domain.jl") 21 | include("opt.jl") 22 | include("reporting.jl") 23 | 24 | const fudgefactor = 1.0 # prevents divide by zero 25 | 26 | function julia_main()::Cint 27 | try 28 | main(ARGS) 29 | catch 30 | Base.invokelatest(Base.display_error, Base.catch_stack()) 31 | return 1 32 | end 33 | return 0 # if things finished successfully 34 | end 35 | 36 | main(args::Vector{String}) = main(first(args)) 37 | 38 | function main(path::AbstractString) 39 | # Read config and set defaults 40 | config = path |> readconfig |> configuredefaults! |> formatconfig! 41 | return main(config) 42 | end 43 | 44 | function main(config::Dict) 45 | # Read data 46 | i, a, s, n = AllocationOpt.read(config) 47 | 48 | # Write the data if it was queried rather than read from file 49 | isnothing(config["readdir"]) && write(i, a, s, n, config) 50 | 51 | # Get the subgraphs on which we can allocate 52 | fs = allocatablesubgraphs(s, config) 53 | 54 | # Get the indexer stake 55 | pinnedvec = pinned(fs, config) 56 | σpinned = pinnedvec |> sum 57 | σ = availablestake(Val(:indexer), i) - frozen(a, config) - σpinned 58 | @assert σ > 0 "No stake available to allocate with the configured frozenlist and pinnedlist" 59 | 60 | # Allocated tokens on filtered subgraphs 61 | Ω = stake(Val(:subgraph), fs) .+ fudgefactor 62 | 63 | # Signal on filtered subgraphs 64 | ψ = signal(Val(:subgraph), fs) 65 | 66 | # Signal on all subgraphs 67 | Ψ = signal(Val(:network), n) 68 | 69 | # New tokens issued over allocation lifetime 70 | Φ = newtokenissuance(n, config) 71 | 72 | # Get indices of subgraphs that can get indexing rewards 73 | rixs = deniedzeroixs(fs) 74 | 75 | # Get max number of allocations 76 | K = min(config["max_allocations"], length(rixs)) 77 | 78 | # Get gas cost in GRT 79 | g = config["gas"] 80 | 81 | # Get optimal values 82 | config["verbose"] && @info "Optimizing" 83 | xs, nonzeros, profitmatrix = optimize(Ω, ψ, σ, K, Φ, Ψ, g, rixs, config) 84 | 85 | # Add the pinned stake back in 86 | xs .= xs .+ pinnedvec 87 | 88 | # Ensure that the indexer stake is not exceeded 89 | σmax = σ + σpinned 90 | for x in sum(xs; dims=1) 91 | isnan(x) || 92 | x ≤ σmax || 93 | error("Tried to allocate more stake than is available by $(x - σmax)") 94 | end 95 | 96 | # Write the result values 97 | # Group by unique number of nonzeros 98 | groupixs = groupunique(nonzeros) 99 | groupixs = Dict(keys(groupixs) .=> values(groupixs)) 100 | 101 | config["verbose"] && @info "Writing results report" 102 | # For each set of nonzeros, find max profit (should be the same other than rounding) 103 | popts = bestprofitpernz.(values(groupixs), Ref(profitmatrix)) |> sortprofits! 104 | nreport = min(config["num_reported_options"], length(popts)) 105 | 106 | # Create JSON string 107 | strategies = 108 | strategydict.(popts[1:nreport], Ref(xs), Ref(nonzeros), Ref(fs), Ref(profitmatrix)) 109 | reportdata = JSON.json(Dict("strategies" => strategies)) 110 | 111 | # Write JSON string to file 112 | writejson(reportdata, config) 113 | 114 | config["verbose"] && @info "Executing best strategy" 115 | # Use config for using actionqueue or rules with the top profit batch 116 | ix = first(popts)[:index] 117 | execute(a, ix, fs, xs, profitmatrix, config) 118 | 119 | return nothing 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /docs/src/output.md: -------------------------------------------------------------------------------- 1 | # Understanding The Output 2 | 3 | After a successful run, the Allocation Optimizer will produce a JSON output in your `writedir` under the name *report.json*. 4 | Let's walk through how to read this file. 5 | 6 | This report contains the various strategies that the Allocation Optimizer recommends based on the network state and your config file. 7 | The `num_reported_options` field in your config will determine the maximum number of outputs in the report. 8 | 9 | Let's break down the following example *report.json* 10 | 11 | ```json 12 | { 13 | "strategies": [ 14 | { 15 | "num_allocations":2, 16 | "profit":229779.02830650925, 17 | "allocations": [ 18 | { 19 | "allocationAmount":"13539706.9", 20 | "profit":169430.57951963632, 21 | "deploymentID":"QmT2Y7SHXGRt7EKXzFPYMjnrgX64PQr86EjbsHoDLNzwLy" 22 | }, 23 | { 24 | "allocationAmount":"9194401.2", 25 | "profit":60348.44878687295, 26 | "deploymentID":"QmXT4PhR9pwAUqkxg6tgqR8xWUsTYN7V8UgmqhrrimcqXf" 27 | } 28 | ] 29 | }, 30 | { 31 | "num_allocations":1, 32 | "profit":190114.2718246217, 33 | "allocations": [ 34 | { 35 | "allocationAmount":"22734108.1", 36 | "profit":190114.2718246217, 37 | "deploymentID":"QmT2Y7SHXGRt7EKXzFPYMjnrgX64PQr86EjbsHoDLNzwLy" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ``` 44 | 45 | which the Optimizer generated from the following config. 46 | 47 | ```toml 48 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 49 | writedir = "data" 50 | readdir = "data" 51 | max_allocations = 2 52 | whitelist = [] 53 | blacklist = [] 54 | frozenlist = [] 55 | pinnedlist = [] 56 | allocation_lifetime = 28 57 | gas = 100 58 | min_signal = 100 59 | verbose = true 60 | num_reported_options = 2 61 | execution_mode = "rules" 62 | ``` 63 | 64 | !!! note 65 | If you use this same config, you could get a slightly different report depending on the network state. 66 | 67 | 68 | The JSON has two `strategies`. 69 | They are sorted by the one with the highest expected profit appearing first. 70 | In the first one here, we have two allocations, which you could see from the `num_allocations` field, or by counting the number of entries under the `allocations` field. 71 | At the top level of each strategy, `profit` describes the total expected profit in GRT for all allocations. 72 | 73 | !!! warning 74 | Notice in the config file that our allocation lifetime was 28 epochs. 75 | The profit is over that lifetime. 76 | However, it is impossible for us to know how the network state will evolve in the future. 77 | All results from the Optimizer assume that the network state will remain static. 78 | As a result, you should take these numbers with a grain of salt and use your knowledge as an indexer 79 | to determine whether the Optimizer's strategy could be tweaked in your favour. 80 | 81 | Then, we go into the `allocations`. 82 | Each entry in this list contains the amount of GRT the strategy wants you to stake, the expected profit of this allocation, and the subgraph deployment ID onto which you should make this allocation. 83 | 84 | In addition to the *report.json*, since we specified `execution_mode` as `"rules"` in the config, the Optimizer will print out indexer rules that you can run to execute the best strategy. 85 | In our case, 86 | 87 | ```bash 88 | graph indexer rules stop QmNoe4VQFSKAC3uiq48UQASg4QeqFVSKYYGwnxNjNi6GYX 89 | graph indexer rules stop QmRDGLp6BHwiH9HAE2NYEE3f7LrKuRqziHBv76trT4etgU 90 | graph indexer rules stop QmUVskWrz1ZiQZ76AtyhcfFDEH1ELnRpoyEhVL8p6NFTbR 91 | graph indexer rules stop QmXZiV6S13ha6QXq4dmaM3TB4CHcDxBMvGexSNu9Kc28EH 92 | graph indexer rules stop QmYN4ofRb5CUg1WdpLhhNTVCuiiAt29hBKGjTnnxYh9zYt 93 | graph indexer rules stop Qmaz1R8vcv9v3gUfksqiS9JUz7K9G8S5By3JYn8kTiiP5K 94 | graph indexer rules stop QmcBSr5R3K2M5tk8qeHFaX8pxAhdViYhcKD8ZegYuTcUhC 95 | graph indexer rules set QmT2Y7SHXGRt7EKXzFPYMjnrgX64PQr86EjbsHoDLNzwLy decisionBasis always allocationAmount 13539706.9 96 | graph indexer rules set QmXT4PhR9pwAUqkxg6tgqR8xWUsTYN7V8UgmqhrrimcqXf decisionBasis always allocationAmount 9194401.2 97 | ``` 98 | 99 | If instead you use `"actionqueue"` as the `execution_mode`, the Optimizer will populate your instance of the action queue. 100 | -------------------------------------------------------------------------------- /docs/src/calling-another.md: -------------------------------------------------------------------------------- 1 | # Calling From Another Language 2 | 3 | It's possible to call the Allocation Optimizer from another language. 4 | There are already some solutions out there that do this. 5 | In general, if you're going to take this approach, we assume that you know what you're doing. 6 | We won't hold your hand through the process as we don't want to officially support a multitude of different languages. 7 | Instead, we'll point you to resources you can use for various languages. 8 | 9 | !!! warning 10 | We don't recommend you use this approach, as you may have to delay integrating bugfixes and new features while you are figuring out how to integrate the new code. 11 | People using the mainline routes will always be able to more quickly access the updates we push. 12 | We especially don't recommend this route if you're not a developer. 13 | We will not support bugs that arise from integrating the tool into third-party scripts. 14 | 15 | * [C/C++](https://docs.julialang.org/en/v1/manual/embedding/) 16 | * [Python](https://docs.juliahub.com/PythonCall/WdXsa/0.9.12/) 17 | * [Rust](https://docs.rs/jlrs/latest/jlrs/) 18 | 19 | ## Under The Hood 20 | 21 | Before we let you go, we'll explain at a high-level the main function. 22 | Roughly speaking, we can break down this problem into the following steps. 23 | 24 | 1. Read the data, where from the network subgraph or from local CSVs. The currency used is GRT. 25 | 1. Filter the data based on the whitelist, blacklist, pinnedlist, and frozenlist 26 | 1. Get the values we care about for the indexing rewards 27 | * `σ` - The indexer's stake 28 | * `Ω` - A vector of floats containing the sum of the allocations of all other indexers on each subgraph in our data 29 | * `ψ` - A vector of floats representing the signal on each subgraph in our data. 30 | * `Ψ` - A float representing the total signal in the network 31 | * `K` - The maximum number of new allocations 32 | * `g` - The gas cost in GRT 33 | 1. Get the optimal allocation vectors for each `k ∈ 1,...,K` 34 | 1. Sort these by their profit 35 | 1. Report information about the most profitable vectors. 36 | 37 | The full code for the main function with comments follows. 38 | 39 | ```julia 40 | function main(config::Dict) 41 | # Read data 42 | i, a, s, n = AllocationOpt.read(config) 43 | 44 | # Write the data if it was queried rather than read from file 45 | isnothing(config["readdir"]) && write(i, a, s, n, config) 46 | 47 | # Get the subgraphs on which we can allocate 48 | fs = allocatablesubgraphs(s, config) 49 | 50 | # Get the indexer stake 51 | pinnedvec = pinned(fs, config) 52 | σpinned = pinnedvec |> sum 53 | σ = availablestake(Val(:indexer), i) - frozen(a, config) - σpinned 54 | @assert σ > 0 "No stake available to allocate with the configured frozenlist and pinnedlist" 55 | 56 | # Allocated tokens on filtered subgraphs 57 | Ω = stake(Val(:subgraph), fs) .+ fudgefactor 58 | 59 | # Signal on filtered subgraphs 60 | ψ = signal(Val(:subgraph), fs) 61 | 62 | # Signal on all subgraphs 63 | Ψ = signal(Val(:network), n) 64 | 65 | # New tokens issued over allocation lifetime 66 | Φ = newtokenissuance(n, config) 67 | 68 | # Get max number of allocations 69 | K = min(config["max_allocations"], length(fs)) 70 | 71 | # Get gas cost in GRT 72 | g = config["gas"] 73 | 74 | # Get optimal values 75 | config["verbose"] && @info "Optimizing" 76 | xs, nonzeros, profitmatrix = optimize(Ω, ψ, σ, K, Φ, Ψ, g) 77 | 78 | # Add the pinned stake back in 79 | xs .= xs .+ pinnedvec 80 | 81 | # Ensure that the indexer stake is not exceeded 82 | σmax = σ + σpinned 83 | for x in sum(xs; dims=1) 84 | x ≤ σmax || error("Tried to allocate more stake than is available") 85 | end 86 | 87 | # Write the result values 88 | # Group by unique number of nonzeros 89 | groupixs = groupunique(nonzeros) 90 | groupixs = Dict(keys(groupixs) .=> values(groupixs)) 91 | 92 | config["verbose"] && @info "Writing results report" 93 | # For each set of nonzeros, find max profit (should be the same other than rounding) 94 | popts = bestprofitpernz.(values(groupixs), Ref(profitmatrix)) |> sortprofits! 95 | nreport = min(config["num_reported_options"], length(popts)) 96 | 97 | # Create JSON string 98 | strategies = 99 | strategydict.(popts[1:nreport], Ref(xs), Ref(nonzeros), Ref(fs), Ref(profitmatrix)) 100 | reportdata = JSON.json(Dict("strategies" => strategies)) 101 | 102 | # Write JSON string to file 103 | writejson(reportdata, config) 104 | 105 | config["verbose"] && @info "Executing best strategy" 106 | # Use config for using actionqueue or rules with the top profit batch 107 | ix = first(popts)[:index] 108 | execute(a, ix, fs, xs, profitmatrix, config) 109 | 110 | return nothing 111 | end 112 | ``` 113 | 114 | For details on any of the specific functions called, look at the [API Reference](@ref). 115 | -------------------------------------------------------------------------------- /src/configuration.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | configuredefaults!(config::AbstractDict) 6 | 7 | Set default values for the config dictionary if the value was not specified in the config file. 8 | 9 | # Config Specification 10 | - `id::String`: The ID of the indexer for whom we're optimising. No default value. 11 | - `network_subgraph_endpoint::String`: The network subgraph endpoint to query. 12 | By default, `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` 13 | - `writedir::String`: The directory to which to write the results of optimisation. 14 | If don't specify `readdir`, `writedir` also specifies the path to which to save 15 | the input data tables. By default, `"."` 16 | - `readdir::Union{String, Nothing}`: The directory from which to read saved data tables. 17 | This speeds up the process as we won't have to query the network subgraph for the 18 | relevant data. If you don't specify `readdir`, we will query your specified 19 | `network_subgraph_endpoint` for the data and write it to CSV files in `writedir`. 20 | This way, you can use your previous `writedir` as your `readdir` in future runs. 21 | By default, `nothing` 22 | - `whitelist::Vector{String}`: A list of subgraph IPFS hashes that you want to consider 23 | as candidates to which to allocate. If you leave this empty, we'll assume all subgraphs 24 | are in the whitelist. By default, `String[]` 25 | - `blacklist::Vector{String}`: A list of subgraph IPFS hashes that you do not want to 26 | consider allocating to. For example, this list could include broken subgraphs or 27 | subgraphs that you don't want to index. By default, `String[]` 28 | - `frozenlist::Vector{String}`: If you have open allocations that you don't want to change, 29 | add the corresponding subgraph IPFS hashes to this list. By default, `String[]` 30 | - `pinnedlist::Vector{String}`: If you have subgraphs that you absolutely want to be 31 | allocated to, even if only with a negligible amount of GRT, add it to this list. 32 | By default, `String[]` 33 | - `allocation_lifetime::Integer`: The number of epochs for which you expect the allocations 34 | the optimiser finds to be open. By default, `28` 35 | - `gas::Real`: The estimated gas cost in GRT to open/close allocations. By default, `100` 36 | - `min_signal::Real`: The minimum amount of signal in GRT that must be on a subgraph 37 | in order for you to consider allocating to it. By default, `100` 38 | - `max_allocations::Integer`: The maximum number of new allocations you'd like the optimiser 39 | to consider opening. By default, `10` 40 | - `num_reported_options::Integer`: The number of proposed allocation strategies to report. 41 | For example, if you select `10` we'd report best 10 allocation strategies ranked by 42 | profit. By default, `1` 43 | - `verbose::Bool`: If true, the optimiser will print details about what it is doing to 44 | stdout. By default, `false` 45 | - `execution_mode::String`: How the optimiser should execute the allocation strategies it 46 | finds. Options are `"none"`, which won't do anything, `"actionqueue"`, which will 47 | push actions to the action queue, and `"rules"`, which will generate indexing rules. 48 | By default, `"none"` 49 | - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want 50 | to execute the allocation strategies on. If you specify `"actionqueue"`, you must also 51 | specify `indexer_url`. By default, `nothing` 52 | - `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does 53 | not consider gas costs and optimises allocation amount over all subgraph deployments. 54 | Second one is `"fastgas"`. This mode considers gas, may not find the optimal strategy and 55 | could potentially fail to converge. This mode is also used to the top 56 | `num_reported_options` allocation strategies. The final mode is `"optimal"`. 57 | This mode is slower, but it satisfies stronger optimality conditions. 58 | It will find strategies at least as good as `"fastgas"`, but not guaranteed to be better. 59 | By default, `"optimal"` 60 | mode to find the optimal allocation. By default, `"optimal"` 61 | - `protocol_network::String`: Defines the protocol network that allocation transactions 62 | should be sent to. The current protocol network options are "mainnet", "goerli", 63 | "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` 64 | - `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting 65 | the set of possible subgraphs. This list should match the networks available to your 66 | graph-node. By default, the list is a singleton of your protocol network 67 | 68 | ```julia 69 | julia> using AllocationOpt 70 | julia> config = Dict{String, Any}("id" => "0xa") 71 | julia> config = AllocationOpt.configuredefaults!(config) 72 | Dict{String, Any} with 16 entries: 73 | "execution_mode" => "none" 74 | "readdir" => nothing 75 | "writedir" => "." 76 | "num_reported_options" => 1 77 | "id" => "0xa" 78 | "pinnedlist" => String[] 79 | "indexer_url" => nothing 80 | "gas" => 100 81 | "allocation_lifetime" => 28 82 | "blacklist" => String[] 83 | "verbose" => false 84 | "min_signal" => 100 85 | "network_subgraph_endpoint" => "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet" 86 | "whitelist" => String[] 87 | "protocol_network" => "mainnet" 88 | "syncing_networks" => String["mainnet"] 89 | ``` 90 | """ 91 | function configuredefaults!(config::AbstractDict) 92 | @assert haskey(config, "id") 93 | setdefault!( 94 | config, 95 | "network_subgraph_endpoint", 96 | "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet", 97 | ) 98 | setdefault!(config, "writedir", ".") 99 | setdefault!(config, "readdir", nothing) 100 | setdefault!(config, "whitelist", String[]) 101 | setdefault!(config, "blacklist", String[]) 102 | setdefault!(config, "frozenlist", String[]) 103 | setdefault!(config, "pinnedlist", String[]) 104 | setdefault!(config, "allocation_lifetime", 28) 105 | setdefault!(config, "gas", 100) 106 | setdefault!(config, "min_signal", 100) 107 | setdefault!(config, "max_allocations", 10) 108 | setdefault!(config, "num_reported_options", 1) 109 | setdefault!(config, "indexer_url", nothing) 110 | setdefault!(config, "execution_mode", "none") 111 | setdefault!(config, "verbose", false) 112 | setdefault!(config, "opt_mode", "optimal") 113 | setdefault!(config, "protocol_network", "mainnet") 114 | setdefault!(config, "syncing_networks", String[config["protocol_network"]]) 115 | return config 116 | end 117 | 118 | """ 119 | formatconfig!(config::AbstractDict) 120 | 121 | Given a `config`, reformat values that need to be standardised. 122 | 123 | ```julia 124 | julia> using AllocationOpt 125 | julia> config = Dict("id" => "0xA") 126 | julia> AllocationOpt.formatconfig!(config) 127 | Dict{String, String} with 1 entry: 128 | "id" => "0xa" 129 | ``` 130 | """ 131 | function formatconfig!(config::AbstractDict) 132 | config["id"] = config["id"] |> lowercase 133 | return config 134 | end 135 | 136 | """ 137 | readconfig(p::AbstractString) 138 | 139 | Read the config file from path `p`. The config file must be specifed as a TOML. 140 | 141 | See [`configuredefaults!`](@ref) to see which fields you should specify in the config. 142 | 143 | ```julia 144 | julia> using AllocationOpt 145 | julia> path = "myconfig.toml" 146 | julia> config = AllocationOpt.readconfig(path) 147 | Dict{String, Any} with 13 entries: 148 | "execution_mode" => "none" 149 | "writedir" => "data" 150 | "num_reported_options" => 2 151 | "id" => "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 152 | "pinnedlist" => Union{}[] 153 | "gas" => 100 154 | "allocation_lifetime" => 28 155 | "blacklist" => Union{}[] 156 | "verbose" => true 157 | "min_signal" => 100 158 | "whitelist" => Union{}[] 159 | "max_allocations" => 5 160 | "frozenlist" => Union{}[] 161 | "protocol_network" => "mainnet" 162 | "syncing_networks" => String["mainnet", "gnosis"] 163 | ``` 164 | """ 165 | readconfig(p::AbstractString) = p |> TOML.parsefile 166 | -------------------------------------------------------------------------------- /test/reporting.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | @testset "reporting" begin 5 | @testset "groupunique" begin 6 | x = [1, 2, 1, 3, 2, 3] 7 | ixs = AllocationOpt.groupunique(x) 8 | @test ixs[[1]] == [1, 3] 9 | @test ixs[[2]] == [2, 5] 10 | @test ixs[[3]] == [4, 6] 11 | end 12 | @testset "bestprofitpernz" begin 13 | ixs = Dict([1] => [1], [2] => [2]) 14 | ps = [[2.5 5.0]; [2.5 1.0]] 15 | popts = AllocationOpt.bestprofitpernz.(values(ixs), Ref(ps)) 16 | @test popts[1] == (; :profit => 5.0, :index => 1) 17 | @test popts[2] == (; :profit => 6.0, :index => 2) 18 | 19 | ixs = Dict([1] => [1, 2]) 20 | ps = [[2.5 5.0]; [2.5 1.0]] 21 | popts = AllocationOpt.bestprofitpernz.(values(ixs), Ref(ps)) 22 | @test popts[1] == (; :profit => 6.0, :index => 2) 23 | end 24 | @testset "sortprofits!" begin 25 | popts = [(; :profit => 5.0, :index => 2), (; :profit => 6.0, :index => 1)] 26 | @test AllocationOpt.sortprofits!(popts)[1][:profit] == 6.0 27 | end 28 | @testset "reportingtable" begin 29 | s = flextable([ 30 | Dict("stakedTokens" => "1", "signalledTokens" => "2", "ipfsHash" => "Qma"), 31 | Dict("stakedTokens" => "2", "signalledTokens" => "1", "ipfsHash" => "Qmb"), 32 | ]) 33 | xs = [[2.5 5.0]; [2.5 0.0]] 34 | ps = [[3.0 5.0]; [3.0 0.0]] 35 | i = 2 36 | t = AllocationOpt.reportingtable(s, xs, ps, i) 37 | @test t.ipfshash == ["Qma"] 38 | @test t.amount == [5.0] 39 | @test t.profit == [5.0] 40 | end 41 | @testset "strategydict" begin 42 | popts = [(; :profit => 6.0, :index => 1), (; :profit => 5.0, :index => 2)] 43 | xs = [[2.5 5.0]; [2.5 0.0]] 44 | profits = [[3.0 5.0]; [3.0 0.0]] 45 | nonzeros = [2, 1] 46 | fs = flextable([ 47 | Dict("stakedTokens" => "1", "signalledTokens" => "0", "ipfsHash" => "Qma"), 48 | Dict("stakedTokens" => "2", "signalledTokens" => "0", "ipfsHash" => "Qmb"), 49 | ]) 50 | out = 51 | AllocationOpt.strategydict.( 52 | popts, Ref(xs), Ref(nonzeros), Ref(fs), Ref(profits) 53 | ) 54 | expected = [ 55 | Dict( 56 | "num_allocations" => 2, 57 | "profit" => 6.0, 58 | "allocations" => [ 59 | Dict( 60 | "deploymentID" => "Qma", 61 | "allocationAmount" => "2.5", 62 | "profit" => 3.0, 63 | ) 64 | Dict( 65 | "deploymentID" => "Qmb", 66 | "allocationAmount" => "2.5", 67 | "profit" => 3.0, 68 | ) 69 | ], 70 | ) 71 | Dict( 72 | "num_allocations" => 1, 73 | "profit" => 5.0, 74 | "allocations" => [ 75 | Dict( 76 | "deploymentID" => "Qma", 77 | "allocationAmount" => "5", 78 | "profit" => 5.0, 79 | ), 80 | ], 81 | ) 82 | ] 83 | @test out == expected 84 | end 85 | 86 | @testset "writejson" begin 87 | output = "{\"strategies\":[{\"num_allocations\":2,\"profit\":6.0,\"allocations\":[{\"allocationAmount\":\"2.5\",\"profit\":3.0,\"deploymentID\":\"Qma\"},{\"allocationAmount\":\"2.5\",\"profit\":3.0,\"deploymentID\":\"Qmb\"}]},{\"num_allocations\":1,\"profit\":5.0,\"allocations\":[{\"allocationAmount\":\"5\",\"profit\":5.0,\"deploymentID\":\"Qma\"}]}]}" 88 | config = Dict("writedir" => ".") 89 | apply(writejson_success_patch) do 90 | p = AllocationOpt.writejson(output, config) 91 | @test p == "./report.json" 92 | end 93 | rm("./report.json") 94 | end 95 | 96 | @testset "unallocate" begin 97 | a = flextable([Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa")]) 98 | t = flextable([Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb")]) 99 | 100 | config = Dict("frozenlist" => String[], "protocol_network" => "mainnet") 101 | cfg = Dict("frozenlist" => ["Qma"], "protocol_network" => "mainnet") 102 | 103 | @testset "none" begin 104 | @inferred AllocationOpt.unallocate_action(Val(:none), a, t, config) 105 | end 106 | 107 | @testset "rules" begin 108 | @inferred AllocationOpt.unallocate_action(Val(:rules), a, t, config) 109 | out = AllocationOpt.unallocate_action(Val(:rules), a, t, config) 110 | @test out == ["\e[0mgraph indexer rules stop Qma"] 111 | 112 | out = AllocationOpt.unallocate_action(Val(:rules), a, t, cfg) 113 | @test isempty(out) 114 | end 115 | 116 | @testset "actionqueue" begin 117 | apply(mutate_success_patch) do 118 | @inferred AllocationOpt.unallocate_action(Val(:actionqueue), a, t, config) 119 | end 120 | 121 | apply(mutate_success_patch) do 122 | out = AllocationOpt.unallocate_action(Val(:actionqueue), a, t, config) 123 | @test out == [ 124 | Dict( 125 | "status" => AllocationOpt.queued, 126 | "type" => AllocationOpt.unallocate, 127 | "allocationID" => "0xa", 128 | "deploymentID" => "Qma", 129 | "source" => "AllocationOpt", 130 | "reason" => "AllocationOpt", 131 | "priority" => 0, 132 | "protocolNetwork" => "mainnet", 133 | ), 134 | ] 135 | end 136 | 137 | apply(mutate_success_patch) do 138 | out = AllocationOpt.unallocate_action(Val(:actionqueue), a, t, cfg) 139 | @test isempty(out) 140 | end 141 | end 142 | end 143 | 144 | @testset "reallocate" begin 145 | a = flextable([Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa")]) 146 | t = flextable([ 147 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 148 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 149 | ]) 150 | config = Dict("protocol_network" => "mainnet") 151 | 152 | @testset "none" begin 153 | @inferred AllocationOpt.reallocate_action(Val(:none), a, t, config) 154 | end 155 | 156 | @testset "rules" begin 157 | @inferred AllocationOpt.reallocate_action(Val(:rules), a, t, config) 158 | out = AllocationOpt.reallocate_action(Val(:rules), a, t, config) 159 | @test out == [ 160 | "\e[0mgraph indexer rules stop Qma\n\e[1m\e[38;2;255;0;0;249mCheck allocation status being closed before submitting: \e[0mgraph indexer rules set Qma decisionBasis always allocationAmount 1", 161 | ] 162 | end 163 | 164 | @testset "actionqueue" begin 165 | apply(mutate_success_patch) do 166 | @inferred AllocationOpt.reallocate_action(Val(:actionqueue), a, t, config) 167 | end 168 | 169 | apply(mutate_success_patch) do 170 | out = AllocationOpt.reallocate_action(Val(:actionqueue), a, t, config) 171 | @test out == [ 172 | Dict( 173 | "status" => AllocationOpt.queued, 174 | "type" => AllocationOpt.reallocate, 175 | "allocationID" => "0xa", 176 | "deploymentID" => "Qma", 177 | "amount" => "1", 178 | "source" => "AllocationOpt", 179 | "reason" => "Expected profit: 0", 180 | "priority" => 0, 181 | "protocolNetwork" => "mainnet", 182 | ), 183 | ] 184 | end 185 | end 186 | end 187 | 188 | @testset "allocate" begin 189 | a = flextable([Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa")]) 190 | t = flextable([ 191 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 192 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 193 | ]) 194 | config = Dict("protocol_network" => "mainnet") 195 | 196 | @testset "none" begin 197 | @inferred AllocationOpt.allocate_action(Val(:none), a, t, config) 198 | end 199 | 200 | @testset "rules" begin 201 | @inferred AllocationOpt.allocate_action(Val(:rules), a, t, config) 202 | out = AllocationOpt.allocate_action(Val(:rules), a, t, config) 203 | @test out == [ 204 | "\e[0mgraph indexer rules set Qmb decisionBasis always allocationAmount 2" 205 | ] 206 | end 207 | 208 | @testset "actionqueue" begin 209 | apply(mutate_success_patch) do 210 | @inferred AllocationOpt.allocate_action(Val(:actionqueue), a, t, config) 211 | end 212 | 213 | apply(mutate_success_patch) do 214 | out = AllocationOpt.allocate_action(Val(:actionqueue), a, t, config) 215 | @test out == [ 216 | Dict( 217 | "status" => AllocationOpt.queued, 218 | "type" => AllocationOpt.allocate, 219 | "deploymentID" => "Qmb", 220 | "amount" => "2", 221 | "source" => "AllocationOpt", 222 | "reason" => "Expected profit: 0", 223 | "priority" => 0, 224 | "protocolNetwork" => "mainnet", 225 | ), 226 | ] 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/opt.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | @testset "opt" begin 5 | @testset "SemioticOpt analytic" begin 6 | x = zeros(2) 7 | Ω = [1.0, 1.0] 8 | ψ = [10.0, 10.0] 9 | σ = 5.0 10 | f(::Any) = 1:length(ψ) 11 | alg = AllocationOpt.AnalyticOpt(; 12 | x=x, Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 13 | ) 14 | alg = minimize!(f, alg) 15 | @test isapprox(SemioticOpt.x(alg), [2.5, 2.5]; atol=0.1) 16 | 17 | x = zeros(2) 18 | Ω = [1.0, 1.0] 19 | ψ = [0.0, 10.0] 20 | σ = 5.0 21 | alg = AllocationOpt.AnalyticOpt(; 22 | x=x, Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 23 | ) 24 | alg = minimize!(f, alg) 25 | @test isapprox(SemioticOpt.x(alg), [0.0, 5.0]; atol=0.1) 26 | 27 | x = zeros(2) 28 | Ω = [1.0, 10000.0] 29 | ψ = [10.0, 10.0] 30 | σ = 5.0 31 | alg = AllocationOpt.AnalyticOpt(; 32 | x=x, Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 33 | ) 34 | alg = minimize!(f, alg) 35 | @test isapprox(SemioticOpt.x(alg), [5.0, 0.0]; atol=0.1) 36 | end 37 | 38 | @testset "optimizeanalytic" begin 39 | Ω = [1.0, 1.0] 40 | ψ = [10.0, 10.0] 41 | σ = 5.0 42 | @test isapprox(AllocationOpt.optimizeanalytic(Ω, ψ, σ), [2.5, 2.5]; atol=0.1) 43 | 44 | Ω = [1.0, 1.0] 45 | ψ = [0.0, 10.0] 46 | σ = 5.0 47 | @test isapprox(AllocationOpt.optimizeanalytic(Ω, ψ, σ), [0.0, 5.0]; atol=0.1) 48 | 49 | Ω = [1.0, 10000.0] 50 | ψ = [10.0, 10.0] 51 | σ = 5.0 52 | @test isapprox(AllocationOpt.optimizeanalytic(Ω, ψ, σ), [5.0, 0.0]; atol=0.1) 53 | end 54 | 55 | @testset "lipschitzconstant" begin 56 | ψ = [0.0, 1.0] 57 | Ω = [1.0, 1.0] 58 | @test AllocationOpt.lipschitzconstant(ψ, Ω) == 2.0 59 | 60 | ψ = [0.0, 0.0] 61 | Ω = [1.0, 1.0] 62 | @test AllocationOpt.lipschitzconstant(ψ, Ω) == 0.0 63 | end 64 | 65 | @testset "nonzero" begin 66 | v = [1.0, 1.0] 67 | @test AllocationOpt.nonzero(v) == [1.0, 1.0] 68 | 69 | v = [1.0, 0.0] 70 | @test AllocationOpt.nonzero(v) == [1.0] 71 | end 72 | 73 | @testset "optimizek" begin 74 | @testset "fastgas" begin 75 | x₀ = [2.5, 2.5] 76 | Ω = [1.0, 1.0] 77 | ψ = [10.0, 10.0] 78 | σ = 5.0 79 | k = 1 80 | Φ = 1.0 81 | Ψ = 20.0 82 | @test AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) == [5.0, 0.0] 83 | 84 | x₀ = [2.5, 2.5] 85 | Ω = [1.0, 1.0] 86 | ψ = [10.0, 10.0] 87 | σ = 5.0 88 | k = 2 89 | Φ = 1.0 90 | Ψ = 20.0 91 | @test AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) == [2.5, 2.5] 92 | end 93 | 94 | @testset "optimal" begin 95 | Ω = [1.0, 1.0] 96 | ψ = [10.0, 10.0] 97 | σ = 5.0 98 | k = 1 99 | Φ = 1.0 100 | Ψ = 20.0 101 | g = 0.01 102 | x₀ = zeros(length(Ω)) 103 | x = AllocationOpt.optimizek(Val(:optimal), x₀, Ω, ψ, σ, k, Φ, Ψ, g) 104 | @test x == [5.0, 0.0] 105 | 106 | Ω = [1.0, 1.0] 107 | ψ = [10.0, 10.0] 108 | σ = 5.0 109 | k = 2 110 | Φ = 1.0 111 | Ψ = 20.0 112 | g = 0.01 113 | x₀ = zeros(length(Ω)) 114 | x = AllocationOpt.optimizek(Val(:optimal), x₀, Ω, ψ, σ, k, Φ, Ψ, g) 115 | @test x == [2.5, 2.5] 116 | end 117 | end 118 | 119 | @testset "optimize" begin 120 | @testset "fastnogas" begin 121 | rixs = [1, 2] 122 | Ω = [1.0, 1.0] 123 | ψ = [10.0, 10.0] 124 | σ = 5.0 125 | K = 2 126 | Φ = 1.0 127 | Ψ = 20.0 128 | g = 0.0 129 | xs, nonzeros, profits = AllocationOpt.optimize( 130 | Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs 131 | ) 132 | @test xs == [2.5; 2.5;;] 133 | @test nonzeros == [2] 134 | @test isapprox(profits, [0.35; 0.35]; atol=0.1) 135 | 136 | rixs = [2] 137 | Ω = [1.0, 1.0] 138 | ψ = [10.0, 10.0] 139 | σ = 5.0 140 | K = 1 141 | Φ = 1.0 142 | Ψ = 20.0 143 | g = 0.0 144 | xs, nonzeros, profits = AllocationOpt.optimize( 145 | Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs 146 | ) 147 | @test xs == [0.0; 5.0;;] 148 | @test nonzeros == [1] 149 | @test isapprox(profits, [0.0, 0.41]; atol=0.1) 150 | end 151 | 152 | @testset "fastgas" begin 153 | rixs = [1, 2] 154 | Ω = [1.0, 1.0] 155 | ψ = [10.0, 10.0] 156 | σ = 5.0 157 | K = 2 158 | Φ = 1.0 159 | Ψ = 20.0 160 | g = 0.01 161 | xs, nonzeros, profits = AllocationOpt.optimize( 162 | Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs 163 | ) 164 | @test xs == [[5.0 2.5]; [0.0 2.5]] 165 | @test nonzeros == [1, 2] 166 | @test isapprox(profits, [[0.41 0.35]; [0.0 0.35]]; atol=0.1) 167 | 168 | rixs = [2] 169 | Ω = [1.0, 1.0] 170 | ψ = [10.0, 10.0] 171 | σ = 5.0 172 | K = 1 173 | Φ = 1.0 174 | Ψ = 20.0 175 | g = 0.01 176 | xs, nonzeros, profits = AllocationOpt.optimize( 177 | Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs 178 | ) 179 | @test xs == [0.0; 5.0;;] 180 | @test nonzeros == [1] 181 | @test isapprox(profits, [0.0, 0.41]; atol=0.1) 182 | end 183 | 184 | @testset "optimal" begin 185 | rixs = [1, 2] 186 | Ω = [1.0, 1.0] 187 | ψ = [10.0, 10.0] 188 | σ = 5.0 189 | K = 2 190 | Φ = 1.0 191 | Ψ = 20.0 192 | g = 0.01 193 | xs, nonzeros, profits = AllocationOpt.optimize( 194 | Val(:optimal), Ω, ψ, σ, K, Φ, Ψ, g, rixs 195 | ) 196 | @test xs == [[5.0 2.5]; [0.0 2.5]] 197 | @test nonzeros == [1, 2] 198 | @test isapprox(profits, [[0.41 0.35]; [0.0 0.35]]; atol=0.1) 199 | 200 | rixs = [2] 201 | Ω = [1.0, 1.0] 202 | ψ = [10.0, 10.0] 203 | σ = 5.0 204 | K = 1 205 | Φ = 1.0 206 | Ψ = 20.0 207 | g = 0.01 208 | xs, nonzeros, profits = AllocationOpt.optimize( 209 | Val(:optimal), Ω, ψ, σ, K, Φ, Ψ, g, rixs 210 | ) 211 | @test xs == [0.0; 5.0;;] 212 | @test nonzeros == [1] 213 | @test isapprox(profits, [0.0, 0.41]; atol=0.1) 214 | 215 | # Test Early stopping 216 | # Should run the final iteration 217 | rixs = [1, 2, 3, 4] 218 | Ω = [1.0, 1.0, 1.0, 1.0] 219 | ψ = [10.0, 10.0, 0.01, 0.01] 220 | σ = 5.0 221 | K = 4 222 | Φ = 1.0 223 | Ψ = 20.0 224 | g = 0.01 225 | xs, nonzeros, profits = AllocationOpt.optimize( 226 | Val(:optimal), Ω, ψ, σ, K, Φ, Ψ, g, rixs 227 | ) 228 | @test xs ≈ [ 229 | [5.0 2.5 2.5 0.0] 230 | [0.0 2.5 2.5 0.0] 231 | [0.0 0.0 0.0 0.0] 232 | [0.0 0.0 0.0 0.0] 233 | ] 234 | @test nonzeros == [1, 2, 2, 1] 235 | @test isapprox( 236 | profits, 237 | [ 238 | [0.406667 0.347143 0.347143 0.0] 239 | [0.0 0.347143 0.347143 0.0] 240 | [0.0 0.0 0.0 0.0] 241 | [0.0 0.0 0.0 0.0] 242 | ]; 243 | atol=0.1, 244 | ) 245 | end 246 | 247 | @testset "dispatch" begin 248 | config = Dict("opt_mode" => "fastnogas") 249 | rixs = [1, 2] 250 | Ω = [1.0, 1.0] 251 | ψ = [10.0, 10.0] 252 | σ = 5.0 253 | K = 2 254 | Φ = 1.0 255 | Ψ = 20.0 256 | g = 0.01 257 | xs, nonzeros, profits = AllocationOpt.optimize( 258 | Ω, ψ, σ, K, Φ, Ψ, g, rixs, config 259 | ) 260 | @test xs == [2.5; 2.5;;] 261 | @test nonzeros == [2] 262 | @test isapprox(profits, [0.35; 0.35]; atol=0.1) 263 | 264 | config = Dict("opt_mode" => "fastgas") 265 | rixs = [1, 2] 266 | Ω = [1.0, 1.0] 267 | ψ = [10.0, 10.0] 268 | σ = 5.0 269 | K = 2 270 | Φ = 1.0 271 | Ψ = 20.0 272 | g = 0.01 273 | xs, nonzeros, profits = AllocationOpt.optimize( 274 | Ω, ψ, σ, K, Φ, Ψ, g, rixs, config 275 | ) 276 | @test xs == [[5.0 2.5]; [0.0 2.5]] 277 | @test nonzeros == [1, 2] 278 | @test isapprox(profits, [[0.41 0.35]; [0.0 0.35]]; atol=0.1) 279 | 280 | config = Dict("opt_mode" => "optimal") 281 | rixs = [1, 2] 282 | Ω = [1.0, 1.0] 283 | ψ = [10.0, 10.0] 284 | σ = 5.0 285 | K = 2 286 | Φ = 1.0 287 | Ψ = 20.0 288 | g = 0.01 289 | xs, nonzeros, profits = AllocationOpt.optimize( 290 | Ω, ψ, σ, K, Φ, Ψ, g, rixs, config 291 | ) 292 | @test xs == [[5.0 2.5]; [0.0 2.5]] 293 | @test nonzeros == [1, 2] 294 | @test isapprox(profits, [[0.41 0.35]; [0.0 0.35]]; atol=0.1) 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AllocationOpt 2 | 3 | [![Latest](https://img.shields.io/badge/docs-latest-purple.svg)](https://graphprotocol.github.io/allocation-optimizer/latest/) 4 | [![Build Status](https://github.com/graphprotocol/allocation-optimizer/actions/workflows/CI.yml/badge.svg?branch=)](https://github.com/graphprotocol/allocation-optimizer/actions/workflows/CI.yml?query=branch%3A) 5 | [![Coverage](https://codecov.io/gh/graphprotocol/allocation-optimizer/branch/main/graph/badge.svg)](https://codecov.io/gh/graphprotocol/allocation-optimizer) 6 | [![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) 7 | 8 | 9 | AllocationOpt is a library for optimising the stake distribution for The Graph indexers for indexing rewards [The Graph Protocol](https://thegraph.com/en/). 10 | 11 | For details on installation and usage, visit our [documentation](https://graphprotocol.github.io/allocation-optimizer/latest). 12 | For the underlying optimisation method, visit our [blog post](https://semiotic.ai/articles/indexer-allocation-optimisation/). 13 | 14 | ## Usage 15 | 16 | Run the provided binary pointing at the configuration TOML that you would like to use. 17 | 18 | ``` sh 19 | ./app/bin/AllocationOpt /path/to/your_config.toml 20 | ``` 21 | 22 | **NOTE:** This binary only works for x86 Linux. 23 | If you are you a different operating system or architecture, please see the documentation for other options. 24 | 25 | **IMPORTANT:** By default, `opt_mode="optimal"`. 26 | Because of our algorithm, `optimal` mode may take a long time to converge. 27 | If this is the case for you, you have two options. 28 | You can use `opt_mode=fastgas`, which runs a different algorithm. 29 | This algorithm is not guaranteed to find the optimal value, and may fail to ever converge (it could hang). 30 | However, it still considers gas unlike the third option `opt_mode=fastnogas`. 31 | This is your fastest option, but it won't take into account gas costs or your preferences for max allocations. 32 | This mode is appropriate when you have negligible gas fees and are okay with allocating to a large number of subgraphs. 33 | 34 | ## Configuration 35 | 36 | An example configuration TOML file might look as below. 37 | 38 | ``` toml 39 | id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" 40 | writedir = "data" 41 | max_allocations = 10 42 | whitelist = [] 43 | blacklist = [] 44 | frozenlist = [] 45 | pinnedlist = [] 46 | allocation_lifetime = 28 47 | gas = 100 48 | min_signal = 100 49 | verbose = true 50 | num_reported_options = 2 51 | execution_mode = "none" 52 | opt_mode = "optimal" 53 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 54 | protocol_network = "arbitrum" 55 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 56 | ``` 57 | 58 | ### Detailed Field Descriptions 59 | 60 | - `id::String`: The ID of the indexer for whom we're optimising. No default value. 61 | - `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer 62 | support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the 63 | provided API serves the query requests. If unspecified, 64 | `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` 65 | - `writedir::String`: The directory to which to write the results of optimisation. 66 | If don't specify `readdir`, `writedir` also specifies the path to which to save 67 | the input data tables. If unspecified, `"."` 68 | - `readdir::Union{String, Nothing}`: The directory from which to read saved data tables. 69 | This speeds up the process as we won't have to query the network subgraph for the 70 | relevant data. If you don't specify `readdir`, we will query your specified 71 | `network_subgraph_endpoint` for the data and write it to CSV files in `writedir`. 72 | This way, you can use your previous `writedir` as your `readdir` in future runs. 73 | If unspecified, `nothing` 74 | - `whitelist::Vector{String}`: A list of subgraph IPFS hashes that you want to consider 75 | as candidates to which to allocate. If you leave this empty, we'll assume all subgraphs 76 | are in the whitelist. If unspecified, `String[]` 77 | - `blacklist::Vector{String}`: A list of subgraph IPFS hashes that you do not want to 78 | consider allocating to. For example, this list could include broken subgraphs or 79 | subgraphs that you don't want to index. If unspecified, `String[]` 80 | - `frozenlist::Vector{String}`: If you have open allocations that you don't want to change, 81 | add the corresponding subgraph IPFS hashes to this list. If unspecified, `String[]` 82 | - `pinnedlist::Vector{String}`: If you have subgraphs that you absolutely want to be 83 | allocated to, even if only with a negligible amount of GRT, add it to this list. 84 | If unspecified, `String[]` 85 | - `allocation_lifetime::Integer`: The number of epochs for which you expect the allocations 86 | the optimiser finds to be open. If unspecified, `28` 87 | - `gas::Real`: The estimated gas cost in GRT to open/close allocations. If unspecified, `100` 88 | - `min_signal::Real`: The minimum amount of signal in GRT that must be on a subgraph 89 | in order for you to consider allocating to it. If unspecified, `100` 90 | - `max_allocations::Integer`: The maximum number of new allocations you'd like the optimiser 91 | to consider opening. If unspecified, `10` 92 | - `num_reported_options::Integer`: The number of proposed allocation strategies to report. 93 | For example, if you select `10` we'd report best 10 allocation strategies ranked by 94 | profit. Options are reported to a *report.json* in your `writedir`. If unspecified, `1` 95 | - `verbose::Bool`: If true, the optimiser will print details about what it is doing to 96 | stdout. If unspecified, `false` 97 | - `execution_mode::String`: How the optimiser should execute the allocation strategies it 98 | finds. Options are `"none"`, which won't do anything, `"actionqueue"`, which will 99 | push actions to the action queue, and `"rules"`, which will generate indexing rules. 100 | If unspecified, `"none"` 101 | - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want 102 | to execute the allocation strategies on. If you specify `"actionqueue"`, you must also 103 | specify `indexer_url`. If unspecified, `nothing` 104 | - `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does 105 | not consider gas costs and optimises allocation amount over all subgraph deployments. 106 | Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and 107 | could potentially fail to converge. This mode is also used to the top 108 | `num_reported_options` allocation strategies. The final mode is `"optimal"`. 109 | This mode is slower, but it satisfies stronger optimality conditions. 110 | It will find strategies at least as good as `"fast"`, but not guaranteed to be better. 111 | By default, `"optimal"` 112 | - `protocol_network::String`: Defines the protocol network that allocation transactions 113 | should be sent to. The current protocol network options are "mainnet", "goerli", 114 | "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` 115 | - `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting 116 | the set of possible subgraphs. This list should match the networks available to your 117 | graph-node. By default, the list is a singleton of your protocol network 118 | 119 | ### Example Configurations 120 | 121 | #### ActionQueue 122 | 123 | Set `execution_mode` to `"actionqueue"` and provide an `indexer_url`. 124 | 125 | ``` toml 126 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 127 | writedir = "data" 128 | readdir = "data" 129 | max_allocations = 10 130 | whitelist = [] 131 | blacklist = [] 132 | frozenlist = [] 133 | pinnedlist = [] 134 | allocation_lifetime = 28 135 | gas = 100 136 | min_signal = 100 137 | verbose = true 138 | num_reported_options = 2 139 | execution_mode = "actionqueue" 140 | indexer_url = "https://localhost:8000" 141 | ``` 142 | 143 | #### Indexer Rules 144 | 145 | Change `execution_mode` to `"rules"`. 146 | 147 | ``` toml 148 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 149 | writedir = "data" 150 | readdir = "data" 151 | max_allocations = 10 152 | whitelist = [] 153 | blacklist = [] 154 | frozenlist = [] 155 | pinnedlist = [] 156 | allocation_lifetime = 28 157 | gas = 100 158 | min_signal = 100 159 | verbose = true 160 | num_reported_options = 2 161 | execution_mode = "rules" 162 | ``` 163 | 164 | #### Query Data Instead of Reading Local CSVs 165 | 166 | Just don't specify the `readdir`. 167 | 168 | ``` toml 169 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 170 | writedir = "data" 171 | max_allocations = 10 172 | whitelist = [] 173 | blacklist = [] 174 | frozenlist = [] 175 | pinnedlist = [] 176 | allocation_lifetime = 28 177 | gas = 100 178 | min_signal = 100 179 | verbose = true 180 | num_reported_options = 2 181 | execution_mode = "none" 182 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 183 | protocol_network = "arbitrum" 184 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 185 | ``` 186 | 187 | #### Quiet Mode 188 | 189 | We set `verbose` to `false` here to surpress info messages. 190 | 191 | ``` toml 192 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 193 | writedir = "data" 194 | readdir = "data" 195 | max_allocations = 10 196 | whitelist = [] 197 | blacklist = [] 198 | frozenlist = [] 199 | pinnedlist = [] 200 | allocation_lifetime = 28 201 | gas = 100 202 | min_signal = 100 203 | verbose = false 204 | num_reported_options = 2 205 | execution_mode = "none" 206 | ``` 207 | 208 | #### Whitelisting Subgraphs 209 | 210 | Add some subgraph deployment IDs to the `whitelist`. 211 | If, in addition or instead you want to use `blacklist`, `frozenlist`, or `pinnedlist`, you can 212 | similarly add subgraph deployment IDs to those lists. 213 | Notice that we did not change `max_allocations` here. 214 | If `max_allocations` exceeds the number of available subgraphs (2 in this case), the code will 215 | treat the number of available subgraphs as `max_allocations`. 216 | 217 | ``` toml 218 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 219 | writedir = "data" 220 | readdir = "data" 221 | max_allocations = 10 222 | whitelist = [ 223 | "QmUVskWrz1ZiQZ76AtyhcfFDEH1ELnRpoyEhVL8p6NFTbR", 224 | "QmcBSr5R3K2M5tk8qeHFaX8pxAhdViYhcKD8ZegYuTcUhC" 225 | ] 226 | blacklist = [] 227 | frozenlist = [] 228 | pinnedlist = [] 229 | allocation_lifetime = 28 230 | gas = 100 231 | min_signal = 100 232 | verbose = false 233 | num_reported_options = 2 234 | execution_mode = "none" 235 | ``` 236 | -------------------------------------------------------------------------------- /test/data.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | @testset "data" begin 5 | @testset "squery" begin 6 | config = Dict("syncing_networks" => ["mainnet"]) 7 | v, a, f = AllocationOpt.squery(config) 8 | @test v == "subgraphDeployments" 9 | @test f == ["ipfsHash", "signalledTokens", "stakedTokens", "deniedAt"] 10 | @test a == Dict{String,Union{Dict{String,Dict{String,Vector{String}}},String}}( 11 | "where" => Dict("manifest_" => Dict("network_in" => ["mainnet"])) 12 | ) 13 | end 14 | 15 | @testset "iquery" begin 16 | id = "0xa" 17 | v, a, f = AllocationOpt.iquery(id) 18 | @test v == "indexer" 19 | @test f == ["delegatedTokens", "stakedTokens", "lockedTokens"] 20 | @test a == Dict{String,Union{Dict{String,String},String,Int64}}("id" => id) 21 | end 22 | 23 | @testset "aquery" begin 24 | config = Dict("id" => "0xa") 25 | v, a, f = AllocationOpt.aquery(config["id"]) 26 | @test v == "allocations" 27 | @test f == ["allocatedTokens", "id", "subgraphDeployment{ipfsHash}"] 28 | @test a == Dict{String,Union{Dict{String,String},String}}( 29 | "where" => Dict("status" => "Active", "indexer" => config["id"]) 30 | ) 31 | end 32 | 33 | @testset "nquery" begin 34 | v, a, f = AllocationOpt.nquery() 35 | @test v == "graphNetwork" 36 | @test f == [ 37 | "id", 38 | "networkGRTIssuancePerBlock", 39 | "epochLength", 40 | "totalTokensSignalled", 41 | "currentEpoch", 42 | ] 43 | @test a == Dict("id" => 1) 44 | end 45 | 46 | @testset "savenames" begin 47 | paths = ( 48 | "mypath/indexer.csv", 49 | "mypath/allocation.csv", 50 | "mypath/subgraph.csv", 51 | "mypath/network.csv", 52 | ) 53 | path = "mypath" 54 | vals = AllocationOpt.savenames(path) 55 | for (v, p) in zip(vals, paths) 56 | @test v == p 57 | end 58 | end 59 | 60 | @testset "correcttypes!" begin 61 | @testset "indexer" begin 62 | i = flextable([ 63 | Dict("stakedTokens" => "1", "delegatedTokens" => "0", "lockedTokens" => "0") 64 | ]) 65 | AllocationOpt.correcttypes!(Val(:indexer), i) 66 | @test i.stakedTokens == [1e-18] 67 | @test i.delegatedTokens == [0] 68 | @test i.lockedTokens == [0] 69 | end 70 | 71 | @testset "subgraph" begin 72 | s = flextable([ 73 | Dict( 74 | "stakedTokens" => "1", 75 | "signalledTokens" => "0", 76 | "ipfsHash" => "Qma", 77 | "deniedAt" => 0, 78 | ), 79 | Dict( 80 | "stakedTokens" => "2", 81 | "signalledTokens" => "0", 82 | "ipfsHash" => "Qmb", 83 | "deniedAt" => 0, 84 | ), 85 | Dict( 86 | "stakedTokens" => "3", 87 | "signalledTokens" => "0", 88 | "ipfsHash" => "Qmc", 89 | "deniedAt" => 0, 90 | ), 91 | ]) 92 | AllocationOpt.correcttypes!(Val(:subgraph), s) 93 | @test s.stakedTokens == [1e-18, 2e-18, 3e-18] 94 | @test s.signalledTokens == [0, 0, 0] 95 | @test s.ipfsHash == ["Qma", "Qmb", "Qmc"] 96 | @test s.deniedAt == [0, 0, 0] 97 | end 98 | 99 | @testset "allocation" begin 100 | a = flextable([ 101 | Dict( 102 | "allocatedTokens" => "1", 103 | "subgraphDeployment.ipfsHash" => "Qma", 104 | "id" => "0xa", 105 | ), 106 | Dict( 107 | "allocatedTokens" => "2", 108 | "subgraphDeployment.ipfsHash" => "Qmb", 109 | "id" => "0xb", 110 | ), 111 | Dict( 112 | "allocatedTokens" => "3", 113 | "subgraphDeployment.ipfsHash" => "Qmc", 114 | "id" => "0xc", 115 | ), 116 | ]) 117 | AllocationOpt.correcttypes!(Val(:allocation), a) 118 | @test a.allocatedTokens == [1e-18, 2e-18, 3e-18] 119 | @test getproperty(a, Symbol("subgraphDeployment.ipfsHash")) == 120 | ["Qma", "Qmb", "Qmc"] 121 | end 122 | 123 | @testset "network" begin 124 | n = flextable([ 125 | Dict( 126 | "totalTokensSignalled" => "100", 127 | "currentEpoch" => 1, 128 | "id" => "1", 129 | "networkGRTIssuancePerBlock" => "100", 130 | "epochLength" => 1, 131 | ), 132 | ]) 133 | AllocationOpt.correcttypes!(Val(:network), n) 134 | @test n.totalTokensSignalled == [1e-16] 135 | @test n.currentEpoch == [1] 136 | @test n.id == ["1"] 137 | @test n.networkGRTIssuancePerBlock == [1e-16] 138 | @test n.epochLength == [1] 139 | end 140 | 141 | @testset "all" begin 142 | i = flextable([ 143 | Dict("stakedTokens" => "1", "delegatedTokens" => "0", "lockedTokens" => "0") 144 | ]) 145 | s = flextable([ 146 | Dict( 147 | "stakedTokens" => "1", 148 | "signalledTokens" => "0", 149 | "ipfsHash" => "Qma", 150 | "deniedAt" => 0, 151 | ), 152 | Dict( 153 | "stakedTokens" => "2", 154 | "signalledTokens" => "0", 155 | "ipfsHash" => "Qmb", 156 | "deniedAt" => 0, 157 | ), 158 | Dict( 159 | "stakedTokens" => "3", 160 | "signalledTokens" => "0", 161 | "ipfsHash" => "Qmc", 162 | "deniedAt" => 0, 163 | ), 164 | ]) 165 | a = flextable([ 166 | Dict( 167 | "allocatedTokens" => "1", 168 | "subgraphDeployment.ipfsHash" => "Qma", 169 | "id" => "0xa", 170 | ), 171 | Dict( 172 | "allocatedTokens" => "2", 173 | "subgraphDeployment.ipfsHash" => "Qmb", 174 | "id" => "0xb", 175 | ), 176 | Dict( 177 | "allocatedTokens" => "3", 178 | "subgraphDeployment.ipfsHash" => "Qmc", 179 | "id" => "0xc", 180 | ), 181 | ]) 182 | n = flextable([ 183 | Dict( 184 | "totalTokensSignalled" => "100", 185 | "currentEpoch" => 1, 186 | "id" => "1", 187 | "networkGRTIssuancePerBlock" => "100", 188 | "epochLength" => 1, 189 | ), 190 | ]) 191 | i, a, s, n = AllocationOpt.correcttypes!(i, a, s, n) 192 | @test i.stakedTokens == [1e-18] 193 | @test i.delegatedTokens == [0] 194 | @test i.lockedTokens == [0] 195 | @test s.stakedTokens == [1e-18, 2e-18, 3e-18] 196 | @test s.signalledTokens == [0, 0, 0] 197 | @test s.ipfsHash == ["Qma", "Qmb", "Qmc"] 198 | @test s.deniedAt == [0, 0, 0] 199 | @test a.allocatedTokens == [1e-18, 2e-18, 3e-18] 200 | @test getproperty(a, Symbol("subgraphDeployment.ipfsHash")) == 201 | ["Qma", "Qmb", "Qmc"] 202 | @test n.totalTokensSignalled == [1e-16] 203 | @test n.currentEpoch == [1] 204 | @test n.id == ["1"] 205 | @test n.networkGRTIssuancePerBlock == [1e-16] 206 | @test n.epochLength == [1] 207 | end 208 | end 209 | 210 | @testset "read" begin 211 | @testset "from files" begin 212 | config = Dict("verbose" => false, "readdir" => "") 213 | apply(read_csv_success_patch) do 214 | i, a, s, n = AllocationOpt.read(config) 215 | @test i.X == ["b", "c", "a", "c"] 216 | end 217 | 218 | config = Dict("verbose" => false, "readdir" => "foo") 219 | apply(read_csv_success_patch) do 220 | @test_throws ArgumentError AllocationOpt.read(config) 221 | end 222 | end 223 | 224 | @testset "from network subgraph" begin 225 | config = Dict( 226 | "id" => "0xa", 227 | "verbose" => false, 228 | "network_subgraph_endpoint" => "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet", 229 | "readdir" => nothing, 230 | "syncing_networks" => ["mainnet"], 231 | ) 232 | apply(paginated_query_success_patch) do 233 | apply(query_success_patch) do 234 | i, a, s, n = AllocationOpt.read(config) 235 | @test i.stakedTokens == [1e-17] 236 | @test s.signalledTokens == [1e-18, 2e-18] 237 | @test s.stakedTokens == [0.0, 2e-18] 238 | @test n.totalTokensSignalled == [1e-16] 239 | end 240 | end 241 | end 242 | end 243 | 244 | @testset "write" begin 245 | config = Dict("verbose" => false, "writedir" => "tmp") 246 | t = flextable([Dict("foo" => 1, "bar" => 2)]) 247 | i, a, s, n = repeat([t], 4) 248 | apply(write_success_patch) do 249 | ps = AllocationOpt.write(i, a, s, n, config) 250 | @test ps == [ 251 | "tmp/indexer.csv", 252 | "tmp/allocation.csv", 253 | "tmp/subgraph.csv", 254 | "tmp/network.csv", 255 | ] 256 | end 257 | end 258 | 259 | @testset "subtractindexer!" begin 260 | s = flextable([ 261 | Dict("ipfsHash" => "Qmb", "stakedTokens" => 20), 262 | Dict("ipfsHash" => "Qma", "stakedTokens" => 10), 263 | Dict("ipfsHash" => "Qmc", "stakedTokens" => 5), 264 | ]) 265 | a = flextable([ 266 | Dict( 267 | "allocatedTokens" => 5, 268 | "subgraphDeployment.ipfsHash" => "Qma", 269 | "id" => "0xa", 270 | ), 271 | Dict( 272 | "allocatedTokens" => 10, 273 | "subgraphDeployment.ipfsHash" => "Qmb", 274 | "id" => "0xb", 275 | ), 276 | ]) 277 | a, s = AllocationOpt.subtractindexer!(a, s) 278 | @test s.stakedTokens == [5, 10, 5] 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /test/domain.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | @testset "domain" begin 5 | @testset "togrt" begin 6 | @test AllocationOpt.togrt("1000000000000000000") == 1.0 7 | end 8 | 9 | @testset "accessors" begin 10 | @testset "network" begin 11 | n = flextable([ 12 | Dict( 13 | "id" => 1, 14 | "networkGRTIssuancePerBlock" => 1, 15 | "epochLength" => 28, 16 | "totalTokensSignalled" => 2, 17 | "currentEpoch" => 1, 18 | ), 19 | ]) 20 | @test AllocationOpt.blockissuance(Val(:network), n) == 1 21 | @test AllocationOpt.blocksperepoch(Val(:network), n) == 28 22 | @test AllocationOpt.signal(Val(:network), n) == 2 23 | @test AllocationOpt.currentepoch(Val(:network), n) == 1 24 | end 25 | 26 | @testset "allocation" begin 27 | x = flextable([ 28 | Dict( 29 | "allocatedTokens" => 1, 30 | "subgraphDeployment.ipfsHash" => "Qma", 31 | "id" => "0xa", 32 | ), 33 | ]) 34 | @test AllocationOpt.ipfshash(Val(:allocation), x) == ["Qma"] 35 | @test AllocationOpt.stake(Val(:allocation), x) == [1] 36 | @test AllocationOpt.id(Val(:allocation), x) == ["0xa"] 37 | end 38 | 39 | @testset "subgraph" begin 40 | x = flextable([ 41 | Dict( 42 | "stakedTokens" => 1, 43 | "ipfsHash" => "Qma", 44 | "signalledTokens" => 2, 45 | "deniedAt" => 0, 46 | ), 47 | ]) 48 | @test AllocationOpt.ipfshash(Val(:subgraph), x) == ["Qma"] 49 | @test AllocationOpt.stake(Val(:subgraph), x) == [1] 50 | @test AllocationOpt.signal(Val(:subgraph), x) == [2] 51 | @test AllocationOpt.deniedat(Val(:subgraph), x) == [0] 52 | end 53 | 54 | @testset "indexer" begin 55 | x = flextable([ 56 | Dict("stakedTokens" => 10, "delegatedTokens" => 5, "lockedTokens" => 1) 57 | ]) 58 | @test AllocationOpt.stake(Val(:indexer), x) == 10 59 | @test AllocationOpt.delegation(Val(:indexer), x) == 5 60 | @test AllocationOpt.locked(Val(:indexer), x) == 1 61 | end 62 | end 63 | 64 | @testset "availablestake" begin 65 | x = flextable([ 66 | Dict("stakedTokens" => 10, "delegatedTokens" => 20, "lockedTokens" => 5) 67 | ]) 68 | @test AllocationOpt.availablestake(Val(:indexer), x) == 25 69 | end 70 | 71 | @testset "frozen" begin 72 | a = flextable([ 73 | Dict( 74 | "subgraphDeployment.ipfsHash" => "Qma", 75 | "allocatedTokens" => 5, 76 | "id" => "0xa", 77 | ), 78 | Dict( 79 | "subgraphDeployment.ipfsHash" => "Qmb", 80 | "allocatedTokens" => 10, 81 | "id" => "0xb", 82 | ), 83 | ]) 84 | config = Dict("frozenlist" => ["Qma", "Qmb"]) 85 | @test AllocationOpt.frozen(a, config) == 15 86 | config = Dict("frozenlist" => ["Qmb"]) 87 | @test AllocationOpt.frozen(a, config) == 10 88 | config = Dict("frozenlist" => []) 89 | @test AllocationOpt.frozen(a, config) == 0 90 | end 91 | 92 | @testset "deniedzeroixs" begin 93 | s = flextable([ 94 | Dict("ipfsHash" => "Qma", "signalledTokens" => 5.0, "deniedAt" => 0), 95 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 10.0, "deniedAt" => 10), 96 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 15.0, "deniedAt" => 0), 97 | ]) 98 | @test AllocationOpt.deniedzeroixs(s) == [1, 3] 99 | end 100 | 101 | @testset "pinned" begin 102 | s = flextable([ 103 | Dict("ipfsHash" => "Qma", "signalledTokens" => 5.0), 104 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 10.0), 105 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 15.0), 106 | ]) 107 | config = Dict("pinnedlist" => ["Qma", "Qmb"]) 108 | @test AllocationOpt.pinned(s, config) == [0.1, 0.1, 0.0] 109 | config = Dict("pinnedlist" => ["Qmb"]) 110 | @test AllocationOpt.pinned(s, config) == [0.0, 0.1, 0.0] 111 | config = Dict("pinnedlist" => []) 112 | @test AllocationOpt.pinned(s, config) == [0.0, 0.0, 0.0] 113 | end 114 | 115 | @testset "allocatablesubgraphs" begin 116 | s = flextable([ 117 | Dict("ipfsHash" => "Qma", "signalledTokens" => 5.0), 118 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 10.0), 119 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 15.0), 120 | ]) 121 | 122 | config = Dict( 123 | "whitelist" => String[], 124 | "blacklist" => String[], 125 | "frozenlist" => String["Qma", "Qmb"], 126 | "pinnedlist" => String[], 127 | "min_signal" => 0.0, 128 | ) 129 | fs = AllocationOpt.allocatablesubgraphs(s, config) 130 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qmc"] 131 | 132 | config = Dict( 133 | "whitelist" => String[], 134 | "blacklist" => String["Qma"], 135 | "frozenlist" => String["Qmb"], 136 | "pinnedlist" => String[], 137 | "min_signal" => 0.0, 138 | ) 139 | fs = AllocationOpt.allocatablesubgraphs(s, config) 140 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qmc"] 141 | 142 | config = Dict( 143 | "whitelist" => String["Qmb", "Qmc"], 144 | "blacklist" => String[], 145 | "frozenlist" => String[], 146 | "pinnedlist" => String[], 147 | "min_signal" => 0.0, 148 | ) 149 | fs = AllocationOpt.allocatablesubgraphs(s, config) 150 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qmb", "Qmc"] 151 | 152 | config = Dict( 153 | "whitelist" => String["Qmb", "Qmc"], 154 | "blacklist" => String["Qma"], 155 | "frozenlist" => String[], 156 | "pinnedlist" => String[], 157 | "min_signal" => 0.0, 158 | ) 159 | fs = AllocationOpt.allocatablesubgraphs(s, config) 160 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qmb", "Qmc"] 161 | 162 | config = Dict( 163 | "whitelist" => String[], 164 | "blacklist" => String[], 165 | "frozenlist" => String[], 166 | "pinnedlist" => String[], 167 | "min_signal" => 0.0, 168 | ) 169 | fs = AllocationOpt.allocatablesubgraphs(s, config) 170 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qma", "Qmb", "Qmc"] 171 | 172 | config = Dict( 173 | "whitelist" => String[], 174 | "blacklist" => String[], 175 | "frozenlist" => String[], 176 | "pinnedlist" => String[], 177 | "min_signal" => 7.0, 178 | ) 179 | fs = AllocationOpt.allocatablesubgraphs(s, config) 180 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qmb", "Qmc"] 181 | 182 | config = Dict( 183 | "whitelist" => String["Qma"], 184 | "blacklist" => String[], 185 | "frozenlist" => String[], 186 | "pinnedlist" => String["Qmb"], 187 | "min_signal" => 0.0, 188 | ) 189 | fs = AllocationOpt.allocatablesubgraphs(s, config) 190 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qma", "Qmb"] 191 | 192 | config = Dict( 193 | "whitelist" => String[], 194 | "blacklist" => String[], 195 | "frozenlist" => String[], 196 | "pinnedlist" => String["Qmb"], 197 | "min_signal" => 0.0, 198 | ) 199 | fs = AllocationOpt.allocatablesubgraphs(s, config) 200 | @test AllocationOpt.ipfshash(Val(:subgraph), fs) == ["Qma", "Qmb", "Qmc"] 201 | end 202 | 203 | @testset "newtokenissuance" begin 204 | n = flextable([ 205 | Dict( 206 | "id" => 1, 207 | "networkGRTIssuancePerBlock" => 1, 208 | "epochLength" => 1, 209 | "totalTokensSignalled" => 2, 210 | "currentEpoch" => 1, 211 | ), 212 | ]) 213 | config = Dict("allocation_lifetime" => 0) 214 | @test AllocationOpt.newtokenissuance(n, config) == 0 215 | 216 | n = flextable([ 217 | Dict( 218 | "id" => 1, 219 | "networkGRTIssuancePerBlock" => 1, 220 | "epochLength" => 0, 221 | "totalTokensSignalled" => 2, 222 | "currentEpoch" => 1, 223 | ), 224 | ]) 225 | config = Dict("allocation_lifetime" => 1) 226 | @test AllocationOpt.newtokenissuance(n, config) == 0 227 | 228 | n = flextable([ 229 | Dict( 230 | "id" => 1, 231 | "networkGRTIssuancePerBlock" => 0, 232 | "epochLength" => 1, 233 | "totalTokensSignalled" => 2, 234 | "currentEpoch" => 1, 235 | ), 236 | ]) 237 | config = Dict("allocation_lifetime" => 1) 238 | @test AllocationOpt.newtokenissuance(n, config) == 0 239 | 240 | n = flextable([ 241 | Dict( 242 | "id" => 1, 243 | "networkGRTIssuancePerBlock" => 1, 244 | "epochLength" => 1, 245 | "totalTokensSignalled" => 2, 246 | "currentEpoch" => 1, 247 | ), 248 | ]) 249 | config = Dict("allocation_lifetime" => 1) 250 | @test AllocationOpt.newtokenissuance(n, config) == 1 251 | end 252 | 253 | @testset "indexingreward" begin 254 | @testset "scalar input" begin 255 | ψ = 0.0 256 | Ω = 1.0 257 | Φ = 1.0 258 | Ψ = 2.0 259 | x = 1.0 260 | @test AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) == 0.0 261 | 262 | ψ = 1.0 263 | Ω = 1.0 264 | Φ = 1.0 265 | Ψ = 2.0 266 | x = 1.0 267 | @test AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) == 0.25 268 | end 269 | 270 | @testset "vector input" begin 271 | ψ = [0.0, 1.0] 272 | Ω = [1.0, 1.0] 273 | Φ = 1.0 274 | Ψ = 2.0 275 | x = [1.0, 0.0] 276 | @test AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) == 0.0 277 | 278 | ψ = [0.0, 1.0] 279 | Ω = [1.0, 1.0] 280 | Φ = 1.0 281 | Ψ = 2.0 282 | x = [0.0, 1.0] 283 | @test AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) == 0.25 284 | 285 | ψ = [1.0, 1.0] 286 | Ω = [1.0, 1.0] 287 | Φ = 1.0 288 | Ψ = 2.0 289 | x = [1.0, 1.0] 290 | @test AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) == 0.5 291 | end 292 | 293 | @testset "specifying ixs" begin 294 | ixs = Int32[2] 295 | ψ = [0.0, 1.0] 296 | Ω = [1.0, 1.0] 297 | Φ = 1.0 298 | Ψ = 2.0 299 | x = [0.0, 1.0] 300 | @test AllocationOpt.indexingreward(ixs, x, Ω, ψ, Φ, Ψ) == 0.25 301 | 302 | ixs = Int32[1] 303 | ψ = [0.0, 1.0] 304 | Ω = [1.0, 1.0] 305 | Φ = 1.0 306 | Ψ = 2.0 307 | x = [0.0, 1.0] 308 | @test AllocationOpt.indexingreward(ixs, x, Ω, ψ, Φ, Ψ) == 0.0 309 | end 310 | end 311 | 312 | @testset "profit" begin 313 | @testset "individual input" begin 314 | r = 10 315 | g = 1 316 | @test AllocationOpt.profit(r, g) == 9 317 | 318 | r = 0 319 | g = 1 320 | @test AllocationOpt.profit(r, g) == 0 321 | end 322 | end 323 | end 324 | -------------------------------------------------------------------------------- /src/opt.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | AnalyticOpt{ 6 | T<:Real, 7 | V<:AbstractArray{T}, 8 | U<:AbstractArray{T}, 9 | A<:AbstractArray{T}, 10 | S<:AbstractVector{<:Hook}, 11 | } <: SemioticOpt.OptAlgorithm 12 | 13 | Optimise the indexing reward analytically. 14 | 15 | # Fields 16 | - `x::V` is the current best guess for the solution. Typically zeros. 17 | - `Ω::U` is the allocation vector of other indexers. 18 | - `ψ::A` is the signal vector. 19 | - `σ::T` is the stake. 20 | - `hooks::S` are the hooks 21 | 22 | # Example 23 | ```julia 24 | julia> using AllocationOpt 25 | julia> using SemioticOpt 26 | julia> x = zeros(2) 27 | julia> Ω = [1.0, 1.0] 28 | julia> ψ = [10.0, 10.0] 29 | julia> σ = 5.0 30 | julia> alg = AllocationOpt.AnalyticOpt(; 31 | x=x, Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 32 | ) 33 | julia> f = x -> x # This doesn't matter. `f` isn't used by the algorithm. 34 | julia> alg = minimize!(f, alg) 35 | julia> SemioticOpt.x(alg) 36 | 2-element Vector{Float64}: 37 | 2.5 38 | 2.5 39 | ``` 40 | """ 41 | Base.@kwdef struct AnalyticOpt{ 42 | T<:Real, 43 | V<:AbstractArray{T}, 44 | U<:AbstractArray{T}, 45 | A<:AbstractArray{T}, 46 | S<:AbstractVector{<:Hook}, 47 | } <: SemioticOpt.OptAlgorithm 48 | x::V 49 | Ω::U 50 | ψ::A 51 | σ::T 52 | hooks::S 53 | end 54 | 55 | """ 56 | x(a::AnalyticOpt) 57 | 58 | Return the current best guess for the solution. 59 | """ 60 | SemioticOpt.x(a::AnalyticOpt) = a.x 61 | """ 62 | x!(a::AnalyticOpt, v) 63 | 64 | In-place setting of `a.x` to `v` 65 | 66 | See [`SemioticOpt.x`](@ref). 67 | """ 68 | function SemioticOpt.x!(a::AnalyticOpt, v) 69 | a.x .= v 70 | return a 71 | end 72 | SemioticOpt.hooks(a::AnalyticOpt) = a.hooks 73 | 74 | """ 75 | iteration(f::Function, a::AnalyticOpt) 76 | 77 | Perform the analytic optimisation. 78 | """ 79 | function SemioticOpt.iteration(f::Function, a::AnalyticOpt) 80 | ixs = f(SemioticOpt.x(a)) 81 | Ω = a.Ω[ixs] 82 | ψ = a.ψ[ixs] 83 | σ = a.σ 84 | v = dual(Ω, ψ, σ) 85 | x = primal(Ω, ψ, v) 86 | y = σsimplex(x, σ) 87 | return y 88 | end 89 | 90 | """ 91 | optimizeanalytic(Ω, ψ, σ) 92 | 93 | Optimise analytically over existing allocation vector `Ω`, signals `ψ`, and stake `σ`. 94 | 95 | ```julia 96 | julia> using AllocationOpt 97 | julia> Ω = [1.0, 7.0] 98 | julia> ψ = [10.0, 5.0] 99 | julia> σ = 5.0 100 | julia> AllocationOpt.optimizeanalytic(Ω, ψ, σ) 101 | 2-element Vector{Float64}: 102 | 3.5283092056122474 103 | 1.4716907943877526 104 | ``` 105 | """ 106 | function optimizeanalytic(Ω, ψ, σ) 107 | f(::Any) = 1:length(ψ) 108 | alg = AllocationOpt.AnalyticOpt(; 109 | x=zero(ψ), Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 110 | ) 111 | minimize!(f, alg) 112 | y = SemioticOpt.x(alg) 113 | return y 114 | end 115 | 116 | """ 117 | primal(Ω, ψ, ν) 118 | 119 | Analytic solution of the primal form of the optimisation problem given signals `ψ`, 120 | allocations `Ω`, and a dual solution vector `ν`. 121 | 122 | !!! note 123 | You should probably not use this function directly. Use [`optimizeanalytic`](@ref) instead. 124 | """ 125 | primal(Ω, ψ, ν) = max.(0.0, .√(ψ .* Ω / ν) - Ω) 126 | 127 | """ 128 | dual(Ω, ψ, σ) 129 | 130 | Analytic solution of the dual form of the optimisation problem given signals `ψ`, 131 | allocation vector `Ω`, and stake `σ`. 132 | 133 | !!! note 134 | You should probably not use this function directly. Use [`optimizeanalytic`](@ref) instead. 135 | """ 136 | function dual(Ω, ψ, σ) 137 | lower_bound = eps(Float64) 138 | upper_bound = (sum(.√(ψ .* Ω)))^2 / σ 139 | sol = find_zero( 140 | x -> sum(max.(0.0, .√(ψ .* Ω / x) .- Ω)) - σ, 141 | (lower_bound, upper_bound), 142 | Roots.Brent(), 143 | ) 144 | return sol 145 | end 146 | 147 | """ 148 | optimize(Ω, ψ, σ, K, Φ, Ψ, g, rixs, config::AbstractDict) 149 | 150 | Find the optimal solution vector given allocations of other indexers `Ω`, signals 151 | `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`, and gas in grt `g`. 152 | `rixs` are the indices of subgraphs that are eligible to receive indexing rewards. 153 | 154 | 155 | Dispatches to [`optimize`](@ref) with the `opt_mode` key. 156 | 157 | If `opt_mode` is `fastgas`, then run projected gradient descent with GSSP and Halpern. 158 | If `opt_mode` is `fastnogas`, then run analytics solution over all eligible subgraphs. 159 | If `opt_mode` is `optimal`, then run Pairwise Greedy Optimisation. 160 | 161 | ```julia 162 | julia> using AllocationOpt 163 | julia> config = Dict("opt_mode" => "fastgas") 164 | julia> rixs = [1, 2] 165 | julia> Ω = [1.0, 1.0] 166 | julia> ψ = [10.0, 10.0] 167 | julia> σ = 5.0 168 | julia> K = 2 169 | julia> Φ = 1.0 170 | julia> Ψ = 20.0 171 | julia> g = 0.01 172 | julia> xs, nonzeros, profits = AllocationOpt.optimize(Ω, ψ, σ, K, Φ, Ψ, g, rixs, config) 173 | ([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) 174 | ``` 175 | """ 176 | function optimize(Ω, ψ, σ, K, Φ, Ψ, g, rixs, config::AbstractDict) 177 | return optimize(Val(Symbol(config["opt_mode"])), Ω, ψ, σ, K, Φ, Ψ, g, rixs) 178 | end 179 | 180 | """ 181 | optimize(::Val{:fastnogas}, Ω, ψ, σ, _, Φ, Ψ, g, rixs) 182 | 183 | Find the analytic optimal vector for given allocations of other indexers `Ω`, signals 184 | `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`. 185 | `g` is the gas, but it is not used since analytic optimisation assumes 0 gas fees. 186 | `rixs` are the indices of subgraphs that are eligible to receive indexing rewards. 187 | 188 | ```julia 189 | julia> using AllocationOpt 190 | julia> rixs = [1, 2] 191 | julia> Ω = [1.0, 1.0] 192 | julia> ψ = [10.0, 10.0] 193 | julia> σ = 5.0 194 | julia> Φ = 1.0 195 | julia> Ψ = 20.0 196 | julia> g = 0.01 197 | julia> xs, nonzeros, profits = AllocationOpt.optimize(Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs) 198 | ([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) 199 | ``` 200 | """ 201 | function optimize(::Val{:fastnogas}, Ω, ψ, σ, _, Φ, Ψ, g, rixs) 202 | if g != 0 203 | @warn "fastnogas mode ignores the gas cost you set in your config" 204 | end 205 | 206 | # Helper function to compute profit 207 | f = x -> profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), zero(typeof(g))) 208 | 209 | # Only use the eligible subgraphs 210 | _Ω = @view Ω[rixs] 211 | _ψ = @view ψ[rixs] 212 | 213 | # Get the analytic solution 214 | _xopt = optimizeanalytic(_Ω, _ψ, σ) 215 | 216 | xopt = zeros(length(Ω), 1) 217 | xopt[rixs, :] .= _xopt 218 | 219 | # Preallocate solution vectors for in-place operations 220 | profits = Matrix{Float64}(undef, length(xopt), 1) 221 | nonzeros = Vector{Int32}(undef, 1) 222 | 223 | # Compute non-zeros and profits 224 | nonzeros[1] = xopt[:] |> nonzero |> length 225 | profits[:, 1] .= f(xopt) 226 | 227 | return xopt, nonzeros, profits 228 | end 229 | 230 | """ 231 | optimize(::Val{:fastgas}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) 232 | 233 | Find the optimal vectors for k ∈ [1,`K`] given allocations of other indexers `Ω`, signals 234 | `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`, and gas in grt `g`. 235 | `rixs` are the indices of subgraphs that are eligible to receive indexing rewards. 236 | 237 | ```julia 238 | julia> using AllocationOpt 239 | julia> rixs = [1, 2] 240 | julia> Ω = [1.0, 1.0] 241 | julia> ψ = [10.0, 10.0] 242 | julia> σ = 5.0 243 | julia> K = 2 244 | julia> Φ = 1.0 245 | julia> Ψ = 20.0 246 | julia> g = 0.01 247 | julia> xs, nonzeros, profits = AllocationOpt.optimize(Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs) 248 | ([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) 249 | ``` 250 | """ 251 | function optimize(val::Val{:fastgas}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) 252 | # Helper function to compute profit 253 | f = x -> profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), g) 254 | 255 | # Only use the eligible subgraphs 256 | _Ω = @view Ω[rixs] 257 | _ψ = @view ψ[rixs] 258 | 259 | # Get the anchor point for Halpern iteration 260 | _xopt = optimizeanalytic(_Ω, _ψ, σ) 261 | 262 | xopt = zeros(length(Ω)) 263 | xopt[rixs] .= _xopt 264 | 265 | # Preallocate solution vectors for in-place operations 266 | x = repeat(xopt, 1, K) 267 | profits = Matrix{Float64}(undef, length(xopt), K) 268 | nonzeros = Vector{Int32}(undef, K) 269 | 270 | # Optimize 271 | @inbounds for k in 1:K 272 | x[rixs, k] .= AllocationOpt.optimizek(val, x[rixs, k], _Ω, _ψ, σ, k, Φ, Ψ) 273 | nonzeros[k] = x[:, k] |> nonzero |> length 274 | profits[:, k] .= f(x[:, k]) 275 | end 276 | 277 | return x, nonzeros, profits 278 | end 279 | 280 | """ 281 | optimizek(::Val{:fastgas}, x₀, Ω, ψ, σ, k, Φ, Ψ) 282 | 283 | Find the optimal `k` sparse vector given initial value `x₀`, allocations of other indexers 284 | `Ω`, signals `ψ`, available stake `σ`, new tokens issued `Φ`, and total signal `Ψ`. 285 | 286 | ```julia 287 | julia> using AllocationOpt 288 | julia> x₀ = [2.5, 2.5] 289 | julia> Ω = [1.0, 1.0] 290 | julia> ψ = [10.0, 10.0] 291 | julia> σ = 5.0 292 | julia> k = 1 293 | julia> Φ = 1.0 294 | julia> Ψ = 20.0 295 | julia> AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) 296 | 2-element Vector{Float64}: 297 | 5.0 298 | 0.0 299 | ``` 300 | """ 301 | function optimizek(::Val{:fastgas}, x₀, Ω, ψ, σ, k, Φ, Ψ) 302 | projection = x -> gssp(x, k, σ) 303 | alg = ProjectedGradientDescent(; 304 | x=x₀, 305 | η=stepsize(lipschitzconstant(ψ, Ω)), 306 | hooks=[ 307 | StopWhen((a; kws...) -> norm(x(a) - kws[:z]) < 1e-32), 308 | HalpernIteration(; x₀=x₀, λ=i -> 1.0 / i), 309 | ], 310 | t=projection, 311 | ) 312 | f = x -> indexingreward(x, ψ, Ω, Φ, Ψ) 313 | sol = minimize!(f, alg) 314 | return floor.(SemioticOpt.x(sol); digits=1) 315 | end 316 | 317 | """ 318 | optimize(::Val{:optimal}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) 319 | 320 | Find the optimal solution vector given allocations of other indexers `Ω`, signals 321 | `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`, and gas in grt `g`. 322 | `rixs` are the indices of subgraphs that are eligible to receive indexing rewards. 323 | 324 | # Example 325 | ```julia 326 | julia> using AllocationOpt 327 | julia> rixs = [1, 2] 328 | julia> Ω = [1.0, 1.0] 329 | julia> ψ = [10.0, 10.0] 330 | julia> σ = 5.0 331 | julia> K = 2 332 | julia> Φ = 1.0 333 | julia> Ψ = 20.0 334 | julia> g = 0.01 335 | julia> xs, nonzeros, profits = AllocationOpt.optimize( 336 | Val(:optimal), Ω, ψ, σ, K, Φ, Ψ, g, rixs 337 | ) 338 | ([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) 339 | ``` 340 | """ 341 | function optimize(val::Val{:optimal}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) 342 | # Helper function to compute profit 343 | f = x -> profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), g) 344 | 345 | # Only use the eligible subgraphs 346 | _Ω = @view Ω[rixs] 347 | _ψ = @view ψ[rixs] 348 | 349 | # Preallocate solution vectors for in-place operations 350 | x = Matrix{Float64}(undef, length(Ω), K) 351 | profits = zeros(length(Ω), K) 352 | # Nonzeros defaults to ones and not zeros because the optimiser will always find 353 | # at least one non-zero, meaning that the ones with zero profits will be filtered out 354 | # during reporting. In other words, this prevents the optimiser from reporting or 355 | # executing something that was never run. 356 | nonzeros = ones(Int32, K) 357 | 358 | # Optimize 359 | @inbounds for k in 1:K 360 | x[:, k] .= k == 1 ? zeros(length(Ω)) : x[:, k - 1] 361 | x[rixs, k] .= AllocationOpt.optimizek(val, x[rixs, k], _Ω, _ψ, σ, k, Φ, Ψ, g) 362 | nonzeros[k] = x[:, k] |> nonzero |> length 363 | profits[:, k] .= f(x[:, k]) 364 | # Early stoppping if converged 365 | if k > 1 366 | if norm(x[:, k] - x[:, k - 1]) ≤ 0.1 367 | break 368 | end 369 | end 370 | end 371 | 372 | return x, nonzeros, profits 373 | end 374 | 375 | """ 376 | optimizek(::Val{:optimal}, x₀, Ω, ψ, σ, k, Φ, Ψ, g) 377 | 378 | Find the optimal `k` sparse vector given allocations of other indexers `Ω`, signals 379 | `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`, and gas `g`. 380 | 381 | # Example 382 | ```julia 383 | julia> using AllocationOpt 384 | julia> Ω = [1.0, 1.0] 385 | julia> ψ = [10.0, 10.0] 386 | julia> σ = 5.0 387 | julia> k = 1 388 | julia> Φ = 1.0 389 | julia> Ψ = 20.0 390 | julia> g = 0.01 391 | julia> x₀ = zeros(length(Ω)) 392 | julia> x = AllocationOpt.optimizek(Val(:optimal), x₀, Ω, ψ, σ, k, Φ, Ψ, g) 393 | 2-element Vector{Float64}: 394 | 5.0 395 | 0.0 396 | ``` 397 | """ 398 | function optimizek(::Val{:optimal}, x₀, Ω, ψ, σ, k, Φ, Ψ, g) 399 | # Helper function to compute profit 400 | obj = x -> -profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), g) |> sum 401 | 402 | # Function to get support for analytic optimisation 403 | f(x, ixs) = ixs 404 | 405 | # Set up optimizer 406 | function makeanalytic(x) 407 | return AllocationOpt.AnalyticOpt(; 408 | x=x, Ω=Ω, ψ=ψ, σ=σ, hooks=[StopWhen((a; kws...) -> kws[:i] > 1)] 409 | ) 410 | end 411 | 412 | # Can't make any more swaps, so stop. Also assign the final value of x. 413 | function stop_full(a; kws...) 414 | v = length(kws[:z]) == length(SemioticOpt.nonzeroixs(kws[:z])) 415 | if v 416 | kws[:op](a, kws[:z]) 417 | end 418 | return v 419 | end 420 | 421 | alg = PairwiseGreedyOpt(; 422 | kmax=k, 423 | x=x₀, 424 | xinit=zeros(length(ψ)), 425 | f=f, 426 | a=makeanalytic, 427 | hooks=[ 428 | StopWhen((a; kws...) -> kws[:f](kws[:z]) ≥ kws[:f](SemioticOpt.x(a))), 429 | StopWhen(stop_full), 430 | ], 431 | ) 432 | sol = minimize!(obj, alg) 433 | 434 | return floor.(SemioticOpt.x(sol); digits=1) 435 | end 436 | 437 | """ 438 | lipschitzconstant(ψ, Ω) 439 | 440 | The Lipschitz constant of the indexing reward function given signals `ψ` and 441 | allocations `Ω`. 442 | 443 | ```julia 444 | julia> using AllocationOpt 445 | julia> ψ = [0.0, 1.0] 446 | julia> Ω = [1.0, 1.0] 447 | julia> AllocationOpt.lipschitzconstant(ψ, Ω) 448 | 2.0 449 | ``` 450 | """ 451 | lipschitzconstant(ψ, Ω) = maximum(2 * ψ ./ Ω .^ 3) 452 | 453 | """ 454 | nonzero(v::AbstractVector) 455 | 456 | Get the non-zero elements of vector `v`. 457 | 458 | ```julia 459 | julia> using AllocationOpt 460 | julia> v = [0.0, 1.0] 461 | julia> AllocationOpt.nonzero(v) 462 | 1-element view(::Vector{Float64}, [2]) with eltype Float64: 463 | 1.0 464 | ``` 465 | """ 466 | nonzero(v::AbstractVector) = SAC.filterview(x -> x != 0, v) 467 | -------------------------------------------------------------------------------- /src/data.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-Licen se-Identifier: MIT 3 | 4 | """ 5 | squery(config::AbstractDict) 6 | 7 | Return the components of a GraphQL query for subgraphs. 8 | 9 | For use with the TheGraphData.jl package. 10 | 11 | ```julia 12 | julia> using AllocationOpt 13 | julia> config = Dict("syncing_networking" => ["mainnet"]) 14 | julia> value, args, fields = AllocationOpt.squery(config) 15 | ("subgraphDeployments", Dict{String, Union{Dict{String, String}, String}}(), ["ipfsHash", "signalledTokens", "stakedTokens"]) 16 | ``` 17 | 18 | # Extended Help 19 | You can find TheGraphData.jl at https://github.com/semiotic-ai/TheGraphData.jl 20 | """ 21 | function squery(config::AbstractDict) 22 | v = "subgraphDeployments" 23 | a = Dict{String,Any}( 24 | "where" => Dict{String,Any}( 25 | "manifest_" => Dict{String,Any}("network_in" => config["syncing_networks"]) 26 | ), 27 | ) 28 | f = ["ipfsHash", "signalledTokens", "stakedTokens", "deniedAt"] 29 | return v, a, f 30 | end 31 | 32 | """ 33 | iquery(id::AbstractString) 34 | 35 | Return the components of a GraphQL query for the stake of indexer `id`. 36 | 37 | For use with the TheGraphData.jl package. 38 | 39 | ```julia 40 | julia> using AllocationOpt 41 | julia> id = "0xa" 42 | julia> value, args, fields = AllocationOpt.iquery(id) 43 | ("indexer", Dict{String, Union{Int64, Dict{String, String}, String}}("id" => "0xa"), ["delegatedTokens", "stakedTokens", "lockedTokens"]) 44 | ``` 45 | 46 | # Extended Help 47 | You can find TheGraphData.jl at https://github.com/semiotic-ai/TheGraphData.jl 48 | """ 49 | function iquery(id::AbstractString) 50 | v = "indexer" 51 | a = Dict{String,Union{Dict{String,String},String,Int64}}("id" => id) 52 | f = ["delegatedTokens", "stakedTokens", "lockedTokens"] 53 | return v, a, f 54 | end 55 | 56 | """ 57 | aquery(id::AbstractString) 58 | 59 | Return the components of a GraphQL query for active allocations of indexer `id`. 60 | 61 | For use with the TheGraphData.jl package. 62 | 63 | ```julia 64 | julia> using AllocationOpt 65 | julia> id = "0xa" 66 | julia> value, args, fields = AllocationOpt.aquery(id) 67 | ("allocations", Dict{String, Union{Dict{String, String}, String}}("where" => Dict("status" => "Active", "indexer" => "0xa")), ["allocatedTokens", "id", "subgraphDeployment{ipfsHash}"]) 68 | ``` 69 | 70 | # Extended Help 71 | You can find TheGraphData.jl at https://github.com/semiotic-ai/TheGraphData.jl 72 | """ 73 | function aquery(id::AbstractString) 74 | v = "allocations" 75 | a = Dict{String,Union{Dict{String,String},String}}( 76 | "where" => Dict("status" => "Active", "indexer" => id) 77 | ) 78 | f = ["allocatedTokens", "id", "subgraphDeployment{ipfsHash}"] 79 | return v, a, f 80 | end 81 | 82 | """ 83 | nquery() 84 | 85 | Return the components of a GraphQL query for network parameters. 86 | 87 | For use with the TheGraphData.jl package. 88 | 89 | ```julia 90 | julia> using AllocationOpt 91 | julia> value, args, fields = AllocationOpt.nquery() 92 | ("graphNetwork", Dict("id" => 1), ["id", "networkGRTIssuance", "epochLength", "totalTokensSignalled", "currentEpoch"]) 93 | ``` 94 | 95 | # Extended Help 96 | You can find TheGraphData.jl at https://github.com/semiotic-ai/TheGraphData.jl 97 | """ 98 | function nquery() 99 | v = "graphNetwork" 100 | a = Dict("id" => 1) 101 | f = [ 102 | "id", 103 | "networkGRTIssuancePerBlock", 104 | "epochLength", 105 | "totalTokensSignalled", 106 | "currentEpoch", 107 | ] 108 | return v, a, f 109 | end 110 | 111 | """ 112 | savenames(p::AbstractString) 113 | 114 | Return a generator of the generic names of the CSV files containing the data with the 115 | path specified by `p`. 116 | 117 | ```julia 118 | julia> using AllocationOpt 119 | julia> path = "mypath" 120 | julia> paths = AllocationOpt.savenames(path) 121 | Base.Generator{NTuple{4, String}, AllocationOpt.var"#1#2"{String}}(AllocationOpt.var"#1#2"{String}("mypath"), ("indexer.csv", "allocation.csv", "subgraph.csv", "network.csv")) 122 | ``` 123 | """ 124 | function savenames(p::AbstractString) 125 | return Base.Generator( 126 | x -> joinpath(p, x), 127 | ("indexer.csv", "allocation.csv", "subgraph.csv", "network.csv"), 128 | ) 129 | end 130 | 131 | """ 132 | read(f::AbstractString, config::AbstractDict) 133 | 134 | Read the CSV files from `f` and return the tables from those files. 135 | 136 | ```julia 137 | julia> using AllocationOpt 138 | julia> i, a, s, n = AllocationOpt.read("myreaddir", Dict("verbose" => true)) 139 | ``` 140 | """ 141 | function read(f::AbstractString, config::AbstractDict) 142 | d = FlexTable[] 143 | for p in savenames(f) 144 | config["verbose"] && @info "Reading data from $p" 145 | try 146 | push!(d, flextable(@mock(TheGraphData.read(p)))) 147 | catch e 148 | ArgumentError( 149 | "Could not read $p. If you meant to query the network subgraph, remove the `readdir` argument from the config. Else your specified `readdir` is probably incorrect.", 150 | ) |> throw 151 | end 152 | end 153 | i, a, s, n = d 154 | return i, a, s, n 155 | end 156 | 157 | """ 158 | read(::Nothing, config::AbstractDict) 159 | 160 | Query the required data from the provided endpoint in the `config`. 161 | 162 | ```julia 163 | julia> using AllocationOpt 164 | julia> config = Dict( 165 | "verbose" => true, 166 | "network_subgraph_endpoint" => "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet", 167 | ) 168 | julia> i, a, s, n = AllocationOpt.read(nothing, config) 169 | ``` 170 | """ 171 | function read(::Nothing, config::AbstractDict{String,Any}) 172 | url = config["network_subgraph_endpoint"] 173 | client!(url) 174 | config["verbose"] && @info "Querying data from $url" 175 | i = flextable(@mock(query(iquery(config["id"])...))) 176 | d = flatten.(@mock(paginated_query(aquery(config["id"])...))) 177 | a = if isempty(d) 178 | FlexTable( 179 | Dict( 180 | key => String[] for 181 | key in ["subgraphDeployment.ipfsHash", "allocatedTokens", "id"] 182 | ), 183 | ) 184 | else 185 | flextable(d) 186 | end 187 | 188 | s = flextable(@mock(paginated_query(squery(config)...))) 189 | n = flextable(@mock(query(nquery()...))) 190 | 191 | # Convert string types to GRT 192 | i, a, s, n = correcttypes!(i, a, s, n) 193 | 194 | # Subtract indexer allocations from total allocation on subgraph 195 | a, s = subtractindexer!(a, s) 196 | 197 | return i, a, s, n 198 | end 199 | 200 | """ 201 | read(config::AbstractDict) 202 | 203 | Given a `config`, read the data in as flextables. 204 | 205 | If you have specified a "readdir" in the config, this will read from CSV files in that 206 | directory. Otherwise, this will query the specified `"network_subgraph_endpoint"` 207 | 208 | ```julia 209 | julia> using AllocationOpt 210 | julia> config = Dict("verbose" => false, "readdir" => "mydatadir") 211 | julia> i, a, s, n = AllocationOpt.read(config) # Read data from CSVs 212 | ``` 213 | 214 | ```julia 215 | julia> using AllocationOpt 216 | julia> config = Dict( 217 | "verbose" => false, 218 | "network_subgraph_endpoint" => "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet", 219 | "readdir" => nothing, 220 | ) 221 | julia> i, a, s, n = AllocationOpt.read(config) # Query GQL endpoint 222 | ``` 223 | """ 224 | function read(config::AbstractDict{String,Any}) 225 | readdir::Union{String,Nothing} = config["readdir"] 226 | i, a, s, n = read(readdir, config) 227 | return i, a, s, n 228 | end 229 | 230 | """ 231 | write(i::FlexTable, a::FlexTable, s::FlexTable, n::FlexTable, config::AbstractDict) 232 | 233 | Write the tables to the `writedir` specified in the `config`. 234 | 235 | 236 | ```julia 237 | julia> using AllocationOpt 238 | julia> using TheGraphData 239 | julia> config = Dict("verbose" => true, "writedir" => "datadir") 240 | julia> t = flextable([ 241 | Dict("ipfsHash" => "Qma", "signalledTokens" => "1"), 242 | Dict("ipfsHash" => "Qmb", "signalledTokens" => "2"), 243 | ]) 244 | julia> i, a, s, n = repeat([t,], 4) 245 | juila> AllocationOpt.write(i, a, s, n, config) 246 | ``` 247 | """ 248 | function write(i::FlexTable, a::FlexTable, s::FlexTable, n::FlexTable, config::AbstractDict) 249 | writedir = config["writedir"] 250 | ps = String[] 251 | for (d, p) in zip((i, a, s, n), savenames(writedir)) 252 | config["verbose"] && @info "Writing table to $p" 253 | push!(ps, @mock(TheGraphData.write(p, d))) 254 | end 255 | return ps 256 | end 257 | 258 | """ 259 | correcttypes!(::Val{:indexer}, i::FlexTable) 260 | 261 | Convert the string currency fields in the indexer table to be in GRT. 262 | 263 | ```julia 264 | julia> using AllocationOpt 265 | julia> using TheGraphData 266 | julia> i = flextable([ 267 | Dict( 268 | "stakedTokens" => "1", 269 | "delegatedTokens" => "0", 270 | "id" => "0xa", 271 | "lockedTokens" => "0", 272 | ), 273 | ]) 274 | julia> AllocationOpt.correcttypes!(Val(:indexer), i) 275 | FlexTable with 4 columns and 1 row: 276 | stakedTokens delegatedTokens id lockedTokens 277 | ┌───────────────────────────────────────────────── 278 | 1 │ 1.0e-18 0.0 0xa 0.0 279 | ``` 280 | """ 281 | function correcttypes!(::Val{:indexer}, i::FlexTable) 282 | i.stakedTokens = i.stakedTokens .|> togrt 283 | i.delegatedTokens = i.delegatedTokens .|> togrt 284 | i.lockedTokens = i.lockedTokens .|> togrt 285 | return i 286 | end 287 | 288 | """ 289 | correcttypes!(::Val{:subgraph}, s::FlexTable) 290 | 291 | Convert the string currency fields in the subgraph table to be in GRT. 292 | 293 | ```julia 294 | julia> using AllocationOpt 295 | julia> using TheGraphData 296 | julia> s = flextable([ 297 | Dict( 298 | "stakedTokens" => "1", 299 | "signalledTokens" => "0", 300 | "ipfsHash" => "Qma", 301 | "deniedAt" => 0, 302 | ), 303 | ]) 304 | julia> AllocationOpt.correcttypes!(Val(:subgraph), s) 305 | FlexTable with 4 columns and 1 row: 306 | deniedAt stakedTokens signalledTokens ipfsHash 307 | ┌────────────────────────────────────────────────── 308 | 1 │ 0 1.0e-18 0.0 Qma 309 | ``` 310 | """ 311 | function correcttypes!(::Val{:subgraph}, s::FlexTable) 312 | s.stakedTokens = s.stakedTokens .|> togrt 313 | s.signalledTokens = s.signalledTokens .|> togrt 314 | return s 315 | end 316 | 317 | """ 318 | correcttypes!(::Val{:allocation}, a::FlexTable) 319 | 320 | Convert the string currency fields in the allocation table to be in GRT. 321 | 322 | ```julia 323 | julia> using AllocationOpt 324 | julia> using TheGraphData 325 | julia> a = flextable([ 326 | Dict( 327 | "allocatedTokens" => "1", 328 | "subgraphDeployment.ipfsHash" => "Qma", 329 | ), 330 | ]) 331 | julia> AllocationOpt.correcttypes!(Val(:allocation), a) 332 | FlexTable with 2 columns and 1 row: 333 | subgraphDeployment.ipfsHash allocatedTokens 334 | ┌───────────────────────────────────────────── 335 | 1 │ Qma 1.0e-18 336 | ``` 337 | """ 338 | function correcttypes!(::Val{:allocation}, a::FlexTable) 339 | a.allocatedTokens = a.allocatedTokens .|> togrt 340 | return a 341 | end 342 | 343 | """ 344 | correcttypes!(::Val{:network}, n::FlexTable) 345 | 346 | Convert the string currency fields in the network table to be in GRT. 347 | 348 | ```julia 349 | julia> using AllocationOpt 350 | julia> using TheGraphData 351 | julia> n = flextable([ 352 | Dict( 353 | "id" => 1, 354 | "networkGRTIssuancePerBlock" => "1", 355 | "epochLength" => 28, 356 | "totalTokensSignalled" => "2", 357 | "currentEpoch" => 1, 358 | ) 359 | ]) 360 | julia> AllocationOpt.correcttypes!(Val(:network), n) 361 | FlexTable with 6 columns and 1 row: 362 | totalTokensSignalled currentEpoch id networkGRTIssuancePerBlock epochLength 363 | ┌──────────────────────────────────────────────────────────────────────────────── 364 | 1 │ 2.0e-18 1 1 1.0e-18 28 365 | ``` 366 | """ 367 | function correcttypes!(::Val{:network}, n::FlexTable) 368 | n.totalTokensSignalled = n.totalTokensSignalled .|> togrt 369 | n.networkGRTIssuancePerBlock = n.networkGRTIssuancePerBlock .|> togrt 370 | return n 371 | end 372 | 373 | """ 374 | correcttypes!(i::FlexTable, a::FlexTable, s::FlexTable, n::FlexTable) 375 | 376 | Convert all tables to be in GRT. 377 | 378 | ```julia 379 | julia> using AllocationOpt 380 | julia> using TheGraphData 381 | julia> i = flextable([ 382 | Dict( 383 | "stakedTokens" => "1", 384 | "delegatedTokens" => "0", 385 | "id" => "0xa", 386 | "lockedTokens" => "0", 387 | ), 388 | ]) 389 | julia> s = flextable([ 390 | Dict( 391 | "stakedTokens" => "1", 392 | "signalledTokens" => "0", 393 | "ipfsHash" => "Qma", 394 | ), 395 | ]) 396 | julia> a = flextable([ 397 | Dict( 398 | "allocatedTokens" => "1", 399 | "subgraphDeployment.ipfsHash" => "Qma", 400 | ), 401 | ]) 402 | julia> n = flextable([ 403 | Dict( 404 | "id" => 1, 405 | "networkGRTIssuancePerBlock" => "1", 406 | "epochLength" => 28, 407 | "totalTokensSignalled" => "2", 408 | "currentEpoch" => 1, 409 | ) 410 | ]) 411 | julia> i, a, s, n = AllocationOpt.correcttypes!(i, a, s, n) 412 | ``` 413 | """ 414 | function correcttypes!(i::FlexTable, a::FlexTable, s::FlexTable, n::FlexTable) 415 | i = correcttypes!(Val(:indexer), i) 416 | a = correcttypes!(Val(:allocation), a) 417 | s = correcttypes!(Val(:subgraph), s) 418 | n = correcttypes!(Val(:network), n) 419 | return i, a, s, n 420 | end 421 | 422 | """ 423 | subtractindexer!(a::FlexTable, s::FlexTable) 424 | 425 | Subtract the indexer's allocated tokens from the total allocated tokens on each subgraph. 426 | 427 | ```julia 428 | julia> using AllocationOpt 429 | julia> using TheGraphData 430 | julia> s = flextable([ 431 | Dict("ipfsHash" => "Qmb", "stakedTokens" => 20), 432 | Dict("ipfsHash" => "Qma", "stakedTokens" => 10), 433 | Dict("ipfsHash" => "Qmc", "stakedTokens" => 5), 434 | ]) 435 | julia> a = flextable([ 436 | Dict("subgraphDeployment.ipfsHash" => "Qma", "allocatedTokens" => 5, "id" => "0xa"), 437 | Dict("subgraphDeployment.ipfsHash" => "Qmb", "allocatedTokens" => 10, "id" => "0xb"), 438 | ]) 439 | julia> a, s = AllocationOpt.subtractindexer!(a, s) 440 | (NamedTuple[(var"subgraphDeployment.ipfsHash" = "Qma", allocatedTokens = 5, id = "0xa"), (var"subgraphDeployment.ipfsHash" = "Qmb", allocatedTokens = 10, id = "0xb")], NamedTuple[(stakedTokens = 5.0, ipfsHash = "Qma"), (stakedTokens = 10, ipfsHash = "Qmb"), (stakedTokens = 5, ipfsHash = "Qmc")]) 441 | ``` 442 | """ 443 | function subtractindexer!(a::FlexTable, s::FlexTable) 444 | # O(N) algorithm rather than using joins, which would be O(MN) 445 | 446 | # Sort both tables by ipfshash 447 | s = sort(s; by=getproperty(:ipfsHash)) 448 | a = sort(a; by=getproperty(Symbol("subgraphDeployment.ipfsHash"))) 449 | 450 | na = length(a) 451 | # Return early if there's no allocations 452 | if isempty(a) 453 | return a, s 454 | end 455 | 456 | # Preallocate vector of staked tokens on subgraphs 457 | ts = stake(Val(:subgraph), s) 458 | 459 | # Loop over subgraphs 460 | # If the subgraph ipfs == the allocation hash: 461 | # Update the staked tokens 462 | # If we've gone through all the allocations, break 463 | # Else, update the allocation table index and get the new allocation subgraph hash 464 | ix = 1 465 | aix = ipfshash(Val(:allocation), a)[ix] 466 | for (i, rs) in enumerate(s) 467 | if ipfshash(Val(:subgraph), rs) == aix 468 | ts[i] = stake(Val(:subgraph), rs) - stake(Val(:allocation), a[ix]) 469 | if ix == na 470 | break 471 | end 472 | ix += 1 473 | aix = ipfshash(Val(:allocation), a)[ix] 474 | end 475 | end 476 | 477 | # Update the staked tokens 478 | s.stakedTokens = ts 479 | 480 | return a, s 481 | end 482 | -------------------------------------------------------------------------------- /src/domain.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | const ethtogrt = 1e18 5 | const pinnedamount = 0.1 6 | 7 | """ 8 | togrt(x::AbstractString) 9 | 10 | Convert `x` to GRT. 11 | 12 | !!! note 13 | This function is meant to be used with freshly queried data, so it operates on strings. 14 | 15 | ```julia 16 | julia> using AllocationOpt 17 | julia> AllocationOpt.togrt("1") 18 | 1.0e-18 19 | ``` 20 | """ 21 | togrt(x::AbstractString) = parse(Float64, x) / ethtogrt 22 | 23 | """ 24 | blockissuance(::Val{:network}, x) 25 | 26 | The tokens issued per block. 27 | 28 | ```julia 29 | julia> using AllocationOpt 30 | julia> using TheGraphData 31 | julia> n = flextable([ 32 | Dict( 33 | "id" => 1, 34 | "networkGRTIssuancePerBlock" => 1, 35 | "epochLength" => 28, 36 | "totalTokensSignalled" => 2, 37 | "currentEpoch" => 1, 38 | ) 39 | ]) 40 | julia> AllocationOpt.blockissuance(Val(:network), n) 41 | 1 42 | """ 43 | blockissuance(::Val{:network}, x) = x.networkGRTIssuancePerBlock |> only 44 | 45 | """ 46 | blocksperepoch(::Val{:network}, x) 47 | 48 | The number of blocks in each epoch. 49 | 50 | ```julia 51 | julia> using AllocationOpt 52 | julia> using TheGraphData 53 | julia> n = flextable([ 54 | Dict( 55 | "id" => 1, 56 | "networkGRTIssuancePerBlock" => 1, 57 | "epochLength" => 28, 58 | "totalTokensSignalled" => 2, 59 | "currentEpoch" => 1, 60 | ) 61 | ]) 62 | julia> AllocationOpt.blocksperepoch(Val(:network), n) 63 | 28 64 | """ 65 | blocksperepoch(::Val{:network}, x) = x.epochLength |> only 66 | 67 | """ 68 | signal(::Val{:network}, x) 69 | 70 | The total signal in the network 71 | 72 | ```julia 73 | julia> using AllocationOpt 74 | julia> using TheGraphData 75 | julia> n = flextable([ 76 | Dict( 77 | "id" => 1, 78 | "networkGRTIssuancePerBlock" => 1, 79 | "epochLength" => 28, 80 | "totalTokensSignalled" => 2, 81 | "currentEpoch" => 1, 82 | ) 83 | ]) 84 | julia> AllocationOpt.signal(Val(:network), n) 85 | 2 86 | """ 87 | signal(::Val{:network}, x) = x.totalTokensSignalled |> only 88 | 89 | """ 90 | currentepoch(::Val{:network}, x) 91 | 92 | The current epoch. 93 | 94 | ```julia 95 | julia> using AllocationOpt 96 | julia> using TheGraphData 97 | julia> n = flextable([ 98 | Dict( 99 | "id" => 1, 100 | "networkGRTIssuancePerBlock" => 1, 101 | "epochLength" => 28, 102 | "totalTokensSignalled" => 2, 103 | "currentEpoch" => 1, 104 | ) 105 | ]) 106 | julia> AllocationOpt.currentepoch(Val(:network), n) 107 | 1 108 | """ 109 | currentepoch(::Val{:network}, x) = x.currentEpoch |> only 110 | 111 | """ 112 | ipfshash(::Val{:allocation}, x) 113 | 114 | Get the ipfs hash of `x` when `x` is part of the allocation table. 115 | 116 | ```julia 117 | julia> using AllocationOpt 118 | julia> using TheGraphData 119 | julia> x = flextable([ 120 | Dict( 121 | "subgraphDeployment.ipfsHash" => "Qma", 122 | ), 123 | ]) 124 | julia> AllocationOpt.ipfshash(Val(:allocation), x) 125 | 1-element view(lazystack(::Vector{Vector{String}}), 1, :) with eltype String: 126 | "Qma" 127 | ``` 128 | """ 129 | ipfshash(::Val{:allocation}, x) = getproperty(x, Symbol("subgraphDeployment.ipfsHash")) 130 | 131 | """ 132 | stake(::Val{:allocation}, x) 133 | 134 | Get the allocated tokens for each allocation in `x`. 135 | 136 | ```julia 137 | julia> using AllocationOpt 138 | julia> using TheGraphData 139 | julia> x = flextable([ 140 | Dict( 141 | "allocatedTokens" => 1, 142 | ), 143 | ]) 144 | julia> AllocationOpt.stake(Val(:allocation), x) 145 | 1-element view(transpose(lazystack(::Vector{Vector{Int64}})), :, 1) with eltype Int64: 146 | 1 147 | ``` 148 | """ 149 | stake(::Val{:allocation}, x) = x.allocatedTokens 150 | 151 | """ 152 | id(::Val{:allocation}, x) 153 | 154 | Get the allocation id for each allocation in `x`. 155 | 156 | ```julia 157 | julia> using AllocationOpt 158 | julia> using TheGraphData 159 | julia> x = flextable([ 160 | Dict( 161 | "id" => "0x1" 162 | ), 163 | ]) 164 | julia> AllocationOpt.id(Val(:allocation), x) 165 | 1-element view(lazystack(::Vector{Vector{String}}), 1, :) with eltype String: 166 | "0x1" 167 | ``` 168 | """ 169 | id(::Val{:allocation}, x) = x.id 170 | 171 | """ 172 | ipfshash(::Val{:subgraph}, x) 173 | 174 | Get the ipfs hash of `x` when `x` is part of the allocation table. 175 | 176 | ```julia 177 | julia> using AllocationOpt 178 | julia> using TheGraphData 179 | julia> x = flextable([ 180 | Dict( 181 | "ipfsHash" => "Qma", 182 | ), 183 | ]) 184 | julia> AllocationOpt.ipfshash(Val(:subgraph), x) 185 | 1-element view(lazystack(::Vector{Vector{String}}), 1, :) with eltype String: 186 | "Qma" 187 | ``` 188 | """ 189 | ipfshash(::Val{:subgraph}, x) = x.ipfsHash 190 | 191 | """ 192 | stake(::Val{:subgraph}, x) 193 | 194 | The tokens staked on the subgraphs in table `x`. 195 | 196 | ```julia 197 | julia> using AllocationOpt 198 | julia> using TheGraphData 199 | julia> x = flextable([ 200 | Dict("stakedTokens" => 10,), 201 | Dict("stakedTokens" => 5,), 202 | ]) 203 | julia> AllocationOpt.stake(Val(:subgraph), x) 204 | 2-element view(transpose(lazystack(::Vector{Vector{Int64}})), :, 1) with eltype Int64: 205 | 10 206 | 5 207 | ``` 208 | """ 209 | stake(::Val{:subgraph}, x) = x.stakedTokens 210 | 211 | """ 212 | signal(::Val{:subgraph}, x) 213 | 214 | The tokens signalled on the subgraphs in table `x`. 215 | 216 | ```julia 217 | julia> using AllocationOpt 218 | julia> using TheGraphData 219 | julia> x = flextable([ 220 | Dict("signalledTokens" => 10,), 221 | Dict("signalledTokens" => 5,), 222 | ]) 223 | julia> AllocationOpt.signal(Val(:subgraph), x) 224 | 2-element view(transpose(lazystack(::Vector{Vector{Int64}})), :, 1) with eltype Int64: 225 | 10 226 | 5 227 | ``` 228 | """ 229 | signal(::Val{:subgraph}, x) = x.signalledTokens 230 | 231 | """ 232 | deniedat(::Val{:subgraph}, x) 233 | 234 | If this value is non-zero, the subgraph doesn't receive indexing rewards. 235 | 236 | ```julia 237 | julia> using AllocationOpt 238 | julia> using TheGraphData 239 | julia> x = flextable([ 240 | Dict("deniedAt" => 10,), 241 | Dict("deniedAt" => 0,), 242 | ]) 243 | julia> AllocationOpt.deniedat(Val(:subgraph), x) 244 | 2-element view(transpose(lazystack(::Vector{Vector{Int64}})), :, 1) with eltype Int64: 245 | 10 246 | 0 247 | ``` 248 | """ 249 | deniedat(::Val{:subgraph}, x) = x.deniedAt 250 | 251 | """ 252 | stake(::Val{:indexer}, x) 253 | 254 | The tokens staked by the indexer in table `x`. 255 | 256 | ```julia 257 | julia> using AllocationOpt 258 | julia> using TheGraphData 259 | julia> x = flextable([ 260 | Dict( 261 | "stakedTokens" => 10, 262 | ), 263 | ]) 264 | julia> AllocationOpt.stake(Val(:indexer), x) 265 | 10 266 | ``` 267 | """ 268 | stake(::Val{:indexer}, x) = x.stakedTokens |> only 269 | 270 | """ 271 | delegation(::Val{:indexer}, x) 272 | 273 | The tokens delegated to the indexer in table `x`. 274 | 275 | ```julia 276 | julia> using AllocationOpt 277 | julia> using TheGraphData 278 | julia> x = flextable([ 279 | Dict( 280 | "delegatedTokens" => 10, 281 | ), 282 | ]) 283 | julia> AllocationOpt.delegation(Val(:indexer), x) 284 | 10 285 | ``` 286 | """ 287 | delegation(::Val{:indexer}, x) = x.delegatedTokens |> only 288 | 289 | """ 290 | locked(::Val{:indexer}, x) 291 | 292 | The locked tokens of the indexer in table `x`. 293 | 294 | ```julia 295 | julia> using AllocationOpt 296 | julia> using TheGraphData 297 | julia> x = flextable([ 298 | Dict( 299 | "lockedTokens" => 10, 300 | ), 301 | ]) 302 | julia> AllocationOpt.locked(Val(:indexer), x) 303 | 10 304 | ``` 305 | """ 306 | locked(::Val{:indexer}, x) = x.lockedTokens |> only 307 | 308 | """ 309 | available(::Val{:indexer}, x) 310 | 311 | The tokens available for the indexer to allocate in table `x`. 312 | 313 | ```julia 314 | julia> using AllocationOpt 315 | julia> using TheGraphData 316 | julia> x = flextable([ 317 | Dict( 318 | "stakedTokens" => 10, 319 | "delegatedTokens" => 20, 320 | "lockedTokens" => 5, 321 | ), 322 | ]) 323 | julia> AllocationOpt.availablestake(Val(:indexer), x) 324 | 25.0 325 | ``` 326 | """ 327 | function availablestake(::Val{:indexer}, x) 328 | val = Val(:indexer) 329 | return stake(val, x) + delegation(val, x) - locked(val, x) 330 | end 331 | 332 | """ 333 | frozen(a::FlexTable, config::AbstractDict) 334 | 335 | The frozen stake of the indexer with allocations `a`. 336 | 337 | ```julia 338 | julia> using AllocationOpt 339 | julia> using TheGraphData 340 | julia> a = flextable([ 341 | Dict("subgraphDeployment.ipfsHash" => "Qma", "allocatedTokens" => 5), 342 | Dict("subgraphDeployment.ipfsHash" => "Qmb", "allocatedTokens" => 10), 343 | ]) 344 | julia> config = Dict("frozenlist" => ["Qma", "Qmb"]) 345 | julia> AllocationOpt.frozen(a, config) 346 | 15.0 347 | ``` 348 | """ 349 | function frozen(a::FlexTable, config::AbstractDict) 350 | frozenallocs = SAC.filterview( 351 | r -> ipfshash(Val(:allocation), r) ∈ config["frozenlist"], a 352 | ) 353 | return sum(stake(Val(:allocation), frozenallocs); init=0.0) 354 | end 355 | 356 | """ 357 | pinned(config::AbstractDict) 358 | 359 | The pinned vector of the indexer. 360 | 361 | ```julia 362 | julia> using AllocationOpt 363 | julia> s = flextable([ 364 | Dict("ipfsHash" => "Qma", "signalledTokens" => 5.0), 365 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 10.0), 366 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 15.0), 367 | ]) 368 | julia> config = Dict("pinnedlist" => ["Qma", "Qmb"]) 369 | julia> AllocationOpt.pinned(s, config) 370 | 3-element Vector{Float64}: 371 | 0.1 372 | 0.1 373 | 0.0 374 | ``` 375 | """ 376 | function pinned(s::FlexTable, config::AbstractDict) 377 | pinnedixs = findall(r -> ipfshash(Val(:subgraph), r) ∈ config["pinnedlist"], s) 378 | v = zeros(length(s)) 379 | v[pinnedixs] .= pinnedamount 380 | return v 381 | end 382 | 383 | """ 384 | allocatablesubgraphs(s::FlexTable, config::AbstractDict) 385 | 386 | For the subgraphs `s` return a view of the subgraphs on which we can allocate. 387 | 388 | ```julia 389 | julia> using AllocationOpt 390 | julia> using TheGraphData 391 | julia> s = flextable([ 392 | Dict("ipfsHash" => "Qma", "signalledTokens" => 10,), 393 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 20), 394 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 5), 395 | ]) 396 | julia> config = Dict( 397 | "whitelist" => String["Qmb", "Qmc"], 398 | "blacklist" => String[], 399 | "frozenlist" => String[], 400 | "pinnedlist" => String[], 401 | "min_signal" => 0.0 402 | ) 403 | julia> fs = AllocationOpt.allocatablesubgraphs(s, config) 404 | FlexTable with 2 columns and 2 rows: 405 | signalledTokens ipfsHash 406 | ┌────────────────────────── 407 | 1 │ 20 Qmb 408 | 2 │ 5 Qmc 409 | ``` 410 | """ 411 | function allocatablesubgraphs(s::FlexTable, config::AbstractDict) 412 | # If no whitelist, whitelist is all subgraphs. Pinned subgraphs are treated as whitelisted. 413 | whitelist = if isempty(config["whitelist"]) 414 | ipfshash(Val(:subgraph), s) 415 | else 416 | config["whitelist"] ∪ config["pinnedlist"] 417 | end 418 | 419 | # For filtering, blacklist contains both the blacklist and frozenlist, 420 | # since frozen allocations aren't considered during optimisation. 421 | blacklist = config["blacklist"] ∪ config["frozenlist"] 422 | 423 | # Anonymous function that returns true if an ipfshash is in the 424 | # whitelist and not in the blacklist 425 | f = x -> x ∈ whitelist && !(x ∈ blacklist) 426 | 427 | # Only choose subgraphs with enough signal 428 | minsignal = config["min_signal"] 429 | g = x -> x ≥ minsignal 430 | 431 | # Filter the subgraph table by our anonymous function 432 | fs = SAC.filterview(s) do r 433 | x = ipfshash(Val(:subgraph), r) 434 | y = signal(Val(:subgraph), r) 435 | return f(x) && g(y) 436 | end 437 | return fs 438 | end 439 | 440 | """ 441 | newtokenissuance(n::FlexTable, config::Dict) 442 | 443 | How many new tokens are issued over the allocation lifetime given network parameters `n`. Calcualted by networkGRTIssuancePerBlock * epochLength * allocation_lifetime 444 | 445 | ```julia 446 | julia> using AllocationOpt 447 | julia> using TheGraphData 448 | julia> n = flextable([ 449 | Dict( 450 | "id" => 1, 451 | "networkGRTIssuancePerBlock" => 2, 452 | "epochLength" => 1, 453 | "totalTokensSignalled" => 2, 454 | "currentEpoch" => 1, 455 | ) 456 | ]) 457 | julia> config = Dict("allocation_lifetime" => 1) 458 | julia> AllocationOpt.newtokenissuance(n, config) 459 | 1.0 460 | ``` 461 | """ 462 | function newtokenissuance(n::FlexTable, config::Dict) 463 | r = blockissuance(Val(:network), n) 464 | t = blocksperepoch(Val(:network), n) * config["allocation_lifetime"] 465 | 466 | newtokens = r * t 467 | return newtokens 468 | end 469 | 470 | """ 471 | indexingreward( 472 | ixs::AbstractArray{Integer}, 473 | x::AbstractVector{Real}, 474 | Ω::AbstractVector{Real}, 475 | ψ::AbstractVector{Real}, 476 | Φ::Real, 477 | Ψ::Real 478 | ) 479 | 480 | The indexing rewards for the allocation vector `x` given signals `ψ`, the existing 481 | allocations on subgraphs `Ω`, token issuance `Φ`, and total signal `Ψ`. Here `ixs` 482 | is a vector of indices `Ω`, and `ψ`. `x` will be filtered by `SemioticOpt`, so we 483 | don't do this here. 484 | 485 | 486 | ```julia 487 | julia> using AllocationOpt 488 | julia> ixs = Int32[2] 489 | julia> ψ = [0.0, 1.0] 490 | julia> Ω = [1.0, 1.0] 491 | julia> Φ = 1.0 492 | julia> Ψ = 2.0 493 | julia> x = [0.0, 1.0] 494 | julia> AllocationOpt.indexingreward(ixs, x, Ω, ψ, Φ, Ψ) 495 | 0.25 496 | ```` 497 | """ 498 | function indexingreward( 499 | ixs::AbstractArray{I}, 500 | x::AbstractVector{T}, 501 | Ω::AbstractVector{T}, 502 | ψ::AbstractVector{T}, 503 | Φ::Real, 504 | Ψ::Real, 505 | ) where {T<:Real,I<:Integer} 506 | return indexingreward(x, Ω[ixs], ψ[ixs], Φ, Ψ) 507 | end 508 | 509 | """ 510 | indexingreward( 511 | x::AbstractVector{Real}, 512 | Ω::AbstractVector{Real}, 513 | ψ::AbstractVector{Real}, 514 | Φ::Real, 515 | Ψ::Real 516 | ) 517 | 518 | The indexing rewards for the allocation vector `x` given signals `ψ`, the existing 519 | allocations on subgraphs `Ω`, token issuance `Φ`, and total signal `Ψ`. 520 | 521 | ```julia 522 | julia> using AllocationOpt 523 | julia> ψ = [0.0, 1.0] 524 | julia> Ω = [1.0, 1.0] 525 | julia> Φ = 1.0 526 | julia> Ψ = 2.0 527 | julia> x = [0.0, 1.0] 528 | julia> AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) 529 | 0.25 530 | ``` 531 | """ 532 | function indexingreward( 533 | x::AbstractVector{T}, Ω::AbstractVector{T}, ψ::AbstractVector{T}, Φ::Real, Ψ::Real 534 | ) where {T<:Real} 535 | return indexingreward.(x, Ω, ψ, Φ, Ψ) |> sum 536 | end 537 | 538 | """ 539 | indexingreward(x::Real, Ω::Real, ψ::Real, Φ::Real, Ψ::Real) 540 | 541 | The indexing rewards for the allocation scalar `x` given signals `ψ`, the existing 542 | allocation on subgraphs `Ω`, token issuance `Φ`, and total signal `Ψ`. 543 | 544 | ```julia 545 | julia> using AllocationOpt 546 | julia> ψ = 0.0 547 | julia> Ω = 1.0 548 | julia> Φ = 1.0 549 | julia> Ψ = 2.0 550 | julia> x = 1.0 551 | julia> AllocationOpt.indexingreward(x, Ω, ψ, Φ, Ψ) 552 | 0.0 553 | ``` 554 | """ 555 | function indexingreward(x::Real, Ω::Real, ψ::Real, Φ::Real, Ψ::Real) 556 | sr = Φ * ψ / Ψ 557 | return sr * x / (x + Ω) 558 | end 559 | 560 | """ 561 | profit(r::Real, g::Real) 562 | 563 | Compute the profit for one allocation with reward `r` and gas cost `g`. 564 | 565 | ```julia 566 | julia> using AllocationOpt 567 | julia> r = 10 568 | julia> g = 1 569 | julia> AllocationOpt.profit(r, g) 570 | 9 571 | ``` 572 | """ 573 | profit(r::Real, g::Real) = r == 0 ? 0 : r - g 574 | 575 | """ 576 | deniedzeroixs(s::FlexTable) 577 | 578 | Find the indices of subgraphs that have "deniedAt" equal to zero. 579 | 580 | ```julia 581 | julia> using AllocationOpt 582 | julia> using TheGraphData 583 | julia> s = flextable([ 584 | Dict("ipfsHash" => "Qma", "signalledTokens" => 5.0, "deniedAt" => 0), 585 | Dict("ipfsHash" => "Qmb", "signalledTokens" => 10.0, "deniedAt" => 10), 586 | Dict("ipfsHash" => "Qmc", "signalledTokens" => 15.0, "deniedAt" => 0), 587 | ]) 588 | julia> AllocationOpt.deniedzeroixs(s) 589 | 2-element Vector{Int64}: 590 | 1 591 | 3 592 | ``` 593 | """ 594 | deniedzeroixs(s::FlexTable) = findall(r -> deniedat(Val(:subgraph), r) == 0, s) 595 | -------------------------------------------------------------------------------- /docs/src/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | When using the optimiser, you change it's behaviour via a configuration file specified as a TOML. 4 | The configuration file serves two purposes. 5 | Firstly, it makes it easier for you to track various settings and their impacts. 6 | Secondly, if something breaks, it makes it easier for us to reproduce what went wrong. 7 | 8 | An example configuration TOML file might look as below. 9 | 10 | ``` toml 11 | id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" 12 | writedir = "data" 13 | max_allocations = 10 14 | whitelist = [] 15 | blacklist = [] 16 | frozenlist = [] 17 | pinnedlist = [] 18 | allocation_lifetime = 28 19 | gas = 100 20 | min_signal = 100 21 | verbose = true 22 | num_reported_options = 2 23 | execution_mode = "none" 24 | opt_mode = "optimal" 25 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 26 | protocol_network = "arbitrum" 27 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 28 | ``` 29 | 30 | ### Detailed Field Descriptions 31 | 32 | - `id::String`: The ID of the indexer for whom we're optimising. No default value. 33 | - `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer 34 | support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the 35 | provided API serves the query requests. If unspecified, 36 | `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` 37 | - `writedir::String`: The directory to which to write the results of optimisation. 38 | If don't specify `readdir`, `writedir` also specifies the path to which to save 39 | the input data tables. If unspecified, `"."` 40 | - `readdir::Union{String, Nothing}`: The directory from which to read saved data tables. 41 | This speeds up the process as we won't have to query the network subgraph for the 42 | relevant data. If you don't specify `readdir`, we will query your specified 43 | `network_subgraph_endpoint` for the data and write it to CSV files in `writedir`. 44 | This way, you can use your previous `writedir` as your `readdir` in future runs. 45 | If unspecified, `nothing` 46 | - `whitelist::Vector{String}`: A list of subgraph IPFS hashes that you want to consider 47 | as candidates to which to allocate. If you leave this empty, we'll assume all subgraphs 48 | are in the whitelist. If unspecified, `String[]` 49 | - `blacklist::Vector{String}`: A list of subgraph IPFS hashes that you do not want to 50 | consider allocating to. For example, this list could include broken subgraphs or 51 | subgraphs that you don't want to index. If unspecified, `String[]` 52 | - `frozenlist::Vector{String}`: If you have open allocations that you don't want to change, 53 | add the corresponding subgraph IPFS hashes to this list. If unspecified, `String[]` 54 | - `pinnedlist::Vector{String}`: If you have subgraphs that you absolutely want to be 55 | allocated to, even if only with a negligible amount of GRT, add it to this list. 56 | If unspecified, `String[]` 57 | - `allocation_lifetime::Integer`: The number of epochs for which you expect the allocations 58 | the optimiser finds to be open. If unspecified, `28` 59 | - `gas::Real`: The estimated gas cost in GRT to open/close allocations. If unspecified, `100` 60 | - `min_signal::Real`: The minimum amount of signal in GRT that must be on a subgraph 61 | in order for you to consider allocating to it. If unspecified, `100` 62 | - `max_allocations::Integer`: The maximum number of new allocations you'd like the optimiser 63 | to consider opening. If unspecified, `10` 64 | - `num_reported_options::Integer`: The number of proposed allocation strategies to report. 65 | For example, if you select `10` we'd report best 10 allocation strategies ranked by 66 | profit. Options are reported to a *report.json* in your `writedir`. If unspecified, `1` 67 | - `verbose::Bool`: If true, the optimiser will print details about what it is doing to 68 | stdout. If unspecified, `false` 69 | - `execution_mode::String`: How the optimiser should execute the allocation strategies it 70 | finds. Options are `"none"`, which won't do anything, `"actionqueue"`, which will 71 | push actions to the action queue, and `"rules"`, which will generate indexing rules. 72 | If unspecified, `"none"` 73 | - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want 74 | to execute the allocation strategies on. If you specify `"actionqueue"`, you must also 75 | specify `indexer_url`. If unspecified, `nothing` 76 | - `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does 77 | not consider gas costs and optimises allocation amount over all subgraph deployments. 78 | Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and 79 | could potentially fail to converge. This mode is also used to the top 80 | `num_reported_options` allocation strategies. The final mode is `"optimal"`. 81 | This mode is slower, but it satisfies stronger optimality conditions. 82 | It will find strategies at least as good as `"fast"`, but not guaranteed to be better. 83 | By default, `"optimal"` 84 | - `protocol_network::String`: Defines the protocol network that allocation transactions 85 | should be sent to. The current protocol network options are "mainnet", "goerli", 86 | "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` 87 | - `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting 88 | the set of possible subgraphs. This list should match the networks available to your 89 | graph-node. By default, the list is a singleton of your protocol network 90 | 91 | ### Example Configurations 92 | 93 | #### ActionQueue 94 | 95 | Set `execution_mode` to `"actionqueue"` and provide an `indexer_url`. 96 | 97 | ``` toml 98 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 99 | writedir = "data" 100 | readdir = "data" 101 | max_allocations = 10 102 | whitelist = [] 103 | blacklist = [] 104 | frozenlist = [] 105 | pinnedlist = [] 106 | allocation_lifetime = 28 107 | gas = 100 108 | min_signal = 100 109 | verbose = true 110 | num_reported_options = 2 111 | execution_mode = "actionqueue" 112 | indexer_url = "https://localhost:8000" 113 | ``` 114 | 115 | #### Indexer Rules 116 | 117 | Change `execution_mode` to `"rules"`. 118 | 119 | ``` toml 120 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 121 | writedir = "data" 122 | readdir = "data" 123 | max_allocations = 10 124 | whitelist = [] 125 | blacklist = [] 126 | frozenlist = [] 127 | pinnedlist = [] 128 | allocation_lifetime = 28 129 | gas = 100 130 | min_signal = 100 131 | verbose = true 132 | num_reported_options = 2 133 | execution_mode = "rules" 134 | ``` 135 | 136 | #### Query data Instead of Reading Local CSVs 137 | 138 | Just don't specify the `readdir`. 139 | 140 | ``` toml 141 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 142 | writedir = "data" 143 | max_allocations = 10 144 | whitelist = [] 145 | blacklist = [] 146 | frozenlist = [] 147 | pinnedlist = [] 148 | allocation_lifetime = 28 149 | gas = 100 150 | min_signal = 100 151 | verbose = true 152 | num_reported_options = 2 153 | execution_mode = "none" 154 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 155 | protocol_network = "arbitrum" 156 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 157 | ``` 158 | 159 | #### Quiet Mode 160 | 161 | We set `verbose` to `false` here to surpress info messages. 162 | 163 | ``` toml 164 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 165 | writedir = "data" 166 | readdir = "data" 167 | max_allocations = 10 168 | whitelist = [] 169 | blacklist = []An example configuration TOML file might look as below. 170 | 171 | ``` toml 172 | id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" 173 | writedir = "data" 174 | max_allocations = 10 175 | whitelist = [] 176 | blacklist = [] 177 | frozenlist = [] 178 | pinnedlist = [] 179 | allocation_lifetime = 28 180 | gas = 100 181 | min_signal = 100 182 | verbose = true 183 | num_reported_options = 2 184 | execution_mode = "none" 185 | opt_mode = "optimal" 186 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" 187 | protocol_network = "arbitrum" 188 | syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] 189 | ``` 190 | 191 | 192 | ### Detailed Field Descriptions 193 | 194 | - `id::String`: The ID of the indexer for whom we're optimising. No default value. 195 | - `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer 196 | support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the 197 | provided API serves the query requests. If unspecified, 198 | `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` 199 | - `writedir::String`: The directory to which to write the results of optimisation. 200 | If don't specify `readdir`, `writedir` also specifies the path to which to save 201 | the input data tables. If unspecified, `"."` 202 | - `readdir::Union{String, Nothing}`: The directory from which to read saved data tables. 203 | This speeds up the process as we won't have to query the network subgraph for the 204 | relevant data. If you don't specify `readdir`, we will query your specified 205 | `network_subgraph_endpoint` for the data and write it to CSV files in `writedir`. 206 | This way, you can use your previous `writedir` as your `readdir` in future runs. 207 | If unspecified, `nothing` 208 | - `whitelist::Vector{String}`: A list of subgraph IPFS hashes that you want to consider 209 | as candidates to which to allocate. If you leave this empty, we'll assume all subgraphs 210 | are in the whitelist. If unspecified, `String[]` 211 | - `blacklist::Vector{String}`: A list of subgraph IPFS hashes that you do not want to 212 | consider allocating to. For example, this list could include broken subgraphs or 213 | subgraphs that you don't want to index. If unspecified, `String[]` 214 | - `frozenlist::Vector{String}`: If you have open allocations that you don't want to change, 215 | add the corresponding subgraph IPFS hashes to this list. If unspecified, `String[]` 216 | - `pinnedlist::Vector{String}`: If you have subgraphs that you absolutely want to be 217 | allocated to, even if only with a negligible amount of GRT, add it to this list. 218 | If unspecified, `String[]` 219 | - `allocation_lifetime::Integer`: The number of epochs for which you expect the allocations 220 | the optimiser finds to be open. If unspecified, `28` 221 | - `gas::Real`: The estimated gas cost in GRT to open/close allocations. If unspecified, `100` 222 | - `min_signal::Real`: The minimum amount of signal in GRT that must be on a subgraph 223 | in order for you to consider allocating to it. If unspecified, `100` 224 | - `max_allocations::Integer`: The maximum number of new allocations you'd like the optimiser 225 | to consider opening. If unspecified, `10` 226 | - `num_reported_options::Integer`: The number of proposed allocation strategies to report. 227 | For example, if you select `10` we'd report best 10 allocation strategies ranked by 228 | profit. Options are reported to a *report.json* in your `writedir`. If unspecified, `1` 229 | - `verbose::Bool`: If true, the optimiser will print details about what it is doing to 230 | stdout. If unspecified, `false` 231 | - `execution_mode::String`: How the optimiser should execute the allocation strategies it 232 | finds. Options are `"none"`, which won't do anything, `"actionqueue"`, which will 233 | push actions to the action queue, and `"rules"`, which will generate indexing rules. 234 | If unspecified, `"none"` 235 | - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want 236 | to execute the allocation strategies on. If you specify `"actionqueue"`, you must also 237 | specify `indexer_url`. If unspecified, `nothing` 238 | - `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does 239 | not consider gas costs and optimises allocation amount over all subgraph deployments. 240 | Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and 241 | could potentially fail to converge. This mode is also used to the top 242 | `num_reported_options` allocation strategies. The final mode is `"optimal"`. 243 | This mode is slower, but it satisfies stronger optimality conditions. 244 | It will find strategies at least as good as `"fast"`, but not guaranteed to be better. 245 | By default, `"optimal"` 246 | - `protocol_network::String`: Defines the protocol network that allocation transactions 247 | should be sent to. The current protocol network options are "mainnet", "goerli", 248 | "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` 249 | - `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting 250 | the set of possible subgraphs. This list should match the networks available to your 251 | graph-node. By default, the list is a singleton of your protocol network 252 | 253 | ### Example Configurations 254 | 255 | #### ActionQueue 256 | 257 | Set `execution_mode` to `"actionqueue"` and provide an `indexer_url`. 258 | 259 | ``` toml 260 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 261 | writedir = "data" 262 | readdir = "data" 263 | max_allocations = 10 264 | whitelist = [] 265 | blacklist = [] 266 | frozenlist = [] 267 | pinnedlist = [] 268 | allocation_lifetime = 28 269 | gas = 100 270 | min_signal = 100 271 | verbose = true 272 | num_reported_options = 2 273 | execution_mode = "actionqueue" 274 | indexer_url = "https://localhost:8000" 275 | protocol_network = "arbitrum" 276 | syncing_network = ["mainnet"] 277 | ``` 278 | 279 | #### Indexer Rules 280 | 281 | Change `execution_mode` to `"rules"`. 282 | 283 | ``` toml 284 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 285 | writedir = "data" 286 | readdir = "data" 287 | max_allocations = 10 288 | whitelist = [] 289 | blacklist = [] 290 | frozenlist = [] 291 | pinnedlist = [] 292 | allocation_lifetime = 28 293 | gas = 100 294 | min_signal = 100 295 | verbose = true 296 | num_reported_options = 2 297 | execution_mode = "rules" 298 | ``` 299 | 300 | #### Query data Instead of Reading Local CSVs 301 | 302 | Just don't specify the `readdir`. 303 | 304 | ``` toml 305 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 306 | writedir = "data" 307 | max_allocations = 10 308 | whitelist = [] 309 | blacklist = [] 310 | frozenlist = [] 311 | pinnedlist = [] 312 | allocation_lifetime = 28 313 | gas = 100 314 | min_signal = 100 315 | verbose = true 316 | num_reported_options = 2 317 | execution_mode = "none" 318 | ``` 319 | 320 | #### Query data for specified networks 321 | 322 | Specify the network subgraph endpoint for networks other than The Graph network on Ethereum mainnet. Here we use the endpoint to goerli network subgraph. 323 | 324 | ``` toml 325 | id = "0xE9a1CABd57700B17945Fd81feeFba82340D9568F" 326 | network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-goerli" 327 | ``` 328 | 329 | Other available endpoints examples are 330 | - Mainnet (default): https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet 331 | - Arbitrum-One: https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum 332 | - Arbitrum-Goerli: https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum-goerli 333 | 334 | #### Quiet Mode 335 | 336 | We set `verbose` to `false` here to surpress info messages. 337 | 338 | ``` toml 339 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 340 | writedir = "data" 341 | readdir = "data" 342 | max_allocations = 10 343 | whitelist = [] 344 | blacklist = [] 345 | frozenlist = [] 346 | pinnedlist = [] 347 | allocation_lifetime = 28 348 | gas = 100 349 | min_signal = 100 350 | verbose = false 351 | num_reported_options = 2 352 | execution_mode = "none" 353 | ``` 354 | 355 | #### Whitelisting Subgraphs 356 | 357 | Add some subgraph deployment IDs to the `whitelist`. 358 | If, in addition or instead you want to use `blacklist`, `frozenlist`, or `pinnedlist`, you can 359 | similarly add subgraph deployment IDs to those lists. 360 | Notice that we did not change `max_allocations` here. 361 | If `max_allocations` exceeds the number of available subgraphs (2 in this case), the code will 362 | treat the number of available subgraphs as `max_allocations`. 363 | 364 | ``` toml 365 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 366 | writedir = "data" 367 | readdir = "data" 368 | max_allocations = 10 369 | whitelist = [ 370 | "QmUVskWrz1ZiQZ76AtyhcfFDEH1ELnRpoyEhVL8p6NFTbR", 371 | "QmcBSr5R3K2M5tk8qeHFaX8pxAhdViYhcKD8ZegYuTcUhC" 372 | ] 373 | blacklist = [] 374 | frozenlist = [] 375 | pinnedlist = [] 376 | allocation_lifetime = 28 377 | gas = 100 378 | min_signal = 100 379 | verbose = false 380 | num_reported_options = 2 381 | execution_mode = "none" 382 | ``` 383 | 384 | 385 | 2 386 | execution_mode = "none" 387 | ``` 388 | 389 | #### Whitelisting Subgraphs 390 | 391 | Add some subgraph deployment IDs to the `whitelist`. 392 | If, in addition or instead you want to use `blacklist`, `frozenlist`, or `pinnedlist`, you can 393 | similarly add subgraph deployment IDs to those lists. 394 | Notice that we did not change `max_allocations` here. 395 | If `max_allocations` exceeds the number of available subgraphs (2 in this case), the code will 396 | treat the number of available subgraphs as `max_allocations`. 397 | 398 | ``` toml 399 | id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" 400 | writedir = "data" 401 | readdir = "data" 402 | max_allocations = 10 403 | whitelist = [ 404 | "QmUVskWrz1ZiQZ76AtyhcfFDEH1ELnRpoyEhVL8p6NFTbR", 405 | "QmcBSr5R3K2M5tk8qeHFaX8pxAhdViYhcKD8ZegYuTcUhC" 406 | ] 407 | blacklist = [] 408 | frozenlist = [] 409 | pinnedlist = [] 410 | allocation_lifetime = 28 411 | gas = 100 412 | min_signal = 100 413 | verbose = false 414 | num_reported_options = 2 415 | execution_mode = "none" 416 | ``` -------------------------------------------------------------------------------- /src/reporting.jl: -------------------------------------------------------------------------------- 1 | # Copyright 2022-, The Graph Foundation 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | groupunique(x::AbstractVector) 6 | 7 | Find the indices of each unique value in `x` 8 | 9 | ```julia 10 | julia> using AllocationOpt 11 | julia> x = [1, 2, 1, 3, 2, 3] 12 | julia> AllocationOpt.groupunique(x) 13 | Dict{Vector{Int64}, Vector{Int64}} with 3 entries: 14 | [3] => [4, 6] 15 | [1] => [1, 3] 16 | [2] => [2, 5] 17 | ``` 18 | """ 19 | function groupunique(x::AbstractVector) 20 | ixs = SAC.groupfind(unique, x) 21 | ixs = Dict(keys(ixs) .=> values(ixs)) 22 | return ixs 23 | end 24 | 25 | """ 26 | bestprofitpernz(ixs::AbstractVector{Integer}, profitmatrix::AbstractMatrix{Real}) 27 | 28 | Compute the best profit amongst the given `ixs` given profit matrix `p` 29 | 30 | ```julia 31 | julia> using AllocationOpt 32 | julia> ixs = Dict([1] => [1], [2] => [2]) 33 | julia> profitmatrix = [[2.5 5.0]; [2.5 1.0]] 34 | julia> AllocationOpt.bestprofitpernz.(values(ixs), Ref(profitmatrix)) 35 | 2-element Vector{NamedTuple{(:profit, :index), Tuple{Float64, Int64}}}: 36 | (profit = 5.0, index = 1) 37 | (profit = 6.0, index = 2) 38 | ``` 39 | """ 40 | function bestprofitpernz( 41 | ixs::AbstractVector{T}, p::AbstractMatrix{S} 42 | ) where {T<:Integer,S<:Real} 43 | # Sum the ixth profit vector and find the max over all of them 44 | v, i = findmax(map(ix -> p[:, ix] |> sum, ixs)) 45 | return (; :profit => v, :index => ixs[i]) 46 | end 47 | 48 | """ 49 | sortprofits!(NamedTuple{Tuple{Float64, Int64}}) 50 | Sort the nonzero best profits from highest to lowest 51 | 52 | ```julia 53 | julia> using AllocationOpt 54 | julia> popts = [ 55 | (; :profit => 5.0, :index => 2), 56 | (; :profit => 6.0, :index => 1) 57 | ] 58 | julia> popts = AllocationOpt.sortprofits!(popts) 59 | 2-element Vector{NamedTuple{(:profit, :index), Tuple{Float64, Int64}}}: 60 | (profit = 6.0, index = 1) 61 | (profit = 5.0, index = 2) 62 | ``` 63 | """ 64 | function sortprofits!(popts::AbstractVector{N}) where {N<:NamedTuple} 65 | return sort!(popts; by=x -> x[:profit], rev=true) 66 | end 67 | 68 | """ 69 | reportingtable( 70 | s::FlexTable, xs::AbstractMatrix{Real}, ps::AbstractMatrix{Real}, i::Integer 71 | ) 72 | 73 | Construct a table for the strategy mapping the ipfshash, allocation amount, and profit 74 | 75 | ```julia 76 | julia> using AllocationOpt 77 | julia> s = flextable([ 78 | Dict("stakedTokens" => "1", "signalledTokens" => "2", "ipfsHash" => "Qma"), 79 | Dict("stakedTokens" => "2", "signalledTokens" => "1", "ipfsHash" => "Qmb"), 80 | ]) 81 | julia> xs = [[2.5 5.0]; [2.5 0.0]] 82 | julia> ps = [[3.0 5.0]; [3.0 0.0]] 83 | julia> i = 1 84 | julia> AllocationOpt.reportingtable(s, xs, ps, i) 85 | FlexTable with 3 columns and 2 rows: 86 | ipfshash amount profit 87 | ┌───────────────────────── 88 | 1 │ Qma 2.5 3.0 89 | 2 │ Qmb 2.5 3.0 90 | ``` 91 | """ 92 | function reportingtable( 93 | s::FlexTable, xs::AbstractMatrix{T}, ps::AbstractMatrix{T}, i::Integer 94 | ) where {T<:Real} 95 | # Associate ipfs with allocation and profit vectors 96 | t = flextable((; :ipfshash => s.ipfsHash, :amount => xs[:, i], :profit => ps[:, i])) 97 | 98 | # Filter table to only include nonzeros 99 | ft = SAC.filterview(r -> r.amount > 0, t) 100 | 101 | return ft 102 | end 103 | 104 | """ 105 | strategydict( 106 | p::NamedTuple, 107 | xs::AbstractMatrix{Real}, 108 | nonzeros::AbstractVector{Integer}, 109 | fs::FlexTable, 110 | profitmatrix::AbstractMatrix{Real} 111 | ) 112 | 113 | For a profit, index pair `p`, generate the nested dictionary representing the data to 114 | convert to a JSON string. `xs` is the allocation strategy matrix, `nonzeros` are the number 115 | of nonzeros in each allocation strategy, `fs` is a table containing subgraph ipfshashes, 116 | and the `profitmatrix` is a matrix containing profit for each allocation in `xs` 117 | 118 | ```julia 119 | julia> using AllocationOpt 120 | julia> using TheGraphData 121 | julia> popts = [ 122 | (; :profit => 6.0, :index => 1), 123 | (; :profit => 5.0, :index => 2) 124 | ] 125 | julia> xs = [[2.5 5.0]; [2.5 0.0]] 126 | julia> profits = [[3.0 5.0]; [3.0 0.0]] 127 | julia> nonzeros = [2, 1] 128 | julia> fs = flextable([ 129 | Dict("stakedTokens" => "1", "signalledTokens" => "0", "ipfsHash" => "Qma"), 130 | Dict("stakedTokens" => "2", "signalledTokens" => "0", "ipfsHash" => "Qmb"), 131 | ]) 132 | julia> AllocationOpt.strategydict.(popts, Ref(xs), Ref(nonzeros), Ref(fs), Ref(profits)) 133 | 2-element Vector{Dict{String, Any}}: 134 | Dict("num_allocations" => 2, "profit" => 6.0, "allocations" => Dict{String, Any}[Dict("allocationAmount" => "2.5", "profit" => 3.0, "deploymentID" => "Qma"), Dict("allocationAmount" => "2.5", "profit" => 3.0, "deploymentID" => "Qmb")]) 135 | Dict("num_allocations" => 1, "profit" => 5.0, "allocations" => Dict{String, Any}[Dict("allocationAmount" => "5", "profit" => 5.0, "deploymentID" => "Qma")]) 136 | ``` 137 | """ 138 | function strategydict( 139 | p::NamedTuple, 140 | xs::AbstractMatrix{T}, 141 | nonzeros::AbstractVector{I}, 142 | fs::FlexTable, 143 | profitmatrix::AbstractMatrix{T}, 144 | ) where {T<:Real,I<:Integer} 145 | i = p[:index] 146 | 147 | ft = reportingtable(fs, xs, profitmatrix, i) 148 | 149 | nnz = nonzeros[i] 150 | sp = p[:profit] 151 | allocations = map(ft) do r 152 | return Dict( 153 | "deploymentID" => r.ipfshash, 154 | "allocationAmount" => format(r.amount), 155 | "profit" => r.profit, 156 | ) 157 | end 158 | 159 | # Construct dictionary 160 | strategy = Dict("num_allocations" => nnz, "profit" => sp, "allocations" => allocations) 161 | return strategy 162 | end 163 | 164 | """ 165 | function writejson(results::AbstractString, config::AbstractDict) 166 | 167 | Write the optimized results to the `writedir` specified in the `config`. 168 | 169 | ```julia 170 | julia> Using AllocationOpt 171 | julia> results = "{\"strategies\":[{\"num_allocations\":2,\"profit\":6.0,\"allocations\":[{\"allocationAmount\":2.5,\"profit\":3.0,\"deploymentID\":\"Qma\"},{\"allocationAmount\":2.5,\"profit\":3.0,\"deploymentID\":\"Qmb\"}]},{\"num_allocations\":1,\"profit\":5.0,\"allocations\":[{\"allocationAmount\":5.0,\"profit\":5.0,\"deploymentID\":\"Qma\"}]}]}" 172 | julia> config = Dict{"writedir" => "."} 173 | julia> AllocationOpt.writejson(results, config) 174 | ``` 175 | """ 176 | function writejson(results::AbstractString, config::AbstractDict) 177 | p = joinpath(config["writedir"], "report.json") 178 | f = open(abspath(p), "w") 179 | @mock(JSON.print(f, JSON.parse(results))) 180 | close(f) 181 | return p 182 | end 183 | 184 | """ 185 | unallocate_action(::Val{:none}, a, t, config) 186 | 187 | Do nothing. 188 | 189 | ```julia 190 | julia> using AllocationOpt 191 | julia> a = flextable([ 192 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 193 | ]) 194 | julia> t = flextable([ 195 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 196 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 197 | ]) 198 | julia> AllocationOpt.unallocate_action(Val(:none), a, t, Dict()) 199 | ``` 200 | """ 201 | unallocate_action(::Val{:none}, a, t, config) = nothing 202 | 203 | """ 204 | reallocate_action(::Val{:none}, a, t, config) 205 | 206 | Do nothing. 207 | 208 | ```julia 209 | julia> using AllocationOpt 210 | julia> using TheGraphData 211 | julia> a = flextable([ 212 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 213 | ]) 214 | julia> t = flextable([ 215 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 216 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 217 | ]) 218 | julia> AllocationOpt.reallocate_action(Val(:none), a, t, Dict()) 219 | """ 220 | reallocate_action(::Val{:none}, a, t, config) = nothing 221 | 222 | """ 223 | allocate_action(::Val{:none}, a, t, config) 224 | 225 | Do nothing. 226 | 227 | ```julia 228 | julia> using AllocationOpt 229 | julia> using TheGraphData 230 | julia> a = flextable([ 231 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 232 | ]) 233 | julia> t = flextable([ 234 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 235 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 236 | ]) 237 | julia> AllocationOpt.allocate_action(Val(:none), a, t, Dict()) 238 | ``` 239 | """ 240 | allocate_action(::Val{:none}, a, t, config) = nothing 241 | 242 | """ 243 | unallocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 244 | 245 | Print a rule that stops old allocations that the optimiser has not chosen and that aren't 246 | frozen. 247 | 248 | ```julia 249 | julia> using AllocationOpt 250 | julia> a = flextable([ 251 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 252 | ]) 253 | julia> t = flextable([ 254 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 255 | ]) 256 | julia> AllocationOpt.unallocate_action(Val(:rules), a, t, Dict("frozenlist" => [])) 257 | graph indexer rules stop Qma 258 | 1-element Vector{String}: 259 | "\e[0mgraph indexer rules stop Qma" 260 | ``` 261 | """ 262 | function unallocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 263 | frozenlist = config["frozenlist"] 264 | existingipfs = ipfshash(Val(:allocation), a) 265 | proposedipfs = t.ipfshash 266 | ipfses = closeipfs(existingipfs, proposedipfs, frozenlist) 267 | actions::Vector{String} = map(ipfs -> "\e[0mgraph indexer rules stop $(ipfs)", ipfses) 268 | println.(actions) 269 | return actions 270 | end 271 | 272 | """ 273 | reallocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 274 | 275 | Print a rule that reallocates the old allocation with a new allocation amount 276 | 277 | ```julia 278 | julia> using AllocationOpt 279 | julia> using TheGraphData 280 | julia> a = flextable([ 281 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 282 | ]) 283 | julia> t = flextable([ 284 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 285 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 286 | ]) 287 | julia> AllocationOpt.reallocate_action(Val(:rules), a, t, Dict()) 288 | graph indexer rules stop Qma 289 | Check allocation status being closed before submitting: graph indexer rules set Qma decisionBasis always allocationAmount 1 290 | 1-element Vector{String}: 291 | "\e[0mgraph indexer rules stop Qm" ⋯ 122 bytes ⋯ "asis always allocationAmount 1" 292 | ``` 293 | """ 294 | function reallocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 295 | existingipfs = ipfshash(Val(:allocation), a) 296 | # Filter table to only include subgraphs that are already allocated 297 | ti = SAC.filterview(r -> r.ipfshash ∈ existingipfs, t) 298 | ipfses = ti.ipfshash 299 | amounts = ti.amount 300 | 301 | actions::Vector{String} = map( 302 | (ipfs, amount) -> 303 | "\e[0mgraph indexer rules stop $(ipfs)\n\e[1m\e[38;2;255;0;0;249mCheck allocation status being closed before submitting: \e[0mgraph indexer rules set $(ipfs) decisionBasis always allocationAmount $(format(amount))", 304 | ipfses, 305 | amounts, 306 | ) 307 | println.(actions) 308 | return actions 309 | end 310 | 311 | """ 312 | allocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 313 | 314 | Print the rules that allocates to new subgraphs. 315 | 316 | ```julia 317 | julia> using AllocationOpt 318 | julia> using TheGraphData 319 | julia> a = flextable([ 320 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 321 | ]) 322 | julia> t = flextable([ 323 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 324 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 325 | ]) 326 | julia> AllocationOpt.allocate_action(Val(:rules), a, t, Dict()) 327 | graph indexer rules set Qmb decisionBasis always allocationAmount 2 328 | 1-element Vector{String}: 329 | "\e[0mgraph indexer rules set Qmb decisionBasis always allocationAmount 2" 330 | """ 331 | function allocate_action(::Val{:rules}, a::FlexTable, t::FlexTable, config::AbstractDict) 332 | existingipfs = ipfshash(Val(:allocation), a) 333 | # Filter table to only include subgraphs that are not already allocated 334 | ts = SAC.filterview(r -> r.ipfshash ∉ existingipfs, t) 335 | ipfses = ts.ipfshash 336 | amounts = ts.amount 337 | 338 | actions::Vector{String} = map( 339 | (ipfs, amount) -> 340 | "\e[0mgraph indexer rules set $(ipfs) decisionBasis always allocationAmount $(format(amount))", 341 | ipfses, 342 | amounts, 343 | ) 344 | println.(actions) 345 | return actions 346 | end 347 | 348 | """ 349 | closeipfs(existingipfs, proposedipfs, frozenlist) 350 | 351 | Get the list of the ipfs hashes of allocations to close. 352 | 353 | ```julia 354 | julia> using AllocationOpt 355 | julia> AllocationOpt.closeipfs(["Qma"], ["Qmb"], String[]) 356 | 1-element Vector{String}: 357 | "Qma" 358 | ``` 359 | """ 360 | function closeipfs(existingipfs, proposedipfs, frozenlist) 361 | return setdiff(setdiff(existingipfs, proposedipfs), frozenlist) 362 | end 363 | 364 | @enum ActionStatus begin 365 | queued 366 | approved 367 | pending 368 | success 369 | failed 370 | canceled 371 | end 372 | 373 | @enum ActionType begin 374 | allocate 375 | unallocate 376 | reallocate 377 | collect 378 | end 379 | 380 | """ 381 | unallocate_action(::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict) 382 | 383 | Create and push the unallocate actions to the action queue. 384 | 385 | ```julia 386 | julia> using AllocationOpt 387 | julia> using TheGraphData 388 | julia> a = flextable([ 389 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 390 | ]) 391 | julia> t = flextable([ 392 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 393 | ]) 394 | julia> config = Dict( 395 | "frozenlist" => [], 396 | "indexer_url" => "http://localhost:18000" 397 | ) 398 | julia> TheGraphData.client!(config["indexer_url"]) 399 | julia> AllocationOpt.unallocate_action(Val(:actionqueue), a, t, config) 400 | 1-element Vector{Dict{String, Any}}: 401 | Dict("priority" => 0, "status" => AllocationOpt.queued, "allocationID" => "0xa", "source" => "AllocationOpt", "reason" => "AllocationOpt", "type" => AllocationOpt.unallocate, "deploymentID" => "Qma", "protocolNetwork" => "mainnet") 402 | """ 403 | function unallocate_action( 404 | ::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict 405 | ) 406 | toallocatelist = config["frozenlist"] ∪ t.ipfshash 407 | ft = SAC.filterview(r -> ipfshash(Val(:allocation), r) ∉ toallocatelist, a) 408 | 409 | actions::Vector{Dict{String,Any}} = map( 410 | r -> Dict( 411 | "status" => queued, 412 | "type" => unallocate, 413 | "allocationID" => id(Val(:allocation), r), 414 | "deploymentID" => ipfshash(Val(:allocation), r), 415 | "source" => "AllocationOpt", 416 | "reason" => "AllocationOpt", 417 | "priority" => 0, 418 | "protocolNetwork" => config["protocol_network"], 419 | ), 420 | ft, 421 | ) 422 | 423 | # Send graphql mutation to action queue 424 | @mock(mutate("queueActions", Dict("actions" => actions); direct_write=true)) 425 | 426 | return actions 427 | end 428 | 429 | """ 430 | reallocate_action(::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict) 431 | 432 | Create and push reallocate actions to the action queue. 433 | 434 | ```julia 435 | julia> using AllocationOpt 436 | julia> using TheGraphData 437 | julia> a = flextable([ 438 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 439 | ]) 440 | julia> t = flextable([ 441 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 442 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 443 | ]) 444 | julia> config = Dict("indexer_url" => "http://localhost:18000") 445 | julia> TheGraphData.client!(config["indexer_url"]) 446 | julia> AllocationOpt.reallocate_action(Val(:actionqueue), a, t, config) 447 | 1-element Vector{Dict{String, Any}}: 448 | Dict("amount" => "1", "priority" => 0, "status" => AllocationOpt.queued, "allocationID" => "0xa", "source" => "AllocationOpt", "reason" => "Expected profit: 0", "type" => AllocationOpt.reallocate, "deploymentID" => "Qma", "protocolNetwork" => "mainnet") 449 | ``` 450 | """ 451 | function reallocate_action( 452 | ::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict 453 | ) 454 | ti = SAC.innerjoin( 455 | getproperty(:ipfshash), getproperty(Symbol("subgraphDeployment.ipfsHash")), t, a 456 | ) 457 | 458 | actions::Vector{Dict{String,Any}} = map( 459 | r -> Dict( 460 | "status" => queued, 461 | "type" => reallocate, 462 | "allocationID" => id(Val(:allocation), r), 463 | "deploymentID" => ipfshash(Val(:allocation), r), 464 | "amount" => format(r.amount), 465 | "source" => "AllocationOpt", 466 | "reason" => "Expected profit: $(format(r.profit))", 467 | "priority" => 0, 468 | "protocolNetwork" => config["protocol_network"], 469 | ), 470 | ti, 471 | ) 472 | 473 | # Send graphql mutation to action queue 474 | @mock(mutate("queueActions", Dict("actions" => actions); direct_write=true)) 475 | 476 | return actions 477 | end 478 | 479 | """ 480 | allocate_action(::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict) 481 | 482 | Create and push allocate actions to the action queue. 483 | 484 | ```julia 485 | julia> using AllocationOpt 486 | julia> using TheGraphData 487 | julia> a = flextable([ 488 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 489 | ]) 490 | julia> t = flextable([ 491 | Dict("amount" => "1", "profit" => "0", "ipfshash" => "Qma"), 492 | Dict("amount" => "2", "profit" => "0", "ipfshash" => "Qmb"), 493 | ]) 494 | julia> config = Dict( 495 | "indexer_url" => "http://localhost:18000" 496 | ) 497 | julia> TheGraphData.client!(config["indexer_url"]) 498 | julia> AllocationOpt.allocate_action(Val(:actionqueue), a, t, config) 499 | 1-element Vector{Dict{String, Any}}: 500 | Dict("amount" => "2", "priority" => 0, "status" => AllocationOpt.queued, "source" => "AllocationOpt", "reason" => "Expected profit: 0", "type" => AllocationOpt.allocate, "deploymentID" => "Qmb", "protocolNetwork" => "mainnet") 501 | ``` 502 | """ 503 | function allocate_action( 504 | ::Val{:actionqueue}, a::FlexTable, t::FlexTable, config::AbstractDict 505 | ) 506 | existingipfs = ipfshash(Val(:allocation), a) 507 | # Filter table to only include subgraphs that are not already allocated 508 | ts = SAC.filterview(r -> r.ipfshash ∉ existingipfs, t) 509 | 510 | actions::Vector{Dict{String,Any}} = map( 511 | r -> Dict( 512 | "status" => queued, 513 | "type" => allocate, 514 | "deploymentID" => r.ipfshash, 515 | "amount" => format(r.amount), 516 | "source" => "AllocationOpt", 517 | "reason" => "Expected profit: $(format(r.profit))", 518 | "priority" => 0, 519 | "protocolNetwork" => config["protocol_network"], 520 | ), 521 | ts, 522 | ) 523 | 524 | # Send graphql mutation to action queue 525 | @mock(mutate("queueActions", Dict("actions" => actions); direct_write=true)) 526 | 527 | return actions 528 | end 529 | 530 | """ 531 | execute( 532 | a::FlexTable, 533 | ix::Integer, 534 | s::FlexTable, 535 | xs::AbstractMatrix{T}, 536 | ps::AbstractMatrix{T}, 537 | config::AbstractDict 538 | ) where {T<:Real} 539 | 540 | Execute the actions picked by the optimiser. 541 | 542 | ```julia 543 | julia> using AllocationOpt 544 | julia> using TheGraphData 545 | julia> a = flextable([ 546 | Dict("subgraphDeployment.ipfsHash" => "Qma", "id" => "0xa") 547 | ]) 548 | julia> xs = [[2.5 5.0]; [2.5 0.0]] 549 | julia> ps = [[3.0 5.0]; [3.0 0.0]] 550 | julia> s = flextable([ 551 | Dict("stakedTokens" => "1", "signalledTokens" => "0", "ipfsHash" => "Qma"), 552 | Dict("stakedTokens" => "2", "signalledTokens" => "0", "ipfsHash" => "Qmb"), 553 | ]) 554 | julia> config = Dict("execution_mode" => "none") 555 | julia> ix = 1 556 | julia> AllocationOpt.execute(a, ix, s, xs, ps, config) 557 | ``` 558 | """ 559 | function execute( 560 | a::FlexTable, 561 | ix::Integer, 562 | s::FlexTable, 563 | xs::AbstractMatrix{T}, 564 | ps::AbstractMatrix{T}, 565 | config::AbstractDict, 566 | ) where {T<:Real} 567 | # Construct t 568 | t = reportingtable(s, xs, ps, ix) 569 | 570 | mode = Val(Symbol(config["execution_mode"])) 571 | 572 | indexerurlclient(mode, config) 573 | _ = unallocate_action(mode, a, t, config) 574 | _ = reallocate_action(mode, a, t, config) 575 | _ = allocate_action(mode, a, t, config) 576 | 577 | return nothing 578 | end 579 | 580 | indexerurlclient(::Val{:actionqueue}, config::AbstractDict) = client!(config["indexer_url"]) 581 | indexerurlclient(::Any, config::AbstractDict) = nothing 582 | -------------------------------------------------------------------------------- /Manifest.toml: -------------------------------------------------------------------------------- 1 | # This file is machine-generated - editing it directly is not advised 2 | 3 | julia_version = "1.10.4" 4 | manifest_format = "2.0" 5 | project_hash = "dfe00eabbb715663ec2bf76f0cf0fd9257b705b0" 6 | 7 | [[deps.AbstractFFTs]] 8 | deps = ["LinearAlgebra"] 9 | git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" 10 | uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" 11 | version = "1.5.0" 12 | weakdeps = ["ChainRulesCore", "Test"] 13 | 14 | [deps.AbstractFFTs.extensions] 15 | AbstractFFTsChainRulesCoreExt = "ChainRulesCore" 16 | AbstractFFTsTestExt = "Test" 17 | 18 | [[deps.Accessors]] 19 | deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "MacroTools", "Markdown", "Test"] 20 | git-tree-sha1 = "c0d491ef0b135fd7d63cbc6404286bc633329425" 21 | uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" 22 | version = "0.1.36" 23 | 24 | [deps.Accessors.extensions] 25 | AccessorsAxisKeysExt = "AxisKeys" 26 | AccessorsIntervalSetsExt = "IntervalSets" 27 | AccessorsStaticArraysExt = "StaticArrays" 28 | AccessorsStructArraysExt = "StructArrays" 29 | AccessorsUnitfulExt = "Unitful" 30 | 31 | [deps.Accessors.weakdeps] 32 | AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" 33 | IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" 34 | Requires = "ae029012-a4dd-5104-9daa-d747884805df" 35 | StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" 36 | StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" 37 | Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" 38 | 39 | [[deps.Adapt]] 40 | deps = ["LinearAlgebra", "Requires"] 41 | git-tree-sha1 = "6a55b747d1812e699320963ffde36f1ebdda4099" 42 | uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" 43 | version = "4.0.4" 44 | weakdeps = ["StaticArrays"] 45 | 46 | [deps.Adapt.extensions] 47 | AdaptStaticArraysExt = "StaticArrays" 48 | 49 | [[deps.ArgTools]] 50 | uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" 51 | version = "1.1.1" 52 | 53 | [[deps.Artifacts]] 54 | uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" 55 | 56 | [[deps.Base64]] 57 | uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" 58 | 59 | [[deps.BitFlags]] 60 | git-tree-sha1 = "2dc09997850d68179b69dafb58ae806167a32b1b" 61 | uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" 62 | version = "0.1.8" 63 | 64 | [[deps.CEnum]] 65 | git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" 66 | uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" 67 | version = "0.5.0" 68 | 69 | [[deps.CSV]] 70 | deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] 71 | git-tree-sha1 = "6c834533dc1fabd820c1db03c839bf97e45a3fab" 72 | uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" 73 | version = "0.10.14" 74 | 75 | [[deps.ChainRules]] 76 | deps = ["Adapt", "ChainRulesCore", "Compat", "Distributed", "GPUArraysCore", "IrrationalConstants", "LinearAlgebra", "Random", "RealDot", "SparseArrays", "SparseInverseSubset", "Statistics", "StructArrays", "SuiteSparse"] 77 | git-tree-sha1 = "227985d885b4dbce5e18a96f9326ea1e836e5a03" 78 | uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2" 79 | version = "1.69.0" 80 | 81 | [[deps.ChainRulesCore]] 82 | deps = ["Compat", "LinearAlgebra"] 83 | git-tree-sha1 = "71acdbf594aab5bbb2cec89b208c41b4c411e49f" 84 | uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" 85 | version = "1.24.0" 86 | weakdeps = ["SparseArrays"] 87 | 88 | [deps.ChainRulesCore.extensions] 89 | ChainRulesCoreSparseArraysExt = "SparseArrays" 90 | 91 | [[deps.CodecZlib]] 92 | deps = ["TranscodingStreams", "Zlib_jll"] 93 | git-tree-sha1 = "59939d8a997469ee05c4b4944560a820f9ba0d73" 94 | uuid = "944b1d66-785c-5afd-91f1-9de20f533193" 95 | version = "0.7.4" 96 | 97 | [[deps.CommonSolve]] 98 | git-tree-sha1 = "0eee5eb66b1cf62cd6ad1b460238e60e4b09400c" 99 | uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" 100 | version = "0.2.4" 101 | 102 | [[deps.CommonSubexpressions]] 103 | deps = ["MacroTools", "Test"] 104 | git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" 105 | uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" 106 | version = "0.3.0" 107 | 108 | [[deps.Compat]] 109 | deps = ["TOML", "UUIDs"] 110 | git-tree-sha1 = "b1c55339b7c6c350ee89f2c1604299660525b248" 111 | uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" 112 | version = "4.15.0" 113 | weakdeps = ["Dates", "LinearAlgebra"] 114 | 115 | [deps.Compat.extensions] 116 | CompatLinearAlgebraExt = "LinearAlgebra" 117 | 118 | [[deps.CompilerSupportLibraries_jll]] 119 | deps = ["Artifacts", "Libdl"] 120 | uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" 121 | version = "1.1.1+0" 122 | 123 | [[deps.CompositionsBase]] 124 | git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" 125 | uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" 126 | version = "0.1.2" 127 | weakdeps = ["InverseFunctions"] 128 | 129 | [deps.CompositionsBase.extensions] 130 | CompositionsBaseInverseFunctionsExt = "InverseFunctions" 131 | 132 | [[deps.ConcurrentUtilities]] 133 | deps = ["Serialization", "Sockets"] 134 | git-tree-sha1 = "6cbbd4d241d7e6579ab354737f4dd95ca43946e1" 135 | uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" 136 | version = "2.4.1" 137 | 138 | [[deps.ConstructionBase]] 139 | deps = ["LinearAlgebra"] 140 | git-tree-sha1 = "260fd2400ed2dab602a7c15cf10c1933c59930a2" 141 | uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" 142 | version = "1.5.5" 143 | 144 | [deps.ConstructionBase.extensions] 145 | ConstructionBaseIntervalSetsExt = "IntervalSets" 146 | ConstructionBaseStaticArraysExt = "StaticArrays" 147 | 148 | [deps.ConstructionBase.weakdeps] 149 | IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" 150 | StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" 151 | 152 | [[deps.DataAPI]] 153 | git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" 154 | uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" 155 | version = "1.16.0" 156 | 157 | [[deps.DataValueInterfaces]] 158 | git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" 159 | uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" 160 | version = "1.0.0" 161 | 162 | [[deps.Dates]] 163 | deps = ["Printf"] 164 | uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" 165 | 166 | [[deps.Dictionaries]] 167 | deps = ["Indexing", "Random", "Serialization"] 168 | git-tree-sha1 = "35b66b6744b2d92c778afd3a88d2571875664a2a" 169 | uuid = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" 170 | version = "0.4.2" 171 | 172 | [[deps.DiffResults]] 173 | deps = ["StaticArraysCore"] 174 | git-tree-sha1 = "782dd5f4561f5d267313f23853baaaa4c52ea621" 175 | uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" 176 | version = "1.1.0" 177 | 178 | [[deps.DiffRules]] 179 | deps = ["IrrationalConstants", "LogExpFunctions", "NaNMath", "Random", "SpecialFunctions"] 180 | git-tree-sha1 = "23163d55f885173722d1e4cf0f6110cdbaf7e272" 181 | uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" 182 | version = "1.15.1" 183 | 184 | [[deps.Distributed]] 185 | deps = ["Random", "Serialization", "Sockets"] 186 | uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" 187 | 188 | [[deps.DocStringExtensions]] 189 | deps = ["LibGit2"] 190 | git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" 191 | uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" 192 | version = "0.9.3" 193 | 194 | [[deps.Downloads]] 195 | deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] 196 | uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" 197 | version = "1.6.0" 198 | 199 | [[deps.ExceptionUnwrapping]] 200 | deps = ["Test"] 201 | git-tree-sha1 = "dcb08a0d93ec0b1cdc4af184b26b591e9695423a" 202 | uuid = "460bff9d-24e4-43bc-9d9f-a8973cb893f4" 203 | version = "0.1.10" 204 | 205 | [[deps.ExprTools]] 206 | git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" 207 | uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" 208 | version = "0.1.10" 209 | 210 | [[deps.FilePathsBase]] 211 | deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"] 212 | git-tree-sha1 = "9f00e42f8d99fdde64d40c8ea5d14269a2e2c1aa" 213 | uuid = "48062228-2e41-5def-b9a4-89aafe57970f" 214 | version = "0.9.21" 215 | 216 | [[deps.FileWatching]] 217 | uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" 218 | 219 | [[deps.FillArrays]] 220 | deps = ["LinearAlgebra"] 221 | git-tree-sha1 = "0653c0a2396a6da5bc4766c43041ef5fd3efbe57" 222 | uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" 223 | version = "1.11.0" 224 | 225 | [deps.FillArrays.extensions] 226 | FillArraysPDMatsExt = "PDMats" 227 | FillArraysSparseArraysExt = "SparseArrays" 228 | FillArraysStatisticsExt = "Statistics" 229 | 230 | [deps.FillArrays.weakdeps] 231 | PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150" 232 | SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" 233 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 234 | 235 | [[deps.Formatting]] 236 | deps = ["Logging", "Printf"] 237 | git-tree-sha1 = "fb409abab2caf118986fc597ba84b50cbaf00b87" 238 | uuid = "59287772-0a20-5a39-b81b-1366585eb4c0" 239 | version = "0.4.3" 240 | 241 | [[deps.ForwardDiff]] 242 | deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] 243 | git-tree-sha1 = "cf0fe81336da9fb90944683b8c41984b08793dad" 244 | uuid = "f6369f11-7733-5829-9624-2563aa707210" 245 | version = "0.10.36" 246 | weakdeps = ["StaticArrays"] 247 | 248 | [deps.ForwardDiff.extensions] 249 | ForwardDiffStaticArraysExt = "StaticArrays" 250 | 251 | [[deps.Future]] 252 | deps = ["Random"] 253 | uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" 254 | 255 | [[deps.GPUArrays]] 256 | deps = ["Adapt", "GPUArraysCore", "LLVM", "LinearAlgebra", "Printf", "Random", "Reexport", "Serialization", "Statistics"] 257 | git-tree-sha1 = "c154546e322a9c73364e8a60430b0f79b812d320" 258 | uuid = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" 259 | version = "10.2.0" 260 | 261 | [[deps.GPUArraysCore]] 262 | deps = ["Adapt"] 263 | git-tree-sha1 = "ec632f177c0d990e64d955ccc1b8c04c485a0950" 264 | uuid = "46192b85-c4d5-4398-a991-12ede77f4527" 265 | version = "0.1.6" 266 | 267 | [[deps.Glob]] 268 | git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496" 269 | uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" 270 | version = "1.3.1" 271 | 272 | [[deps.GraphQLClient]] 273 | deps = ["GraphQLParser", "HTTP", "JSON3", "StructTypes"] 274 | git-tree-sha1 = "a12336fdb6697b77bd03c057cad0acd5a3d92d84" 275 | uuid = "09d831e3-9c21-47a9-bfd8-076871817219" 276 | version = "0.7.6" 277 | 278 | [[deps.GraphQLParser]] 279 | deps = ["Parsers"] 280 | git-tree-sha1 = "1e02ab62f3ee788e15d1691edbe5046a13cba7bc" 281 | uuid = "0ae10fbf-af58-4883-b66b-ff0ac82d20dd" 282 | version = "0.1.3" 283 | 284 | [[deps.HTTP]] 285 | deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] 286 | git-tree-sha1 = "d1d712be3164d61d1fb98e7ce9bcbc6cc06b45ed" 287 | uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" 288 | version = "1.10.8" 289 | 290 | [[deps.IRTools]] 291 | deps = ["InteractiveUtils", "MacroTools"] 292 | git-tree-sha1 = "950c3717af761bc3ff906c2e8e52bd83390b6ec2" 293 | uuid = "7869d1d1-7146-5819-86e3-90919afe41df" 294 | version = "0.4.14" 295 | 296 | [[deps.Indexing]] 297 | git-tree-sha1 = "ce1566720fd6b19ff3411404d4b977acd4814f9f" 298 | uuid = "313cdc1a-70c2-5d6a-ae34-0150d3930a38" 299 | version = "1.1.1" 300 | 301 | [[deps.InlineStrings]] 302 | deps = ["Parsers"] 303 | git-tree-sha1 = "86356004f30f8e737eff143d57d41bd580e437aa" 304 | uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" 305 | version = "1.4.1" 306 | 307 | [deps.InlineStrings.extensions] 308 | ArrowTypesExt = "ArrowTypes" 309 | 310 | [deps.InlineStrings.weakdeps] 311 | ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" 312 | 313 | [[deps.InteractiveUtils]] 314 | deps = ["Markdown"] 315 | uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" 316 | 317 | [[deps.InverseFunctions]] 318 | deps = ["Test"] 319 | git-tree-sha1 = "e7cbed5032c4c397a6ac23d1493f3289e01231c4" 320 | uuid = "3587e190-3f89-42d0-90ee-14403ec27112" 321 | version = "0.1.14" 322 | weakdeps = ["Dates"] 323 | 324 | [deps.InverseFunctions.extensions] 325 | DatesExt = "Dates" 326 | 327 | [[deps.InvertedIndices]] 328 | git-tree-sha1 = "0dc7b50b8d436461be01300fd8cd45aa0274b038" 329 | uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" 330 | version = "1.3.0" 331 | 332 | [[deps.IrrationalConstants]] 333 | git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2" 334 | uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" 335 | version = "0.2.2" 336 | 337 | [[deps.IteratorInterfaceExtensions]] 338 | git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" 339 | uuid = "82899510-4779-5014-852e-03e436cf321d" 340 | version = "1.0.0" 341 | 342 | [[deps.JLLWrappers]] 343 | deps = ["Artifacts", "Preferences"] 344 | git-tree-sha1 = "7e5d6779a1e09a36db2a7b6cff50942a0a7d0fca" 345 | uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" 346 | version = "1.5.0" 347 | 348 | [[deps.JSON]] 349 | deps = ["Dates", "Mmap", "Parsers", "Unicode"] 350 | git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" 351 | uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 352 | version = "0.21.4" 353 | 354 | [[deps.JSON3]] 355 | deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] 356 | git-tree-sha1 = "eb3edce0ed4fa32f75a0a11217433c31d56bd48b" 357 | uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" 358 | version = "1.14.0" 359 | 360 | [deps.JSON3.extensions] 361 | JSON3ArrowExt = ["ArrowTypes"] 362 | 363 | [deps.JSON3.weakdeps] 364 | ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" 365 | 366 | [[deps.LLVM]] 367 | deps = ["CEnum", "LLVMExtra_jll", "Libdl", "Preferences", "Printf", "Requires", "Unicode"] 368 | git-tree-sha1 = "389aea28d882a40b5e1747069af71bdbd47a1cae" 369 | uuid = "929cbde3-209d-540e-8aea-75f648917ca0" 370 | version = "7.2.1" 371 | 372 | [deps.LLVM.extensions] 373 | BFloat16sExt = "BFloat16s" 374 | 375 | [deps.LLVM.weakdeps] 376 | BFloat16s = "ab4f0b2a-ad5b-11e8-123f-65d77653426b" 377 | 378 | [[deps.LLVMExtra_jll]] 379 | deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML"] 380 | git-tree-sha1 = "88b916503aac4fb7f701bb625cd84ca5dd1677bc" 381 | uuid = "dad2f222-ce93-54a1-a47d-0025e8a3acab" 382 | version = "0.0.29+0" 383 | 384 | [[deps.Lazy]] 385 | deps = ["MacroTools"] 386 | git-tree-sha1 = "1370f8202dac30758f3c345f9909b97f53d87d3f" 387 | uuid = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0" 388 | version = "0.15.1" 389 | 390 | [[deps.LazyArtifacts]] 391 | deps = ["Artifacts", "Pkg"] 392 | uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" 393 | 394 | [[deps.LazyStack]] 395 | deps = ["ChainRulesCore", "Compat", "LinearAlgebra"] 396 | git-tree-sha1 = "aff621f1f49e9262a34aaf0d57d02ea3b35aec60" 397 | uuid = "1fad7336-0346-5a1a-a56f-a06ba010965b" 398 | version = "0.1.3" 399 | 400 | [[deps.LibCURL]] 401 | deps = ["LibCURL_jll", "MozillaCACerts_jll"] 402 | uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" 403 | version = "0.6.4" 404 | 405 | [[deps.LibCURL_jll]] 406 | deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] 407 | uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" 408 | version = "8.4.0+0" 409 | 410 | [[deps.LibGit2]] 411 | deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] 412 | uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" 413 | 414 | [[deps.LibGit2_jll]] 415 | deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] 416 | uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" 417 | version = "1.6.4+0" 418 | 419 | [[deps.LibSSH2_jll]] 420 | deps = ["Artifacts", "Libdl", "MbedTLS_jll"] 421 | uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" 422 | version = "1.11.0+1" 423 | 424 | [[deps.Libdl]] 425 | uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" 426 | 427 | [[deps.LinearAlgebra]] 428 | deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] 429 | uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 430 | 431 | [[deps.LogExpFunctions]] 432 | deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] 433 | git-tree-sha1 = "a2d09619db4e765091ee5c6ffe8872849de0feea" 434 | uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" 435 | version = "0.3.28" 436 | 437 | [deps.LogExpFunctions.extensions] 438 | LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" 439 | LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" 440 | LogExpFunctionsInverseFunctionsExt = "InverseFunctions" 441 | 442 | [deps.LogExpFunctions.weakdeps] 443 | ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" 444 | ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" 445 | InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" 446 | 447 | [[deps.Logging]] 448 | uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" 449 | 450 | [[deps.LoggingExtras]] 451 | deps = ["Dates", "Logging"] 452 | git-tree-sha1 = "c1dd6d7978c12545b4179fb6153b9250c96b0075" 453 | uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" 454 | version = "1.0.3" 455 | 456 | [[deps.MacroTools]] 457 | deps = ["Markdown", "Random"] 458 | git-tree-sha1 = "2fa9ee3e63fd3a4f7a9a4f4744a52f4856de82df" 459 | uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" 460 | version = "0.5.13" 461 | 462 | [[deps.Markdown]] 463 | deps = ["Base64"] 464 | uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" 465 | 466 | [[deps.MbedTLS]] 467 | deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] 468 | git-tree-sha1 = "c067a280ddc25f196b5e7df3877c6b226d390aaf" 469 | uuid = "739be429-bea8-5141-9913-cc70e7f3736d" 470 | version = "1.1.9" 471 | 472 | [[deps.MbedTLS_jll]] 473 | deps = ["Artifacts", "Libdl"] 474 | uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" 475 | version = "2.28.2+1" 476 | 477 | [[deps.Mmap]] 478 | uuid = "a63ad114-7e13-5084-954f-fe012c677804" 479 | 480 | [[deps.Mocking]] 481 | deps = ["Compat", "ExprTools"] 482 | git-tree-sha1 = "bf17d9cb4f0d2882351dfad030598f64286e5936" 483 | uuid = "78c3b35d-d492-501b-9361-3d52fe80e533" 484 | version = "0.7.8" 485 | 486 | [[deps.MozillaCACerts_jll]] 487 | uuid = "14a3606d-f60d-562e-9121-12d972cd8159" 488 | version = "2023.1.10" 489 | 490 | [[deps.NaNMath]] 491 | deps = ["OpenLibm_jll"] 492 | git-tree-sha1 = "0877504529a3e5c3343c6f8b4c0381e57e4387e4" 493 | uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" 494 | version = "1.0.2" 495 | 496 | [[deps.NetworkOptions]] 497 | uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" 498 | version = "1.2.0" 499 | 500 | [[deps.OpenBLAS_jll]] 501 | deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] 502 | uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" 503 | version = "0.3.23+4" 504 | 505 | [[deps.OpenLibm_jll]] 506 | deps = ["Artifacts", "Libdl"] 507 | uuid = "05823500-19ac-5b8b-9628-191a04bc5112" 508 | version = "0.8.1+2" 509 | 510 | [[deps.OpenSSL]] 511 | deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] 512 | git-tree-sha1 = "38cb508d080d21dc1128f7fb04f20387ed4c0af4" 513 | uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" 514 | version = "1.4.3" 515 | 516 | [[deps.OpenSSL_jll]] 517 | deps = ["Artifacts", "JLLWrappers", "Libdl"] 518 | git-tree-sha1 = "a028ee3cb5641cccc4c24e90c36b0a4f7707bdf5" 519 | uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" 520 | version = "3.0.14+0" 521 | 522 | [[deps.OpenSpecFun_jll]] 523 | deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] 524 | git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1" 525 | uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" 526 | version = "0.5.5+0" 527 | 528 | [[deps.OrderedCollections]] 529 | git-tree-sha1 = "dfdf5519f235516220579f949664f1bf44e741c5" 530 | uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" 531 | version = "1.6.3" 532 | 533 | [[deps.PackageCompiler]] 534 | deps = ["Artifacts", "Glob", "LazyArtifacts", "Libdl", "Pkg", "Printf", "RelocatableFolders", "TOML", "UUIDs", "p7zip_jll"] 535 | git-tree-sha1 = "48d4429862157ad5500c4f61444db1b8c32e0a2b" 536 | uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" 537 | version = "2.1.17" 538 | 539 | [[deps.PackageExtensionCompat]] 540 | git-tree-sha1 = "fb28e33b8a95c4cee25ce296c817d89cc2e53518" 541 | uuid = "65ce6f38-6b18-4e1d-a461-8949797d7930" 542 | version = "1.0.2" 543 | weakdeps = ["Requires", "TOML"] 544 | 545 | [[deps.Parsers]] 546 | deps = ["Dates", "PrecompileTools", "UUIDs"] 547 | git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" 548 | uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" 549 | version = "2.8.1" 550 | 551 | [[deps.Pkg]] 552 | deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] 553 | uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" 554 | version = "1.10.0" 555 | 556 | [[deps.PooledArrays]] 557 | deps = ["DataAPI", "Future"] 558 | git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" 559 | uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" 560 | version = "1.4.3" 561 | 562 | [[deps.PrecompileTools]] 563 | deps = ["Preferences"] 564 | git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" 565 | uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" 566 | version = "1.2.1" 567 | 568 | [[deps.Preferences]] 569 | deps = ["TOML"] 570 | git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" 571 | uuid = "21216c6a-2e73-6563-6e65-726566657250" 572 | version = "1.4.3" 573 | 574 | [[deps.Printf]] 575 | deps = ["Unicode"] 576 | uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" 577 | 578 | [[deps.REPL]] 579 | deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] 580 | uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 581 | 582 | [[deps.Random]] 583 | deps = ["SHA"] 584 | uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 585 | 586 | [[deps.RealDot]] 587 | deps = ["LinearAlgebra"] 588 | git-tree-sha1 = "9f0a1b71baaf7650f4fa8a1d168c7fb6ee41f0c9" 589 | uuid = "c1ae055f-0cd5-4b69-90a6-9a35b1a98df9" 590 | version = "0.1.0" 591 | 592 | [[deps.Reexport]] 593 | git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" 594 | uuid = "189a3867-3050-52da-a836-e630ba90ab69" 595 | version = "1.2.2" 596 | 597 | [[deps.RelocatableFolders]] 598 | deps = ["SHA", "Scratch"] 599 | git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" 600 | uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" 601 | version = "1.0.1" 602 | 603 | [[deps.Requires]] 604 | deps = ["UUIDs"] 605 | git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" 606 | uuid = "ae029012-a4dd-5104-9daa-d747884805df" 607 | version = "1.3.0" 608 | 609 | [[deps.Roots]] 610 | deps = ["Accessors", "ChainRulesCore", "CommonSolve", "Printf"] 611 | git-tree-sha1 = "1ab580704784260ee5f45bffac810b152922747b" 612 | uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" 613 | version = "2.1.5" 614 | 615 | [deps.Roots.extensions] 616 | RootsForwardDiffExt = "ForwardDiff" 617 | RootsIntervalRootFindingExt = "IntervalRootFinding" 618 | RootsSymPyExt = "SymPy" 619 | RootsSymPyPythonCallExt = "SymPyPythonCall" 620 | 621 | [deps.Roots.weakdeps] 622 | ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" 623 | IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" 624 | SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" 625 | SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" 626 | 627 | [[deps.SHA]] 628 | uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" 629 | version = "0.7.0" 630 | 631 | [[deps.Scratch]] 632 | deps = ["Dates"] 633 | git-tree-sha1 = "3bac05bc7e74a75fd9cba4295cde4045d9fe2386" 634 | uuid = "6c6a2e73-6563-6170-7368-637461726353" 635 | version = "1.2.1" 636 | 637 | [[deps.SemioticOpt]] 638 | deps = ["Accessors", "InvertedIndices", "Lazy", "LinearAlgebra", "Zygote"] 639 | git-tree-sha1 = "bc2a070579895b48ac3ae1d05905761d5bf11b37" 640 | repo-rev = "main" 641 | repo-url = "https://github.com/semiotic-ai/SemioticOpt.jl.git" 642 | uuid = "a0c0fc10-8635-40d2-82ff-39f738735ee1" 643 | version = "0.7.0" 644 | 645 | [[deps.SentinelArrays]] 646 | deps = ["Dates", "Random"] 647 | git-tree-sha1 = "90b4f68892337554d31cdcdbe19e48989f26c7e6" 648 | uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" 649 | version = "1.4.3" 650 | 651 | [[deps.Serialization]] 652 | uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" 653 | 654 | [[deps.SimpleBufferStream]] 655 | git-tree-sha1 = "874e8867b33a00e784c8a7e4b60afe9e037b74e1" 656 | uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" 657 | version = "1.1.0" 658 | 659 | [[deps.Sockets]] 660 | uuid = "6462fe0b-24de-5631-8697-dd941f90decc" 661 | 662 | [[deps.SparseArrays]] 663 | deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] 664 | uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" 665 | version = "1.10.0" 666 | 667 | [[deps.SparseInverseSubset]] 668 | deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] 669 | git-tree-sha1 = "52962839426b75b3021296f7df242e40ecfc0852" 670 | uuid = "dc90abb0-5640-4711-901d-7e5b23a2fada" 671 | version = "0.1.2" 672 | 673 | [[deps.SpecialFunctions]] 674 | deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] 675 | git-tree-sha1 = "2f5d4697f21388cbe1ff299430dd169ef97d7e14" 676 | uuid = "276daf66-3868-5448-9aa4-cd146d93841b" 677 | version = "2.4.0" 678 | weakdeps = ["ChainRulesCore"] 679 | 680 | [deps.SpecialFunctions.extensions] 681 | SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" 682 | 683 | [[deps.SplitApplyCombine]] 684 | deps = ["Dictionaries", "Indexing"] 685 | git-tree-sha1 = "c06d695d51cfb2187e6848e98d6252df9101c588" 686 | uuid = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" 687 | version = "1.2.3" 688 | 689 | [[deps.StaticArrays]] 690 | deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] 691 | git-tree-sha1 = "6e00379a24597be4ae1ee6b2d882e15392040132" 692 | uuid = "90137ffa-7385-5640-81b9-e52037218182" 693 | version = "1.9.5" 694 | weakdeps = ["ChainRulesCore", "Statistics"] 695 | 696 | [deps.StaticArrays.extensions] 697 | StaticArraysChainRulesCoreExt = "ChainRulesCore" 698 | StaticArraysStatisticsExt = "Statistics" 699 | 700 | [[deps.StaticArraysCore]] 701 | git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" 702 | uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" 703 | version = "1.4.3" 704 | 705 | [[deps.Statistics]] 706 | deps = ["LinearAlgebra", "SparseArrays"] 707 | uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 708 | version = "1.10.0" 709 | 710 | [[deps.Strided]] 711 | deps = ["LinearAlgebra", "StridedViews", "TupleTools"] 712 | git-tree-sha1 = "bd9bd1c70cfc115cc3a30213fc725125a6b43652" 713 | uuid = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" 714 | version = "2.1.0" 715 | 716 | [[deps.StridedViews]] 717 | deps = ["LinearAlgebra", "PackageExtensionCompat"] 718 | git-tree-sha1 = "7ba0fdd6a050bb7bb2069095e91bfd7268d9a45a" 719 | uuid = "4db3bf67-4bd7-4b4e-b153-31dc3fb37143" 720 | version = "0.3.0" 721 | 722 | [deps.StridedViews.extensions] 723 | StridedViewsCUDAExt = "CUDA" 724 | 725 | [deps.StridedViews.weakdeps] 726 | CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" 727 | 728 | [[deps.StructArrays]] 729 | deps = ["ConstructionBase", "DataAPI", "Tables"] 730 | git-tree-sha1 = "f4dc295e983502292c4c3f951dbb4e985e35b3be" 731 | uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" 732 | version = "0.6.18" 733 | weakdeps = ["Adapt", "GPUArraysCore", "SparseArrays", "StaticArrays"] 734 | 735 | [deps.StructArrays.extensions] 736 | StructArraysAdaptExt = "Adapt" 737 | StructArraysGPUArraysCoreExt = "GPUArraysCore" 738 | StructArraysSparseArraysExt = "SparseArrays" 739 | StructArraysStaticArraysExt = "StaticArrays" 740 | 741 | [[deps.StructTypes]] 742 | deps = ["Dates", "UUIDs"] 743 | git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" 744 | uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" 745 | version = "1.10.0" 746 | 747 | [[deps.SuiteSparse]] 748 | deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] 749 | uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" 750 | 751 | [[deps.SuiteSparse_jll]] 752 | deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] 753 | uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" 754 | version = "7.2.1+1" 755 | 756 | [[deps.TOML]] 757 | deps = ["Dates"] 758 | uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" 759 | version = "1.0.3" 760 | 761 | [[deps.TableTraits]] 762 | deps = ["IteratorInterfaceExtensions"] 763 | git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" 764 | uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" 765 | version = "1.0.1" 766 | 767 | [[deps.Tables]] 768 | deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits"] 769 | git-tree-sha1 = "cb76cf677714c095e535e3501ac7954732aeea2d" 770 | uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" 771 | version = "1.11.1" 772 | 773 | [[deps.Tar]] 774 | deps = ["ArgTools", "SHA"] 775 | uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" 776 | version = "1.10.0" 777 | 778 | [[deps.TensorCast]] 779 | deps = ["ChainRulesCore", "Compat", "LazyStack", "LinearAlgebra", "MacroTools", "Random", "StaticArrays", "TransmuteDims"] 780 | git-tree-sha1 = "82a477d3afe53673393dd387806e34cd682a5ac4" 781 | uuid = "02d47bb6-7ce6-556a-be16-bb1710789e2b" 782 | version = "0.4.8" 783 | 784 | [[deps.Test]] 785 | deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] 786 | uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 787 | 788 | [[deps.TheGraphData]] 789 | deps = ["CSV", "GraphQLClient", "Mocking", "TensorCast", "TypedTables"] 790 | git-tree-sha1 = "96c38695d46bd3acbc60fe280eb8dec45464f7eb" 791 | repo-rev = "main" 792 | repo-url = "https://github.com/semiotic-ai/TheGraphData.jl.git" 793 | uuid = "871720c8-5dfb-4fa2-998e-3fe6ebd08819" 794 | version = "0.2.1" 795 | 796 | [[deps.TranscodingStreams]] 797 | git-tree-sha1 = "a947ea21087caba0a798c5e494d0bb78e3a1a3a0" 798 | uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" 799 | version = "0.10.9" 800 | weakdeps = ["Random", "Test"] 801 | 802 | [deps.TranscodingStreams.extensions] 803 | TestExt = ["Test", "Random"] 804 | 805 | [[deps.TransmuteDims]] 806 | deps = ["Adapt", "ChainRulesCore", "GPUArraysCore", "LinearAlgebra", "Requires", "Strided"] 807 | git-tree-sha1 = "5b6f1f2ba5e91983eabc47cb362f92d9a96b579f" 808 | uuid = "24ddb15e-299a-5cc3-8414-dbddc482d9ca" 809 | version = "0.1.16" 810 | 811 | [[deps.TupleTools]] 812 | git-tree-sha1 = "41d61b1c545b06279871ef1a4b5fcb2cac2191cd" 813 | uuid = "9d95972d-f1c8-5527-a6e0-b4b365fa01f6" 814 | version = "1.5.0" 815 | 816 | [[deps.TypedTables]] 817 | deps = ["Adapt", "Dictionaries", "Indexing", "SplitApplyCombine", "Tables", "Unicode"] 818 | git-tree-sha1 = "84fd7dadde577e01eb4323b7e7b9cb51c62c60d4" 819 | uuid = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9" 820 | version = "1.4.6" 821 | 822 | [[deps.URIs]] 823 | git-tree-sha1 = "67db6cc7b3821e19ebe75791a9dd19c9b1188f2b" 824 | uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" 825 | version = "1.5.1" 826 | 827 | [[deps.UUIDs]] 828 | deps = ["Random", "SHA"] 829 | uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" 830 | 831 | [[deps.Unicode]] 832 | uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" 833 | 834 | [[deps.WeakRefStrings]] 835 | deps = ["DataAPI", "InlineStrings", "Parsers"] 836 | git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" 837 | uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" 838 | version = "1.4.2" 839 | 840 | [[deps.WorkerUtilities]] 841 | git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" 842 | uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" 843 | version = "1.6.1" 844 | 845 | [[deps.Zlib_jll]] 846 | deps = ["Libdl"] 847 | uuid = "83775a58-1f1d-513f-b197-d71354ab007a" 848 | version = "1.2.13+1" 849 | 850 | [[deps.Zygote]] 851 | deps = ["AbstractFFTs", "ChainRules", "ChainRulesCore", "DiffRules", "Distributed", "FillArrays", "ForwardDiff", "GPUArrays", "GPUArraysCore", "IRTools", "InteractiveUtils", "LinearAlgebra", "LogExpFunctions", "MacroTools", "NaNMath", "PrecompileTools", "Random", "Requires", "SparseArrays", "SpecialFunctions", "Statistics", "ZygoteRules"] 852 | git-tree-sha1 = "19c586905e78a26f7e4e97f81716057bd6b1bc54" 853 | uuid = "e88e6eb3-aa80-5325-afca-941959d7151f" 854 | version = "0.6.70" 855 | 856 | [deps.Zygote.extensions] 857 | ZygoteColorsExt = "Colors" 858 | ZygoteDistancesExt = "Distances" 859 | ZygoteTrackerExt = "Tracker" 860 | 861 | [deps.Zygote.weakdeps] 862 | Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" 863 | Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" 864 | Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" 865 | 866 | [[deps.ZygoteRules]] 867 | deps = ["ChainRulesCore", "MacroTools"] 868 | git-tree-sha1 = "27798139afc0a2afa7b1824c206d5e87ea587a00" 869 | uuid = "700de1a5-db45-46bc-99cf-38207098b444" 870 | version = "0.2.5" 871 | 872 | [[deps.libblastrampoline_jll]] 873 | deps = ["Artifacts", "Libdl"] 874 | uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" 875 | version = "5.8.0+1" 876 | 877 | [[deps.nghttp2_jll]] 878 | deps = ["Artifacts", "Libdl"] 879 | uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" 880 | version = "1.52.0+1" 881 | 882 | [[deps.p7zip_jll]] 883 | deps = ["Artifacts", "Libdl"] 884 | uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" 885 | version = "17.4.0+2" 886 | --------------------------------------------------------------------------------