We made a supply and demand simulator. Demand for a good falls as the price rises, and sellers are more willing to sell. Demand for a good rises as the price falls, and sellers are less willing to sell. The goal for this project is to simulate an economy filled with rational actors and observe the principle of supply and demand in action.
A rational actor acts in their best interest. They will, for each good, have a value associated with it. If given the chance to buy something for less than or equal to how much they value it, they will make a purchase. If given the chance to sell for something for more than or equal to their sell price, they'll make a sale.\ An actor will have certain desires and limited means to fullfull those desires. Their means include money, posessions, and their time. Their desires include a fixed need for food and water, as well as an unlimited desire for "happiness". We will evaluate the system based on the "happiness" generated in the system.\ In an economy there are limited resources but unlimited desires. How the resources are distributed is the basis of economics. In our simulator we will model these types of resources:
Essentials : Different kinds of food as well as water. Necessary for survival.
Luxury Goods : After buying the essentials an actor will want to buy as many luxuries as they can afford.
Capital : Money that's able to purchase the above.
Time : Limited time in a day. A day is as long for the rich as the poor.
Measured in dollars ($\$$). If we use 32 bits, that gives of a maximum value of over $\$$ 4 trillion, more than enough for our needs.
Data format | Representing | Class |
---|---|---|
32b | Dollars | Capital |
A count of how much water someone has. Someone's capacity to hold water is fairly limited, but they need it regularly to survive. Water is something that will continuously decrease. As it's something essential for survival, people are theoretically willing to pay their entire life savings for it if they're desperately thirsty. This is limited by the limited means people have to buy water (no one is infinitely rich) and by supply being high (unsold water makes the seller no money).\ We will keep track of an actor's thirst meter and increment it every day, decrementing if someone has enough water to reduce their meter. For example, if someone's thirst meter at the start of a day is 40 units and the person has 4 units of water (or a substitute for water), their new thirst meter will become 10 (consume 4 units of water, add 10 units because the meter decrements 10 units per day).
Data format | Usage rate | Recovery Rate | Class |
---|---|---|---|
8b | 8 thirst units per diem | 10 thirst units per water unit | Essential |
A class of consumable goods needed for survival. Like with thirst there will be a hunger meter associated with each person that increases over time, and that person will starve to death if the meter reaches a threshold. For reducing the hunger meter there will be various subtitute goods, and we're treating a unit of one type of food equal to any other type of food for reducing the hunger meter.
Quantity format | Value | Class |
---|---|---|
8b | 32b | Food |
We will have a variety of foods with their own supplies and values. People will consume foods to fill their hunger meter, and if someone has different goods they will tend to try to eat a more varied diet, placing less desire in eating a single good repeatedly. To implement this we need to keep track of how many of a particular good has been eaten by someone.
We have the following foods in our simulation:
Potatoes
Fish
Dodos
Pineapples
A quantified measure of how happy someone is. If they don't lack any of the essentials, they'll want to spend time and/or money on things that make them happy. You won't die if you're miserable, but you would still want to maximize your happines.
Format | Class |
---|---|
32b | Happiness |
Not fancy or expensive per se, but good purely created to increase happiness. Watching a movie is fun, but is watching 16 movies a day every day fun? Here we'll model the decline in extra happiness per luxury good using the law of diminishing returns. So we'll have a variety of luxury goods available to purchase so that one may pick a luxury activity that gives them the best marginal utility.
Value | Quantity Consumed | Quantity Possessed | Class |
---|---|---|---|
32b | 8b | 8b | Luxury Good |
Luxuries we used:
Clothing (beyond the starter set of clothing)
Oil (for heat)
Mexican Candy
Starbucks
Stickers
To add an extra layer to the simulation we assign an actor a specialization in a particular good, giving them an advantage in production in a certain good. The production of mechanics will be explained later, but the idea is that a specialist for an item should have a higher expected quantity produced of a good compared to a non specialist.
Size | Class |
---|---|
4b | Metadata |
Here's a breakdown of information associated with an actor in our simulation with 10 goods.
Field | Size | Description |
---|---|---|
Specialization | 4b | Index 0 - 9 of good that's this actor's specialty. |
Hunger | 8b | Measure 0 - 255 of how hungry the actor is. |
Thirst | 8b | Measure 0 - 255 of how thirsty the actor is. |
Cash | 32b | How much money the actor has. |
Happiness | 32b | A measure of the actor's happiness. |
Inventory | 8b x 10 | Quantity owned of each good. |
Value | 5b x 10 | Value of good, power of 2. |
Cooldown | 8b x 10 | Quantity consumed of each good. |
Alive | 1b | Is this actor alive? |
Total | 295b | A human being is fully represented by 295 bits. |
All people, rich or poor, have the same amount of time in a day. How they spend their time is up to them. So there are three things people can do with their time:
Work
: People can work to produce goods and services to sell in their economy (and might have competitive advantages?) Production will use up an entire workday, if they choose to work.
Leisure
: People can spend time on leisure activities that increase happiness at the cost of time.
In this section I'll explain the four main functions that each actor goes through within one iteration of the algorithm (one unit of time): Interaction, Production, Consumption, and Adjustment. Each actor is looked at sequentially and goes through these steps: make purchases, choose what to produce (if anything), consume resources, and adjust their evaluation of each good.
The crux of the project, how is a trade performed in this economy? There are people who want to buy and people who want to sell. For a bit of simplificity let's say an actor interacts with its eight neighbors. This is roughly what an "interaction" looks like when an actor is in a "buy" phase with an arbitrary example good.
Buyer's Value of Wall-E | Transaction | Lowest Seller's Value |
---|---|---|
\$64 | Sale of 1 water for \$48 | \$32 |
\$100 | Sale of 1 copy of Wall-E on DVD for \$100 | \$100 |
\$1,000 | No sale | \$2,000 |
\$100 | Sale for \$51 plus a value revision for the seller. | \$2 |
An actor will, for each good, check the best offer from their neighbors, and engage in a trade if the conditions are right (buyer has the cash? seller has the good?), allowing for a maximum of 10 trades per actor per iteration (1 trade for each good).\ On the other hand, this means that an actor will play the "seller" during their neighbor's "buy" phase. Here the seller's offer will be compete against other seller's sell offers for the current actor's purchase.\ Being conscientious of how we would fit the simulation onto hardware, we had actors store value in terms of powers of 2, not a dollar amount. During the transation, this is converted into a dollar amount (with a left shift) and compared with the buyer's cash. A buyer can't offer more than all the cash they have, so we take the minimum of the maximum cash or the value.\ We experimentally found that the simulation would be more interesting if sellers could revise their sale price if there was extreme discrepancy between buyer's offers and the seller's offer. This was implemented by increasing the seller's value by 1 power of 2 if the discrepancy is more than 2 powers of 2. For example, if buyer has value $\$2^5$ and seller $\$2^2$.
They will have rules determine what item they should produce, and whether they find it's worth it to produce anything at all. They will want to produce what they think is the highest value item, and determine if it's worth making if the utility generated exceeds a threshold.\ We will assign each good a "probability" that should be inversely proportional to rarity of that good, and that will be our mechanism to make the availability of goods finite. We assigned these such that there was an expected quantity of that particular good produced by a person in a day. In our software simulation we played around with different expected values, and found a set of values that limited resources enough to be interesting, yet once trading and specialization were enabled, produced a flourishing economy that included the trade of luxury goods.\ To randomize production, as well as make expected quantities definable to one decimal point, we implemented a "die roll" mechanic, where for a given good with probability p, you will produce 1 of that good if a random number 0-99 is less than p for a die roll. You're given 10 die rolls, so a good with probability 10\% will have an expected value of $\$100.10 = 1.00$.\ This probability could be multiplied by a specialization factor. If an actor is specialized in a good, that good has a base $10\%$ production probability per die roll, and the specialization factor is 2, then a production probability of $20\%$ and an expected value of $\$100.10*2 = 1.00$.\ An actor will determine what good is worth producing at the end of each day based on their array of values. Not just what good is the most valuable, but what is the expected value of production of that good, keeping in mind the expected quantity can range from 0.0 to 10.0?\ If good A is worth $ \$ $ 64 to this person and this person expects to make 5 of this good, it will be produced over good B if it's worth \$128 but the person expects to make 1.4 of this good.\ Note that, if with the special multiplier, the probability of production per die roll can exceed $100\%$. In this case we subtract $100\%$ from the probability, add one one to the quantity produced, and use the roll on the remaining $prob - 100\%$. This allows an actor to produce up to 20 of a good per roll.
Good | Prob. | Expected Q. | Special. Mult. | Special. Expected Q. |
---|---|---|---|---|
Water | 19 % | 1.9 | $\times12$ | \$20.0$ |
Potatoes | 14 % | 1.4 | $\times4$ | \$5.6$ |
Fish | 12 % | 1.2 | $\times4$ | \$4.8$ |
Clothing | 2 % | 0.2 | $\times10$ | \$2.0$ |
Oil | 3 % | 0.3 | $\times20$ | \$6.0$ |
Dodos | 11 % | 1.1 | $\times4$ | \$4.4$ |
Pineapples | 13 % | 1.3 | $\times4$ | \$5.2$ |
Mexican Candy | 25 % | 2.5 | $\times8$ | \$20.0$ |
Starbucks | 14 % | 1.4 | $\times8$ | \$11.2$ |
Stickers | 5 % | 0.5 | $\times40$ | \$20.0$ |
We needed some basic logic to use resources. I'll split this into three subsections explaining each of the three categories of consumables, but before I do that I must qualify two terms I'll use in the explanations for foods and luxury goods: utility and desire. I'll explain them in further detail in the section but for the purpose of organization, understand that the desire is a function of how much that actor has consumed of that good, and there is a different desire function associated with each good. $$desire_{Good A} = g_A(cooldown_{Good B})$$
For water it's pretty simple, keep drinking water out of your inventory until you're no longer thirsty. The algorithm starts by incrementing the thirst meter by a predetermined constant amount. For efficiency a water unit shouldn't be consumed if the thirst meter is less than the amount of relief. We also treated Starbucks as a substitute for water, so if your thirst meter is still high after exhausting your water supply you may drink a pumpkin spice latte to reduce your thirst meter.
Food works in a similar manner to water, except the different food items (potatoes, fish, dodos, pineapples) don't have a pretermined priority over another. The hunger meter increments by a pretermined amount each day. We are treating the different units of food as equal: 1 unit of potatoes gives about the same nourishment as 1 unit of dodo bird. If the actor has different food items to available to them, they will pick the food item with the highest calculated desire first. The actor will continue doing this until their hunger meter is low (below the relief given by a unit of food) or they run out of food. We make sure to increment the "cooldown" of each consumed food to keep track how many of each good the actor has eaten.
We will say that people only have the time to consume 2 luxury goods, so the person needs to decide which 2 luxuries to consume. Instead of a continually incrementing "luxury" meter, we have an accumulating variable "happiness" associated with each actor, and increment the happiness by a certain amount. The actor will pick 2 (potentially the same) luxuries that have the highest calculated desire values, consume them, and increment their happiness by the calculated desire. The "cooldown" for the 2 (or less) consumed goods is incremented to mark those goods have been consumed.
The last step is to define something loose and subjective the actors must determine: value and desire.
To explain value I will introduce a new metavariable called "utility". Utility will be defined as a function of the inventory of a good a person has, and the idea is that people will decrease their willingness to buy something if they have more. I borrowed some inspiration in defining the utility function to be an exponential (McGeary & Foster, 2003). The economy simulation in the paper models the total utility gained by an actor using this function. $$U = \sum_{t=0}^{\infty}{d_t\sum_{i=1}^{n}{\frac{a_{t,i}}{s_{t,i}}[e^{s_{t,i}X_{t,i}} -1]}}$$ And the paper discretizes this for a single timestep: $$U1 = \sum_{i=1}^{n}{\frac{a_{\tilde{t},i}}{s_{\tilde{t},i}}[e^{s_{\tilde{t},i}X_{\tilde{t},i}} - 1]}$$ Which is meant to model the law of diminishing returns: the more you have of something, the less additional utility you would gain from getting another of it. Decomposing this:
$U1$: Utility of one more good.\ $a_{\tilde{t}, i}$: Linear weight given to the good.\ $s_{\tilde{t}, i}$: Saturation rate of the good, how fast you get satisfied. A negative number more than -1.\ $X_{\tilde{t},i}$: Quantity of the good owned.
If we condense $\frac{a_{\tilde{t}, i}}{s_{\tilde{t}, i}}$ into a value for each good $w_i$ (and keep the weight/saturation rate as constants over time) and use 2 as the exponent base (just multiplying the saturation rate $s_i$ by a factor of $\frac{1}{\ln(2)}$) we can simplify the utility for a single good to this:
$u_i = w_i * 2 ^{s_i * X_i}$ And the idea was that a saturation rate $s$ that was a negative power of 2 would make this formula easily solver. For example, a saturation rate of $2^{-3}$: $\$w 2^{s x} = (w << (x >> r)), r = 3$ However we decided to instead implement the exponential function as a lookup table on the hardware. For the simulation we kept the calculation of the exponential.\ With the definition of this utility function, we're left with the task of defining the weights and saturation rates. Defining these turned out to be intuitive: place heavy weights on essential goods, decreasing the weights for non-essentials. Also increase the saturation rate for goods you don't need much of (when you have 10 units of water, the 11th will have a far lower usefullness. If you have 10 units of mexican candy, you might want an eleventh more). The table below shows what values we found to give interesting results.
Good | Weight | Saturation Rate |
---|---|---|
Water | 128 | $-2^{-3}$ |
Potatoes | 70 | $-2^{-4}$ |
Fish | 48 | $-2^{-5}$ |
Clothing | 64 | $-2^{-3}$ |
Oil | 64 | $-2^{-3}$ |
Dodos | 52 | $-2^{-5}$ |
Pineapple | 32 | $-2^{-5}$ |
Mexican Candy | 20 | $-2^{-6}$ |
Starbucks | 16 | $-2^{-6}$ |
Stickers | 8 | $-2^{-7}$ |
It works out that people will generally want to acquire higher weighted goods first, and once they have a stable supply of that good they'll want to acquire other types of goods more. They will become quickly saturated of the higher weighted good while less quickly saturated by luxury goods.
Desire is a variable I implemented in the same way as utility, except it uses the quantity consumed as the "x" instead of the quantity owned by the actor. $$desire_i = w_i * 2^{s_i * cooldown_i}$$ With the cooldown being incremented for a good whenever it's consumed. Since the simulation is designed for a long time, this would need we need a means to reduce the cooldown for a good, or else the quantity consumed for each good will hit a ceiling and never go down. To solve this I implemented a "durability" mechanic where the cooldown for each good has a certain probability to decrease on every adjustment cycle. These had to be tuned to so that the cooldown for each good didn't increment too fast (durabilities approach 0) nor too slow (durabilities approach maximum). Here are values picked that accomplished this.
Good | Chance of Cooldown Reduction |
---|---|
Potatoes | 25% |
Fish | 25% |
Clothing | 2% |
Oil | 3% |
Dodos | 25% |
Pineapples | 25% |
Mexican Candy | 50% |
Starbucks | 25% |
Stickers | 75% |
Also note that desire is not implemented for water. The desire funciton for water simply returns the utility for it.
Before implementing the simulation in Verilog we wanted to verify that the simulation worked well in the first place. We implemented the simulation in C to achieve a fast computation of the algorithm, and since no individual part of the algorithm was too complex we didn't need a higher order language's bells and whistles (which is why we decided against Python). We also monitored a few features to the algorithm to help with determining if the simulation was interesting:
Avg. Happiness
Avg. sale prices for each good
Quantity produced of each good
Quantity sold of each good
Fraction of goods made by a specialist.
Fraction of people alive.
We implemented the functions from section 2 in a sequential manner in the C code, doing the following set of functions for an actor, then moving on to the next actor for all actors:
Interact with 8 nearest neighbors
Produce
Consume
Adjust values/cooldown.
Each actor was represented by a struct holding the data discussed previously, specialty, hunger, thirst, cash, happiness, and arrays of inventory, value, and cooldown for each good.
We ran the simulation multiple times and observing what set of parameters and how certain simplifications could be made without hugely affecting the result. We printed out the mentioned values of interest, as well as each actor's data for verification. Through the simulation it was determined that the simulation was stable after 10 years (3650 iterations) and that a wide variety of goods was produced. For most goods, production was disproportionately done by specialists (with some exceptions), and that luxury goods were produced in great quantities. There was a decent survival rate of $96.75\%$, compared to a $~55\%$ survival rate if trading was disabled. The result was deemed logical, the quantity of goods produced spread out enough to be interesting, and was interesting enough to implement on the hardware as-is.
The principle of the visualization is to deliver both the microscopic and the macroscopic dynamics of the population that we are simulating. The former is being implemented as a grid-like structure where each cell represents an actor, and the values at each instance are being represented by the color changes. And the macroscopic properties are represented as a 3d time-series curve.
At the early stage of the project, we didn’t have a good grasp of how the simulation would look as the hardware implementation was not done. Thus we first created the visualization on computers with our C implementation of the economy simulation, which uses the Simple DirectMedia Layer(SDL) as the graphics interface library. Then we transfer the relevant part of the C code onto the HPS.
The SDL Library provides a slightly different way to interact with the computer graphics, to reduce the hassle of transferring the code onto HPS to use the VGA library, we created a few functions that abstracts the SDL functions to the ones available in the VGA library such as draw_pixel(), draw_line(). Thus, when we migrate the codebase from the computer to the ARM, we would only have to replace these function calls.
To draw the population grid, we created a draw_island() function, which essentially draws each individual actor as rectangles. And we fill these cells with a several color mapping from values(such as cash, units of goods) to colors.
To plot the macroscopic data, we created a plotting function which draws the axes and plots the aggregated data.
Later we were able to migrate it to the HPS easily.
The shift register is the interface for the economy simulation to run on. It was designed to output 9 complete actor data at a time: the current actor for this iteration and their 8 neighbors. Those 9 actors could have their entire data overwritten in one cycle after the compute for fast modification. The actor data at the end of the shift register would be lost on the shift, so it should be written back to memory before shifting. In addition, new actor data would need to be written in at the front.
The logic behind the design is that a correctly sized shift register would hold 2 lines of actor data (the actors being arranged on a grid) plus 3 additional actors, and this would be enough data to avoid re-reading and writing an actor more than once per iteration. I'll show this logic using this scaled diagram.
Imagine the shift register as a ``snake'' slithering around the actor grid. The black dot is the tail of the snake, which should be written back to memory, the white arrow heads a block of memory in the shift register, the gold arrow head the direction the snake will advance in, and the diamonds the relevant actor data needed to perform the algorithm. We write back the tail of the snake, we advance the snake by one square in its path (potentially wrapping to the next column), and feed it the new SE block to create its new head. In this way we can store just enough data in the snake so that only one read and one write is needed per actor per iteration, and the actor's neighbors don't need to be read and written back to immediately.
The model in the diagrams above works for a grid with minimum dimension 8, but we wanted our actor grid to be as rectangular as possible. We decided to use MLAB blocks since we believed we would have enough to do columns of size 256, however this would involve generating 515 memory blocks and we couldn't get the fitter to fit that many memory blocks in the design. We settled on a column size of 128, which meant instantiating $128 \times 2 + 3 = 259$ memory blocks. There wasn't enough M10K blocks to use after adding in the SRAM VGA driver (more on that in the ``Other Hardware'' section) so we needed to use MLAB blocks for each block in the shift register, except for the currently active actors.
The currently active actors (center and its 8 neighbors) were stored in registers so that the entire 320 bits associated with the actor could be spit out at once for the algorithm. There was enough registers to do this, but not store the whole shift register, as that would need 2 lines + 3 amounts of actor data, or about $82 kb$. There are $128,300$ register bits available on the device, but that would leave a small amount of registers available to perform the algorithm or anything else, so it was decided to use the MLAB blocks configured with 10 32 bit words each.
This meant that advancing the shift register would involve multiple cycles, as the MLAB blocks could only access a word at a time. This was considered fine, since the largest source of latency by far for the project would be SDRAM, as long as the MLAB shift operation was efficient. A pipelined MLAB read was considered, but for ease a program a simple state machine was implemented that read an address, waited one cycle, then wrote the data back, making the shift operation take 30 cycles in total. The piecewise shift was test to make sure no data was lost, feeding the mouse of the snake with random test data and ignoring the tail.
We designed a memory controller to operate the shift register, feeding the shift register new actor data, starting a shift operation, and writing back data at the end of the shift register.
To create the memory block we instantiated an SDRAM memory controller IP on the Avalon bus and sized it large enough to hold our actor data. The SDRAM can be sized up to 64MB, which can easily hold our actor data of about 5.3MB. The SDRAM controller takes care of operating the SDRAM memory for us, which includes occasional refreshes of each memory bank and translating an address to a setting for the row/column/address/bank pins on the SDRAM. Placing it on the Avalon bus may introduce some delay as the bus will abitrate multiple masters are trying to use the bus at a time, which is why we decided to borrow the SRAM VGA driver example from the course website (more on that in the next subsection) to not have the graphics portion interfere.\ To make the memory controller accessible to the FPGA we needed to create a bridge to the Avalon bus using Platform designer.
A state machine needed to be developed to execute reads and writes to the bridge, setting the read/write address, write data, and storing the read data. The machine would need to wait for an "acknowledge" signal from the bus to know the transaction has gone through. Since the SDRAM memory controller could only be configured to be 16 bits wide (or else the HPS couldn't interface with it) that meant 20 transactions had to occur to read or write 1 actor's worth of data, with a total of 40 transactions per actor per iteration (write back the tail of the shift register, read in the new head).\ To advance to the next actor, you must do this in order:
Write back the tail.
Advance the snake.
Read in the new head.
To write back back the tail you must iterate through each 16-bit word in the northeast neighbor, and the new head must be read into the southeast neighbor 16 bits at a time, waiting for an "acknowledge" after each transaction.
A few notes on ancillary hardware for the project.
To avoid using the SDRAM for video data, which could add traffic to the Avalon bus, we used the SRAM video driver from the course website. Video data would then be written to M10K memory on the lightweight bus instead of SDRAM. The drawback is that only 8 bits were available to define color instead of 16, but this was a fine tradeoff since the video display would be used for a visual display, and it wasn't important to have a wide color field.
A few pio were added to control and monitor the simulation.
Pause: halts the state machine to allow a fresh grid of actors to be loaded in from the HPS.
Finished: high when an iteration has been finished since the last pause.
Timing: Counts how many clock cycle it takes to finish one iteration.
These control signals allow the HPS to pause the simulator when initializing data as well as only refreshing the VGA display after an iteration.
Lastly, the actual economy compute module had to be created to simulate our economy. The module needed to take in the array of people and return a new array that have, interacted, adjusted, produced, and consumed. The Verilog followed the structure of the c code very closely. Each function could be contained in a module. Constant arrays like the "Probabilities" array could also be defined as parameter arrays in system Verilog. We used the features of System Verilog extensively, especially exploiting the value of structs and logics. With structs, we could structure our code around the person_t struct thus keeping writing and reading from person_t members easy. Without this, we would have had to split up person_t members into separate inputs and outputs. The struct also has a very direct relationship to the synthesizable code, so there wasn't a chance for confusion from abstraction. One thing we had to be careful of was that since we can't take in person_t structs and write to them and read from them, we had to split the module into an input and output person_t. Writing each member of the input person to the output person combinationally unless the module changes the value of the output. Another consideration we had to make was with regards to large lookup tables like our "f" function from the c code. Instead of using the complex arithmetic, from the function, we made a lookup table of the results of the function. This lookup table is about 2100 lines long, way too large to fit in a combinational lookup and would take away almost all our LABs. Since our bottleneck was our SRAM read and write speed, we sacrificed a couple of our clock cycles by making the lookup sequential.
Module to function conversions that were combinational like the trade module were a very simple conversion. Rather than rewrite variables, we created new wires for each result.
Sequential modules on the other hand were a different story. Some for loops could be eliminated while some could not based on their dependency loop. For example, looping through goods could be formed into a generate statement as producing, consuming, and trading could be done across all goods at once. On the other hand, when interacting, each trade had to be done sequentially, so a state machine had to be created based on the index variable.
To keep the interactions between the modules similar to the c functions, we formed all sequential modules with of course a clock and reset signal, they would also have a done output signal. To reflect the c code, running a "function" involved pulling the reset signal low, and waiting until the done signal went high, then grabbing the output value and resetting the module.
The last thing to implement was to interface with RAM. Since the ram stored each person as a bit reg, we needed to convert the reg to a struct. To do this, we made person_to_bits and bits_to_person modules. Each would take in the former as an input and wire the bits to a struct. Since we had knowledge of the amount of bits each member took up as discussed above, we could make our own offsets, grab sections of bits and stick them into the members of the person struct. This could of course be done combinationally. The module was essentially just a rewiring of bits. Therefore, when people are taken from RAM, they are converted to person structs, inserted into the compute module, and reformed back into bits at the output.
Here is a diagram for the overall structure of the compute module.
Here's how the verilog is structured, there is a trade module which combinationally takes in an input buyer, seller, and good to sell and outputs the buyer and seller post trade.
The Interact module takes in a buyer and an 8 long array of sellers. The module has a trade module contained within it. Using a state machine, the interact module loops through the sellers and goods trading each sequentially. The module outputs the post-interaction buyer and sellers when the done signal goes high.
The utility and desire functions from the c simulation are combined in the utilites_and_desires module which combinationally calculates the utility and desire for each good. Though due to the previously discussed lookup table, the module takes 1 cycle for the result to show.
The expected_value module is another combinational module providing the same inputs and outputs as the c code, but for each good once again.
The actual compute module then provides instances of the interact, adjust, produce, and consume modules. They all share a clock, but the interact module takes in the input persons. The module contains a reg with the person being adjusted and feeds the same reg through all modules, with the output person wired to the reg. The way the sequential nature works is that the reset for the adjust module is tied to (interact_done || reset), produce is (adjust_done || reset). Therefore if the compute module is reset, all modules will be reset, but if the reset is lifted, a module will only start when the previous module has completed.
The software testing was quite qualitative since the type restrictions were loose and development and debugging was quick and easy. By looking at the visualization of the economy we could see the behavior we expected based on the initial conditions. For example, no production should result in a starving and dying population. Starting an economy with a high value for a certain good should make people specializing in that good rich.
Since we didn't use floating point for any of our simulator operations, the verilog that we wrote on the FPGA for the compute module should produce the exact same values as the c code given the same random seeds. Using this we could write many unit tests for each module.
We used SVUnit to test our code. This was able to easily produce a dut and make an easy interface to catch errors. We could then run this with the modelsim command line to get quick unit test results and ensure the verilog worked with the c code. For each module, we would of course make a test file in c. We would create the appropriate initial conditions in our person_t structs and input values which because of our system verilog structs, we could easily reproduce on the verilog side. We then took the C output and compared it to our DUT's own output. The first test was always to put conditions on the module so that no activity would occur. An example is a person interacting with its neighbors, but no-one has enough cash to trade. This should result in the output people being the same as the input people. We would always check that these values weren't don't care values. If any of them were, we had an undriven output. This is where it was important to use the "===" equality operator rather than "==", the former will return false if one of the values is don't care, but the latter returns true. Our next type of test would be a simple comparison to the c code outputs. The last test we would do would be checking boundary conditions to make sure that the bit sizes we allocated to each member, struct, or reg was appropriate and not cutting off values. We made a unit testing structure like this for almost every single module in our files ensuring that the verilog at least behaves just as the c simulation does.
Testing on the FPGA was harder. Due to our speed bottleneck, our time-steps were very small, so we couldn't analyze long-term behaviors of the economy and check it against the c code output. We could at least test the initial behavior of the economy. Such as if there is no production and each person has no inventory, there should be no trading or inventory present. Adding production in should slowly grow inventories.
We were able to simulate a population of 128x1024, which is 131072 actors in real time.
As shown in the above image, we split the population into two grids of size 128x512 since the screen would not fit. In the image, it is currently displaying the amount of potatoes one owns, and the curve underneath shows the average number of potatoes among the population over time. The users can change the current data being observed through terminal interactions.
We were able to design a simulation that can handle a large amount of information, which requires a highly optimized design for memory management. We believe our hierarchical design of the system was able to successfully meet that demand. The simulation was able to run smoothly in real-time.
To improve upon the current design, we could add more features such as consumption and taxes to introduce additional complexity to the project.
The group approves this report for inclusion on the course website.
The group approves the video for inclusion on the course youtube channel.
Deemo Chen:
Ignacio:
Liam
[1] McGeary, Foster & Decker, Keith. (2003). Simulation of economic actors using limitedly rational autonomous agents. 1068-1069. 10.1145/860575.860799.