Strengths and Weaknesses of Conviction Voting and Other Mechanisms

My formula here is horribly wrong and over complicated.

@rex Kindly pointed out to me. SME Signal boost coefficient is calculated for the voter, not for the project.

The following should be correct:

def quadratic_fund(str: project) -> float:
    allocation = np.square(np.sum([sqrt(agent.coefficient * agent.votes.get(project)) for agent in system.agents]))
    return allocation
1 Like

The TEC is funding QF and other funding mechanism analysis here: GitHub - CommonsBuild/alloha: Processing for the TE grants round based on the Gitcoin Allo protocol

I’ve made a dataframe implementation based off of @octopus code.

This software is super alpha, version 0.1.0. Expect the structure to change.

The purpose is to facilitate the TECQFSME signal processing for their QF rounds, and also to serve as a research hub for funding mechanisms in token engineering and token sciences.

1 Like

Given a dataframe with rows being donation events, with columns projectId, and amountUSD, here is a vectorized version of QF algo using dataframes:

# Compute Quadratic Funding:
qf = donations_df.groupby('projectId')['amountUSD'].apply(
            lambda x: np.square(np.sum(np.sqrt(x)))
        )

# Scale to be proportional to matching pool
qf_allocations = (qf / qf.sum()) * matching_pool

That’s phenomenal, @linuxiscool !

Note to any students or non-programmers that: the last time we checked this:

np speed >>>> list speed >> for-loop speed

I should do the experiments again so I don’t risk spreading misinformation…computer things change quickly.

If we are using QF with pairwise -bonding coefficient penalties (which GitCoin was using when we got there), it’s only slightly harder to take that into account.

1 Like

Whoo this is so fun. Thanks for replying :octopus: . I really appreciate your comments.

I took the time to write some thoughts:

Vectorization :tada:

YEAH That’s a great point about speed! Numpy is vectorized! Which means you get contiguous data structures (arrays) in memory!!! Because everything in Python is linked lists LMAO!!! So like contiguous memory data structures in python is like not a thing by default.

Numpy grants us contiguous arrays and vectorized functions. A vectorized function applied to an array tends to be like… orders of magnitude faster than say iterating over a list (I believe… pretty bold claim I know… let’s see the experimentation… Banking on anecdotal experience here… Embodiment practice… But also verify).

Soo… Vectorization is really fun.

DataFrame Oriented Software Engineering

For the reasons above, I really enjoy the art of producing Dataframe one liners. I enjoy seeing the loading and transforming of data as dataframes or arrays. I consider them to have the following qualities:

  1. expresiveness
  2. clarity
  3. aesthetics
  4. speed/performance
  5. interprability
  6. standardization

I often think about a prospective future of software engineering practice that uses pandas dataframes as a primary in-memory data structure. Combining this with in memory caches like redis is very powerful. It’s like a data-science-first software engineering practice. Hmmm… that gets me thinking, is anyone doing on-chain in-memory?. Imagine like an ipfs Redis on chain.

I plan on returning with some experimentation results… :bat:

1 Like

To start towards a speedtest I made a minimum viable example for the object oriented approach based off of the code provided by @octopus at the top of the thread.

import numpy as np
from collections import defaultdict

class System:
    agents = []
    
    def __init__(self, agents):
        self.agents = agents


class DefaultDictWithGet(defaultdict):
    """
    This is a funny little class that is a defaultdict that works with the `.get` function that dicts have. Made with help of chatgpt.
    """
    def get(self, key, default=None):
        # If the key is not present, return the default factory value
        if key not in self:
            return self.default_factory()
        return super().get(key, default)


class Agent:
    votes = {}
    name = ''
    
    def __init__(self, name, votes):
        self.name = name
        self.votes = DefaultDictWithGet(int, votes) # Int returns 0 by default
    
    def get(project: str):
        return self.votes[project]

agents = [Agent(name='Shawn', votes={'happysaucepublicgoods':5, 'TECProWrestlingLeague':4}), Agent(name='Kai', votes={'scoobysnacks':10}), Agent(name='Octopus', votes={'TECProWrestlingLeague':9})]

system = System(agents)

def quadratic_fund(project: str) -> float:
    allocation = np.square(np.sum([np.sqrt(agent.votes.get(project)) for agent in system.agents]))
    return allocation

[(project, quadratic_fund(project)) for project in ['happysaucepublicgoods', 'scoobysnacks', 'TECProWrestlingLeague']]

The above code yields:

[('happysaucepublicgoods', 5.000000000000001),
 ('scoobysnacks', 10.000000000000002),
 ('TECProWrestlingLeague', 25.0)]

Don’t mind the python precision errors.

I really love this idea of focusing on dataframes because it’s a relatively easy pathway for getting non-programming users to running experiments on their own parameters, i.e. edit a predefined spreadsheet which can then be collected via a utility script, made into a DataFrame, run through whatever internal logic, and displayed as a predefined output.

Is “uploading a csv” more intuitive than clicking through a nice website GUI? No, and that’s the point. It offers a low-pain but not painless entry point into how the data science structuring actually works. I think of a knowledge commons like a community garden: it is easier to just buy the fruits and flowers, but the sweat and dirt from growing it yourself is desirable…

Thank you so much for staying on this @ygg_anderson

1 Like

Loving this thread, and I’ve been meaning to reply with a few points for a while now!

I believe anything that has a charge/discharge dynamic has the possibility to exhibit low pass filter effects. Often we use the capacitor/battery analogy in electrical systems, but that could also be mass gain/loss in biological systems, or water stored in a dam (or even a bathtub) in a physical system. LPF effects can appear any time there are stocks & flows, where stocks can build up and simulate flows even when there aren’t any (e.g. the tap is off in your bathtub, yet outflow stays consistent for a time because your tub is full with a stock of water). Hope some of those analogies help!

I hope someone corrects me if I’m wrong, but I think QF (on its own) is more of a static optimization criteria than a low-pass filter, since it doesn’t contain a temporal component. CV has LPF properties since individuals ‘conviction’ or preference grows and decays over a time according to the estimator function.

However, as has been discussed in a few DMs & research calls, I’d be curious if you could combine QF and CV into Quadratic Conviction Funding (QCF?). Much more thought probably needs to go into the concept, but the initial discussion came about as an attempt to simplify the UX of TEC Gitcoin Grants - what if, rather than doing a discrete round of donations every month (which are a huge time & attention cost on the donor), each community member had a dashboard of their preferred WG projects & a sliding scale as to what % of their total monthly support goes to each project? The TEC could then use that as a continuous signal from the community for (quadratically) streaming funds from the TEC treasury to individual “investable workstreams” (i.e. subDAO bonding curves) on an ongoing basis. Perhaps the quadratic calculations would still have to happen at monthly intervals, but at least the time & attention cost on donors could be cut down by a system that “remembers” their preferences and simply prompts them each month if they would like to update their previous rounds’ preferences.

I couldn’t agree more with the need for that assessment! I’ve had a few discussions with Luke Duncan about some of the data analysis potential of early CV experiments, and would love to see some of that empirical discussion & post mortem analysis funded at some point. I’ve been collating some notes on a CV research & modeling effort that I would love to dive further into, if there is further interest in this direction.

A short synthesis of the problems with CV, as experienced in the TEC:

  • Requires competitive proposal environment
  • Requires consistent pool of funds for ongoing dispersal
  • Passed proposals greatly deflated the “abstain” pool relative to active proposals, leading to conviction growing faster than normal after large proposals passed

Some mechanisms that could address those problems (in no particular order):

  • Create “expiry” window for proposals in CV, to ensure proposals can’t just sit there forever gaining momentum
  • Utilize existing “social consensus veto” mechanisms like Celeste for proposals that are not in line with community needs
  • Add in “negative” conviction to counterbalance “up-only” conviction
  • Adjust the “abstain” pool dynamics to prevent large swings in “conviction inertia” that allowed some proposals to pass faster than expected
  • Adjust estimator function of conviction growth to increase more slowly
  • Implement streaming proposals/Osmotic Funding (basically Conviction Voting + Superfluid) for individual contributors who are consistent in their efforts in the TEC, like 1Hive has been experimenting with

Thanks for starting up this great thread @octopus, and for all the great contributions! I’m eager to see more analysis & discussion of CV (& all its latest evolutions)!

3 Likes

Hi @JeffEmmett I am really glad that you are here for the discussion; I love hearing your ideas.

I’m going to try speaking engineering, even though it’s not my first language. I’m mainly going to be asking questions; I really want to understand this.

I learned what I know about filter theory in the context of digital signal processing (specifically acoustics), so my mental definition of filter would be something like “a filter is a map from sequences to sequences”, so the analogies are helpful.

Do you have a sense of when you are thinking “low-pass filter” vs. just “delay line”? …( recognizing that a low-pass filter is often implemented as a weighted sum of delayed copies of a signal-- but I think it’s possible to have a distinction between the design approaches even when they lead to the same mechanism).

And then I struggle even more with comparing low-pass filters to capacitors – reading something like "A capacitor can be used as part of a high pass, low pass, or band pass filter, depending on how it’s connected to other parts " (from
Is a capacitor a high-pass filter or a band-pass filter? - Electrical Engineering Stack Exchange). Do you have a good reference on these building blocks that isn’t overspecified to a particular branch of engineering?

The trouble I personally have with this analog/concept for Conviction Voting in the TEC (especially as it relates to retrospective) is that it seems the mechanism worked exactly as intended, and yet the overall system did not. I’m still not sure that I’ve been able to understand this dichotomy to my own satisfaction.

How can a filter have issues because it didn’t have any “noise” to filter out? It is something like, “We only wanted to let low signals through, and all we had was low signals?” It seems like this is a case of “Make the System Follow Its Own Rules” strategy from Alinsky’s Rules for Radicals. .

Incidentally, I do see how QF could work as a kind of low-pass filter where @gideonro discusses it: think of the incoming donations signal as a histogram: donation amounts are the frequency and height of bars is the gain. Even though QF doesn’t work as a linear operator, the dampening of single impulse signals is still useful here as a measure of its impact. The key is to see the signals coming into QF as living in the frequency domain, rather than the time domain.

I really appreciate your summary “short synthesis”. and would like to work towards quantifying these effects mathematically.

My main hope here is to gain a deeper understanding not only of the mechanism, but also of the design lessons that can be carried forward to other mechanisms.

1 Like

Loving the post @JeffEmmett thanks for posting. I’m only yet half way through reading because I’m learning a lot as I go.

You mention a gain system as a bathtub. The Charge Discharge dynamic. This reminds me that @octopus and I collaborated on the haunted bathtub param model at LTF.

The model exposes a bathtub model with the following parameters:

import param as pm
class HauntedBathTub(pm.Parameterized):
    G = pm.Number(0.01, bounds=(0,1), step=0.001, doc="(GAIN) Constant rate of water flow.")
    L = pm.Number(0.02, bounds=(0,1), step=0.001, doc="(LOSS) Constant rate of water drain.")
    water_is_on = pm.Boolean(True, doc="Whether the water is ON or not.")
    drain_is_open = pm.Boolean(True, doc="Whether the drain is OPEN or not.")
    tub_water_level = pm.Number(0.5, bounds=(0,1), step=0.01, doc="The current water level in the tub.")
    increment = pm.Action(lambda self: self._increment())

Haunted Bathtub Notebook

The notebook also demonstrates the interactive model in panel and renders the bathtub animation using holoviews.

I still owe this thread a OO X DF speedtest performance experiment for QF calc. Unless someone beats me to it.

1 Like

Thanks so much for bringing this up @linuxiscool That session was a lot of fun.

This was intended to be a hands-on visualizer for Chapter 2 of “Thinking in Systems” by Meadows, with different types of inflow and outflow into the subtle stock (the Bathtub). It was “Haunted” because the inflow and outflow were either “on” or “off” at a constant rate and were controlled by outside factors, rather than us.

1 Like