/* Distributed under the Apache License, Version 2.0. See accompanying NOTICE file for details.*/ using System; using System.Collections.Generic; using UnityEngine; using Pulse; using Pulse.CDM; // This is very similar to PulseEngineDriver, only we will use a SEScenario to hold data requests and engine initialization data [ExecuteInEditMode] public class PulseEngineScenarioDriver : PulseEngineSource { public TextAsset scenarioJson; // Scenario file to use [NonSerialized] protected SEScenario scenario; // A scenario file protected List> actions = new List>();// The scenario actions and the time they need to be processed protected List> activeActions = new List>(); // List of actions we are processing and will remove from actions list // MARK: Monobehavior methods // Called when the inspector inputs are modified protected virtual void OnValidate() { // Round down to closest factor of 0.02. Need to use doubles due to // issues with floats multiplication (0.1 -> 0.0999999) sampleRate = Math.Round(sampleRate / 0.02) * 0.02; } // Called when application or editor opens protected virtual void Awake() { // Create our data container data = ScriptableObject.CreateInstance(); // Store data field names // The rest of the data values are in order of the data_requests list data.fields = new StringList();// Field names data.timeStampList = new DoubleList(); // One or more datasets from the engine data.valuesTable = new List();// The values received from the engine // The first field is always the simulation time in seconds data.fields.Add("Simulation Time(s)"); data.valuesTable.Add(new DoubleList()); foreach (var request in data_requests) { data.fields.Add(request.ToString().Replace("/", "\u2215")); data.valuesTable.Add(new DoubleList()); } pullAllData = (sampleRate == pulseTimeStep); } // Called at the first frame when the component is enabled protected virtual void Start() { // Ensure we only read data if the application is playing // and we have a state file to initialize the engine with if (!Application.isPlaying) return; // Allocate PulseEngine with path to logs and needed data files string dateAndTimeVar = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string logFilePath = Application.persistentDataPath + "/" + gameObject.name + dateAndTimeVar + ".log"; engine = new PulseUnityEngine(); engine.SetLogFilename(logFilePath); scenario = new SEScenario(); // Initialize engine state from tje state file content if (scenarioJson != null) { if (!scenario.SerializeFromString(scenarioJson.text, eSerializationFormat.JSON)) Debug.unityLogger.LogError("PulsePhysiologyEngine", "Unable to load scenario file " + scenarioJson); } else { // You do not have to use the Editor control if you don't want to, // You could simply specify a file on disk via use of the Streaming Assets folder string streamingScenarioFilename = Application.streamingAssetsPath + "/test_scenario.json"; if (!scenario.SerializeFromFile(streamingScenarioFilename)) { Debug.unityLogger.LogError("PulsePhysiologyEngine", "Unable to load scenario file "+ streamingScenarioFilename); return; } // You could also procedurally create a scenario scenario.SetName("Scenario"); scenario.SetDescription("Simple Scenario to demonstraight building a scenario by the CDM API"); scenario.GetPatientConfiguration().SetPatientFile("StandardMale.json"); // Any extra data you want SEDataRequest dr = SEDataRequest.CreatePhysiologyDataRequest("BloodVolume", VolumeUnit.mL); scenario.GetDataRequestManager().GetDataRequests().Add(dr); } // !!! NOTE !!! // We need to combine any predefined editor data requests with data requests provided in the scenario // PLEASE ENSURE THERE ARE NO DUPLICATE DATA REQUESTS, THE ENGINE WILL NOT INITIALIZE // IT IS UNABLE TO PROPERLY ORDER THE PullData ARRAY WITH DUPLIATES // Push the scenario data requests to the back of the data container foreach (var request in scenario.GetDataRequestManager().GetDataRequests()) { data.fields.Add(request.ToString().Replace("/", "\u2215")); data.valuesTable.Add(new DoubleList()); } // The vitals monitor is expecting the data_requests associated with the monitor to be in the beginning of data_values // So we need to push these to the front of the scenario data request list in reverse order // So data requests in the scenario file, or that you procedurally created will be AFTER // the vitals_monitor_data_requests in the data_values array for (int i=data_requests.Count; i>0; i--) scenario.GetDataRequestManager().GetDataRequests().Insert(0, data_requests[i-1]); // So the file/procedurally created requests will start at index data_requests.length // If you have duplicates, you are just going to get the same data multiple times if (scenario.HasEngineState()) { // This code is assuming the scenario engine state file is relative to the application streaming path string state = Application.streamingAssetsPath+"/Data/states/"+scenario.GetEngineState(); if (!engine.SerializeFromFile(state, scenario.GetDataRequestManager())) { Debug.unityLogger.LogError("PulsePhysiologyEngine", "Unable to load state file " + state); return; } } else if(scenario.HasPatientConfiguration()) { if (!engine.InitializeEngine(scenario.GetPatientConfiguration(), scenario.GetDataRequestManager())) { Debug.unityLogger.LogError("PulsePhysiologyEngine", "Unable to initialize patient"); return; } } else { Debug.unityLogger.LogError("PulsePhysiologyEngine", "Invalid Scenario provided"); return; } // Go through the scenario actions and figure out what time they need to be processed double simTime_s = 0; foreach (SEAction a in scenario.GetActions()) { if (a is SEAdvanceTime) { simTime_s += ((SEAdvanceTime)a).GetTime().GetValue(TimeUnit.s); } else actions.Add(new Tuple(simTime_s, a)); } pulseTime = 0; pulseSampleTime = 0; } // Called before every frame protected virtual void Update() { // Ensure we only broadcast data if the application is playing // and there a valid pulse engine to simulate data from if (!Application.isPlaying || engine == null || pauseUpdate) return; double timeElapsed = Time.time - pulseTime; if (timeElapsed < pulseTimeStep) return;// Not running yet // Clear PulseData container if (!data.timeStampList.IsEmpty()) { data.timeStampList.Clear(); for (int j = 0; j < data.valuesTable.Count; ++j) data.valuesTable[j].Clear(); } // Iterate over multiple time steps if needed int numberOfDataPointsNeeded = (int)Math.Floor(timeElapsed / pulseTimeStep); //if (numberOfDataPointsNeeded > 2) // Debug.unityLogger.Log("Big Catchup "+ numberOfDataPointsNeeded + ", timeElapsed = " + timeElapsed); for (int i = 0; i < numberOfDataPointsNeeded; ++i) { // Check to see if we need to process any actions at this time foreach (var e in actions) { if (e.Item1 <= pulseTime) { activeActions.Add(e); if (!engine.ProcessAction(e.Item2)) Debug.unityLogger.LogError("PulsePhysiologyEngine", "Could not process action " + e.Item2.ToString()); } } foreach (var e in activeActions) actions.Remove(e); activeActions.Clear(); // Increment pulse time pulseTime += pulseTimeStep; pulseSampleTime += pulseTimeStep; // Advance simulation by time step bool success = engine.AdvanceTime_s(pulseTimeStep); if (!success) continue; // Copy simulated data to data container (if its time) if (pullAllData || pulseSampleTime >= sampleRate) { pulseSampleTime = 0; data.timeStampList.Add(pulseTime); data_values = engine.PullData(); for (int j = 0; j < data_values.Length; ++j) data.valuesTable[j].Add((float)data_values[j]); } } } protected virtual void OnApplicationQuit() { engine = null; } }