#include "Episode/EpisodeSubSystem.h" #include "Actors/MujocoStaticMeshActor.h" #include "Actors/MujocoVolumeActor.h" #include "Kismet/GameplayStatics.h" #include "Robot/RobotPawn.h" #include "Robot/PilotComponent/RobotPilotComponent.h" #include "LuckyDataTransferSubsystem.h" #include "Components/TextRenderComponent.h" #include "Engine/TextRenderActor.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Dom/JsonObject.h" #include "Serialization/JsonWriter.h" #include "Serialization/JsonSerializer.h" #include "_Utils/FileUtils.h" UEpisodeSubSystem::UEpisodeSubSystem() { } void UEpisodeSubSystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); if (ULuckyDataTransferSubsystem* DataTransferSubSystem = GetWorld()->GetSubsystem()) { DataTransfer = DataTransferSubSystem; } } void UEpisodeSubSystem::Deinitialize() { StopTicking(); Super::Deinitialize(); } void UEpisodeSubSystem::Tick(float DeltaTime) { // TODO we want to get this outside of the Tick if (!bTickEnabled) return; // If no robot or no object if (!EpisodeTargetObject || !CurrentRobot) return; // if capture hasn't started if (!bIsCapturing || CapturedEpisodes >= EpisodesToCapture) return; // Here we are capturing the data, running an episode if (!bIsEpisodeRunning) { StartEpisode(); } else { const bool bIsEpisodeCompleted = CheckEpisodeCompletion(); // Write Image on the disk if (DataTransfer) DataTransfer->WriteImageToDisk(CurrentRobot->PhysicsSceneProxy->GetMujocoData().time); EpisodeFrames++; if (!bIsEpisodeCompleted) return; EndEpisode(); if (CapturedEpisodes < EpisodesToCapture) { StartEpisode(); } else { EndTraining(); } } } void UEpisodeSubSystem::StartTicking() { const FTickerDelegate TickDelegate = FTickerDelegate::CreateLambda([this](const float DeltaTime) { Tick(DeltaTime); return bTickEnabled; }); TickHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate); } void UEpisodeSubSystem::StopTicking() { bTickEnabled = false; FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); } void UEpisodeSubSystem::UpdateDebugTextActor() const { if (!IsValid(DebugTextActor)) return; const auto TextRender = DebugTextActor->GetTextRender(); const FString Txt = FString::Printf(TEXT("Episodes run: %i \nSuccess: %i \nFailed: %i"), CapturedEpisodes, SuccessEpisodes, FailEpisodes); TextRender->SetText(FText::FromString(Txt)); } void UEpisodeSubSystem::StartTraining(const int32 EpisodesCountIn, FString BaseImageDataPathIn, FString TaskDescriptionIn) { // Debug const auto DebugTextActorPtr = UGameplayStatics::GetActorOfClass(this->GetWorld(), ATextRenderActor::StaticClass()); if (DebugTextActorPtr && Cast(DebugTextActorPtr)) { DebugTextActor = Cast(DebugTextActorPtr); } // Robot and Exercise FindEpisodeObjectFromScene(); FindRobotPawnFromScene(); EpisodesToCapture = EpisodesCountIn; SuccessEpisodes = 0; FailEpisodes = 0; StartEpisode(); // Data ConfigureDataCapture(); BaseImageDataPath = BaseImageDataPathIn; TaskDescription = TaskDescriptionIn; StartTicking(); } void UEpisodeSubSystem::EndTraining() { StopTicking(); CreateEpisodesStatsJsonFile(); // Create jsonl files } void UEpisodeSubSystem::StartEpisode() { // Robot should be in its ready state - overriden per PilotComponent if (!CurrentRobot->RobotPilotComponent->GetIsReadyForTraining()) return; // Let's hardcode this for now, and figure out later how to do it correctly with Anuj/Ethan inputs const FTransform RobotTransform = CurrentRobot->RobotActor->GetActorTransform(); constexpr float HardCodedRewardDistanceFromRobotPivot = 15.f; // TODO This should not be hardcoded as it depends from robot type EpisodeRewardZone = FTransform{ // TODO RobotArm right is the forward vector due to rotation the Robot -90 yaw at robot spawn - FIX ME RobotTransform.GetLocation() + RobotTransform.GetRotation().GetForwardVector() * HardCodedRewardDistanceFromRobotPivot * (FMath::RandBool() ? 1 : -1) }; // Ask the bot to give a reachable location for the Training Object Transform EpisodeObjectBaseTransform = CurrentRobot->RobotPilotComponent->GetReachableTransform(); // Move Scenario Object to its location - Done in the PhysicsScene CurrentRobot->PhysicsSceneProxy->UpdateGeomTransform(EpisodeTargetObject->MainActorBody.GetName(), EpisodeObjectBaseTransform); // Set Target on the bot - it will go grab the object CurrentRobot->RobotPilotComponent->SetRobotTarget(EpisodeObjectBaseTransform); CurrentRobot->RobotPilotComponent->SetRobotCurrentRewardZone(EpisodeRewardZone); // Enable Tick checks bIsEpisodeRunning = true; bIsCapturing = true; UpdateDebugTextActor(); } void UEpisodeSubSystem::EndEpisode() { // Gather the robot data const FTrainingEpisodeData TrainingEpisodeData = CurrentRobot->RobotPilotComponent->GetTrainingEpisodeData(); // Create episodes_stats.jsonl single line and append to EpisodeStatLines CreateEpisodeStatJsonLine(TrainingEpisodeData); // create a parquet file CreateEpisodeParquetFile(); // Convert images into video // TODO Find a good FFMPEG plugin - maybe the Unreal base one is good // Reset values for the next episode EpisodeFrames = 0; } bool UEpisodeSubSystem::CheckEpisodeCompletion() { const auto GeomTransform = CurrentRobot->PhysicsSceneProxy->GetGeometryTransform(EpisodeTargetObject->MainActorBody.GetName()); const auto Loc = GeomTransform.GetLocation(); const auto DistanceFromStart = FVector::Distance(EpisodeObjectBaseTransform.GetLocation(), Loc); if (DistanceFromStart <= 2) return false; // Episode is running // TODO This can be used to early detect episode failure and restart the episode faster const auto DotUp = FVector::DotProduct(FVector::UpVector, GeomTransform.GetRotation().GetUpVector()); // Robot did not finish the episode yet if (!CurrentRobot->RobotPilotComponent->GetIsInRestState()) return false; // Here we are away from Start zone and Robot has finished the exercise const auto DistanceToReward = FVector::Distance(EpisodeRewardZone.GetLocation(), Loc); if (DistanceToReward < EpisodeRewardZoneRadius) { SuccessEpisodes++; } else { FailEpisodes++; } CapturedEpisodes++; return true; } void UEpisodeSubSystem::FindEpisodeObjectFromScene() { TArray MujocoObjects; UGameplayStatics::GetAllActorsOfClass(this->GetWorld(), AMujocoStaticMeshActor::StaticClass(), MujocoObjects); if (MujocoObjects.IsValidIndex(0) && Cast(MujocoObjects[0])) { EpisodeTargetObject = Cast(MujocoObjects[0]); } } void UEpisodeSubSystem::FindRobotPawnFromScene() { TArray RobotPawns; UGameplayStatics::GetAllActorsOfClass(this->GetWorld(), ARobotPawn::StaticClass(), RobotPawns); if (RobotPawns.IsValidIndex(0) && Cast(RobotPawns[0])) { CurrentRobot = Cast(RobotPawns[0]); } } void UEpisodeSubSystem::InitCameras() { // TODO Fix the spawning of sensors in Cpp and spawn them using a config? // TODO How people can move the camera themselves? // Find all sensors in the scene TArray Sensors; UGameplayStatics::GetAllActorsOfClass(this->GetWorld(), ALuckySensorPawnBase::StaticClass(), Sensors); for (const auto Sensor : Sensors) { if (const auto Camera = Cast(Sensor)) Cameras.Add(Camera); } } void UEpisodeSubSystem::ConfigureDataCapture() { if (!DataTransfer) return; DataTransfer->CreateCaptureSessionID(); InitCameras(); for (const auto& Cam : Cameras) { DataTransfer->RegisterSensor(Cam.Get()); Cam->SensorInfo.bActive = true; } } void UEpisodeSubSystem::CreateEpisodeStatJsonLine(const FTrainingEpisodeData& TrainingEpisodeData) { // EpisodeStatLines. const TSharedPtr Root = MakeShared(); Root->SetNumberField("episode_index", CapturedEpisodes); const TSharedPtr Stats = MakeShared(); Stats->SetObjectField("action", MakeShared(TrainingEpisodeData.ControlsStats)); Stats->SetObjectField("observation.state", MakeShared(TrainingEpisodeData.JointsStats)); // TODO Once all json and parquet files are written on disk and the PR is merged into main and tested, we will do it // TODO "observation.images.webcam" // TODO "timestamp" // TODO "frame_index" // TODO "episode_index" // TODO "index" // TODO "task_index" // Append Root->SetObjectField("stats", Stats); // Serialize into FString FString Output; const TSharedRef< TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > > Writer = TJsonWriterFactory< TCHAR, TCondensedJsonPrintPolicy >::Create(&Output); FJsonSerializer::Serialize(Root.ToSharedRef(), Writer); EpisodeStatLines.Add(Output); } void UEpisodeSubSystem::CreateEpisodeParquetFile() { // TODO Use Anuj plugin to create one parquet file per episode } void UEpisodeSubSystem::ConvertImagesToVideo() { // TODO Once every json and parquet tasks are done } void UEpisodeSubSystem::CreateEpisodesStatsJsonFile() { // TODO Do not use FJsonObject - simply concat the FStrings into a file UFileUtils::WriteJsonlFile(EpisodeStatLines, FPaths::ProjectSavedDir(), FString("episodes_stats")); // Create a jsonl file and store in the correct directory // concat TArray EpisodeStatLines into a single file // https://huggingface.co/datasets/youliangtan/so100_strawberry_grape/blob/main/meta/episodes_stats.jsonl } void UEpisodeSubSystem::CreateEpisodesJsonFile() { // Create a jsonl file and store in the correct directory // https://huggingface.co/datasets/youliangtan/so100_strawberry_grape/blob/main/meta/episodes.jsonl } void UEpisodeSubSystem::CreateInfoJsonFile() { // https://huggingface.co/datasets/youliangtan/so100_strawberry_grape/blob/main/meta/info.json } void UEpisodeSubSystem::CreateTasksJsonFile() { // https://huggingface.co/datasets/youliangtan/so100_strawberry_grape/blob/main/meta/tasks.jsonl }