diff --git a/VectoCore/Models/Simulation/Data/ModalResult.cs b/VectoCore/Models/Simulation/Data/ModalResult.cs index a8d6ec2f6f81abf3880cd53193b9285829e30e9e..5167a72d20a84d7791db47dcf85286ff8977204c 100644 --- a/VectoCore/Models/Simulation/Data/ModalResult.cs +++ b/VectoCore/Models/Simulation/Data/ModalResult.cs @@ -15,7 +15,7 @@ namespace TUGraz.VectoCore.Models.Simulation.Data public ModalResults() { foreach (ModalResultField value in Enum.GetValues(typeof (ModalResultField))) { - var col = new DataColumn(value.GetName(), value.GetDataType()) {Caption = value.GetCaption()}; + var col = new DataColumn(value.GetName(), value.GetDataType()) { Caption = value.GetCaption() }; Columns.Add(col); } } @@ -169,7 +169,7 @@ namespace TUGraz.VectoCore.Models.Simulation.Data [ModalResultField(typeof (double))] grad, /// <summary> - /// [-] Gear. "0" = clutch opened / neutral. "0.5" = lock-up clutch is open (AT with torque converter only, see + /// [-] GearData. "0" = clutch opened / neutral. "0.5" = lock-up clutch is open (AT with torque converter only, see /// Gearbox) /// </summary> [ModalResultField(typeof (double))] Gear, diff --git a/VectoCore/Models/SimulationComponent/Data/CombustionEngineData.cs b/VectoCore/Models/SimulationComponent/Data/CombustionEngineData.cs index c626636c854d43937b6d9783983fdc5117c3aa9c..f22cee58bdd9152a09c6a6717f668ff12687492e 100644 --- a/VectoCore/Models/SimulationComponent/Data/CombustionEngineData.cs +++ b/VectoCore/Models/SimulationComponent/Data/CombustionEngineData.cs @@ -169,7 +169,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data { var curve = _fullLoadCurves.FirstOrDefault(kv => kv.Key.Contains(gear)); if (curve.Key.Equals(null)) { - throw new KeyNotFoundException(string.Format("Gear '{0}' was not found in the FullLoadCurves.", gear)); + throw new KeyNotFoundException(string.Format("GearData '{0}' was not found in the FullLoadCurves.", gear)); } return curve.Value; @@ -235,7 +235,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data /// <summary> /// Multiple Full Load and Drag Curves (.vfld) can be defined and assigned to different gears. - /// Gear "0" must be assigned for idling and Engine Only Mode. + /// GearData "0" must be assigned for idling and Engine Only Mode. /// </summary> public class DataFullLoadCurve { diff --git a/VectoCore/Models/SimulationComponent/Data/DrivingCycleData.cs b/VectoCore/Models/SimulationComponent/Data/DrivingCycleData.cs index a1e4032d5a4a9070912bc18e1267e7841a2506b1..a61f1e7e71a59f5f50da2e87ad4b844963441757 100644 --- a/VectoCore/Models/SimulationComponent/Data/DrivingCycleData.cs +++ b/VectoCore/Models/SimulationComponent/Data/DrivingCycleData.cs @@ -8,421 +8,421 @@ using TUGraz.VectoCore.Utils; namespace TUGraz.VectoCore.Models.SimulationComponent.Data { - public class DrivingCycleData : SimulationComponentData - { - public enum CycleType - { - EngineOnly, - TimeBased, - DistanceBased - } - - public List<DrivingCycleEntry> Entries { get; set; } - - public static DrivingCycleData ReadFromFileEngineOnly(string fileName) - { - return ReadFromFile(fileName, CycleType.EngineOnly); - } - - public static DrivingCycleData ReadFromFileDistanceBased(string fileName) - { - return ReadFromFile(fileName, CycleType.DistanceBased); - } - - public static DrivingCycleData ReadFromFileTimeBased(string fileName) - { - return ReadFromFile(fileName, CycleType.TimeBased); - } - - public static DrivingCycleData ReadFromFile(string fileName, CycleType type) - { - var log = LogManager.GetLogger<DrivingCycleData>(); - - var parser = CreateDataParser(type); - var data = VectoCSVFile.Read(fileName); - var entries = parser.Parse(data).ToList(); - - log.Info(string.Format("Data loaded. Number of Entries: {0}", entries.Count)); - - var cycle = new DrivingCycleData { Entries = entries }; - return cycle; - } - - private static IDataParser CreateDataParser(CycleType type) - { - switch (type) { - case CycleType.EngineOnly: - return new EngineOnlyDataParser(); - case CycleType.TimeBased: - return new TimeBasedDataParser(); - case CycleType.DistanceBased: - return new DistanceBasedDataParser(); - default: - throw new ArgumentOutOfRangeException("type"); - } - } - - private static class Fields - { - /// <summary> - /// [m] Travelled distance used for distance-based cycles. If t is also defined this column will be ignored. - /// </summary> - public const string Distance = "s"; - - /// <summary> - /// [s] Used for time-based cycles. If neither this nor the distance s is defined the data will be interpreted as 1Hz. - /// </summary> - public const string Time = "t"; - - /// <summary> - /// [km/h] Required except for Engine Only Mode calculations. - /// </summary> - public const string VehicleSpeed = "v"; - - /// <summary> - /// [%] Optional. - /// </summary> - public const string RoadGradient = "grad"; - - /// <summary> - /// [s] Required for distance-based cycles. Not used in time based cycles. stop defines the time the vehicle spends in - /// stop phases. - /// </summary> - public const string StoppingTime = "stop"; - - /// <summary> - /// [kW] "Aux_xxx" Supply Power input for each auxiliary defined in the .vecto file , where xxx matches the ID of the - /// corresponding Auxiliary. ID's are not case sensitive and must not contain space or special characters. - /// </summary> - // todo: implement additional aux as dictionary! - public const string AuxiliarySupplyPower = "Aux_"; - - /// <summary> - /// [rpm] If n is defined VECTO uses that instead of the calculated engine speed value. - /// </summary> - public const string EngineSpeed = "n"; - - /// <summary> - /// [-] Gear input. Overwrites the gear shift model. - /// </summary> - public const string Gear = "gear"; - - /// <summary> - /// [kW] This power input will be directly added to the engine power in addition to possible other auxiliaries. Also - /// used in Engine Only Mode. - /// </summary> - public const string AdditionalAuxPowerDemand = "Padd"; - - /// <summary> - /// [km/h] Only required if Cross Wind Correction is set to Vair and Beta Input. - /// </summary> - public const string AirSpeedRelativeToVehicle = "vair_res"; - - /// <summary> - /// [°] Only required if Cross Wind Correction is set to Vair and Beta Input. - /// </summary> - public const string WindYawAngle = "vair_beta"; - - /// <summary> - /// [kW] Effective engine power at clutch. Only required in Engine Only Mode. Alternatively torque Me can be defined. - /// Use DRAG to define motoring operation. - /// </summary> - public const string EnginePower = "Pe"; - - /// <summary> - /// [Nm] Effective engine torque at clutch. Only required in Engine Only Mode. Alternatively power Pe can be defined. - /// Use DRAG to define motoring operation. - /// </summary> - public const string EngineTorque = "Me"; - } - - public class DrivingCycleEntry - { - /// <summary> - /// [m] Travelled distance used for distance-based cycles. If "t" - /// is also defined this column will be ignored. - /// </summary> - public double Distance { get; set; } - - /// <summary> - /// [s] Used for time-based cycles. If neither this nor the distance - /// "s" is defined the data will be interpreted as 1Hz. - /// </summary> - public double Time { get; set; } - - /// <summary> - /// [m/s] Required except for Engine Only Mode calculations. - /// </summary> - public MeterPerSecond VehicleSpeed { get; set; } - - /// <summary> - /// [%] Optional. - /// </summary> - public double RoadGradient { get; set; } - - /// <summary> - /// [s] Required for distance-based cycles. Not used in time based - /// cycles. "stop" defines the time the vehicle spends in stop phases. - /// </summary> - public double StoppingTime { get; set; } - - /// <summary> - /// [W] Supply Power input for each auxiliary defined in the - /// .vecto file where xxx matches the ID of the corresponding - /// Auxiliary. ID's are not case sensitive and must not contain - /// space or special characters. - /// </summary> - public Dictionary<string, Watt> AuxiliarySupplyPower { get; set; } - - /// <summary> - /// [rad/s] If "n" is defined VECTO uses that instead of the - /// calculated engine speed value. - /// </summary> - public PerSecond EngineSpeed { get; set; } - - /// <summary> - /// [-] Gear input. Overwrites the gear shift model. - /// </summary> - public double Gear { get; set; } - - /// <summary> - /// [W] This power input will be directly added to the engine - /// power in addition to possible other auxiliaries. Also used in - /// Engine Only Mode. - /// </summary> - public Watt AdditionalAuxPowerDemand { get; set; } - - /// <summary> - /// [m/s] Only required if Cross Wind Correction is set to Vair and Beta Input. - /// </summary> - public MeterPerSecond AirSpeedRelativeToVehicle { get; set; } - - /// <summary> - /// [°] Only required if Cross Wind Correction is set to Vair and Beta Input. - /// </summary> - public double WindYawAngle { get; set; } - - /// <summary> - /// [Nm] Effective engine torque at clutch. Only required in - /// Engine Only Mode. Alternatively power "Pe" can be defined. - /// Use "DRAG" to define motoring operation. - /// </summary> - public NewtonMeter EngineTorque { get; set; } - - public bool Drag { get; set; } - } - - #region DataParser - - private interface IDataParser - { - IEnumerable<DrivingCycleEntry> Parse(DataTable table); - } - - /// <summary> - /// Reader for Auxiliary Supply Power. - /// </summary> - private static class AuxSupplyPowerReader - { - /// <summary> - /// [W]. Reads Auxiliary Supply Power (defined by Fields.AuxiliarySupplyPower-Prefix). - /// </summary> - public static Dictionary<string, Watt> Read(DataRow row) - { - return row.Table.Columns.Cast<DataColumn>(). - Where(col => col.ColumnName.StartsWith(Fields.AuxiliarySupplyPower)). - ToDictionary(col => col.ColumnName.Substring(Fields.AuxiliarySupplyPower.Length - 1), - col => row.ParseDouble(col).SI().Kilo.Watt.Cast<Watt>()); - } - } - - private class DistanceBasedDataParser : IDataParser - { - public IEnumerable<DrivingCycleEntry> Parse(DataTable table) - { - ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); - - return table.Rows.Cast<DataRow>().Select(row => new DrivingCycleEntry { - Distance = row.ParseDouble(Fields.Distance), - VehicleSpeed = row.ParseDouble(Fields.VehicleSpeed).SI().Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), - RoadGradient = row.ParseDoubleOrGetDefault(Fields.RoadGradient), - AdditionalAuxPowerDemand = - row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), - EngineSpeed = - row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), - Gear = row.ParseDoubleOrGetDefault(Fields.Gear), - AirSpeedRelativeToVehicle = - row.ParseDoubleOrGetDefault(Fields.AirSpeedRelativeToVehicle) - .SI() - .Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), - WindYawAngle = row.ParseDoubleOrGetDefault(Fields.WindYawAngle), - AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) - }); - } - - private static void ValidateHeader(string[] header) - { - var allowedCols = new[] { - Fields.Distance, - Fields.VehicleSpeed, - Fields.RoadGradient, - Fields.StoppingTime, - Fields.EngineSpeed, - Fields.Gear, - Fields.AdditionalAuxPowerDemand, - Fields.AirSpeedRelativeToVehicle, - Fields.WindYawAngle - }; - - foreach ( - var col in - header.Where(col => !(allowedCols.Contains(col) || col.StartsWith(Fields.AuxiliarySupplyPower))) - ) { - throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); - } - - if (!header.Contains(Fields.VehicleSpeed)) { - throw new VectoException(string.Format("Column '{0}' is missing.", Fields.VehicleSpeed)); - } - - if (!header.Contains(Fields.Distance)) { - throw new VectoException(string.Format("Column '{0}' is missing.", Fields.Distance)); - } - - if (header.Contains(Fields.AirSpeedRelativeToVehicle) ^ header.Contains(Fields.WindYawAngle)) { - throw new VectoException( - string.Format("Both Columns '{0}' and '{1}' must be defined, or none of them.", - Fields.AirSpeedRelativeToVehicle, Fields.WindYawAngle)); - } - } - } - - private class TimeBasedDataParser : IDataParser - { - public IEnumerable<DrivingCycleEntry> Parse(DataTable table) - { - ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); - - var entries = table.Rows.Cast<DataRow>().Select((row, index) => new DrivingCycleEntry { - Time = row.ParseDoubleOrGetDefault(Fields.Time, index), - VehicleSpeed = row.ParseDouble(Fields.VehicleSpeed).SI().Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), - RoadGradient = row.ParseDoubleOrGetDefault(Fields.RoadGradient), - AdditionalAuxPowerDemand = - row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), - Gear = row.ParseDoubleOrGetDefault(Fields.Gear), - EngineSpeed = - row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), - AirSpeedRelativeToVehicle = - row.ParseDoubleOrGetDefault(Fields.AirSpeedRelativeToVehicle) - .SI() - .Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), - WindYawAngle = row.ParseDoubleOrGetDefault(Fields.WindYawAngle), - AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) - }).ToArray(); - - return entries; - } - - private static void ValidateHeader(string[] header) - { - var allowedCols = new[] { - Fields.Time, - Fields.VehicleSpeed, - Fields.RoadGradient, - Fields.EngineSpeed, - Fields.Gear, - Fields.AdditionalAuxPowerDemand, - Fields.AirSpeedRelativeToVehicle, - Fields.WindYawAngle - }; - - foreach ( - var col in - header.Where(col => !(allowedCols.Contains(col) || col.StartsWith(Fields.AuxiliarySupplyPower))) - ) { - throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); - } - - if (!header.Contains(Fields.VehicleSpeed)) { - throw new VectoException(string.Format("Column '{0}' is missing.", Fields.VehicleSpeed)); - } - - if (header.Contains(Fields.AirSpeedRelativeToVehicle) ^ header.Contains(Fields.WindYawAngle)) { - throw new VectoException( - string.Format("Both Columns '{0}' and '{1}' must be defined, or none of them.", - Fields.AirSpeedRelativeToVehicle, Fields.WindYawAngle)); - } - } - } - - private class EngineOnlyDataParser : IDataParser - { - public IEnumerable<DrivingCycleEntry> Parse(DataTable table) - { - ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); - var absTime = new TimeSpan(0, 0, 0); - - foreach (DataRow row in table.Rows) { - var entry = new DrivingCycleEntry { - EngineSpeed = - row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), - AdditionalAuxPowerDemand = - row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), - AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) - }; - if (row.Table.Columns.Contains(Fields.EngineTorque)) { - if (row.Field<string>(Fields.EngineTorque).Equals("<DRAG>")) { - entry.Drag = true; - } else { - entry.EngineTorque = row.ParseDouble(Fields.EngineTorque).SI<NewtonMeter>(); - } - } else { - if (row.Field<string>(Fields.EnginePower).Equals("<DRAG>")) { - entry.Drag = true; - } else { - entry.EngineTorque = - Formulas.PowerToTorque(row.ParseDouble(Fields.EnginePower).SI().Kilo.Watt.Cast<Watt>(), - entry.EngineSpeed); - } - } - entry.Time = absTime.TotalSeconds; - absTime += new TimeSpan(0, 0, 1); - - yield return entry; - } - } - - private static void ValidateHeader(string[] header) - { - var allowedCols = new[] { - Fields.EngineTorque, - Fields.EnginePower, - Fields.EngineSpeed, - Fields.AdditionalAuxPowerDemand - }; - - foreach (var col in header.Where(col => !allowedCols.Contains(col))) { - throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); - } - - if (!header.Contains(Fields.EngineSpeed)) { - throw new VectoException(string.Format("Column '{0}' is missing.", Fields.EngineSpeed)); - } - - if (!(header.Contains(Fields.EngineTorque) || header.Contains(Fields.EnginePower))) { - throw new VectoException( - string.Format("Columns missing: Either column '{0}' or column '{1}' must be defined.", - Fields.EngineTorque, Fields.EnginePower)); - } - - if (header.Contains(Fields.EngineTorque) && header.Contains(Fields.EnginePower)) { - LogManager.GetLogger<DrivingCycleData>() - .WarnFormat("Found column '{0}' and column '{1}': only column '{0}' will be used.", - Fields.EngineTorque, Fields.EnginePower); - } - } - } - - #endregion - } + public class DrivingCycleData : SimulationComponentData + { + public enum CycleType + { + EngineOnly, + TimeBased, + DistanceBased + } + + public List<DrivingCycleEntry> Entries { get; set; } + + public static DrivingCycleData ReadFromFileEngineOnly(string fileName) + { + return ReadFromFile(fileName, CycleType.EngineOnly); + } + + public static DrivingCycleData ReadFromFileDistanceBased(string fileName) + { + return ReadFromFile(fileName, CycleType.DistanceBased); + } + + public static DrivingCycleData ReadFromFileTimeBased(string fileName) + { + return ReadFromFile(fileName, CycleType.TimeBased); + } + + public static DrivingCycleData ReadFromFile(string fileName, CycleType type) + { + var log = LogManager.GetLogger<DrivingCycleData>(); + + var parser = CreateDataParser(type); + var data = VectoCSVFile.Read(fileName); + var entries = parser.Parse(data).ToList(); + + log.Info(string.Format("Data loaded. Number of Entries: {0}", entries.Count)); + + var cycle = new DrivingCycleData { Entries = entries }; + return cycle; + } + + private static IDataParser CreateDataParser(CycleType type) + { + switch (type) { + case CycleType.EngineOnly: + return new EngineOnlyDataParser(); + case CycleType.TimeBased: + return new TimeBasedDataParser(); + case CycleType.DistanceBased: + return new DistanceBasedDataParser(); + default: + throw new ArgumentOutOfRangeException("type"); + } + } + + private static class Fields + { + /// <summary> + /// [m] Travelled distance used for distance-based cycles. If t is also defined this column will be ignored. + /// </summary> + public const string Distance = "s"; + + /// <summary> + /// [s] Used for time-based cycles. If neither this nor the distance s is defined the data will be interpreted as 1Hz. + /// </summary> + public const string Time = "t"; + + /// <summary> + /// [km/h] Required except for Engine Only Mode calculations. + /// </summary> + public const string VehicleSpeed = "v"; + + /// <summary> + /// [%] Optional. + /// </summary> + public const string RoadGradient = "grad"; + + /// <summary> + /// [s] Required for distance-based cycles. Not used in time based cycles. stop defines the time the vehicle spends in + /// stop phases. + /// </summary> + public const string StoppingTime = "stop"; + + /// <summary> + /// [kW] "Aux_xxx" Supply Power input for each auxiliary defined in the .vecto file , where xxx matches the ID of the + /// corresponding Auxiliary. ID's are not case sensitive and must not contain space or special characters. + /// </summary> + // todo: implement additional aux as dictionary! + public const string AuxiliarySupplyPower = "Aux_"; + + /// <summary> + /// [rpm] If n is defined VECTO uses that instead of the calculated engine speed value. + /// </summary> + public const string EngineSpeed = "n"; + + /// <summary> + /// [-] GearData input. Overwrites the gear shift model. + /// </summary> + public const string Gear = "gear"; + + /// <summary> + /// [kW] This power input will be directly added to the engine power in addition to possible other auxiliaries. Also + /// used in Engine Only Mode. + /// </summary> + public const string AdditionalAuxPowerDemand = "Padd"; + + /// <summary> + /// [km/h] Only required if Cross Wind Correction is set to Vair and Beta Input. + /// </summary> + public const string AirSpeedRelativeToVehicle = "vair_res"; + + /// <summary> + /// [°] Only required if Cross Wind Correction is set to Vair and Beta Input. + /// </summary> + public const string WindYawAngle = "vair_beta"; + + /// <summary> + /// [kW] Effective engine power at clutch. Only required in Engine Only Mode. Alternatively torque Me can be defined. + /// Use DRAG to define motoring operation. + /// </summary> + public const string EnginePower = "Pe"; + + /// <summary> + /// [Nm] Effective engine torque at clutch. Only required in Engine Only Mode. Alternatively power Pe can be defined. + /// Use DRAG to define motoring operation. + /// </summary> + public const string EngineTorque = "Me"; + } + + public class DrivingCycleEntry + { + /// <summary> + /// [m] Travelled distance used for distance-based cycles. If "t" + /// is also defined this column will be ignored. + /// </summary> + public double Distance { get; set; } + + /// <summary> + /// [s] Used for time-based cycles. If neither this nor the distance + /// "s" is defined the data will be interpreted as 1Hz. + /// </summary> + public double Time { get; set; } + + /// <summary> + /// [m/s] Required except for Engine Only Mode calculations. + /// </summary> + public MeterPerSecond VehicleSpeed { get; set; } + + /// <summary> + /// [%] Optional. + /// </summary> + public double RoadGradient { get; set; } + + /// <summary> + /// [s] Required for distance-based cycles. Not used in time based + /// cycles. "stop" defines the time the vehicle spends in stop phases. + /// </summary> + public double StoppingTime { get; set; } + + /// <summary> + /// [W] Supply Power input for each auxiliary defined in the + /// .vecto file where xxx matches the ID of the corresponding + /// Auxiliary. ID's are not case sensitive and must not contain + /// space or special characters. + /// </summary> + public Dictionary<string, Watt> AuxiliarySupplyPower { get; set; } + + /// <summary> + /// [rad/s] If "n" is defined VECTO uses that instead of the + /// calculated engine speed value. + /// </summary> + public PerSecond EngineSpeed { get; set; } + + /// <summary> + /// [-] GearData input. Overwrites the gear shift model. + /// </summary> + public double Gear { get; set; } + + /// <summary> + /// [W] This power input will be directly added to the engine + /// power in addition to possible other auxiliaries. Also used in + /// Engine Only Mode. + /// </summary> + public Watt AdditionalAuxPowerDemand { get; set; } + + /// <summary> + /// [m/s] Only required if Cross Wind Correction is set to Vair and Beta Input. + /// </summary> + public MeterPerSecond AirSpeedRelativeToVehicle { get; set; } + + /// <summary> + /// [°] Only required if Cross Wind Correction is set to Vair and Beta Input. + /// </summary> + public double WindYawAngle { get; set; } + + /// <summary> + /// [Nm] Effective engine torque at clutch. Only required in + /// Engine Only Mode. Alternatively power "Pe" can be defined. + /// Use "DRAG" to define motoring operation. + /// </summary> + public NewtonMeter EngineTorque { get; set; } + + public bool Drag { get; set; } + } + + #region DataParser + + private interface IDataParser + { + IEnumerable<DrivingCycleEntry> Parse(DataTable table); + } + + /// <summary> + /// Reader for Auxiliary Supply Power. + /// </summary> + private static class AuxSupplyPowerReader + { + /// <summary> + /// [W]. Reads Auxiliary Supply Power (defined by Fields.AuxiliarySupplyPower-Prefix). + /// </summary> + public static Dictionary<string, Watt> Read(DataRow row) + { + return row.Table.Columns.Cast<DataColumn>(). + Where(col => col.ColumnName.StartsWith(Fields.AuxiliarySupplyPower)). + ToDictionary(col => col.ColumnName.Substring(Fields.AuxiliarySupplyPower.Length - 1), + col => row.ParseDouble(col).SI().Kilo.Watt.Cast<Watt>()); + } + } + + private class DistanceBasedDataParser : IDataParser + { + public IEnumerable<DrivingCycleEntry> Parse(DataTable table) + { + ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); + + return table.Rows.Cast<DataRow>().Select(row => new DrivingCycleEntry { + Distance = row.ParseDouble(Fields.Distance), + VehicleSpeed = row.ParseDouble(Fields.VehicleSpeed).SI().Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), + RoadGradient = row.ParseDoubleOrGetDefault(Fields.RoadGradient), + AdditionalAuxPowerDemand = + row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), + EngineSpeed = + row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), + Gear = row.ParseDoubleOrGetDefault(Fields.Gear), + AirSpeedRelativeToVehicle = + row.ParseDoubleOrGetDefault(Fields.AirSpeedRelativeToVehicle) + .SI() + .Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), + WindYawAngle = row.ParseDoubleOrGetDefault(Fields.WindYawAngle), + AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) + }); + } + + private static void ValidateHeader(string[] header) + { + var allowedCols = new[] { + Fields.Distance, + Fields.VehicleSpeed, + Fields.RoadGradient, + Fields.StoppingTime, + Fields.EngineSpeed, + Fields.Gear, + Fields.AdditionalAuxPowerDemand, + Fields.AirSpeedRelativeToVehicle, + Fields.WindYawAngle + }; + + foreach ( + var col in + header.Where(col => !(allowedCols.Contains(col) || col.StartsWith(Fields.AuxiliarySupplyPower))) + ) { + throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); + } + + if (!header.Contains(Fields.VehicleSpeed)) { + throw new VectoException(string.Format("Column '{0}' is missing.", Fields.VehicleSpeed)); + } + + if (!header.Contains(Fields.Distance)) { + throw new VectoException(string.Format("Column '{0}' is missing.", Fields.Distance)); + } + + if (header.Contains(Fields.AirSpeedRelativeToVehicle) ^ header.Contains(Fields.WindYawAngle)) { + throw new VectoException( + string.Format("Both Columns '{0}' and '{1}' must be defined, or none of them.", + Fields.AirSpeedRelativeToVehicle, Fields.WindYawAngle)); + } + } + } + + private class TimeBasedDataParser : IDataParser + { + public IEnumerable<DrivingCycleEntry> Parse(DataTable table) + { + ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); + + var entries = table.Rows.Cast<DataRow>().Select((row, index) => new DrivingCycleEntry { + Time = row.ParseDoubleOrGetDefault(Fields.Time, index), + VehicleSpeed = row.ParseDouble(Fields.VehicleSpeed).SI().Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), + RoadGradient = row.ParseDoubleOrGetDefault(Fields.RoadGradient), + AdditionalAuxPowerDemand = + row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), + Gear = row.ParseDoubleOrGetDefault(Fields.Gear), + EngineSpeed = + row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), + AirSpeedRelativeToVehicle = + row.ParseDoubleOrGetDefault(Fields.AirSpeedRelativeToVehicle) + .SI() + .Kilo.Meter.Per.Hour.Cast<MeterPerSecond>(), + WindYawAngle = row.ParseDoubleOrGetDefault(Fields.WindYawAngle), + AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) + }).ToArray(); + + return entries; + } + + private static void ValidateHeader(string[] header) + { + var allowedCols = new[] { + Fields.Time, + Fields.VehicleSpeed, + Fields.RoadGradient, + Fields.EngineSpeed, + Fields.Gear, + Fields.AdditionalAuxPowerDemand, + Fields.AirSpeedRelativeToVehicle, + Fields.WindYawAngle + }; + + foreach ( + var col in + header.Where(col => !(allowedCols.Contains(col) || col.StartsWith(Fields.AuxiliarySupplyPower))) + ) { + throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); + } + + if (!header.Contains(Fields.VehicleSpeed)) { + throw new VectoException(string.Format("Column '{0}' is missing.", Fields.VehicleSpeed)); + } + + if (header.Contains(Fields.AirSpeedRelativeToVehicle) ^ header.Contains(Fields.WindYawAngle)) { + throw new VectoException( + string.Format("Both Columns '{0}' and '{1}' must be defined, or none of them.", + Fields.AirSpeedRelativeToVehicle, Fields.WindYawAngle)); + } + } + } + + private class EngineOnlyDataParser : IDataParser + { + public IEnumerable<DrivingCycleEntry> Parse(DataTable table) + { + ValidateHeader(table.Columns.Cast<DataColumn>().Select(col => col.ColumnName).ToArray()); + var absTime = new TimeSpan(0, 0, 0); + + foreach (DataRow row in table.Rows) { + var entry = new DrivingCycleEntry { + EngineSpeed = + row.ParseDoubleOrGetDefault(Fields.EngineSpeed).SI().Rounds.Per.Minute.Cast<PerSecond>(), + AdditionalAuxPowerDemand = + row.ParseDoubleOrGetDefault(Fields.AdditionalAuxPowerDemand).SI().Kilo.Watt.Cast<Watt>(), + AuxiliarySupplyPower = AuxSupplyPowerReader.Read(row) + }; + if (row.Table.Columns.Contains(Fields.EngineTorque)) { + if (row.Field<string>(Fields.EngineTorque).Equals("<DRAG>")) { + entry.Drag = true; + } else { + entry.EngineTorque = row.ParseDouble(Fields.EngineTorque).SI<NewtonMeter>(); + } + } else { + if (row.Field<string>(Fields.EnginePower).Equals("<DRAG>")) { + entry.Drag = true; + } else { + entry.EngineTorque = + Formulas.PowerToTorque(row.ParseDouble(Fields.EnginePower).SI().Kilo.Watt.Cast<Watt>(), + entry.EngineSpeed); + } + } + entry.Time = absTime.TotalSeconds; + absTime += new TimeSpan(0, 0, 1); + + yield return entry; + } + } + + private static void ValidateHeader(string[] header) + { + var allowedCols = new[] { + Fields.EngineTorque, + Fields.EnginePower, + Fields.EngineSpeed, + Fields.AdditionalAuxPowerDemand + }; + + foreach (var col in header.Where(col => !allowedCols.Contains(col))) { + throw new VectoException(string.Format("Column '{0}' is not allowed.", col)); + } + + if (!header.Contains(Fields.EngineSpeed)) { + throw new VectoException(string.Format("Column '{0}' is missing.", Fields.EngineSpeed)); + } + + if (!(header.Contains(Fields.EngineTorque) || header.Contains(Fields.EnginePower))) { + throw new VectoException( + string.Format("Columns missing: Either column '{0}' or column '{1}' must be defined.", + Fields.EngineTorque, Fields.EnginePower)); + } + + if (header.Contains(Fields.EngineTorque) && header.Contains(Fields.EnginePower)) { + LogManager.GetLogger<DrivingCycleData>() + .WarnFormat("Found column '{0}' and column '{1}': only column '{0}' will be used.", + Fields.EngineTorque, Fields.EnginePower); + } + } + } + + #endregion + } } \ No newline at end of file diff --git a/VectoCore/Models/SimulationComponent/Data/Gearbox/Gear.cs b/VectoCore/Models/SimulationComponent/Data/Gearbox/GearData.cs similarity index 70% rename from VectoCore/Models/SimulationComponent/Data/Gearbox/Gear.cs rename to VectoCore/Models/SimulationComponent/Data/Gearbox/GearData.cs index 5423929ec048e8ab6dc3ad865f05d869827cf5c9..87ef6a9b307b60ee0774e1408d28f8318269ef11 100644 --- a/VectoCore/Models/SimulationComponent/Data/Gearbox/Gear.cs +++ b/VectoCore/Models/SimulationComponent/Data/Gearbox/GearData.cs @@ -1,6 +1,6 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Gearbox { - public class Gear + public class GearData { public ShiftPolygon ShiftPolygon { get; protected set; } @@ -10,7 +10,10 @@ public bool TorqueConverterActive { get; protected set; } // TODO: think about refactoring... - public Gear(TransmissionLossMap lossMap, Gearbox.ShiftPolygon shiftPolygon, double ratio, bool torqueconverterActive) + public double AverageEfficiency { get; set; } + + public GearData(TransmissionLossMap lossMap, Gearbox.ShiftPolygon shiftPolygon, double ratio, + bool torqueconverterActive) { LossMap = lossMap; ShiftPolygon = shiftPolygon; diff --git a/VectoCore/Models/SimulationComponent/Data/Gearbox/TransmissionLossMap.cs b/VectoCore/Models/SimulationComponent/Data/Gearbox/TransmissionLossMap.cs index e47d9abe27ac36db3b2e355698964d9f7e26f9fc..c0d1f11fc4040faf069d9e5ecbfee9fa0021443d 100644 --- a/VectoCore/Models/SimulationComponent/Data/Gearbox/TransmissionLossMap.cs +++ b/VectoCore/Models/SimulationComponent/Data/Gearbox/TransmissionLossMap.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Runtime.InteropServices; using System.Runtime.Remoting.Messaging; using Common.Logging; using Newtonsoft.Json; @@ -12,7 +13,10 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Gearbox { public class TransmissionLossMap { - [JsonProperty] private List<GearLossMapEntry> _entries; + [JsonProperty] private readonly List<GearLossMapEntry> _entries; + + private readonly DelauneyMap _lossMap; // Input Speed, Output Torque (to Wheels) => Input Torque (Engine) + private readonly DelauneyMap _reverseLossMap; // Input Speed, Input Torque (Engine) => Output Torque (Wheels) public static TransmissionLossMap ReadFromFile(string fileName) { @@ -39,7 +43,8 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Gearbox entries = CreateFromColumIndizes(data); } - return new TransmissionLossMap { _entries = entries }; + + return new TransmissionLossMap(entries); } private static bool HeaderIsValid(DataColumnCollection columns) @@ -75,6 +80,45 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Gearbox }).ToList(); } + private TransmissionLossMap(List<GearLossMapEntry> entries) + { + _entries = entries; + _lossMap = new DelauneyMap(); + _reverseLossMap = new DelauneyMap(); + foreach (var entry in _entries) { + _lossMap.AddPoint(entry.InputSpeed.Double(), entry.InputTorque.Double() - entry.TorqueLoss.Double(), + entry.InputTorque.Double()); + _reverseLossMap.AddPoint(entry.InputSpeed.Double(), entry.InputTorque.Double(), + entry.InputTorque.Double() - entry.TorqueLoss.Double()); + } + _lossMap.Triangulate(); + _reverseLossMap.Triangulate(); + } + + /// <summary> + /// Compute the required torque at the input of the gearbox (from engine) + /// </summary> + /// <param name="angularVelocity">[1/s] angular speed of the shaft</param> + /// <param name="gbxOutTorque">[Nm] torque requested by the previous componend (towards the wheels)</param> + /// <returns>[Nm] torque requested from the next component (towards the engine)</returns> + public NewtonMeter GearboxInTorque(PerSecond angularVelocity, NewtonMeter gbxOutTorque) + { + // TODO: extrapolate! + return _lossMap.Interpolate(angularVelocity.Double(), gbxOutTorque.Double()).SI<NewtonMeter>(); + } + + /// <summary> + /// Compute the available torque at the output of the gearbox (towards wheels) + /// </summary> + /// <param name="angularVelocity">[1/s] angular speed of the shaft</param> + /// <param name="gbxInTorque">[Nm] torque provided by the engine at the gearbox' input shaft</param> + /// <returns>[Nm] torque provided to the next component (towards the wheels)</returns> + public NewtonMeter GearboxOutTorque(PerSecond angularVelocity, NewtonMeter gbxInTorque) + { + // TODO extrapolate! + return _reverseLossMap.Interpolate(angularVelocity.Double(), gbxInTorque.Double()).SI<NewtonMeter>(); + } + public GearLossMapEntry this[int i] { get { return _entries[i]; } diff --git a/VectoCore/Models/SimulationComponent/Data/GearboxData.cs b/VectoCore/Models/SimulationComponent/Data/GearboxData.cs index 6f5aad8f43687dac2422c9188ae822b1d98a5b49..91ffee3d2124b7227d05883454e459f3a12f6a2b 100644 --- a/VectoCore/Models/SimulationComponent/Data/GearboxData.cs +++ b/VectoCore/Models/SimulationComponent/Data/GearboxData.cs @@ -46,7 +46,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data /// }, /// { /// "Ratio": 6.38, - /// "LossMap": "Indirect Gear.vtlm", + /// "LossMap": "Indirect GearData.vtlm", /// "TCactive": false, /// "ShiftPolygon": "ShiftPolygon.vgbs" /// }, @@ -65,9 +65,13 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data } [DataMember] - public Gear AxleGear { get; protected set; } + public GearData AxleGearData { get; protected set; } - [DataMember] private Dictionary<int, Gear> _gearData = new Dictionary<int, Gear>(); + //private double _axleGearEfficiency; + + [DataMember] private Dictionary<uint, GearData> _gearData = new Dictionary<uint, GearData>(); + + //private Dictionary<uint, double> _gearEfficiency = new Dictionary<uint, double>(); [DataMember] private Data _data; @@ -80,6 +84,8 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data public static GearboxData ReadFromJson(string json, string basePath = "") { + var lossMaps = new Dictionary<string, TransmissionLossMap>(); + var gearboxData = new GearboxData(); var d = JsonConvert.DeserializeObject<Data>(json); @@ -89,16 +95,24 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data gearboxData._data = d; - for (var i = 0; i < d.Body.Gears.Count; i++) { - //foreach (var gearSettings in d.Body.Gears) { - var gearSettings = d.Body.Gears[i]; - var lossMap = TransmissionLossMap.ReadFromFile(Path.Combine(basePath, gearSettings.LossMap)); + for (uint i = 0; i < d.Body.Gears.Count; i++) { + var gearSettings = d.Body.Gears[(int) i]; + var lossMapPath = Path.Combine(basePath, gearSettings.LossMap); + TransmissionLossMap lossMap; + if (lossMaps.ContainsKey(lossMapPath)) { + lossMap = lossMaps[lossMapPath]; + lossMaps.Add(lossMapPath, lossMap); + } else { + lossMap = TransmissionLossMap.ReadFromFile(lossMapPath); + } + var shiftPolygon = !String.IsNullOrEmpty(gearSettings.ShiftPolygon) ? ShiftPolygon.ReadFromFile(Path.Combine(basePath, gearSettings.ShiftPolygon)) : null; - var gear = new Gear(lossMap, shiftPolygon, gearSettings.Ratio, gearSettings.TCactive); + + var gear = new GearData(lossMap, shiftPolygon, gearSettings.Ratio, gearSettings.TCactive); if (i == 0) { - gearboxData.AxleGear = gear; + gearboxData.AxleGearData = gear; } else { gearboxData._gearData.Add(i, gear); } @@ -121,12 +135,62 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data return gearboxData; } + + public void CalculateAverageEfficiency(CombustionEngineData engineData) + { + var angularVelocityStep = (2.0 / 3.0) * (engineData.GetFullLoadCurve(0).RatedSpeed() - engineData.IdleSpeed) / 10.0; + + var axleGearEfficiencySum = 0.0; + var axleGearSumCount = 0; + + foreach (var gearEntry in _gearData) { + var gearEfficiencySum = 0.0; + var gearSumCount = 0; + for (var angularVelocity = engineData.IdleSpeed + angularVelocityStep; + angularVelocity < engineData.GetFullLoadCurve(0).RatedSpeed(); + angularVelocity += angularVelocityStep) { + var fullLoadStationaryTorque = engineData.GetFullLoadCurve(gearEntry.Key).FullLoadStationaryTorque(angularVelocity); + var torqueStep = (2.0 / 3.0) * fullLoadStationaryTorque / 10.0; + for (var engineOutTorque = (1.0 / 3.0) * fullLoadStationaryTorque; + engineOutTorque < fullLoadStationaryTorque; + engineOutTorque += torqueStep) { + var engineOutPower = Formulas.TorqueToPower(engineOutTorque, angularVelocity); + var gearboxOutPower = + Formulas.TorqueToPower( + gearEntry.Value.LossMap.GearboxOutTorque(angularVelocity, engineOutTorque), angularVelocity); + if (gearboxOutPower > engineOutPower) { + gearboxOutPower = engineOutPower; + } + + gearEfficiencySum += ((engineOutPower - gearboxOutPower) / engineOutPower).Double(); + gearSumCount += 1; + + + // axle gear + var angularVelocityAxleGear = angularVelocity / gearEntry.Value.Ratio; + var axlegearOutPower = + Formulas.TorqueToPower( + AxleGearData.LossMap.GearboxOutTorque(angularVelocityAxleGear, + Formulas.PowerToTorque(engineOutPower, angularVelocityAxleGear)), + angularVelocityAxleGear); + if (axlegearOutPower > engineOutPower) { + axlegearOutPower = engineOutPower; + } + axleGearEfficiencySum += (axlegearOutPower / engineOutPower).Double(); + axleGearSumCount += 1; + } + } + gearEntry.Value.AverageEfficiency = gearEfficiencySum / gearSumCount; + } + AxleGearData.AverageEfficiency = axleGearEfficiencySum / axleGearSumCount; + } + public int GearsCount() { return _data.Body.Gears.Count; } - public Gear this[int i] + public GearData this[uint i] { get { return _gearData[i]; } } diff --git a/VectoCore/Models/SimulationComponent/Impl/AxleGear.cs b/VectoCore/Models/SimulationComponent/Impl/AxleGear.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5cd77fcf4ecfc591658b63dc569b952a23cc500 --- /dev/null +++ b/VectoCore/Models/SimulationComponent/Impl/AxleGear.cs @@ -0,0 +1,38 @@ +using System; +using TUGraz.VectoCore.Models.Connector.Ports; +using TUGraz.VectoCore.Models.SimulationComponent.Data.Gearbox; +using TUGraz.VectoCore.Utils; + +namespace TUGraz.VectoCore.Models.SimulationComponent.Impl +{ + public class AxleGear : IPowerTrainComponent, ITnInPort, ITnOutPort + { + private ITnOutPort _nextComponent; + private GearData _gearDataData; + + public AxleGear(GearData gearDataData) + { + _gearDataData = gearDataData; + } + + public ITnInPort InShaft() + { + return this; + } + + public ITnOutPort OutShaft() + { + return this; + } + + public void Connect(ITnOutPort other) + { + _nextComponent = other; + } + + public IResponse Request(TimeSpan absTime, TimeSpan dt, NewtonMeter torque, PerSecond angularVelocity) + { + return _nextComponent.Request(absTime, dt, torque, angularVelocity); + } + } +} \ No newline at end of file diff --git a/VectoCore/VectoCore.csproj b/VectoCore/VectoCore.csproj index ffbb6943027bce5d5bf687975ee9aed8674c3957..a7ee6a040fb0086e19f0ebaf550df80f20ffea34 100644 --- a/VectoCore/VectoCore.csproj +++ b/VectoCore/VectoCore.csproj @@ -123,7 +123,7 @@ <Compile Include="Models\SimulationComponent\Data\Engine\FuelConsumptionMap.cs" /> <Compile Include="Models\SimulationComponent\Data\Engine\FullLoadCurve.cs" /> <Compile Include="Models\SimulationComponent\Data\GearboxData.cs" /> - <Compile Include="Models\SimulationComponent\Data\Gearbox\Gear.cs" /> + <Compile Include="Models\SimulationComponent\Data\Gearbox\GearData.cs" /> <Compile Include="Models\SimulationComponent\Data\Gearbox\TransmissionLossMap.cs" /> <Compile Include="Models\SimulationComponent\Data\Gearbox\ShiftPolygon.cs" /> <Compile Include="Models\SimulationComponent\Data\Gearbox\TorqueConverterData.cs" /> @@ -132,6 +132,7 @@ <Compile Include="Models\SimulationComponent\IClutch.cs" /> <Compile Include="Models\SimulationComponent\IEngineOnlyDrivingCycle.cs" /> <Compile Include="Models\SimulationComponent\IDriverDemandDrivingCycle.cs" /> + <Compile Include="Models\SimulationComponent\Impl\AxleGear.cs" /> <Compile Include="Models\SimulationComponent\Impl\Clutch.cs" /> <Compile Include="Models\SimulationComponent\Impl\Retarder.cs" /> <Compile Include="Models\SimulationComponent\IPowerTrainComponent.cs" /> diff --git a/VectoCoreTest/Models/SimulationComponentData/GearboxDataTest.cs b/VectoCoreTest/Models/SimulationComponentData/GearboxDataTest.cs index 00e2133e57c42cf0b03b92b5a9fe0e26235126b3..68186eb953ed2a45a17ded03d599bb7ae0e1fdeb 100644 --- a/VectoCoreTest/Models/SimulationComponentData/GearboxDataTest.cs +++ b/VectoCoreTest/Models/SimulationComponentData/GearboxDataTest.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; using TUGraz.VectoCore.Models.SimulationComponent.Data; using TUGraz.VectoCore.Utils; @@ -18,7 +19,7 @@ namespace TUGraz.VectoCore.Tests.Models.SimulationComponentData Assert.AreEqual(1.0, gbxData.TractionInterruption.Double(), 0.0001); Assert.AreEqual(9, gbxData.GearsCount()); - Assert.AreEqual(3.240355, gbxData.AxleGear.Ratio, 0.0001); + Assert.AreEqual(3.240355, gbxData.AxleGearData.Ratio, 0.0001); Assert.AreEqual(1.0, gbxData[7].Ratio, 0.0001); Assert.AreEqual(-400, gbxData[1].ShiftPolygon[0].Torque.Double(), 0.0001); @@ -29,5 +30,24 @@ namespace TUGraz.VectoCore.Tests.Models.SimulationComponentData Assert.AreEqual(-350, gbxData[1].LossMap[15].InputTorque.Double(), 0.0001); Assert.AreEqual(13.072, gbxData[1].LossMap[15].TorqueLoss.Double(), 0.0001); } + + [TestMethod] + public void TestInterpolation() + { + var gbxData = GearboxData.ReadFromFile(GearboxFile); + + var v = 11.72958; + var rdyn = 520; + var angSpeed = ((60 * v) / (2 * rdyn * Math.PI / 1000) * gbxData.AxleGearData.Ratio).SI<PerSecond>(); + var PvD = 169640.7.SI<Watt>(); + + var torqueToWheels = Formulas.PowerToTorque(PvD, angSpeed); + var torqueFromEngine = gbxData.AxleGearData.LossMap.GearboxOutTorque(angSpeed, torqueToWheels); + + var powerEngine = Formulas.TorqueToPower(torqueFromEngine, angSpeed); + var loss = powerEngine - PvD; + + Assert.AreEqual(5551.5799, loss.Double(), 0.0001); + } } } \ No newline at end of file