Skip to content

Develop Side Channels tutorial #3391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions com.unity.ml-agents/Runtime/Academy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,27 @@ public void EnableAutomaticStepping()
m_FixedUpdateStepper = m_StepperObject.AddComponent<AcademyFixedUpdateStepper>();
}

/// <summary>
/// Registers SideChannel to the Academy to send and receive data with Python.
/// If IsCommunicatorOn is false, the SideChannel will not be registered.
/// </summary>
/// <param name="sideChannel"> The side channel to be registered.</param>
public void RegisterSideChannel(SideChannel channel)
{
LazyInitialization();
Communicator?.RegisterSideChannel(channel);
}

/// <summary>
/// Unregisters SideChannel to the Academy. If the side channel was not registered,
/// nothing will happen.
/// </summary>
/// <param name="sideChannel"> The side channel to be unregistered.</param>
public void UnregisterSideChannel(SideChannel channel)
{
Communicator?.UnregisterSideChannel(channel);
}

/// <summary>
/// Disable stepping of the Academy during the FixedUpdate phase. If this is called, the Academy must be
/// stepped manually by the user by calling Academy.EnvironmentStep().
Expand Down
12 changes: 12 additions & 0 deletions com.unity.ml-agents/Runtime/Grpc/RpcCommunicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,18 @@ public void RegisterSideChannel(SideChannel sideChannel)
m_SideChannels.Add(sideChannel.ChannelType(), sideChannel);
}

/// <summary>
/// Unregisters a side channel from the communicator.
/// </summary>
/// <param name="sideChannel"> The side channel to be unregistered.</param>
public void UnregisterSideChannel(SideChannel sideChannel)
{
if (m_SideChannels.ContainsKey(sideChannel.ChannelType()))
{
m_SideChannels.Remove(sideChannel.ChannelType());
}
}

/// <summary>
/// Grabs the messages that the registered side channels will send to Python at the current step
/// into a singe byte array.
Expand Down
6 changes: 6 additions & 0 deletions com.unity.ml-agents/Runtime/ICommunicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,11 @@ internal interface ICommunicator : IDisposable
/// </summary>
/// <param name="sideChannel"> The side channel to be registered.</param>
void RegisterSideChannel(SideChannel sideChannel);

/// <summary>
/// Unregisters a side channel from the communicator.
/// </summary>
/// <param name="sideChannel"> The side channel to be unregistered.</param>
void UnregisterSideChannel(SideChannel sideChannel);
}
}
180 changes: 177 additions & 3 deletions docs/Python-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,12 @@ side channel to read and write properties.
`FloatPropertiesChannel` has three methods:

* `set_property` Sets a property in the Unity Environment.
* key: The string identifier of the property.
* value: The float value of the property.
* key: The string identifier of the property.
* value: The float value of the property.

* `get_property` Gets a property in the Unity Environment. If the property was not found, will return None.
* key: The string identifier of the property.
* key: The string identifier of the property.

* `list_properties` Returns a list of all the string identifiers of the properties

```python
Expand All @@ -266,3 +268,175 @@ Once a property has been modified in Python, you can access it in C# after the n
var sharedProperties = Academy.Instance.FloatProperties;
float property1 = sharedProperties.GetPropertyWithDefault("parameter_1", 0.0f);
```

#### [Advanced] Create your own SideChannel

You can create your own `SideChannel` in C# and Python and use it to communicate data between the two.

##### Unity side
The side channel will have to implement the `SideChannel` abstract class. There are two methods
that must be implemented :

* `ChannelType()` : Must return an integer identifying the side channel (This number must be the same on C#
and Python). There can only be one side channel of a certain type during communication.
* `OnMessageReceived(byte[] data)` : You must implement this method to specify what the side channel will be doing
with the data received from Python. The data is a `byte[]` argument.

To send a byte array from C# to Python, call the `base.QueueMessageToSend(data)` method inside the side channel.
The `data` argument must be a `byte[]`.

To register a side channel on the Unity side, call `Academy.Instance.RegisterSideChannel` with the side channel
as only argument.

##### Python side
The side channel will have to implement the `SideChannel` abstract class. You must implement :

* `channel_type(self) -> int` (property) : Must return an integer identifying the side channel (This number must
be the same on C# and Python). There can only be one side channel of a certain type during communication.
* `on_message_received(self, data: bytes) -> None` : You must implement this method to specify what the
side channel will be doing with the data received from Unity. The data is a `byte[]` argument.

To send a byte array from Python to C#, call the `super().queue_message_to_send(bytes_data)` method inside the
side channel. The `bytes_data` argument must be a `bytes` object.

To register a side channel on the Python side, pass the side channel as argument when creating the
`UnityEnvironment` object. One of the arguments of the constructor (`side_channels`) is a list of side channels.

##### Example implementation

Here is a simple implementation of a Side Channel that will exchange strings between C# and Python
(encoded as ascii).

One the C# side :
Here is an implementation of a `StringLogSideChannel` that will listed to the `UnityEngine.Debug.LogError` calls in
the game :

```csharp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code (and RegisterStringLogSideChannel ) should be an actual .cs file in the Examples directory. Otherwise it's way too easy for it to get out of date during refactors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW we might be able to populate the package docs based on code; there's a thread I started here https://unity.slack.com/archives/C070VFPRV/p1575917230249200

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this critique isn't specific to this example, large blocks of uncompiled unexecuted code always makes me nervous; see also the jupyter notebook)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very fair concern. I would like to have a tutorial rather than code because I would like users to make their own rather than rely on our own implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would solve the problem if there was a way to show code from a file in md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a block of markdown code and actual .cs file are equally likely to get copy-pasted. If it's in the Examples and not the SDK, we're free to change the implementation later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think links from the docs to the .cs and vice versa is sufficient for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather keep the code in there. I would argue that If I add the .cs script in the Examples folder, it will still be untested code. If I add the .cs I would have to add the python code as well and we do not have a place to put this code right now (I do not want to put if in yet another notebook).
I do agree that it is very bad to have large amount of code in markdown as tutorial (looking at you RollerBall) but I do not think I like the alternative. Maybe we should put a hold on this PR for now ?

using UnityEngine;
using MLAgents;
using System.Text;

public class StringLogSideChannel : SideChannel
{
public override int ChannelType()
{
return (int)SideChannelType.UserSideChannelStart + 1;
}

public override void OnMessageReceived(byte[] data)
{
var receivedString = Encoding.ASCII.GetString(data);
Debug.Log("From Python : " + receivedString);
}

public void SendDebugStatementToPython(string logString, string stackTrace, LogType type)
{
if (type == LogType.Error)
{
var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace;
var encodedString = Encoding.ASCII.GetBytes(stringToSend);
base.QueueMessageToSend(encodedString);
}
}
}
```

We also need to register this side channel to the Academy and to the `Application.logMessageReceived` events,
so we write a simple MonoBehavior for this. (Do not forget to attach it to a GameObject in the scene).

```csharp
using UnityEngine;
using MLAgents;


public class RegisterStringLogSideChannel : MonoBehaviour
{

StringLogSideChannel stringChannel;
public void Awake()
{
// We create the Side Channel
stringChannel = new StringLogSideChannel();

// When a Debug.Log message is created, we send it to the stringChannel
Application.logMessageReceived += stringChannel.SendDebugStatementToPython;

// Just in case the Academy has not yet initialized
Academy.Instance.RegisterSideChannel(stringChannel);
}

public void OnDestroy()
{
// De-register the Debug.Log callback
Application.logMessageReceived -= stringChannel.SendDebugStatementToPython;
if (Academy.IsInitialized){
Academy.Instance.UnregisterSideChannel(stringChannel);
}
}

public void Update()
{
// Optional : If the space bar is pressed, raise an error !
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.LogError("This is a fake error. Space bar was pressed in Unity.");
}
}
}
```

And here is the script on the Python side. This script creates a new Side channel type (`StringLogChannel`) and
launches a `UnityEnvironment` with that side channel.

```python

from mlagents_envs.environment import UnityEnvironment
from mlagents_envs.side_channel.side_channel import SideChannel, SideChannelType
import numpy as np


# Create the StringLogChannel class
class StringLogChannel(SideChannel):
@property
def channel_type(self) -> int:
return SideChannelType.UserSideChannelStart + 1

def on_message_received(self, data: bytes) -> None:
"""
Note :We must implement this method of the SideChannel interface to
receive messages from Unity
"""
# We simply print the data received interpreted as ascii
print(data.decode("ascii"))

def send_string(self, data: str) -> None:
# Convert the string to ascii
bytes_data = data.encode("ascii")
# We call this method to queue the data we want to send
super().queue_message_to_send(bytes_data)

# Create the channel
string_log = StringLogChannel()

# We start the communication with the Unity Editor and pass the string_log side channel as input
env = UnityEnvironment(base_port=UnityEnvironment.DEFAULT_EDITOR_PORT, side_channels=[string_log])
env.reset()
string_log.send_string("The environment was reset")

group_name = env.get_agent_groups()[0] # Get the first group_name
for i in range(1000):
step_data = env.get_step_result(group_name)
n_agents = step_data.n_agents() # Get the number of agents
# We send data to Unity : A string with the number of Agent at each
string_log.send_string(
"Step " + str(i) + " occurred with " + str(n_agents) + " agents."
)
env.step() # Move the simulation forward

env.close()

```

Now, if you run this script and press `Play` the Unity Editor when prompted, The console in the Unity Editor will
display a message at every Python step. Additionally, if you press the Space Bar in the Unity Engine, a message will
appear in the terminal.