Skip to content

Commit

Permalink
feat: add MemoryBelief (#24)
Browse files Browse the repository at this point in the history
* feat: add Belief, IBelief and Beliefset

* docs: add documentation

* test: add tests for Belief

* test: add tests for Beliefset

* fix: fix Reflection bug in Beliefset

* fix: make Beliefset constructor protected

* docs: change summaries to remarks

* fix: adhere to naming conventions for unit tests

* chore: change name Beliefset to BeliefSet

* chore: change name Beliefset to BeliefSet

* fix: change name TResource to TObservation and add more tests

* feat: add MemoryBelief skeleton

* fix: improve documentation and namings

* fix: improve IBelief and Belief summaries

* fix: more improvements to docs and tests

* chore: correct test comments

* chore: rename Beliefset.cs to BeliefSet.cs

* chore: rename BeliefsetTests.cs to BeliefSetTests.cs

* chore: remove Believe folder

* fix: use actual BeliefSet in Goals

* feat: add IBeliefSet

* chore: rename believe to belief in comments

* feat: add circular array datatype

* feat: MemoryBelief now uses CircularArray

* chore: add missing comments

* chore: conform to sonar

* fix: make changes according to feedback

* fix: GetAllMemories now works as intended

* fix: work in some more feedback

* fix: change exception type and remove comments

* chore: replace resource with obsevation

* chore: fix styling and comments

* chore: fix spacing in IBeliefSet

* chore: add back todo

---------

Co-authored-by: Jens Steenmetz <jj.steenmetz@gmail.com>
Co-authored-by: Jens Steenmetz <35835627+JensSteenmetz@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 2, 2024
1 parent 553533f commit a9d1765
Show file tree
Hide file tree
Showing 5 changed files with 415 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Aplib.Core/Belief/Belief.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public Belief(TReference reference, Func<TReference, TObservation> getObservatio
/// Generates/updates the observation if the shouldUpdate condition is satisfied.
/// The observation is then updated by calling the getObservationFromReference function.
/// </summary>
public void UpdateBelief()
public virtual void UpdateBelief()
{
if (_shouldUpdate()) _observation = _getObservationFromReference(_reference);
}
Expand Down
102 changes: 102 additions & 0 deletions Aplib.Core/Belief/MemoryBelief.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;

namespace Aplib.Core.Belief
{
/// <summary>
/// The <see cref="MemoryBelief{TReference, TObservation}"/> class represents the agent's belief of a single object,
/// but with additional "memory" of previous observations.
/// Some <i>object reference</i> is used to generate/update an <i>observation</i>
/// (i.e., some piece of information on the game state as perceived by an agent).
/// This belief also stores a limited amount of previous observations in memory.
/// </summary>
/// <remarks>
/// It implements the <see cref="IBelief"/> interface.
/// It supports implicit conversion to <typeparamref name="TObservation"/>.
/// </remarks>
/// <typeparam name="TReference">The type of the reference used to generate/update the observation.</typeparam>
/// <typeparam name="TObservation">The type of the observation the belief represents.</typeparam>
public class MemoryBelief<TReference, TObservation> : Belief<TReference, TObservation>
{
/// <summary>
/// A "memorized" resouce, from the last time the belief was updated.
/// </summary>
private readonly CircularArray<TObservation> _memorizedObservations;

/// <summary>
/// Initializes a new instance of the <see cref="MemoryBelief{TReference, TObservation}"/> class with an object reference,
/// and a function to generate/update the observation using the object reference.
/// Also initializes the memory array with a specified number of slots.
/// </summary>
/// <param name="reference">The reference used to generate/update the observation.</param>
/// <param name="getObservationFromReference">A function that takes a reference and generates/updates a observation.</param>
/// <param name="framesToRemember">The number of frames to remember back.</param>
public MemoryBelief(TReference reference, Func<TReference, TObservation> getObservationFromReference, int framesToRemember)
: base(reference, getObservationFromReference)
{
_memorizedObservations = new(framesToRemember);
}

/// <summary>
/// Initializes a new instance of the <see cref="MemoryBelief{TReference, TObservation}"/> class with an object reference,
/// a function to generate/update the observation using the object reference,
/// and a condition on when the observation should be updated.
/// Also initializes the memory array with a specified number of slots.
/// </summary>
/// <param name="reference">The reference used to generate/update the observation.</param>
/// <param name="getObservationFromReference">A function that takes a reference and generates/updates a observation.</param>
/// <param name="framesToRemember">The number of frames to remember back.</param>
/// <param name="shouldUpdate">A function that sets a condition on when the observation should be updated.</param>
public MemoryBelief(TReference reference, Func<TReference, TObservation> getObservationFromReference, int framesToRemember,
Func<bool> shouldUpdate)
: base(reference, getObservationFromReference, shouldUpdate)
{
_memorizedObservations = new(framesToRemember);
}

/// <summary>
/// Generates/updates the observation.
/// Also stores the previous observation in memory.
/// </summary>
public override void UpdateBelief()
{
// We use the implicit conversion to TObservation to store the observation
_memorizedObservations.Put(this);
base.UpdateBelief();
}

/// <summary>
/// Gets the most recently memorized observation.
/// </summary>
/// <returns> The most recent memory of the observation.</returns>
public TObservation GetMostRecentMemory() => _memorizedObservations.GetFirst();

/// <summary>
/// Gets the memorized observation at a specific index.
/// A higher index means a memory further back in time.
/// If the index is out of bounds, returns the element closest to the index that is in bounds.
/// </summary>
/// <returns> The memory of the observation at the specified index.</returns>
public TObservation GetMemoryAt(int index, bool clamp = false)
{
int lastMemoryIndex = _memorizedObservations.Length - 1;
if (clamp)
index = Math.Clamp(index, 0, lastMemoryIndex);
else if (index < 0 || index > lastMemoryIndex)
throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {lastMemoryIndex}.");
return _memorizedObservations[index];
}

/// <summary>
/// Gets all the memorized observations.
/// The first element is the newest memory.
/// </summary>
/// <returns> An array of all the memorized observations.</returns>
public TObservation[] GetAllMemories()
{
// For now, we return the entire array, but with empty elements for the unused slots
// TODO: make it return only the used slots
return _memorizedObservations.ToArray();
}
}
}

103 changes: 103 additions & 0 deletions Aplib.Core/CircularArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace Aplib.Core
{
/// <summary>
/// An array that wraps around when it reaches its end.
/// Functionally works like a queue with indexing.
/// </summary>
public class CircularArray<T>
{
/// <summary>
/// The length of the array.
/// </summary>
public int Length { get; private set; }
private readonly T[] _array;
private int _head;

/// <summary>
/// Initializes a new instance of the <see cref="CircularArray{T}"/> class.
/// </summary>
/// <param name="size">The size of the array.</param>
public CircularArray(int size)
{
Length = size;
_array = new T[Length];
_head = Length - 1;
}

/// <summary>
/// Initializes a new instance of the <see cref="CircularArray{T}"/> class.
/// </summary>
/// <param name="array">An array to use as the circular array.</param>
public CircularArray(T[] array)
{
Length = array.Length;
_array = array;
_head = Length - 1;
}

/// <summary>
/// Gets the element at the specified index.
/// </summary>
/// <param name="index">The index of the element to get.</param>
/// <returns>The element at the specified index.</returns>
public T this[int index]
{
get => _array[(index + _head + 1) % Length];
set => _array[(index + _head + 1) % Length] = value;
}

/// <summary>
/// Decrements the head of the array.
/// </summary>
private void DecrementHead()
{
_head = (_head - 1 + Length) % Length;
}

/// <summary>
/// Puts an element at the start of the array.
/// </summary>
/// <param name="value">The element to add to the array</param>
public void Put(T value)
{
_array[_head] = value;
DecrementHead();
}

/// <summary>
/// Gets the element at the head of the array.
/// </summary>
/// <returns>The element at the head of the array</returns>
public T GetHead()
{
return _array[_head];
}

/// <summary>
/// Gets the first element of the array.
/// </summary>
/// <returns>The last element of the array</returns>
public T GetFirst()
{
return this[0];
}

/// <summary>
/// Converts the circular array to an array.
/// The head should be the last element of the array.
/// Copies from start to end inclusive.
/// </summary>
/// <param name="start">The start index of the range to copy.</param>
/// <param name="end">The end index of the range to copy.</param>
/// <returns>The circular array as a normal array</returns>
public T[] ToArray(int start = 0, int end = -1)
{
end = end == -1 ? Length - 1 : end;
T[] result = new T[end - start + 1];
for (int i = 0; i < result.Length; i++)
result[i] = this[start + i];

return result;
}
}
}
116 changes: 116 additions & 0 deletions Aplib.Tests/Core/Belief/CircularArrayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Aplib.Core;

namespace Aplib.Tests.Core.Belief;

public class CircularArrayTests
{
/// <summary>
/// Given a CircularArray instance,
/// When an element is put into the array,
/// The array should wrap around when it reaches its end.
/// (i.e., the first element should become the previous second element,
/// and the last element should be the new element)
/// </summary>
[Fact]
public void Put_ArrayIsFull_WrapsAround()
{
// Arrange
CircularArray<int> circularArray = new([1, 2, 3, 4, 5]);

// Act
circularArray.Put(0);

// Assert
Assert.Equal(0, circularArray[0]);
Assert.Equal(1, circularArray[1]);
Assert.Equal(2, circularArray[2]);
}

/// <summary>
/// Given a CircularArray instance,
/// When the head is updated,
/// Putting an element should set the correct index
/// even if the head is not at the start of the array.
/// </summary>
[Fact]
public void Put_HeadIsUpdated_SetsCorrectIndex()
{
// Arrange
CircularArray<int> circularArray = new(3);

// Act
// circularArray.ToArray() == [0, 0, 0]
circularArray[1] = 6;
// circularArray.ToArray() == [0, 6, 0]
circularArray.Put(4);
// circularArray.ToArray() == [4, 0, 6]
circularArray[1] = 5;
// circularArray.ToArray() == [4, 5, 6]

// Assert
Assert.Equal(4, circularArray[0]);
Assert.Equal(5, circularArray[1]);
Assert.Equal(6, circularArray[2]);
}

/// <summary>
/// Given a CircularArray instance,
/// When the head is updated,
/// GetHead should return the correct head.
/// </summary>
[Fact]
public void GetHead_HeadIsUpdated_ReturnsLastElement()
{
// Arrange
CircularArray<int> circularArray = new([1, 2, 3]);
int prevHead = circularArray.GetHead();

// Act
circularArray.Put(0);
int head = circularArray.GetHead();

// Assert
Assert.NotEqual(prevHead, head);
Assert.Equal(2, head);
}

/// <summary>
/// Given a CircularArray instance,
/// When the head is updated,
/// GetLast should return the last element.
/// </summary>
[Fact]
public void GetFirst_HeadIsUpdated_ReturnsFirstElement()
{
// Arrange
CircularArray<int> circularArray = new([1, 2, 3]);
int prevFirst = circularArray.GetFirst();

// Act
circularArray.Put(4);
int firstElement = circularArray.GetFirst();

// Assert
Assert.NotEqual(prevFirst, firstElement);
Assert.Equal(4, firstElement);
}

/// <summary>
/// Given a CircularArray instance,
/// When the head is updated,
/// Converts the circular array to an array in the correct order.
/// </summary>
[Fact]
public void ToArray_HeadIsUpdated_ReturnsCorrectlyOrderedArray()
{
// Arrange
CircularArray<int> circularArray = new([1, 2, 3, 4, 5]);

// Act
circularArray.Put(0);
int[] array = circularArray.ToArray(1, 3);

// Assert
Assert.Equal([1, 2, 3], array);
}
}
Loading

0 comments on commit a9d1765

Please sign in to comment.