using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Barracuda;
using System.IO;
using Unity.MLAgents;
using Unity.MLAgents.Policies;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Unity.MLAgentsExamples
{
///
/// Utility class to allow the NNModel file for an agent to be overriden during inference.
/// This is used internally to validate the file after training is done.
/// The behavior name to override and file path are specified on the commandline, e.g.
/// player.exe --mlagents-override-model-directory /path/to/models
///
/// Additionally, a number of episodes to run can be specified; after this, the application will quit.
/// Note this will only work with example scenes that have 1:1 Agent:Behaviors. More complicated scenes like WallJump
/// probably won't override correctly.
///
public class ModelOverrider : MonoBehaviour
{
HashSet k_SupportedExtensions = new HashSet{"nn", "onnx"};
const string k_CommandLineModelOverrideFlag = "--mlagents-override-model";
const string k_CommandLineModelOverrideDirectoryFlag = "--mlagents-override-model-directory";
const string k_CommandLineModelOverrideExtensionFlag = "--mlagents-override-model-extension";
const string k_CommandLineQuitAfterEpisodesFlag = "--mlagents-quit-after-episodes";
const string k_CommandLineQuitOnLoadFailure = "--mlagents-quit-on-load-failure";
// The attached Agent
Agent m_Agent;
// Assets paths to use, with the behavior name as the key.
Dictionary m_BehaviorNameOverrides = new Dictionary();
string m_BehaviorNameOverrideDirectory;
string m_OverrideExtension = "nn";
// Cached loaded NNModels, with the behavior name as the key.
Dictionary m_CachedModels = new Dictionary();
// Max episodes to run. Only used if > 0
// Will default to 1 if override models are specified, otherwise 0.
int m_MaxEpisodes;
int m_NumSteps;
int m_PreviousNumSteps;
int m_PreviousAgentCompletedEpisodes;
bool m_QuitOnLoadFailure;
[Tooltip("Debug values to be used in place of the command line for overriding models.")]
public string debugCommandLineOverride;
// Static values to keep track of completed episodes and steps across resets
// These are updated in OnDisable.
static int s_PreviousAgentCompletedEpisodes;
static int s_PreviousNumSteps;
int TotalCompletedEpisodes
{
get { return m_PreviousAgentCompletedEpisodes + (m_Agent == null ? 0 : m_Agent.CompletedEpisodes); }
}
int TotalNumSteps
{
get { return m_PreviousNumSteps + m_NumSteps; }
}
public bool HasOverrides
{
get { return m_BehaviorNameOverrides.Count > 0 || !string.IsNullOrEmpty(m_BehaviorNameOverrideDirectory); }
}
public static string GetOverrideBehaviorName(string originalBehaviorName)
{
return $"Override_{originalBehaviorName}";
}
///
/// Get the asset path to use from the commandline arguments.
///
///
void GetAssetPathFromCommandLine()
{
m_BehaviorNameOverrides.Clear();
var maxEpisodes = 0;
string[] commandLineArgsOverride = null;
if (!string.IsNullOrEmpty(debugCommandLineOverride) && Application.isEditor)
{
commandLineArgsOverride = debugCommandLineOverride.Split(' ');
}
var args = commandLineArgsOverride ?? Environment.GetCommandLineArgs();
for (var i = 0; i < args.Length; i++)
{
if (args[i] == k_CommandLineModelOverrideFlag && i < args.Length-2)
{
var key = args[i + 1].Trim();
var value = args[i + 2].Trim();
m_BehaviorNameOverrides[key] = value;
}
else if (args[i] == k_CommandLineModelOverrideDirectoryFlag && i < args.Length-1)
{
m_BehaviorNameOverrideDirectory = args[i + 1].Trim();
}
else if (args[i] == k_CommandLineModelOverrideExtensionFlag && i < args.Length-1)
{
m_OverrideExtension = args[i + 1].Trim().ToLower();
var isKnownExtension = k_SupportedExtensions.Contains(m_OverrideExtension);
// Not supported yet - need to update the model loading code to support
var isOnnx = m_OverrideExtension.Equals("onnx");
if (!isKnownExtension || isOnnx)
{
Debug.LogError($"loading unsupported format: {m_OverrideExtension}");
Application.Quit(1);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#endif
}
}
else if (args[i] == k_CommandLineQuitAfterEpisodesFlag && i < args.Length-1)
{
Int32.TryParse(args[i + 1], out maxEpisodes);
}
else if (args[i] == k_CommandLineQuitOnLoadFailure)
{
m_QuitOnLoadFailure = true;
}
}
if (HasOverrides)
{
// If overriding models, set maxEpisodes to 1 or the command line value
m_MaxEpisodes = maxEpisodes > 0 ? maxEpisodes : 1;
Debug.Log($"setting m_MaxEpisodes to {maxEpisodes}");
}
}
void OnEnable()
{
// Start with these initialized to previous values in the case where we're resetting scenes.
m_PreviousNumSteps = s_PreviousNumSteps;
m_PreviousAgentCompletedEpisodes = s_PreviousAgentCompletedEpisodes;
m_Agent = GetComponent();
GetAssetPathFromCommandLine();
if (HasOverrides)
{
OverrideModel();
}
}
void OnDisable()
{
// Update the static episode and step counts.
// For a single agent in the scene, this will be a straightforward increment.
// If there are multiple agents, we'll increment the count by the Agent that completed the most episodes.
s_PreviousAgentCompletedEpisodes = Mathf.Max(s_PreviousAgentCompletedEpisodes, TotalCompletedEpisodes);
s_PreviousNumSteps = Mathf.Max(s_PreviousNumSteps, TotalNumSteps);
}
void FixedUpdate()
{
if (m_MaxEpisodes > 0)
{
// For Agents without maxSteps, exit as soon as we've hit the target number of episodes.
// For Agents that specify MaxStep, also make sure we've gone at least that many steps.
// Since we exit as soon as *any* Agent hits its target, the maxSteps condition keeps us running
// a bit longer in case there's an early failure.
if (TotalCompletedEpisodes >= m_MaxEpisodes && TotalNumSteps > m_MaxEpisodes * m_Agent.MaxStep)
{
Debug.Log($"ModelOverride reached {TotalCompletedEpisodes} episodes and {TotalNumSteps} steps. Exiting.");
Application.Quit(0);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#endif
}
}
m_NumSteps++;
}
public NNModel GetModelForBehaviorName(string behaviorName)
{
if (m_CachedModels.ContainsKey(behaviorName))
{
return m_CachedModels[behaviorName];
}
string assetPath = null;
if (m_BehaviorNameOverrides.ContainsKey(behaviorName))
{
assetPath = m_BehaviorNameOverrides[behaviorName];
}
else if(!string.IsNullOrEmpty(m_BehaviorNameOverrideDirectory))
{
assetPath = Path.Combine(m_BehaviorNameOverrideDirectory, $"{behaviorName}.{m_OverrideExtension}");
}
if (string.IsNullOrEmpty(assetPath))
{
Debug.Log($"No override for BehaviorName {behaviorName}, and no directory set.");
return null;
}
byte[] model = null;
try
{
model = File.ReadAllBytes(assetPath);
}
catch(IOException)
{
Debug.Log($"Couldn't load file {assetPath} at full path {Path.GetFullPath(assetPath)}", this);
// Cache the null so we don't repeatedly try to load a missing file
m_CachedModels[behaviorName] = null;
return null;
}
// Note - this approach doesn't work for onnx files. Need to replace with
// the equivalent of ONNXModelImporter.OnImportAsset()
var asset = ScriptableObject.CreateInstance();
asset.modelData = ScriptableObject.CreateInstance();
asset.modelData.Value = model;
asset.name = "Override - " + Path.GetFileName(assetPath);
m_CachedModels[behaviorName] = asset;
return asset;
}
///
/// Load the NNModel file from the specified path, and give it to the attached agent.
///
void OverrideModel()
{
bool overrideOk = false;
string overrideError = null;
m_Agent.LazyInitialize();
var bp = m_Agent.GetComponent();
var behaviorName = bp.BehaviorName;
var nnModel = GetModelForBehaviorName(behaviorName);
if (nnModel == null)
{
overrideError =
$"Didn't find a model for behaviorName {behaviorName}. Make " +
$"sure the behaviorName is set correctly in the commandline " +
$"and that the model file exists";
}
else
{
var modelName = nnModel != null ? nnModel.name : "";
Debug.Log($"Overriding behavior {behaviorName} for agent with model {modelName}");
try
{
m_Agent.SetModel(GetOverrideBehaviorName(behaviorName), nnModel);
overrideOk = true;
}
catch (Exception e)
{
overrideError = $"Exception calling Agent.SetModel: {e}";
}
}
if (!overrideOk && m_QuitOnLoadFailure)
{
if(!string.IsNullOrEmpty(overrideError))
{
Debug.LogWarning(overrideError);
}
Application.Quit(1);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#endif
}
}
}
}