diff --git a/Manifest.toml b/Manifest.toml index 7d37bde..f25505c 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.10.4" manifest_format = "2.0" -project_hash = "ff970d92f232ef47417c26099c8defc3792686da" +project_hash = "dfe00eabbb715663ec2bf76f0cf0fd9257b705b0" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] diff --git a/README.md b/README.md index 05ade62..6bbece0 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,22 @@ Run the provided binary pointing at the configuration TOML that you would like t **NOTE:** This binary only works for x86 Linux. If you are you a different operating system or architecture, please see the documentation for other options. -**IMPORTANT:** By default, `opt_mode="fast"`. -Fast-mode is not guaranteed to converge to a global optimum. -If you get strange results, you should try `opt_mode="optimal"`. -This mode is still experimental, and will take longer to run, but you may get more reasonable results with it. +**IMPORTANT:** By default, `opt_mode="optimal"`. +Because of our algorithm, `optimal` mode may take a long time to converge. +If this is the case for you, you have two options. +You can use `opt_mode=fastgas`, which runs a different algorithm. +This algorithm is not guaranteed to find the optimal value, and may fail to ever converge (it could hang). +However, it still considers gas unlike the third option `opt_mode=fastnogas`. +This is your fastest option, but it won't take into account gas costs or your preferences for max allocations. +This mode is appropriate when you have negligible gas fees and are okay with allocating to a large number of subgraphs. ## Configuration An example configuration TOML file might look as below. ``` toml -id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" +id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" writedir = "data" -readdir = "data" max_allocations = 10 whitelist = [] blacklist = [] @@ -47,15 +50,17 @@ verbose = true num_reported_options = 2 execution_mode = "none" opt_mode = "optimal" +network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" +protocol_network = "arbitrum" +syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] ``` - ### Detailed Field Descriptions - `id::String`: The ID of the indexer for whom we're optimising. No default value. -- `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer - support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the - provided API serves the query requests. If unspecified, +- `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer + support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the + provided API serves the query requests. If unspecified, `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` - `writedir::String`: The directory to which to write the results of optimisation. If don't specify `readdir`, `writedir` also specifies the path to which to save @@ -96,13 +101,20 @@ opt_mode = "optimal" - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want to execute the allocation strategies on. If you specify `"actionqueue"`, you must also specify `indexer_url`. If unspecified, `nothing` -- `opt_mode::String`: We support two optimisation modes. One is `"fast"`. This mode is - fast, but may not find the optimal strategy. This mode is also used to the top - `num_reported_options` allocation strategies. The other mode is `"optimal"`. This - mode is slower, but it satisfy stronger optimality conditions. It will find strategies - at least as good as `"fast"`, but not guaranteed to be better. In general, we recommend - exploring config options using `"fast"` mode first, and then using `"optimal"` - mode to find the optimal allocation. By default, `"optimal"` +- `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does + not consider gas costs and optimises allocation amount over all subgraph deployments. + Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and + could potentially fail to converge. This mode is also used to the top + `num_reported_options` allocation strategies. The final mode is `"optimal"`. + This mode is slower, but it satisfies stronger optimality conditions. + It will find strategies at least as good as `"fast"`, but not guaranteed to be better. + By default, `"optimal"` +- `protocol_network::String`: Defines the protocol network that allocation transactions + should be sent to. The current protocol network options are "mainnet", "goerli", + "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` +- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting + the set of possible subgraphs. This list should match the networks available to your + graph-node. By default, the list is a singleton of your protocol network ### Example Configurations @@ -149,7 +161,7 @@ num_reported_options = 2 execution_mode = "rules" ``` -#### Query data Instead of Reading Local CSVs +#### Query Data Instead of Reading Local CSVs Just don't specify the `readdir`. @@ -167,22 +179,11 @@ min_signal = 100 verbose = true num_reported_options = 2 execution_mode = "none" +network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" +protocol_network = "arbitrum" +syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] ``` -#### Query data for specified networks - -Specify the network subgraph endpoint for networks other than The Graph network on Ethereum mainnet. Here we use the endpoint to goerli network subgraph. - -``` toml -id = "0xE9a1CABd57700B17945Fd81feeFba82340D9568F" -network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-goerli" -``` - -Other available endpoints examples are -- Mainnet (default): https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet -- Arbitrum-One: https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum -- Arbitrum-Goerli: https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum-goerli - #### Quiet Mode We set `verbose` to `false` here to surpress info messages. diff --git a/config.toml b/config.toml index f5b1d08..5a71fbc 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,6 @@ id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" writedir = "data" +readdir = "data" max_allocations = 10 whitelist = [] blacklist = [] diff --git a/docs/src/call-julia.md b/docs/src/call-julia.md index b63501f..fcd4e8f 100644 --- a/docs/src/call-julia.md +++ b/docs/src/call-julia.md @@ -1,7 +1,7 @@ # Calling From Julia -If you're okay with having the Julia runtime on your computer, and you don't mind the precompilation time, this is the preferred way to run the Allocation Optimizer. +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. In part, this is because access the to Julia runtime will enable you to add your own features if you'd like. You can even (hopefully) even submit some PRs with features to help other indexers! Compiling on your machine also allows Julia to specialise on idiosyncrasies of your hardware. @@ -13,17 +13,17 @@ That said, here's how to install and use the Allocation Optimizer from Julia. Install Julia! We prefer to use `juliaup`. You can install this via: - + ```bash curl -fsSL https://install.julialang.org | sh ``` - + !!! note - As of writing this documentation, the latest version of Julia is v1.8. + As of writing this documentation, the latest version of Julia is v1.10. This the version the Allocation Optimizer currently uses, and the version `juliaup` will install by default. - If `juliaup` begins to use v1.9, then you may need to use `juliaup` to manually install v1.8 via `juliaup add 1.8`. Then, you can either set the default to be v1.8 using `juliaup default 1.8`, or you can replace every time you see `julia` with `julia +1.8` below. - - + 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. + + Clone this repository and `cd` into it. ```bash @@ -44,7 +44,7 @@ julia> ] pkg> instantiate ``` -Set up your configuration file. +Set up your configuration file. See [Configuration](@ref) for details. From the Julia REPL (the TUI that comes up when you use `julia --project`), run the `main` function with the path to your config. diff --git a/docs/src/configuration.md b/docs/src/configuration.md index c42891e..a85895f 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -7,12 +7,172 @@ Secondly, if something breaks, it makes it easier for us to reproduce what went An example configuration TOML file might look as below. +``` toml +id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" +writedir = "data" +max_allocations = 10 +whitelist = [] +blacklist = [] +frozenlist = [] +pinnedlist = [] +allocation_lifetime = 28 +gas = 100 +min_signal = 100 +verbose = true +num_reported_options = 2 +execution_mode = "none" +opt_mode = "optimal" +network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" +protocol_network = "arbitrum" +syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] +``` + +### Detailed Field Descriptions + +- `id::String`: The ID of the indexer for whom we're optimising. No default value. +- `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer + support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the + provided API serves the query requests. If unspecified, + `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` +- `writedir::String`: The directory to which to write the results of optimisation. + If don't specify `readdir`, `writedir` also specifies the path to which to save + the input data tables. If unspecified, `"."` +- `readdir::Union{String, Nothing}`: The directory from which to read saved data tables. + This speeds up the process as we won't have to query the network subgraph for the + relevant data. If you don't specify `readdir`, we will query your specified + `network_subgraph_endpoint` for the data and write it to CSV files in `writedir`. + This way, you can use your previous `writedir` as your `readdir` in future runs. + If unspecified, `nothing` +- `whitelist::Vector{String}`: A list of subgraph IPFS hashes that you want to consider + as candidates to which to allocate. If you leave this empty, we'll assume all subgraphs + are in the whitelist. If unspecified, `String[]` +- `blacklist::Vector{String}`: A list of subgraph IPFS hashes that you do not want to + consider allocating to. For example, this list could include broken subgraphs or + subgraphs that you don't want to index. If unspecified, `String[]` +- `frozenlist::Vector{String}`: If you have open allocations that you don't want to change, + add the corresponding subgraph IPFS hashes to this list. If unspecified, `String[]` +- `pinnedlist::Vector{String}`: If you have subgraphs that you absolutely want to be + allocated to, even if only with a negligible amount of GRT, add it to this list. + If unspecified, `String[]` +- `allocation_lifetime::Integer`: The number of epochs for which you expect the allocations + the optimiser finds to be open. If unspecified, `28` +- `gas::Real`: The estimated gas cost in GRT to open/close allocations. If unspecified, `100` +- `min_signal::Real`: The minimum amount of signal in GRT that must be on a subgraph + in order for you to consider allocating to it. If unspecified, `100` +- `max_allocations::Integer`: The maximum number of new allocations you'd like the optimiser + to consider opening. If unspecified, `10` +- `num_reported_options::Integer`: The number of proposed allocation strategies to report. + For example, if you select `10` we'd report best 10 allocation strategies ranked by + profit. Options are reported to a *report.json* in your `writedir`. If unspecified, `1` +- `verbose::Bool`: If true, the optimiser will print details about what it is doing to + stdout. If unspecified, `false` +- `execution_mode::String`: How the optimiser should execute the allocation strategies it + finds. Options are `"none"`, which won't do anything, `"actionqueue"`, which will + push actions to the action queue, and `"rules"`, which will generate indexing rules. + If unspecified, `"none"` +- `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want + to execute the allocation strategies on. If you specify `"actionqueue"`, you must also + specify `indexer_url`. If unspecified, `nothing` +- `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does + not consider gas costs and optimises allocation amount over all subgraph deployments. + Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and + could potentially fail to converge. This mode is also used to the top + `num_reported_options` allocation strategies. The final mode is `"optimal"`. + This mode is slower, but it satisfies stronger optimality conditions. + It will find strategies at least as good as `"fast"`, but not guaranteed to be better. + By default, `"optimal"` +- `protocol_network::String`: Defines the protocol network that allocation transactions + should be sent to. The current protocol network options are "mainnet", "goerli", + "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` +- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting + the set of possible subgraphs. This list should match the networks available to your + graph-node. By default, the list is a singleton of your protocol network + +### Example Configurations + +#### ActionQueue + +Set `execution_mode` to `"actionqueue"` and provide an `indexer_url`. + +``` toml +id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" +writedir = "data" +readdir = "data" +max_allocations = 10 +whitelist = [] +blacklist = [] +frozenlist = [] +pinnedlist = [] +allocation_lifetime = 28 +gas = 100 +min_signal = 100 +verbose = true +num_reported_options = 2 +execution_mode = "actionqueue" +indexer_url = "https://localhost:8000" +``` + +#### Indexer Rules + +Change `execution_mode` to `"rules"`. + +``` toml +id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" +writedir = "data" +readdir = "data" +max_allocations = 10 +whitelist = [] +blacklist = [] +frozenlist = [] +pinnedlist = [] +allocation_lifetime = 28 +gas = 100 +min_signal = 100 +verbose = true +num_reported_options = 2 +execution_mode = "rules" +``` + +#### Query data Instead of Reading Local CSVs + +Just don't specify the `readdir`. + +``` toml +id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" +writedir = "data" +max_allocations = 10 +whitelist = [] +blacklist = [] +frozenlist = [] +pinnedlist = [] +allocation_lifetime = 28 +gas = 100 +min_signal = 100 +verbose = true +num_reported_options = 2 +execution_mode = "none" +network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" +protocol_network = "arbitrum" +syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] +``` + +#### Quiet Mode + +We set `verbose` to `false` here to surpress info messages. + ``` toml id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" writedir = "data" readdir = "data" max_allocations = 10 whitelist = [] +blacklist = []An example configuration TOML file might look as below. + +``` toml +id = "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3" +writedir = "data" +max_allocations = 10 +whitelist = [] blacklist = [] frozenlist = [] pinnedlist = [] @@ -22,18 +182,19 @@ min_signal = 100 verbose = true num_reported_options = 2 execution_mode = "none" -opt_mode = "fast" -protocol_network = "mainnet" -syncing_networks = ["mainnet"] +opt_mode = "optimal" +network_subgraph_endpoint = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-arbitrum" +protocol_network = "arbitrum" +syncing_networks = ["mainnet", "gnosis", "arbitrum-one", "arbitrum"] ``` ### Detailed Field Descriptions - `id::String`: The ID of the indexer for whom we're optimising. No default value. -- `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer - support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the - provided API serves the query requests. If unspecified, +- `network_subgraph_endpoint::String`: The network subgraph endpoint to query. The optimizer + support any network (such as mainnet, goerli, arbitrum-one, arbitrum-goerli) as long as the + provided API serves the query requests. If unspecified, `"https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet"` - `writedir::String`: The directory to which to write the results of optimisation. If don't specify `readdir`, `writedir` also specifies the path to which to save @@ -74,18 +235,19 @@ syncing_networks = ["mainnet"] - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want to execute the allocation strategies on. If you specify `"actionqueue"`, you must also specify `indexer_url`. If unspecified, `nothing` -- `opt_mode::String`: We support two optimisation modes. One is `"fast"`. This mode is - fast, but may not find the optimal strategy. This mode is also used to the top - `num_reported_options` allocation strategies. The other mode is `"optimal"`. This - mode is slower, but it satisfy stronger optimality conditions. It will find strategies - at least as good as `"fast"`, but not guaranteed to be better. In general, we recommend - exploring config options using `"fast"` mode first, and then using `"optimal"` - mode to find the optimal allocation. By default, `"optimal"` -- `protocol_network::String`: Defines the protocol network that allocation transactions - should be sent to. The current protocol network options are "mainnet", "goerli", +- `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does + not consider gas costs and optimises allocation amount over all subgraph deployments. + Second one is `"fastgas"`. This mode is fast, but may not find the optimal strategy and + could potentially fail to converge. This mode is also used to the top + `num_reported_options` allocation strategies. The final mode is `"optimal"`. + This mode is slower, but it satisfies stronger optimality conditions. + It will find strategies at least as good as `"fast"`, but not guaranteed to be better. + By default, `"optimal"` +- `protocol_network::String`: Defines the protocol network that allocation transactions + should be sent to. The current protocol network options are "mainnet", "goerli", "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` -- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting - the set of possible subgraphs. This list should match the networks available to your +- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting + the set of possible subgraphs. This list should match the networks available to your graph-node. By default, the list is a singleton of your protocol network ### Example Configurations @@ -220,3 +382,35 @@ execution_mode = "none" ``` +2 +execution_mode = "none" +``` + +#### Whitelisting Subgraphs + +Add some subgraph deployment IDs to the `whitelist`. +If, in addition or instead you want to use `blacklist`, `frozenlist`, or `pinnedlist`, you can +similarly add subgraph deployment IDs to those lists. +Notice that we did not change `max_allocations` here. +If `max_allocations` exceeds the number of available subgraphs (2 in this case), the code will +treat the number of available subgraphs as `max_allocations`. + +``` toml +id = "0xd75c4dbcb215a6cf9097cfbcc70aab2596b96a9c" +writedir = "data" +readdir = "data" +max_allocations = 10 +whitelist = [ + "QmUVskWrz1ZiQZ76AtyhcfFDEH1ELnRpoyEhVL8p6NFTbR", + "QmcBSr5R3K2M5tk8qeHFaX8pxAhdViYhcKD8ZegYuTcUhC" +] +blacklist = [] +frozenlist = [] +pinnedlist = [] +allocation_lifetime = 28 +gas = 100 +min_signal = 100 +verbose = false +num_reported_options = 2 +execution_mode = "none" +``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index ee9b34e..79d4f3a 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -12,17 +12,19 @@ The goal of this project is to enable indexers to quickly determine how to alloc Query fee information is also not public. It is local to each gateway. As a result, we will never be able to optimise with respect to query fees unless this changes. - + !!! note - By default, `opt_mode="fast"`. - Fast-mode is not guaranteed to converge to a global optimum. - If you get strange results, you should try `opt_mode="optimal"`. - This mode is still experimental, and will take longer to run, but you may get more reasonable results with it. - + By default, `opt_mode="optimal"`. + Because of our algorithm, `optimal` mode may take a long time to converge. + If this is the case for you, you have two options. + You can use `opt_mode=fastgas`, which runs a different algorithm. + This algorithm is not guaranteed to find the optimal value, and may fail to ever converge (it could hang). + However, it still considers gas unlike the third option `opt_mode=fastnogas`. + This is your fastest option, but it won't take into account gas costs or your preferences for max allocations. + This mode is appropriate when you have negligible gas fees and are okay with allocating to a large number of subgraphs. + We will focus on usage of the code in this documentation. We refer you to these [blog posts](https://semiotic.ai/articles/indexer-allocation-optimisation/) for more technical details. -We also plan to post a yellowpaper at some point diving into our approach in even more detail. -Stay tuned for that! If interested in how the code works, take a peek [Under The Hood](@ref)! There are a few different ways you can run the allocation optimizer. diff --git a/docs/src/provided-binary.md b/docs/src/provided-binary.md index 9b7e840..f073889 100644 --- a/docs/src/provided-binary.md +++ b/docs/src/provided-binary.md @@ -4,7 +4,7 @@ Using the provided binary has the advantage of being that only method that doesn If you want to get running as quickly as possible, this is the path for you. !!! note - We only release binaries for x86 Linux. + We only release binaries for x86 Linux. We will not support any other architectures or operating systems. 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. diff --git a/src/configuration.jl b/src/configuration.jl index fbbeef1..3a5d283 100644 --- a/src/configuration.jl +++ b/src/configuration.jl @@ -49,18 +49,20 @@ Set default values for the config dictionary if the value was not specified in t - `indexer_url::Union{String, Nothing}`: The URL of the indexer management server you want to execute the allocation strategies on. If you specify `"actionqueue"`, you must also specify `indexer_url`. By default, `nothing` -- `opt_mode::String`: We support two optimisation modes. One is `"fast"`. This mode is - fast, but may not find the optimal strategy. This mode is also used to the top - `num_reported_options` allocation strategies. The other mode is `"optimal"`. This - mode is slower, but it satisfy stronger optimality conditions. It will find strategies - at least as good as `"fast"`, but not guaranteed to be better. In general, we recommend - exploring config options using `"fast"` mode first, and then using `"optimal"` +- `opt_mode::String`: We support three optimisation modes. One is `"fastnogas"`. This mode does + not consider gas costs and optimises allocation amount over all subgraph deployments. + Second one is `"fastgas"`. This mode considers gas, may not find the optimal strategy and + could potentially fail to converge. This mode is also used to the top + `num_reported_options` allocation strategies. The final mode is `"optimal"`. + This mode is slower, but it satisfies stronger optimality conditions. + It will find strategies at least as good as `"fastgas"`, but not guaranteed to be better. + By default, `"optimal"` mode to find the optimal allocation. By default, `"optimal"` -- `protocol_network::String`: Defines the protocol network that allocation transactions - should be sent to. The current protocol network options are "mainnet", "goerli", +- `protocol_network::String`: Defines the protocol network that allocation transactions + should be sent to. The current protocol network options are "mainnet", "goerli", "arbitrum", and "arbitrum-goerli". By default, `"mainnet"` -- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting - the set of possible subgraphs. This list should match the networks available to your +- `syncing_networks::Vector{String}`: The list of syncing networks to support when selecting + the set of possible subgraphs. This list should match the networks available to your graph-node. By default, the list is a singleton of your protocol network ```julia diff --git a/src/opt.jl b/src/opt.jl index cd2ca9e..a0317af 100644 --- a/src/opt.jl +++ b/src/opt.jl @@ -154,12 +154,13 @@ Find the optimal solution vector given allocations of other indexers `Ω`, signa Dispatches to [`optimize`](@ref) with the `opt_mode` key. -If `opt_mode` is `fast`, then run projected gradient descent with GSSP and Halpern. +If `opt_mode` is `fastgas`, then run projected gradient descent with GSSP and Halpern. +If `opt_mode` is `fastnogas`, then run analytics solution over all eligible subgraphs. If `opt_mode` is `optimal`, then run Pairwise Greedy Optimisation. ```julia julia> using AllocationOpt -julia> config = Dict("opt_mode" => "fast") +julia> config = Dict("opt_mode" => "fastgas") julia> rixs = [1, 2] julia> Ω = [1.0, 1.0] julia> ψ = [10.0, 10.0] @@ -177,7 +178,57 @@ function optimize(Ω, ψ, σ, K, Φ, Ψ, g, rixs, config::AbstractDict) end """ - optimize(::Val{:fast}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) + optimize(::Val{:fastnogas}, Ω, ψ, σ, _, Φ, Ψ, g, rixs) + +Find the analytic optimal vector for given allocations of other indexers `Ω`, signals +`ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`. +`g` is the gas, but it is not used since analytic optimisation assumes 0 gas fees. +`rixs` are the indices of subgraphs that are eligible to receive indexing rewards. + +```julia +julia> using AllocationOpt +julia> rixs = [1, 2] +julia> Ω = [1.0, 1.0] +julia> ψ = [10.0, 10.0] +julia> σ = 5.0 +julia> Φ = 1.0 +julia> Ψ = 20.0 +julia> g = 0.01 +julia> xs, nonzeros, profits = AllocationOpt.optimize(Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs) +([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) +``` +""" +function optimize(::Val{:fastnogas}, Ω, ψ, σ, _, Φ, Ψ, g, rixs) + if g != 0 + @warn "fastnogas mode ignores the gas cost you set in your config" + end + + # Helper function to compute profit + f = x -> profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), zero(typeof(g))) + + # Only use the eligible subgraphs + _Ω = @view Ω[rixs] + _ψ = @view ψ[rixs] + + # Get the analytic solution + _xopt = optimizeanalytic(_Ω, _ψ, σ) + + xopt = zeros(length(Ω), 1) + xopt[rixs, :] .= _xopt + + # Preallocate solution vectors for in-place operations + profits = Matrix{Float64}(undef, length(xopt), 1) + nonzeros = Vector{Int32}(undef, 1) + + # Compute non-zeros and profits + nonzeros[1] = xopt[:] |> nonzero |> length + profits[:, 1] .= f(xopt) + + return xopt, nonzeros, profits +end + +""" + optimize(::Val{:fastgas}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) Find the optimal vectors for k ∈ [1,`K`] given allocations of other indexers `Ω`, signals `ψ`, available stake `σ`, new tokens issued `Φ`, total signal `Ψ`, and gas in grt `g`. @@ -193,11 +244,11 @@ julia> K = 2 julia> Φ = 1.0 julia> Ψ = 20.0 julia> g = 0.01 -julia> xs, nonzeros, profits = AllocationOpt.optimize(Val(:fast), Ω, ψ, σ, K, Φ, Ψ, g, rixs) +julia> xs, nonzeros, profits = AllocationOpt.optimize(Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs) ([5.0 2.5; 0.0 2.5], Int32[1, 2], [0.4066666666666667 0.34714285714285714; 0.0 0.34714285714285714]) ``` """ -function optimize(val::Val{:fast}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) +function optimize(val::Val{:fastgas}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) # Helper function to compute profit f = x -> profit.(indexingreward.(x, Ω, ψ, Φ, Ψ), g) @@ -227,7 +278,7 @@ function optimize(val::Val{:fast}, Ω, ψ, σ, K, Φ, Ψ, g, rixs) end """ - optimizek(::Val{:fast}, x₀, Ω, ψ, σ, k, Φ, Ψ) + optimizek(::Val{:fastgas}, x₀, Ω, ψ, σ, k, Φ, Ψ) Find the optimal `k` sparse vector given initial value `x₀`, allocations of other indexers `Ω`, signals `ψ`, available stake `σ`, new tokens issued `Φ`, and total signal `Ψ`. @@ -241,13 +292,13 @@ julia> σ = 5.0 julia> k = 1 julia> Φ = 1.0 julia> Ψ = 20.0 -julia> AllocationOpt.optimizek(Val(:fast), x₀, Ω, ψ, σ, k, Φ, Ψ) +julia> AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) 2-element Vector{Float64}: 5.0 0.0 ``` """ -function optimizek(::Val{:fast}, x₀, Ω, ψ, σ, k, Φ, Ψ) +function optimizek(::Val{:fastgas}, x₀, Ω, ψ, σ, k, Φ, Ψ) projection = x -> gssp(x, k, σ) alg = ProjectedGradientDescent(; x=x₀, diff --git a/test/opt.jl b/test/opt.jl index 6c67862..e79cbe2 100644 --- a/test/opt.jl +++ b/test/opt.jl @@ -71,7 +71,7 @@ end @testset "optimizek" begin - @testset "fast" begin + @testset "fastgas" begin x₀ = [2.5, 2.5] Ω = [1.0, 1.0] ψ = [10.0, 10.0] @@ -79,7 +79,7 @@ k = 1 Φ = 1.0 Ψ = 20.0 - @test AllocationOpt.optimizek(Val(:fast), x₀, Ω, ψ, σ, k, Φ, Ψ) == [5.0, 0.0] + @test AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) == [5.0, 0.0] x₀ = [2.5, 2.5] Ω = [1.0, 1.0] @@ -88,7 +88,7 @@ k = 2 Φ = 1.0 Ψ = 20.0 - @test AllocationOpt.optimizek(Val(:fast), x₀, Ω, ψ, σ, k, Φ, Ψ) == [2.5, 2.5] + @test AllocationOpt.optimizek(Val(:fastgas), x₀, Ω, ψ, σ, k, Φ, Ψ) == [2.5, 2.5] end @testset "optimal" begin @@ -117,7 +117,39 @@ end @testset "optimize" begin - @testset "fast" begin + @testset "fastnogas" begin + rixs = [1, 2] + Ω = [1.0, 1.0] + ψ = [10.0, 10.0] + σ = 5.0 + K = 2 + Φ = 1.0 + Ψ = 20.0 + g = 0.0 + xs, nonzeros, profits = AllocationOpt.optimize( + Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs + ) + @test xs == [2.5; 2.5;;] + @test nonzeros == [2] + @test isapprox(profits, [0.35; 0.35]; atol=0.1) + + rixs = [2] + Ω = [1.0, 1.0] + ψ = [10.0, 10.0] + σ = 5.0 + K = 1 + Φ = 1.0 + Ψ = 20.0 + g = 0.0 + xs, nonzeros, profits = AllocationOpt.optimize( + Val(:fastnogas), Ω, ψ, σ, K, Φ, Ψ, g, rixs + ) + @test xs == [0.0; 5.0;;] + @test nonzeros == [1] + @test isapprox(profits, [0.0, 0.41]; atol=0.1) + end + + @testset "fastgas" begin rixs = [1, 2] Ω = [1.0, 1.0] ψ = [10.0, 10.0] @@ -127,7 +159,7 @@ Ψ = 20.0 g = 0.01 xs, nonzeros, profits = AllocationOpt.optimize( - Val(:fast), Ω, ψ, σ, K, Φ, Ψ, g, rixs + Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs ) @test xs == [[5.0 2.5]; [0.0 2.5]] @test nonzeros == [1, 2] @@ -142,7 +174,7 @@ Ψ = 20.0 g = 0.01 xs, nonzeros, profits = AllocationOpt.optimize( - Val(:fast), Ω, ψ, σ, K, Φ, Ψ, g, rixs + Val(:fastgas), Ω, ψ, σ, K, Φ, Ψ, g, rixs ) @test xs == [0.0; 5.0;;] @test nonzeros == [1] @@ -213,7 +245,23 @@ end @testset "dispatch" begin - config = Dict("opt_mode" => "fast") + config = Dict("opt_mode" => "fastnogas") + rixs = [1, 2] + Ω = [1.0, 1.0] + ψ = [10.0, 10.0] + σ = 5.0 + K = 2 + Φ = 1.0 + Ψ = 20.0 + g = 0.01 + xs, nonzeros, profits = AllocationOpt.optimize( + Ω, ψ, σ, K, Φ, Ψ, g, rixs, config + ) + @test xs == [2.5; 2.5;;] + @test nonzeros == [2] + @test isapprox(profits, [0.35; 0.35]; atol=0.1) + + config = Dict("opt_mode" => "fastgas") rixs = [1, 2] Ω = [1.0, 1.0] ψ = [10.0, 10.0]