2

I play with CodeWorld and use the "entrypoint" activityOf() to simulate a physical system consisting of tanks. Here we have one tank with an outlet flow qout(h) that depends on the height of volume level. Of course, for serious simulation work one should use other software, but still fun to do here!

program = activityOf(initial, change, tank_water)
composedOf = pictures

-- Initial value and parameter
initial(rs) = 5
g = 9.81
area = 0.015

-- Inlet flow
qin = 0.0

-- Dynamics using Euler approximation
qout(h) = area*sqrt(2*g*max(h,0))
change(h, TimePassing(dt)) = max(h,0) - qout(h)*dt + qin*dt
change(h, other) = h

-- Animation of system
tank_water(h) = composedOf [tank, water(h), graph, coordinatePlane]  

tank = translated(thickRectangle(width,height,0.2),position,4)
width = 3
height = 8
position = -5

water(h) = translated(colored(solidPolygon [(-width/2, 0), (-width/2, h), (width/2, h), (width/2,0)], 
                          light(blue)),position,0)

-- Graph of evolution of h(t) - here used monitored values and about 5 seconds between each data point
graph = polyline [(0,5), (1,3.7), (2,2.3), (3,1.3), (4,0.7), (5,0.2), (6, 0)]

For educational purpose I think it is good to combine an animation with a graph in a diagram that shows how the height develops over time.  Here I entered in the code a "fake" graph since it was easy to measure up (and I could also enter the analytical solution to make the graph adapt to parameters).

I wonder how one can include a subsystem that collect data from the animation and present it in the graph as the simulation proceed. Any ideas?

One cumbersome idea I think of is to extend the model state that include the measured points to be collected during the simulation. We can beforehand say that we collect 10 samples of h and t with a time distance of say 5 seconds and we expand the graph as the data comes in.

Another idea is to somehow "add-on" some general data logger to the activityOf()-program that store data on a file that you afterwards can study with any software. Perhaps such a logger is already available in the Haskell-environment?

But I am very uncertain of how to do this and here are perhaps some better and more general way to do it?

janpeter
  • 681
  • 8
  • 22

2 Answers2

1

I’ll respond using syntax and types form the “regular” Haskell environment, https://code.world/haskell.

The type of a activityOf is:

activityOf ::
   world ->
   (Event -> world -> world) ->
   (world -> Picture) ->
   IO ()

and as you observe, there is no built in way to log values and graph them. Adding that to activityOf seems wrong (there are so many thing we might want to add).

But what type would you want for such a function? Maybe the following:

activityWithGraphOf ::
  world ->
  (Event -> world -> world) ->
  (world -> Picture) ->
  (world -> Double) ->
  IO ()

With this abstraction in mind, I would go and implement that function

  • it’d itself use activityOf of course
  • it’d keep track of the world state
  • it’d keep track of time
  • it’d keep track of the the recent graph values
  • maybe it splits incoming TimePassing in two, if needed, to sample the graph at the right intervals.
  • and it’d compose the Picture provided by the wrapped activity with the graph

Sounds like a fun exercise. And now I’d have a generic function that I can use with many graph-drawing simulations.

Joachim Breitner
  • 25,395
  • 6
  • 78
  • 139
  • Looks as a good start to me. In this way code using activityOf could easily be re-used for the new "entrypoint", right? What I understand development of this new "entrypoint" must be done in the richer Haskell environment and not within CodeWorld. And in the end "installed" in CodeWorld. Do I understand it right? – janpeter Oct 21 '21 at 06:02
  • Here is a small typo on the line second from the bottom. Replace word with world. Further I think you need to "log" a number of numbers. over some time. Then I guess output is a list [Double], or rather a table, or matrix, but not sure how you express that. – janpeter Oct 21 '21 at 06:04
  • This entry point is just a normal function, you can define and use that in the CodeWorld environment as well. – Joachim Breitner Oct 21 '21 at 09:44
  • “it’d keep track of the the recent graph values” – that’s where the list or table is stored; it’s inside `activityWithGraphOf`. – Joachim Breitner Oct 21 '21 at 09:45
  • If I understand you right you mean to keep program = activityOf(world, change, picture) and then inside define a function activityWithGraphOf(world,change, picture, table) . That function should map to the tuple (world,change,picture) not to IO, I think, but I cannot get it to work. I suspect one must change the entrypoint, otherwise there i no way to store the graph, I think. Perhaps you could extend the sketch a bit? – janpeter Oct 21 '21 at 15:32
  • In other words, I tend to come back to my cumbersome idea above that by letting world include both the physical state (height of the tank) and the table (the logging table of that state). Then you could simply just use activityOf() as is. The drawback is that table may need to be predefined in size or can you define it as unspecified (infinite) and have some "lazy evaluation" of it? Or is here something better? – janpeter Oct 21 '21 at 16:07
  • Now, you have the `world` of the “inner activity” (the one you pass to `activityWithGraphOf`). And then there is a different data type for the “world” that `activityWithGraphOf` is passing to `activityOf`. Abstraction for the win! – Joachim Breitner Oct 21 '21 at 20:21
0

I made a solution "the cumbersome" way. Wish I could structure it better from the input I have got. Appreciate suggestions for improvements so that the code could be shorter. I feel clumsy with tuple-handling. Further creation of the table and later graph could be more separated from the tank-process. That would facilitate re-use to other simulation tasks in CodeWorld. Improvements welcome!

--

I have now updated the code and made it shorter but also added more data points in the table. The state_and_table must be a tuple in CodeWorld and handling of tuples not possible with an index. Further, you can now change the pump rate qin with arrows up and down. 

Expand to two tanks (or more) make it more of a challenge to control the water height and leave that as an exercise to you! Enjoy!

Link to run https://code.world/#P2uQaw2KBSbyspnQSn5E4Gw

program = activityOf(initial, change, tank_water)

-- Initial values of the total state, i.e. state_and_table
initial(rs) = state_and_table
state_and_table = (time_0, h_0, u_0,
                   0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                   0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
time_0 = 0
h_0 = 7
u_0 = 0

-- Functions to read elements of state_and_table 
time_of(time_value,_,_,_,_,_,_,_,_,_,_,_,_,
                       _,_,_,_,_,_,_,_,_,_) = time_value
h_of(_,h_value,_,_,_,_,_,_,_,_,_,_,_,
                 _,_,_,_,_,_,_,_,_,_) = h_value
u_of(_,_,u_value,_,_,_,_,_,_,_,_,_,_,
                 _,_,_,_,_,_,_,_,_,_) = u_value
table_entry((_,_,_,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,
             x11,x12,x13,x14,x15,x16,x17,x18,x19,x20), k)
 | k==1 = x1
 | k==2 = x2
 | k==3 = x3
 | k==4 = x4
 | k==5 = x5
 | k==6 = x6
 | k==7 = x7
 | k==8 = x8
 | k==9 = x9
 | k==10 = x10
 | k==11 = x11
 | k==12 = x12
 | k==13 = x13
 | k==14 = x14
 | k==15 = x15
 | k==16 = x16
 | k==17 = x17
 | k==18 = x18
 | k==19 = x19
 | k==20 = x20

-- Events of recording state to table at the following times
table_time = [ 5*t-5 | t <- [1..20]]

-- Parameters related to the water flow
g = 9.81
area = 0.025

-- Parameters of the tank
width = 5
height = 8
position = -5

-- Inlet and outlet flow
qin(u) = max(u, 0)
qout(h) = area*sqrt(2*g*max(h,0))  -- Bernoulli's physical model

-- Change of state_and_table 
change(state_and_table, TimePassing(dt)) = 

   -- Update time
   (time_of(state_and_table) + dt,

   -- Physical state equation -- Euler approximation of differantial equation  
   max(h_of(state_and_table),0) - qout(h_of(state_and_table))*dt + qin(u_of(state_and_table))*dt, 

   -- Control variable u
   u_of(state_and_table),

   -- Event of recording state to table at predfined times
   table_update(state_and_table,1),
   table_update(state_and_table,2),
   table_update(state_and_table,3),
   table_update(state_and_table,4),
   table_update(state_and_table,5),
   table_update(state_and_table,6),
   table_update(state_and_table,7),
   table_update(state_and_table,8),
   table_update(state_and_table,9),
   table_update(state_and_table,10),
   table_update(state_and_table,11),
   table_update(state_and_table,12),
   table_update(state_and_table,13),
   table_update(state_and_table,14),
   table_update(state_and_table,15),
   table_update(state_and_table,16),
   table_update(state_and_table,17),
   table_update(state_and_table,18),
   table_update(state_and_table,19),
   table_update(state_and_table,20))

-- Key input
change(state_and_table, KeyPress("Up")) = 
  (time_of(state_and_table),
   h_of(state_and_table),
   u_of(state_and_table)+0.1,
   table_update(state_and_table,1),
   table_update(state_and_table,2),
   table_update(state_and_table,3),
   table_update(state_and_table,4),
   table_update(state_and_table,5),
   table_update(state_and_table,6),
   table_update(state_and_table,7),
   table_update(state_and_table,8),
   table_update(state_and_table,9),
   table_update(state_and_table,10),
   table_update(state_and_table,11),
   table_update(state_and_table,12),
   table_update(state_and_table,13),
   table_update(state_and_table,14),
   table_update(state_and_table,15),
   table_update(state_and_table,16),
   table_update(state_and_table,17),
   table_update(state_and_table,18),
   table_update(state_and_table,19),
   table_update(state_and_table,20))

change(state_and_table, KeyPress("Down")) = 
  (time_of(state_and_table),
   h_of(state_and_table),
   u_of(state_and_table)-0.1,
   table_update(state_and_table,1),
   table_update(state_and_table,2),
   table_update(state_and_table,3),
   table_update(state_and_table,4),
   table_update(state_and_table,5),
   table_update(state_and_table,6),
   table_update(state_and_table,7),
   table_update(state_and_table,8),
   table_update(state_and_table,9),
   table_update(state_and_table,10),
   table_update(state_and_table,11),
   table_update(state_and_table,12),
   table_update(state_and_table,13),
   table_update(state_and_table,14),
   table_update(state_and_table,15),
   table_update(state_and_table,16),
   table_update(state_and_table,17),
   table_update(state_and_table,18),
   table_update(state_and_table,19),
   table_update(state_and_table,20))

-- Default equation
change(state_and_table, other) = state_and_table

table_update(state_and_table, k)
 | time_of(state_and_table) > table_time # k && table_entry(state_and_table,k)==0 = h_of(state_and_table)
 | otherwise = table_entry(state_and_table, k)

-- Animation of system
composedOf = pictures
tank_water(state_and_table) = composedOf [headline, pump(qin(u_of(state_and_table))),
                                          tank, water(h_of(state_and_table)), 
                                          graph(state_and_table),  
                                          coordinatePlane]  

headline = translated(lettering("Tank dynamics"),5,7.5)
pump(qin) = translated(composedOf [lettering("- pump rate ="), translated(lettering(printed(qin)),3.7,0)], 4.5,6.5)
tank = translated(thickRectangle(width,height,0.2),position,4)
water(h) = translated(colored(solidPolygon [(-width/2, 0), (-width/2, h), (width/2, h), (width/2,0)], 
                              light(blue)), position,0)

-- Graph of evolution of h(t) - note time scale is 1 = 10 s etc
scale = 1/10
graph(state_and_table) = colored(polyline [(scale*table_time#k, table_entry(state_and_table, k)) 
                                            | k <- [1..length(table_time)], table_entry(state_and_table, k) /=0], 
                                 light(blue))
janpeter
  • 681
  • 8
  • 22