Jump to content

LSL functions and probabilities


Innula Zenovka
 Share

You are about to reply to a thread that has been inactive for 88 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

This is where I reveal, yet again, my compete ignorance of mathematics.      I dimly remember things from high school but when I come to apply them,  it's no more than half-remembered fragments of a foreign language.

Anyway, I was recently trying to think how to script it to ensure that something would happen within n seconds, but randomly within that, while trying to ensure that, as time passed, the likelihood that whatever it was would happen increased.

That is, in this particular case,  I wanted my pathfinding critter to behave more or less realistically -- after wandering for a bit, it stops for at least 10 seconds, and then thinks about about what to do.  Probably once it's arrived somewhere it'll at least want to stay there a bit, foraging or resting or whatever, but the more time passes, the more likely it is to want to move on.    Sometimes it'll want to move on right away, and sometimes it'll want to stay there up to a minute or so, and most of the time somewhere between that.

I came up with this, which does what I need

integer iNeedToBeat;
integer iRandom;
default
{
	state_entry()


	path_update(integer Type, list Reserved)
	{
		if(PU_GOAL_REACHED == Type){
			iNeedToBeat = 100;
			llSetTimerEvent(1.0);
		}
	}

	timer()
	{
	
		if((integer)llFrand(100.0)>(--iNeedToBeat)){
			if(iNeedToBeat < 90){
					//then do whatever it is, so long as at least 10 seconds have passed 
			}
		}
	}
}

That is,  the pathfinding creature will always wait at least 10 seconds before deciding if it wants to do something new, and then, as time progresses, it becomes increasingly likely to move on to whatever this "something new" is,  until it actually does so.

That's the basic idea, anyway, though in practice it's going to be somewhat more complex than that.

I have two questions, I think. 

First, I'm sure that what I've done there can be done more elegantly with some mathematical formula, if only I knew how to express it in LSL.    Can anyone help me?

Second, more generally, how else can we readily use LSL to calculate probabilities, and for what kinds of purposes might be do it?     

There must be dozens of other interesting things we can do with chance, odds and probability other than skill gaming and gachas.    There's breedables, of course.  What else? 

Link to comment
Share on other sites

In these situations I tend to go for something like this:

float min_delay = 10.0;
float max_delay = 100.0;
float delay = min_delay + llFrand (max_delay - min_delay);
llSetTimerEvent (delay);

I'm not sure if this is what you're looking for here, though.

I use this for cycling through stands (and freestyle sits) in my AO.

Link to comment
Share on other sites

So, you can obviously do all this without functions, but naming things makes the intentions more clear for examples to follow:

// use this for a single 'dice roll' like event. 
// example: if the probability of doing the thing is 1/6:
// if(prob_check(0.166)) {do it};
integer prob_check(float prob)
{	return llFrand(1.0)<prob;
}

// use this to select an item from list items,
// where the probability of selecting each item is the list weights.

list select_from_list(list items,list weights)
{
  float total_weight = llListStatistics(LIST_SUM,weights);
  float f = llFrand(total_weight);
  float cumulative_sum = total_weight;
  
  integer index = llGetListLength(weights);
  while(~--index)
  {
    cumulative_sum -= llList2Float(weights,index);
    if(f>=cumulative_sum) return llList2List(items,index,index);
  }
  llOwnerSay("s_f_l: Programming error.");
  return [];
}

basically anything you might need to do with making something random happen, boils down to one of those 2 functions, and really, prob_check is just a specialized case of select_from_list. The more complicated things are usually continuous generalizations of select_from_list for specific applications.

As in your example another thing is selecting the probability for a future check based on the outcome of a current check. you can do that any number of ways, but 2 that come to my mind:

float dependent_prob_default=0.1; // the probability of a dependent thing happening.
float dependent_prob=dependent_prob_default;
float d_factor= // the change in probability after an unsuccessful try.
//1.02; // if using *=, will become increasingly likely.
  0.02; // if using +=, wll become 2% more likely each time, and will definitely happen after 50 checks.

integer dependent_prob_check()
{  if(prob_check(dependent_prob))
   {// reset the probability after a successful check:
       dependent_prob=dependent_prob_default;
       return TRUE;
   }else
   {// choose an option for how to modify the probability:
       //dependent_prob*=d_factor;
       dependent_prob+=d_factor;
       return FALSE;
   }
}

for your specific example,  I'd go with @KT Kingsley's general method, or if you really wanted to be fancy you could try something like:

float delay = min + llList2Float(select_from_list([0,2,4,6,8,10],[1,2,3,4,5,6]));
llSetTimerEvent(delay);

which would have a 1/21 chance of waiting for min seconds, 2/21 chance of waiting for min+2 . . .

  • Thanks 1
Link to comment
Share on other sites

9 minutes ago, KT Kingsley said:

In these situations I tend to go for something like this:

float min_delay = 10.0;
float max_delay = 100.0;
float delay = min_delay + llFrand (max_delay - min_delay);
llSetTimerEvent (delay);

I'm not sure if this is what you're looking for here, though.

I use this for cycling through stands (and freestyle sits) in my AO.

In my example, I'm saying that, after 10 seconds, the probability of something happening (critter moving to a new location/choosing a new animation/whatever) starts at 10% and then increases by 1% for each second that elapses.   

So for the first fifty seconds, it's more likely than not the critter will carry on doing whatever it is, though the likelihood it will decide on a new activity is slowly increasing over time and after fifty seconds it becomes increasingly likely it'll go off and do something else until, after 100 seconds have elapsed, it's certain to change.

 

Link to comment
Share on other sites

1 hour ago, Innula Zenovka said:

In my example, I'm saying that, after 10 seconds, the probability of something happening (critter moving to a new location/choosing a new animation/whatever) starts at 10% and then increases by 1% for each second that elapses.   

Just, of note, the two ideas:

1) The probability for the thing happening increases with the time elapsed,

2) The amount of time it will take the thing to happen is selected from a distribution that weights longer time durations more heavily

are equivalent, and supposing I don't make any mistakes, or errors in the fuzzy logic in my thoughts:

float dependent_prob_default=0.02; // the probability of a dependent thing happening.
float dependent_prob=dependent_prob_default;
float d_factor=0.02
integer dependent_prob_check(); // see above, using the += version.

default
{
  state_entry()
  {
    llSetTimerEvent(10);
  }
  timer()
  {
    llSetTimerEvent(1);
    if(dependent_prob_check())
    {	//do the thing.
    }
  }
}

and

list select_from_list(list items, list weights); // see above

default
{
  state_entry()
  {
    list temp;
    integer index = 50;
    while(--index)
    {
      list+=(float)index;
    }
    llSetTimerEvent(10 +llList2Float(select_from_list(temp,temp));
  }
  timer()
  {
    //do the thing.
  }
}

Should be almost exactly equivalent. (up to floating point arithmetic making things weird sometimes.) The first is a bit inefficient in computation, the second inefficient in memory. (Edit: I take it back, they might be slightly different, but they still have the same general flavor)

something like:

default
{
  state_entry()
  {
    integer rand = llFrand(1.0);
    llSetTimerEvent(10+(1-(rand*rand))*50);
  }
  timer()
  {
    //do the thing.
  }
}

would be similar and the most efficient. (confuses me, would have to test whether it's weighted twords the low or high end and adjust the formula accordingly)

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

5 minutes ago, Innula Zenovka said:

I know it's not what I described, exactly, but I suspect I may have some sort of sine wave in mind, if only I knew how to express it.

Reading through this thread, the first thing that came to mind was that llFrand generates psuedorandom numbers in a linear distribution, whereas natural processes generally follow some other frequency distribution (e.g.,  Gaussian) where events are more likely to occur around some central timing and with decreasing likelihood at the beginning or end of the permissible interval. Of course it doesn't need to be Gaussian, and most likely nobody would notice exactly what distribution is at play (except when wagering is involved).

So I searched the wiki for "gaussian" and found this which points to this tidy spot of math, all of which seems overkill for the intent here.

A simpler approach would be to generate a list of values that correspond to the delay intervals, so the first element in the list is the shortest permissible interval, the last element is the longest, and we index into that list by integer-forced llFrand() over the arbitrary number of discrete values we use to represent the distribution.

The values themselves would actually be the ogive cumulative over the frequency distribution we're modeling, so they monotonically increase in a shape something like an (offset and scaled) arctan—llAtan2()—thus invoking a sine-adjacent trig function.

Or maybe this has nothing to do with the sine wave you were visualizing.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Use llGetTime to get the time since the script started or the last call to llResetTime()..

Here's some psuedo code. I use next_stop_to_think to define how long it will take before I get going again. I always think for 5 seconds.

.... 
float   next_stop_to_think;
default
{
	.... 

	timer()
	{
      	float elapsed = llGetTime();
        if (elapsed > next_stop_to_think)
        {
           // Wait 
          llSay(0, "Thinking...");
          next_stop_to_think = 5 + llFrand(10); // We'll stop to think again in 5 to 15s
          llSetTimerEvent(5.0); // Think for 5s
          llResetTime(); // So that elapsed will always be time since last stopped to think.
        } else {
           llSetTimerEvent(1.0); // Ensure timer resumes normal pace
          	
        }
	}
}

 

  • Thanks 1
Link to comment
Share on other sites

1 hour ago, Innula Zenovka said:

I know it's not what I described, exactly, but I suspect I may have some sort of sine wave in mind, if only I knew how to express it.

If the probability of something happening increases with time as you have described then you are more likely to find an exponential function fitting your needs.

 

this is a fudge, I know, but it should simulate what you want to achieve.

 

have a predetermined trigger level somewhere around 90, determined by a random number added to a minimum level.

start your timer counter at -10, increase by one  until it is greater than 0.

Once greater than 0, increase it by e to the power of the last increment, obviously save the new increment  for the next timer calculation.

Check and take action when it exceeds the trigger.

 

Lots of scope for setting the timer value to a variable amount each time to add to the uncertainty

Edited by Profaitchikenz Haiku
  • Thanks 1
Link to comment
Share on other sites

i would go with the list approach as well. As is able to be easily tailored to fit the app. Example:

 

timer()
{
   if (condition == CHANGE_TO_WAIT)
   {
      //... stop moving animation
      //... start waiting animation

      condition = CHANGE_TO_MOVING;
      float wait = 10. + llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15));      
      llSetTimerEvent(wait);
   }
   else if (condition == CHANGE_TO_MOVING)
   {
      //... stop waiting animation
      //... start moving animation
      
      condition = IS_MOVING;
      llSetTimerEvent(1.0);
   }
   else if (condition == IS_MOVING)
   {
      //... do moving stuff

      //... on some happening then
      condition = CHANGE_TO_WAIT;
      llSetTimerEvent(1.0);

   }
}

/*
  llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15));
  
  15 elements
  
  5 times will wait 10 + 1 seconds
  4 times will wait 10 + 2 seconds
  3 times will wait 10 + 3 seconds
  2 times will wait 10 + 4 seconds
  1 times will wait 10 + 5 seconds 

  which sans 10 seconds is the gaussian distribution

  5 * 1 = 5
  4 * 2	= 8
  3 * 3	= 9
  2 * 4	= 8
  1 * 5 = 5
  ---------
         35
*/

 

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

16 hours ago, Mollymews said:
float wait = 10. + llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15)); 

FWIW is equivalent to:

float wait = 10. + llList2Float(select_from_list([5,4,3,2,1],[1,2,3,4,5])); 

which might have a bit more overhead for the specific example, but should scale a bit better.

Or if you wanted to get more specific:

list select_from_list_gaussian(list items)
{
   integer index = llGetListLength(items);
  float cumulative_sum = index*(index+1)*0.5; // sum from 1 .. index.
  float f = llFrand(cumulative_sum);

  while(~--index)
  {
    cumulative_sum -= (index+1);
    if(f>=cumulative_sum) return llList2List(items,index,index);
  }
  llOwnerSay("s_f_l_g: Programming error.");
  return [];
}
// . . .
float wait = 10. + llList2Float(select_from_list_gaussian([5,4,3,2,1]));

 

Edited by Quistess Alpha
Fix an off-by-1 error.
  • Thanks 1
Link to comment
Share on other sites

I'm trying to design something that will handle generic  behaviour for my pathfinding critters when nothing happens to disturb them -- they simply play a few animations for a few minutes and then they go off and do something somewhere else.

So far I've got this (but I haven't really had a chance to look at Tessa's latest post.

float fTotalRestTime = 290.0;//so max 5 minutes
float fMinRestTime = 10.0;
float fTotalAnimationTime = 20.0;//so max 30 seconds animation
integer iIndex;
//integer iMax;
list lTimes;
list lAnimationsToPlayWhileStopped;

string strCurrentAnim;
string strPreviousAnim;

default
{
	state_entry(){
		lAnimationsToPlayWhileStopped = llListRandomize(lAnimationsToPlayWhileStopped,0);
		//iMax = -llGetListLength(lAnimationsToPlayWhileStopped);
		float fRand  = llFrand(1.0);
		float f = (fRand*fRand);
		float fPeriod= fMinRestTime+(1-f)*(fMinRestTime+fTotalRestTime);//up to five minutes in all
		llOwnerSay("fPeriod is "+(string)fPeriod);

		//now build a list of how often to change anims
		do{
			fRand = llFrand(1.0);
			f = (fRand*fRand);
			lTimes +=[fMinRestTime+ (1-f)*fTotalAnimationTime ];//animations  play for between 10 and 20 seconds
		}
		while(llListStatistics(LIST_STAT_SUM, lTimes)<fPeriod);

		llOwnerSay(llList2CSV(lTimes));
		llOwnerSay("total of lTimes is "+(string)llListStatistics(LIST_STAT_SUM,lTimes));
		//this is always going to be slightly more than fPeriod, but close enough for what I need
		iIndex = -llGetListLength(lTimes);

		//start first animation
		strCurrentAnim = llList2String(llListRandomize(lAnimationsToPlayWhileStopped,1),0);
		llStartObjectAnimation(strPreviousAnim = strCurrentAnim);
		llSetTimerEvent(llList2Float(lTimes,iIndex));
	}


	timer(){
		if(++iIndex){
				do{ //choose a new anim 
						strCurrentAnim = llList2String(lAnimationsToPlayWhileStopped,0);
						if(strCurrentAnim == strPreviousAnim){
							strCurrentAnim = llList2String(lAnimationsToPlayWhileStopped,1);
						}
				}
				while(strCurrentAnim == strPreviousAnim);
				lAnimationsToPlayWhileStopped = llListRandomize(lAnimationsToPlayWhileStopped,1);
				llStartObjectAnimation(strCurrentAnim);
				llStopObjectAnimation(strPreviousAnim = strCurrentAnim);
				llSetTimerEvent(llList2Float(lTimes,iIndex));
		  }

		else{
			llSetTimerEvent(0.0);
			//do some pathfinding stuff
		}		
	}
}

 

Edited by Innula Zenovka
  • Like 1
Link to comment
Share on other sites

going back to your initial posit: The animal is more likely to move the longer it waits

another way to do this is using combinatorics. More specifically a factorial method. With a proviso that animations are of related durations to help provide close to seamless transitions. Example:
 

edit typo. gah! double gah! is what happens when type in absolute numbers off the top of your head. (the magnitude is 5 not 4)
 

list sAnims = ["some", "ani", "ma, "tions"];

string sAnim;
integer iTicker;

timer()
{
   iTicker = (++iTicker) % 5;
   if (iTicker)
   {
      integer len = llGetListLength(sAnims);
      integer i = (integer)llFrand(len);
      string anim = llList2String(sAnims, i);
      
      // allow the same animation to play in successive intervals
      // like a cow grazing for example 
      if (anim != sAnim)
      {  
         llStartObjectAnimation(anim);
      	 llStopObjectAnimation(sAnim);
         sAnim = anim;
      }
      
      /* always change animation
      if (anim == sAnim)
      	 anim = llList2String(sAnims, (++i) % len);
      llStartObjectAnimation(anim);
      llStopObjectAnimation(sAnim);
      sAnim = anim;
      */
      
      float wait = 30. * ((integer)llFrand(5 - iTicker) + 1);       
      llSetTimerEvent(wait);
   }
   else // ticker is 0
   {
      // path finding
      llSetTimerEvent(0.0);

   }   
}

/*

   float wait = 30. * ((integer)llFrand(5 - iTicker) + 1);   

   combinations: factorial(4) = 24

   max = 120 + 90 + 60 + 30 = 300 (5 minutes)
   min =  30 + 30 + 30 + 30 = 120 (2 minutes)

   30. assumes animations of 30 second duration and/or some divisor of 30. [30,15,10,6,5,3,2]

   float wait = 10. * ((integer)llFrand(5 - iTicker) + 1);       

   max =  40 + 30 + 20 + 10 = 100
   min =  10 + 10 + 10 + 10 =  40

   10. assumes animations of 10 second duration and/or some divisor of 10. [10,5,2]

*/

 

Edited by Mollymews
  • Thanks 1
Link to comment
Share on other sites

On 9/11/2021 at 11:53 AM, Innula Zenovka said:

First, I'm sure that what I've done there can be done more elegantly with some mathematical formula, if only I knew how to express it in LSL.    Can anyone help me?

Second, more generally, how else can we readily use LSL to calculate probabilities, and for what kinds of purposes might be do it?

When you want a certain behavior, it's best to work backward from the results you want and craft a simple approximation. You'd be surprised how often simple is better -- more intuitive, more codeable, more tuneable.

For instance, your complicated approach with repeated tests and an increasing chance after each failure is actually more likely to pass early than late. The behavior will trigger within 15 seconds 55% of the time and have a 95% chance of happening within 25 seconds. Here's a graph of how often the behavior lasts exactly X seconds before switching. I bet you didn't expect that since it sounds like you wanted longer delays to be more common.

chart.png.cfb5a1c7cbb7209b338631f103d633d8.png

To weight a result toward the high end, one kind of mathy way is to take the floating point number at the core of the randomizer -- the one that's in the 0.0 to 1.0 range -- and do something to it that increases the values in the middle but leaves the 0 and 1 values alone. One straightforward option: take the square root. That tweak has to be made before multiplying out to cover the range of values you need. In LSL, where we only have llFrand() and it multiplies by the range for us, that means doing this:

llFrand(max)

becomes

llPow(llFrand(1.0), 0.5) * max

If you want a stronger or weaker skew, tweak the 0.5.


There's also a non-mathy way to skew results high: generate two random numbers and use the bigger one.


Skewing random numbers low means squaring the 0.0-1.0 core value (power 2.0 instead of 0.5) if you're mathing it, or rolling two unskewed numbers and picking the smaller one if you're not.


If you want to do something with a bell curve -- something more likely to pick a number in the middle of a range than one near the ends -- one simple way is to add N random numbers that each cover 1/Nth the range.

Say you want an eyeblink every 4 to 10 seconds. If you use 4 + llFrand(6) you're just as likely to get short or long blinks as average ones. To skew the blinks toward average delays you could use 4 + llFrand(3) + llFrand(3) instead. 4 + llFrand(2) + llFrand(2) + llFrand(2) would make the bias even stronger.

 

P.S. The "select from an array" approach is also very simple, flexible and tuneable.

Edited by Quarrel Kukulcan
  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Two more general pieces of advise,

1) graph things or do simulations to make sure things work how you think they do,

2) you can exactly mirror a distribution by subtracting it from the maximum (and adding the minimum).

Some very basic distribution tests:

Quote

Quoted for collapsability:

list select_from_list_gaussian(list items)
{
  integer index = llGetListLength(items);
  float cumulative_sum = index*(index+1)*0.5; // sum from 1 .. index.
  float f = llFrand(cumulative_sum);

  while(~--index)
  {
    cumulative_sum -= (index+1);
    //llOwnerSay((string)cumulative_sum);
    if(f>=cumulative_sum) return llList2List(items,index,index);
  }
  llOwnerSay(llList2CSV([cumulative_sum,f]));
  llOwnerSay("s_f_l_g: Programming error.");
  return [];
}
default
{
    state_entry()
    {
        // sum of Frands:
        list test = [0,0,0,0,0,0,0,0,0,0];
        integer index = 1000;
        while(~--index)
        {
            integer rand = (integer)(llFrand(3)+
                llFrand(3)+
                llFrand(4));
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        llSay(0,"basic Sum");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
        // square frand:
        test = [0,0,0,0,0,0,0,0,0,0];
        index = 1000;
        while(~--index)
        {
            float Frand = llFrand(1.);
            integer rand = (integer)(10*(Frand*Frand));
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        llSay(0,"Square");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
        // 1- square
        test = [0,0,0,0,0,0,0,0,0,0];
        index = 1000;
        while(~--index)
        {
            float Frand = llFrand(1.);
            integer rand = (integer)(10*(1-(Frand*Frand)));
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        llSay(0,"1-Square");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
        // square-root
        test = [0,0,0,0,0,0,0,0,0,0];
        index = 1000;
        while(~--index)
        {
            float Frand = llFrand(1.);
            integer rand = (integer)(10*(llPow(Frand,0.5)));
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        //llSay(0,"debug.");
        llSay(0,"root");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
        // 1 - square-root
        test = [0,0,0,0,0,0,0,0,0,0];
        index = 1000;
        while(~--index)
        {
            float Frand = llFrand(1.);
            integer rand = (integer)(10*(1-llPow(Frand,0.5)));
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        llSay(0,"1-root");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
        // "gaussian"
        test = [0,0,0,0,0,0,0,0,0,0];
        index = 1000;
        while(~--index)
        {
            integer rand = llList2Integer(select_from_list_gaussian(
                [0,1,2,3,4,5,6,7,8,9]),0);
            integer old = llList2Integer(test,rand);
            test = llListReplaceList(test,[++old],rand,rand);
        }
        llSay(0,"gaussian");
        llSleep(0.2);
        llSay(0, llList2CSV(test));
    }
}

 

and their results:

Quote

[17:45] Object: basic Sum
[17:45] Object: 3, 36, 79, 166, 200, 235, 164, 88, 27, 2
[17:45] Object: Square
[17:45] Object: 309, 122, 108, 102, 61, 65, 62, 52, 66, 53
[17:45] Object: 1-Square
[17:45] Object: 57, 61, 56, 51, 70, 90, 79, 101, 109, 326

[17:45] Object: root
[17:45] Object: 5, 36, 49, 63, 96, 102, 160, 149, 157, 183
[17:45] Object: 1-root
[17:45] Object: 145, 190, 152, 138, 129, 86, 70, 53, 30, 7
[17:45] Object: gaussian
[17:45] Object: 23, 43, 58, 76, 100, 108, 123, 134, 157, 178

(( theoretical gausian: 18,36,55,73,91,109,127,145,164,182 ))

as you can see, '1-square' and 'root' are both weighted towards the right, but 'root' has less of an abrupt spike at the end than '1-square' ; 'Gaussian' is the most even, and not too dissimilar from the 'root' distribution.

Edited by Quistess Alpha
  • Like 1
Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 88 days.

Please take a moment to consider if this thread is worth bumping.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...