Code development platform for open source projects from the European Union institutions

Skip to content
Snippets Groups Projects
Commit 2bd72f2a authored by Markus Quaritsch's avatar Markus Quaritsch
Browse files

implementing first idea of shift polygons for PEV

parent 0ee22d95
No related branches found
No related tags found
No related merge requests found
......@@ -165,6 +165,8 @@ namespace TUGraz.VectoCommon.Models
public NewtonMeter TorqueRequest { get; set; }
public NewtonMeter InertiaTorque { get; set; }
public PerSecond AvgDrivetrainSpeed { get; set; }
public NewtonMeter MaxDriveTorqueEM { get; set; }
public NewtonMeter TorqueRequestEmMap { get; set; }
}
......
......@@ -601,9 +601,9 @@ namespace TUGraz.VectoCore.Models.Declaration
switch (type)
{
case GearboxType.AMT:
// TODO MQ: 2020-10-14: compute for AMT with ICE and AMT with EM differently
//return ComputeEfficiencyShiftPolygon(gearIdx, fullLoadCurve, gears, engine, axlegearRatio, dynamicTyreRadius);
case GearboxType.MT:
// TODO MQ: 2020-10-14: compute for AMT with ICE and AMT with EM differently
return ComputeEfficiencyShiftPolygon(gearIdx, fullLoadCurve, gears, engine, axlegearRatio, dynamicTyreRadius);
case GearboxType.MT:
return ComputeManualTransmissionShiftPolygon(
gearIdx, fullLoadCurve, gears, engine, axlegearRatio, dynamicTyreRadius);
case GearboxType.ATSerial:
......@@ -631,19 +631,117 @@ namespace TUGraz.VectoCore.Models.Declaration
var downShift = new List<ShiftPolygon.ShiftPolygonEntry>();
var upShift = new List<ShiftPolygon.ShiftPolygonEntry>();
if (gearIdx > 0) {
downShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxGenerationTorque * emRatio * 1.1, 0.RPMtoRad()));
downShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxDriveTorque * emRatio * 1.1, 0.RPMtoRad()));
var nMax = fullLoadCurve.NP80low;
var nMin = 0.1 * fullLoadCurve.RatedSpeed;
downShift.AddRange(DownshiftLineDrive(fullLoadCurve, nMin, nMax));
downShift.AddRange(DownshiftLineDrag(fullLoadCurve, nMin, nMax));
}
if (gearIdx >= gears.Count - 1) {
return new ShiftPolygon(downShift, upShift);
}
upShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxGenerationTorque * emRatio * 1.1, fullLoadCurve.MaxSpeed / emRatio * 0.9));
upShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxDriveTorque * emRatio * 1.1, fullLoadCurve.MaxSpeed / emRatio * 0.9));
upShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxGenerationTorque * 1.1, fullLoadCurve.MaxSpeed * 0.9));
upShift.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxDriveTorque * 1.1, fullLoadCurve.MaxSpeed * 0.9));
return new ShiftPolygon(downShift, upShift);
}
private static List<ShiftPolygon.ShiftPolygonEntry> DownshiftLineDrive(ElectricMotorFullLoadCurve fullLoadCurve, PerSecond nMin, PerSecond nMax)
{
var retVal = new List<ShiftPolygon.ShiftPolygonEntry>();
var downShiftPoints = fullLoadCurve
.FullLoadEntries.Where(fldEntry => fldEntry.MotorSpeed >= nMin && fldEntry.MotorSpeed <= nMax)
.Select(
fldEntry =>
new Point(fldEntry.MotorSpeed.Value(), fldEntry.FullDriveTorque.Value() * ShiftPolygonEngineFldMargin))
.ToList();
//retVal.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxDriveTorque * 1.1, nMax));
if (downShiftPoints.Count == 0) {
// coarse grid points in FLD
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.MaxDriveTorque * 1.1,
nMax));
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullLoadDriveTorque(nMax) * ShiftPolygonEngineFldMargin,
nMax));
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullLoadDriveTorque(nMin) * ShiftPolygonEngineFldMargin,
nMin));
} else {
if (downShiftPoints.Min(x => x.X) > nMin) {
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullLoadDriveTorque(nMin) * ShiftPolygonEngineFldMargin,
nMax));
}
retVal.AddRange(
downShiftPoints.Select(
x => new ShiftPolygon.ShiftPolygonEntry(
x.Y.SI<NewtonMeter>() * ShiftPolygonEngineFldMargin, x.X.SI<PerSecond>())));
if (downShiftPoints.Max(x => x.X) < nMax) {
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullLoadDriveTorque(nMax) * ShiftPolygonEngineFldMargin,
nMax));
}
}
return retVal;
}
private static List<ShiftPolygon.ShiftPolygonEntry> DownshiftLineDrag(ElectricMotorFullLoadCurve fullLoadCurve, PerSecond nMin, PerSecond nMax)
{
var retVal = new List<ShiftPolygon.ShiftPolygonEntry>();
var downShiftPoints = fullLoadCurve
.FullLoadEntries.Where(fldEntry => fldEntry.MotorSpeed >= nMin && fldEntry.MotorSpeed <= nMax)
.Select(
fldEntry =>
new Point(fldEntry.MotorSpeed.Value(), fldEntry.FullGenerationTorque.Value() * ShiftPolygonEngineFldMargin))
.ToList();
//retVal.Add(new ShiftPolygon.ShiftPolygonEntry(fullLoadCurve.MaxGenerationTorque * 1.1, nMax));
if (downShiftPoints.Count == 0) {
// coarse grid points in FLD
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullGenerationTorque(nMin) * ShiftPolygonEngineFldMargin,
nMin));
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullGenerationTorque(nMax) * ShiftPolygonEngineFldMargin,
nMax));
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.MaxGenerationTorque * 1.1,
nMax));
} else {
if (downShiftPoints.Min(x => x.X) > nMin) {
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullGenerationTorque(nMin) * ShiftPolygonEngineFldMargin,
nMax));
}
retVal.AddRange(
downShiftPoints.Select(
x => new ShiftPolygon.ShiftPolygonEntry(
x.Y.SI<NewtonMeter>() * ShiftPolygonEngineFldMargin, x.X.SI<PerSecond>())));
if (downShiftPoints.Max(x => x.X) < nMax) {
retVal.Add(
new ShiftPolygon.ShiftPolygonEntry(
fullLoadCurve.FullGenerationTorque(nMax) * ShiftPolygonEngineFldMargin,
nMax));
}
}
return retVal;
}
public static ShiftPolygon ComputeEfficiencyShiftPolygon(
int gearIdx, EngineFullLoadCurve fullLoadCurve, IList<ITransmissionInputData> gears, CombustionEngineData engine,
double axlegearRatio, Meter dynamicTyreRadius)
......
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using TUGraz.VectoCommon.Exceptions;
using TUGraz.VectoCommon.Models;
using TUGraz.VectoCommon.Utils;
using TUGraz.VectoCore.Models.SimulationComponent.Data.Engine;
namespace TUGraz.VectoCore.Models.SimulationComponent.Data.ElectricMotor {
public class ElectricMotorFullLoadCurve
public class ElectricMotorFullLoadCurve : SimulationComponentData
{
internal readonly List<FullLoadEntry> FullLoadEntries;
private NewtonMeter _maxDriveTorque;
private NewtonMeter _maxGenerationTorque;
private PerSecond _maxSpeed;
private PerSecond _nP80Low;
private Watt _maxPower;
private PerSecond _ratedSpeed;
internal ElectricMotorFullLoadCurve(List<FullLoadEntry> entries)
......@@ -91,6 +97,123 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.ElectricMotor {
get { return FullLoadEntries.Select(x => $"{x.MotorSpeed.AsRPM} {x.FullDriveTorque} {x.FullGenerationTorque}").ToArray(); }
}
public PerSecond NP80low => _nP80Low ?? (_nP80Low = ComputeNP80LowSpeed());
public Watt MaxPower => _maxPower ?? (_maxPower = ComputeMaxPower().Item2);
public PerSecond RatedSpeed => _ratedSpeed ?? (_ratedSpeed = ComputeMaxPower().Item1);
private Tuple<PerSecond, Watt> ComputeMaxPower()
{
var max = new Tuple<PerSecond, Watt>(0.SI<PerSecond>(), 0.SI<Watt>());
for (var idx = 1; idx < FullLoadEntries.Count; idx++) {
var currentMax = FindMaxPower(FullLoadEntries[idx - 1], FullLoadEntries[idx]);
if (currentMax.Item2 > max.Item2) {
max = currentMax;
}
}
return max;
}
private Tuple<PerSecond, Watt> FindMaxPower(FullLoadEntry p1, FullLoadEntry p2)
{
if (p1.MotorSpeed.IsEqual(p2.MotorSpeed)) {
return Tuple.Create(p1.MotorSpeed, p1.FullDriveTorque * p1.MotorSpeed);
}
if (p2.MotorSpeed < p1.MotorSpeed) {
var tmp = p1;
p1 = p2;
p2 = tmp;
}
// y = kx + d
var k = (-p2.FullDriveTorque + p1.FullDriveTorque) / (p2.MotorSpeed - p1.MotorSpeed);
var d = -p2.FullDriveTorque - k * p2.MotorSpeed;
if (k.IsEqual(0)) {
return Tuple.Create(p2.MotorSpeed, -p2.FullDriveTorque * p2.MotorSpeed);
}
var engineSpeedMaxPower = -d / (2 * k);
if (engineSpeedMaxPower.IsSmaller(p1.MotorSpeed) || engineSpeedMaxPower.IsGreater(p2.MotorSpeed)) {
if (-p2.FullDriveTorque * p2.MotorSpeed > -p1.FullDriveTorque * p1.MotorSpeed) {
return Tuple.Create(p2.MotorSpeed, -p2.FullDriveTorque * p2.MotorSpeed);
}
return Tuple.Create(p1.MotorSpeed, -p1.FullDriveTorque * p1.MotorSpeed);
}
var engineTorqueMaxPower = FullLoadDriveTorque(engineSpeedMaxPower);
return Tuple.Create(engineSpeedMaxPower, -engineTorqueMaxPower * engineSpeedMaxPower);
}
private PerSecond ComputeNP80LowSpeed()
{
var retVal = FindEngineSpeedForPower(0.8 * MaxPower).First();
return retVal;
}
private List<PerSecond> FindEngineSpeedForPower(Watt power)
{
var retVal = new List<PerSecond>();
for (var idx = 1; idx < FullLoadEntries.Count; idx++) {
var solutions = FindEngineSpeedForPower(FullLoadEntries[idx - 1], FullLoadEntries[idx], power);
retVal.AddRange(solutions);
}
retVal.Sort();
return retVal.Distinct(new SI.EqualityComparer<PerSecond>()).ToList();
}
private IEnumerable<PerSecond> FindEngineSpeedForPower(FullLoadEntry p1, FullLoadEntry p2, Watt power)
{
var k = (-p2.FullDriveTorque + p1.FullDriveTorque) / (p2.MotorSpeed - p1.MotorSpeed);
var d = -p2.FullDriveTorque - k * p2.MotorSpeed;
if (k.IsEqual(0, 0.0001)) {
// constant torque: solve linear equation
// power = M * n_eng_avg
if (d.IsEqual(0, 0.0001)) {
return new List<PerSecond>();
}
return FilterSolutions((power / d).ToEnumerable(), p1, p2);
}
// non-constant torque: solve quadratic equation for engine speed (n_eng_avg)
// power = M(n_eng_avg) * n_eng_avg = (k * n_eng_avg + d) * n_eng_avg = k * n_eng_avg^2 + d * n_eng_avg
var retVal = VectoMath.QuadraticEquationSolver(k.Value(), d.Value(), -power.Value());
if (retVal.Length == 0) {
Log.Info("No real solution found for requested power demand: P: {0}, p1: {1}, p2: {2}", power, p1, p2);
}
return FilterSolutions(retVal.Select(x => Math.Round(x, 6).SI<PerSecond>()), p1, p2);
}
private IEnumerable<PerSecond> FilterSolutions(IEnumerable<PerSecond> solutions, FullLoadEntry p1, FullLoadEntry p2)
{
return solutions.Where(
x => x.IsGreaterOrEqual(p1.MotorSpeed.Value()) && x.IsSmallerOrEqual(p2.MotorSpeed.Value()));
}
protected internal Watt ComputeArea(PerSecond lowEngineSpeed, PerSecond highEngineSpeed)
{
var startSegment = FindIndex(lowEngineSpeed);
var endSegment = FindIndex(highEngineSpeed);
var area = 0.SI<Watt>();
if (lowEngineSpeed < FullLoadEntries[startSegment].MotorSpeed) {
// add part of the first segment
area += (FullLoadEntries[startSegment].MotorSpeed - lowEngineSpeed) *
(FullLoadDriveTorque(lowEngineSpeed) + FullLoadEntries[startSegment].FullDriveTorque) / 2.0;
}
for (var i = startSegment + 1; i <= endSegment; i++) {
var speedHigh = FullLoadEntries[i].MotorSpeed;
var torqueHigh = FullLoadEntries[i].FullDriveTorque;
if (highEngineSpeed < FullLoadEntries[i].MotorSpeed) {
// add part of the last segment
speedHigh = highEngineSpeed;
torqueHigh = FullLoadDriveTorque(highEngineSpeed);
}
area += (speedHigh - FullLoadEntries[i - 1].MotorSpeed) * (torqueHigh + FullLoadEntries[i - 1].FullDriveTorque) /
2.0;
}
return area;
}
}
}
\ No newline at end of file
......@@ -339,11 +339,13 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Impl
}
retVal.ElectricMotor.MaxDriveTorque = maxDriveTorqueDt;
retVal.ElectricMotor.MaxDriveTorqueEM = maxDriveTorqueEm;
retVal.ElectricMotor.MaxRecuperationTorque = maxRecuperationTorqueDt;
retVal.ElectricMotor.AngularVelocity = avgEmSpeed;
retVal.ElectricMotor.AvgDrivetrainSpeed = avgDtSpeed;
retVal.ElectricMotor.TorqueRequest = outTorque;
retVal.ElectricMotor.TorqueRequestEmMap = emTorqueMap;
retVal.ElectricMotor.InertiaTorque =
avgDtSpeed.IsEqual(0) ? 0.SI<NewtonMeter>() : inertiaTorqueEm * avgEmSpeed / avgDtSpeed;
......
......@@ -433,7 +433,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Impl
NewtonMeter inTorque, PerSecond inAngularVelocity, GearshiftPosition currentGear, IResponse response)
{
// down shift
if (IsBelowDownShiftCurve(currentGear, inTorque, inAngularVelocity)) {
if (IsBelowDownShiftCurve(currentGear, response.ElectricMotor.TorqueRequestEmMap, response.ElectricMotor.AngularVelocity)) {
currentGear = GearList.Predecessor(currentGear);
//while (SkipGears && currentGear > 1) {
// currentGear--;
......@@ -557,7 +557,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Impl
private GearshiftPosition InitStartGear(Second absTime, NewtonMeter outTorque, PerSecond outAngularVelocity)
{
var emSpeeds = new Dictionary<GearshiftPosition, Tuple<PerSecond, PerSecond>>();
var emSpeeds = new Dictionary<GearshiftPosition, Tuple<PerSecond, PerSecond, double>>();
foreach (var gear in GearList.Reverse()) {
//for (var gear = (uint)GearboxModelData.Gears.Count; gear >= 1; gear--) {
......@@ -572,14 +572,18 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Impl
var fullLoadPower = -(response.ElectricMotor.MaxDriveTorque * response.ElectricMotor.AngularVelocity);
//.DynamicFullLoadPower; //EnginePowerRequest - response.DeltaFullLoad;
var reserve = 1 - response.ElectricMotor.PowerRequest / fullLoadPower;
var reserve = 1 - response.ElectricMotor.TorqueRequestEmMap / response.ElectricMotor.MaxDriveTorqueEM;
var isBelowDownshift = gear.Gear > 1 &&
IsBelowDownshiftCurve(GearboxModelData.Gears[gear.Gear].ShiftPolygon, response.ElectricMotor.TorqueRequest,
response.ElectricMotor.AngularVelocity);
if (reserve >= GearshiftParams.StartTorqueReserve) {
if (reserve >= GearshiftParams.StartTorqueReserve && !isBelowDownshift) {
//_nextGear = gear;
//return gear;
emSpeeds[gear] = Tuple.Create(response.ElectricMotor.AngularVelocity,
(GearshiftParams.StartSpeed * TransmissionRatio * GearboxModelData.Gears[gear.Gear].Ratio)
.Cast<PerSecond>());
.Cast<PerSecond>(), (response.ElectricMotor.ElectricMotorPowerMech / response.ElectricSystem.RESSPowerDemand).Value());
}
}
......@@ -592,12 +596,27 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Impl
return _nextGear;
}
private bool IsBelowDownshiftCurve(ShiftPolygon shiftPolygon, NewtonMeter emTorque, PerSecond emSpeed)
{
foreach (var entry in shiftPolygon.Downshift.Pairwise(Tuple.Create)) {
if (!emTorque.IsBetween(entry.Item1.Torque, entry.Item2.Torque)) {
continue;
}
if (ShiftPolygon.IsLeftOf(emSpeed, emTorque, entry)) {
return true;
}
}
return false;
}
protected bool IsBelowDownShiftCurve(GearshiftPosition gear, NewtonMeter inTorque, PerSecond inEngineSpeed)
{
if (!GearList.HasPredecessor(gear)) {
return false;
}
return GearboxModelData.Gears[gear.Gear].ShiftPolygon.IsBelowDownshiftCurve(inTorque, inEngineSpeed);
return IsBelowDownshiftCurve(GearboxModelData.Gears[gear.Gear].ShiftPolygon, inTorque, inEngineSpeed);
}
protected bool IsAboveDownShiftCurve(GearshiftPosition gear, NewtonMeter inTorque, PerSecond inEngineSpeed)
......
......@@ -864,9 +864,10 @@ namespace TUGraz.VectoCore.Tests.Models.Declaration
}
[TestCase(@"E:\QUAM\Workspace\VECTO-Bugreports\BugReportTests\Bugreport Jobs\20190307_VECTO-904_Extrapolation\OM-18173493.xml")]
[TestCase(@"E:\QUAM\Workspace\VECTO-Bugreports\BugReportTests\Bugreport Jobs\20190307_VECTO-904_Extrapolation\OM-18173493.xml")]
//[TestCase(@"E:\QUAM\Workspace\VECTO_DEV_Hybrid\Generic Vehicles\Declaration Mode\Group5_Tractor_4x2\Class5_Tractor_DECL.xml")]
[Ignore("Confidential data")]
public void ComputeShiftPolygonXML(string xmlJob)
public void ComputeShiftPolygonXML(string xmlJob)
{
var inputData = xmlInputReader.CreateDeclaration(xmlJob);
var dao = new DeclarationDataAdapterHeavyLorry();
......@@ -920,5 +921,43 @@ namespace TUGraz.VectoCore.Tests.Models.Declaration
g++;
}
}
[TestCase()]
public void ComputePEVShiftLines()
{
var pevE2Job = @"TestData\BatteryElectric\GenericVehicleB2\BEV_ENG.vecto";
var inputData = JSONInputDataFactory.ReadJsonJob(pevE2Job) as IEngineeringInputDataProvider;
var gearboxData = inputData.JobInputData.Vehicle.Components.GearboxInputData;
var dao = new EngineeringDataAdapter();
var emData = dao.CreateElectricMachines(inputData.JobInputData.Vehicle.Components.ElectricMachines,
null).FirstOrDefault()?.Item2;
var axlegearRatio = inputData.JobInputData.Vehicle.Components.AxleGearInputData.Ratio;
var vehicleData = dao.CreateVehicleData(inputData.JobInputData.Vehicle);
var r_dyn = vehicleData.DynamicTyreRadius;
var fullLoadCurve = emData.EfficiencyData.VoltageLevels.First().FullLoadCurve.FullLoadEntries.Select(x =>
new EngineFullLoadCurve.FullLoadCurveEntry() {
EngineSpeed = x.MotorSpeed,
TorqueFullLoad = -x.FullDriveTorque,
TorqueDrag = -x.FullGenerationTorque
}).ToList();
var fullLoadCurves = new Dictionary<uint, EngineFullLoadCurve>();
var engineData = new CombustionEngineData() {
IdleSpeed = 600.RPMtoRad()
};
fullLoadCurves[(uint)(0)] = new EngineFullLoadCurve(fullLoadCurve, null) { EngineData = engineData};
var shiftPolygons = new List<ShiftPolygon>();
for (var i = 0; i < gearboxData.Gears.Count; i++) {
shiftPolygons.Add(DeclarationData.Gearbox.ComputeElectricMotorShiftPolygon(i, emData.EfficiencyData.VoltageLevels.First().FullLoadCurve, 1.0, gearboxData.Gears,
axlegearRatio, r_dyn));
fullLoadCurves[(uint)(i + 1)] = new EngineFullLoadCurve(fullLoadCurve, null) { EngineData = engineData};
}
var imageFile = Path.Combine(Path.GetDirectoryName(pevE2Job), Path.GetFileNameWithoutExtension(pevE2Job) + "_shiftlines.png");
ShiftPolygonDrawer.DrawShiftPolygons(Path.GetDirectoryName(pevE2Job), fullLoadCurves, shiftPolygons,
imageFile,
DeclarationData.Gearbox.TruckMaxAllowedSpeed / r_dyn * axlegearRatio * gearboxData.Gears.Last().Ratio);
}
}
}
\ No newline at end of file
......@@ -202,14 +202,14 @@ namespace TUGraz.VectoCore.Tests.Utils
PlotDragLoad(engineFld, chartArea, chart, name);
PlotEngineSpeedLine(engineFld, chartArea, chart, "nPref " + name, Color.DeepSkyBlue,
engineFld.PreferredSpeed.Value() / Constants.RPMToRad);
//PlotEngineSpeedLine(engineFld, chartArea, chart, "nPref " + name, Color.DeepSkyBlue,
// engineFld.PreferredSpeed.Value() / Constants.RPMToRad);
PlotEngineSpeedLine(engineFld, chartArea, chart, "n95h " + name, Color.Red,
engineFld.N95hSpeed.Value() / Constants.RPMToRad);
//PlotEngineSpeedLine(engineFld, chartArea, chart, "n95h " + name, Color.Red,
// engineFld.N95hSpeed.Value() / Constants.RPMToRad);
PlotEngineSpeedLine(engineFld, chartArea, chart, "n85kmh " + name, Color.LimeGreen,
speed85kmh.Value() / Constants.RPMToRad);
//PlotEngineSpeedLine(engineFld, chartArea, chart, "n85kmh " + name, Color.LimeGreen,
// speed85kmh.Value() / Constants.RPMToRad);
PlotEngineSpeedLine(engineFld, chartArea, chart, "nPmax " + name, Color.Coral,
engineFld.RatedSpeed.Value() / Constants.RPMToRad);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment