public ThrusterSolutionMap(ThrusterMap map, double linearAccel, double rotateAccel) { this.Map = map; this.LinearAccel = linearAccel; this.RotateAccel = rotateAccel; }
private static ThrusterSolutionMap GetThrusterSolutionMap(ThrusterMap map, ThrustContributionModel model, MassMatrix inertia, double mass) { // Add up the forces Vector3D sumLinearForce = new Vector3D(); Vector3D sumTorque = new Vector3D(); foreach (ThrusterSetting thruster in map.UsedThrusters) { var contribution = model.Contributions.FirstOrDefault(o => o.Item1 == thruster.ThrusterIndex && o.Item2 == thruster.SubIndex); if (contribution == null) { throw new ApplicationException(string.Format("Didn't find contribution for thruster: {0}, {1}", thruster.ThrusterIndex, thruster.SubIndex)); } sumLinearForce += contribution.Item3.TranslationForce * thruster.Percent; sumTorque += contribution.Item3.Torque * thruster.Percent; } // Divide by mass //F=MA, A=F/M double accel = sumLinearForce.Length / mass; Vector3D projected = inertia.Inertia.GetProjectedVector(sumTorque); double angAccel = sumTorque.Length / projected.Length; if (Math1D.IsInvalid(angAccel)) { angAccel = 0; // this happens when there is no net torque } return new ThrusterSolutionMap(map, accel, angAccel); }
private static double GetScore_Inefficient3(ThrusterMap map, ThrustContributionModel model) { if (map.Flattened.Length != model.Contributions.Length) { throw new ApplicationException("TODO: Handle mismatched map and model"); } // Add up all the thruster forces, regardless of direction double retVal = Enumerable.Range(0, map.Flattened.Length). Select(o => { if (map.Flattened[o].Item1 != model.Contributions[o].Item1 || map.Flattened[o].Item2 != model.Contributions[o].Item2) { throw new ApplicationException("TODO: Handle mismatched map and model"); } // The translation force is the same as the thrust produced by the thruster. Don't want to include // torque, because this method only cares about how much fuel is being used, and adding torque // is an innacurate complication return model.Contributions[o].Item3.TranslationForceLength * map.Flattened[o].Item3; }). Sum(); return retVal; }
// This should be called twice, once for linear and once for rotation. It should add the force length if dot is close to zero. May also want a smaller length if dot is close to -1 private static double GetScore_Inefficient4(ThrusterMap map, ThrustContributionModel model, Vector3D objectiveUnit, bool isLinear) { const double DOT_MAX = .3; // when dot is >= this, there is no error const double PERCENT_NEG = .5; // when dot is -1, this is the returned percent error if (map.Flattened.Length != model.Contributions.Length) { throw new ApplicationException("TODO: Handle mismatched map and model"); } double retVal = Enumerable.Range(0, map.Flattened.Length). Select(o => { if (map.Flattened[o].Item1 != model.Contributions[o].Item1 || map.Flattened[o].Item2 != model.Contributions[o].Item2) { throw new ApplicationException("TODO: Handle mismatched map and model"); } // Get the actual vector Vector3D actualVect; double actualVal; if (isLinear) { actualVect = model.Contributions[o].Item3.TranslationForceUnit; actualVal = model.Contributions[o].Item3.TranslationForceLength; } else { actualVect = model.Contributions[o].Item3.TorqueUnit; actualVal = model.Contributions[o].Item3.TorqueLength; } // Use dot to see how in line this actual vector is with the ideal double dot = Vector3D.DotProduct(objectiveUnit, actualVect); // Convert dot into a percent // - If dot is near zero, error should be maximum, because this doesn't contribute to ideal // - Can't penalize positive and negative dots equally, or it will try to find a minimum fuel useage // that is still balanced, instead of maximum thrust that is balanced (the goal is maximum thrust // without venting fuel out the sides) double percent; if (dot > 0) { if(dot > DOT_MAX) { percent = 0; } else { percent = UtilityCore.GetScaledValue(1, 0, 0, DOT_MAX, dot); } } else { percent = UtilityCore.GetScaledValue(1, PERCENT_NEG, 0, 1, -dot); } return actualVal * map.Flattened[o].Item3 * percent; }). Sum(); return retVal; }
private static double GetScore_Inefficient(ThrusterMap map, ThrustContributionModel model) { // Only focus on linear balance. Torque balance is desired //TODO: This first version only looks a single thrusters. Make a future version that looks a more combinations double retVal = 0; var byThruster = model.Contributions. ToLookup(o => o.Item1). Where(o => o.Count() > 1); foreach (var thruster in byThruster) { var arr = thruster.ToArray(); foreach (var pair in UtilityCore.GetPairs(arr.Length)) { double dot = Vector3D.DotProduct(arr[pair.Item1].Item3.TranslationForceUnit, arr[pair.Item2].Item3.TranslationForceUnit); if (dot < -.95) { // They are directly opposing each other. Figure out the force they are firing, based on the thruster percents double force1 = arr[pair.Item1].Item3.TranslationForceLength * map.Flattened.First(o => o.Item1 == arr[pair.Item1].Item1 && o.Item2 == arr[pair.Item1].Item2).Item3; double force2 = arr[pair.Item2].Item3.TranslationForceLength * map.Flattened.First(o => o.Item1 == arr[pair.Item2].Item1 && o.Item2 == arr[pair.Item2].Item2).Item3; // The min represents waste (because if the weaker thruster were turned off, the net force won't change) double min = Math.Min(force1, force2); // Square it retVal += min * min; } } } return retVal; }
private static double GetScore_Inefficient2(ThrusterMap map, ThrustContributionModel model, double maxPossibleLinear, double maxPossibleRotate) { if (map.Flattened.Length != model.Contributions.Length) { throw new ApplicationException("TODO: Handle mismatched map and model"); } // Add up all of the thruster forces. Don't worry about direction, just get the sum of the magnitude var forces = Enumerable.Range(0, map.Flattened.Length). Select(o => { if (map.Flattened[o].Item1 != model.Contributions[o].Item1 || map.Flattened[o].Item2 != model.Contributions[o].Item2) { throw new ApplicationException("TODO: Handle mismatched map and model"); } return new { Linear = model.Contributions[o].Item3.TranslationForceLength * map.Flattened[o].Item3, Rotate = model.Contributions[o].Item3.TorqueLength * map.Flattened[o].Item3, }; }). ToArray(); double linear = forces.Sum(o => o.Linear); double rotate = forces.Sum(o => o.Rotate); // Subtract max possible linear -= maxPossibleLinear; rotate -= maxPossibleRotate; //linear = Math.Max(linear, 0); // don't let scores go negative //rotate = Math.Max(rotate, 0); linear = Math.Abs(linear); // negative scores are also bad rotate = Math.Abs(rotate); // Return remainder squared return (linear * linear) + (rotate * rotate); }
public static double[] GetThrustMapScore3(ThrusterMap map, ThrustContributionModel model, Vector3D? linear, Vector3D? rotation, double maxPossibleLinear = 0, double maxPossibleRotate = 0) { if (linear == null && rotation == null) { throw new ApplicationException("Objective linear and rotate can't both be null"); } Tuple<Vector3D, Vector3D> forces = ApplyThrust(map, model); double[] retVal = new double[2]; if (forces.Item1.LengthSquared.IsNearZero() && forces.Item2.LengthSquared.IsNearZero()) { // When there is zero contribution, give a large error. Otherwise the winning strategy is not to play :) retVal[0] = MAXERROR; // Balance retVal[1] = MAXERROR; // Inefficient } else { // Balance double linearScore = GetScore_Balance(linear, forces.Item1); double rotateScore = GetScore_Balance(rotation, forces.Item2); retVal[0] = linearScore + rotateScore; // Inefficient linearScore = linear == null ? 0 : GetScore_Inefficient4(map, model, linear.Value, true); rotateScore = rotation == null ? 0 : GetScore_Inefficient4(map, model, rotation.Value, false); retVal[1] = linearScore + rotateScore; } return retVal; }
public static Tuple<Vector3D, Vector3D> ApplyThrust(ThrusterMap map, ThrustContributionModel model) { if (map.Flattened.Length != model.Contributions.Length) { throw new ApplicationException("TODO: Handle mismatched map and model"); } var combined = Enumerable.Range(0, map.Flattened.Length). Select(o => { if (map.Flattened[o].Item1 != model.Contributions[o].Item1 || map.Flattened[o].Item2 != model.Contributions[o].Item2) { throw new ApplicationException("TODO: Handle mismatched map and model"); } return new { Index = map.Flattened[o].Item1, SubIndex = map.Flattened[o].Item2, Percent = map.Flattened[o].Item3, Contribution = model.Contributions[o].Item3, }; }). ToArray(); Vector3D linear = Math3D.GetSum(combined.Select(o => Tuple.Create(o.Contribution.TranslationForce, o.Percent)).ToArray()); Vector3D rotation = Math3D.GetSum(combined.Select(o => Tuple.Create(o.Contribution.Torque, o.Percent)).ToArray()); return Tuple.Create(linear, rotation); }
public static SolutionError GetThrustMapScore(ThrusterMap map, ThrustContributionModel model, Vector3D? linear, Vector3D? rotation, double maxPossibleLinear = 0, double maxPossibleRotate = 0) { if (linear == null && rotation == null) { throw new ApplicationException("Objective linear and rotate can't both be null"); } Tuple<Vector3D, Vector3D> forces = ApplyThrust(map, model); double[] error = new double[3]; if (forces.Item1.LengthSquared.IsNearZero() && forces.Item2.LengthSquared.IsNearZero()) { // When there is zero contribution, give a large error. Otherwise the winning strategy is not to play :) error[0] = MAXERROR; // Balance error[1] = MAXERROR; // UnderPowered } else { // Balance double linearScore = GetScore_Balance(linear, forces.Item1); double rotateScore = GetScore_Balance(rotation, forces.Item2); error[0] = linearScore + rotateScore; // UnderPowered linearScore = GetScore_UnderPowered(linear, forces.Item1, maxPossibleLinear); rotateScore = GetScore_UnderPowered(rotation, forces.Item2, maxPossibleRotate); error[1] = linearScore + rotateScore; } // Inneficient error[2] = GetScore_Inefficient(map, model); // Total // It's important to include the maximize thrust, but just a tiny bit. Without it, the ship will be happy to // fire thrusters in all directions. But if maximize thrust is any stronger, the solution will have too much // unbalance double total = error[0] + (error[1] * .01) + (error[2] * .1); return new SolutionError(error, total); }
private const double MAXERROR = 100000000000; // need something excessively large, but less than double.max #endregion /// <summary> /// This is a wrapper of UtilityAI.DiscoverSolution /// </summary> public static void DiscoverSolutionAsync(Bot bot, Vector3D? idealLinear, Vector3D? idealRotation, CancellationToken? cancel = null, ThrustContributionModel model = null, Action<ThrusterMap> newBestFound = null, Action<ThrusterMap> finalFound = null, DiscoverSolutionOptions<Tuple<int, int, double>> options = null) { long token = bot.Token; // Cache Thrusters Thruster[] allThrusters = bot.Thrusters; if (allThrusters == null || allThrusters.Length == 0) { throw new ArgumentException("This bot has no thrusters"); } // Ensure model is created (if the caller wants to find solutions for several directions at the same time, it would be // more efficient to calculate the model once, and pass to all the solution finders) model = model ?? new ThrustContributionModel(bot.Thrusters, bot.PhysicsBody.CenterOfMass); // Figure out how much force can be generated in the ideal directions double maxForceLinear = idealLinear == null ? 0d : ThrustControlUtil.GetMaximumPossible_Linear(model, idealLinear.Value); double maxForceRotate = idealRotation == null ? 0d : ThrustControlUtil.GetMaximumPossible_Rotation(model, idealRotation.Value); // Mutate 2% of the elements, with a 10% value drift MutateUtility.MuateArgs mutateArgs = new MutateUtility.MuateArgs(false, .02, new MutateUtility.MuateFactorArgs(MutateUtility.FactorType.Distance, .1)); #region delegates //NOTE: While breeding/mutating, they don't get normalized. But error calculation and returned maps need them normalized var delegates = new DiscoverSolutionDelegates<Tuple<int, int, double>>() { GetNewSample = new Func<Tuple<int, int, double>[]>(() => { ThrusterMap map = ThrustControlUtil.GenerateRandomMap(allThrusters, token); return map.Flattened; }), GetError = new Func<Tuple<int, int, double>[], SolutionError>(o => { ThrusterMap map = new ThrusterMap(ThrustControlUtil.Normalize(o), allThrusters, token); return ThrustControlUtil.GetThrustMapScore(map, model, idealLinear, idealRotation, maxForceLinear, maxForceRotate); }), Mutate = new Func<Tuple<int, int, double>[], Tuple<int, int, double>[]>(o => { ThrusterMap map = new ThrusterMap(o, allThrusters, token); return ThrustControlUtil.Mutate(map, mutateArgs, false).Flattened; }), }; if (cancel != null) { delegates.Cancel = cancel.Value; } if (newBestFound != null) { delegates.NewBestFound = new Action<SolutionResult<Tuple<int, int, double>>>(o => { ThrusterMap map = new ThrusterMap(ThrustControlUtil.Normalize(o.Item), allThrusters, token); newBestFound(map); }); } if (finalFound != null) { delegates.FinalFound = new Action<SolutionResult<Tuple<int, int, double>>>(o => { ThrusterMap map = new ThrusterMap(ThrustControlUtil.Normalize(o.Item), allThrusters, token); finalFound(map); }); } #endregion // Do it //NOTE: If options.ThreadShare is set, then there's no reason to do this async, but there's no harm either Task.Run(() => UtilityAI.DiscoverSolution(delegates, options)); }
/// <summary> /// This scales all thrusters so that they fire as strong as possible /// </summary> /// <remarks> /// When generating a random mapping, the strongest thruster may only be firing at 50%. This method would /// make that thruster fire at 100% (and scale all other thrusters to compensate) /// </remarks> public static ThrusterMap Normalize(ThrusterMap map) { return new ThrusterMap(Normalize(map.Flattened), map.AllThrusters, map.BotToken); }
public static ThrusterMap Mutate(ThrusterMap map, MutateUtility.MuateArgs args, bool shouldNormalize = true) { var flattened = map.Flattened.ToArray(); // ensure it's a clone int changeCount = args.GetChangeCount(flattened.Length); if (changeCount == 0 && flattened.Length > 0) { changeCount = 1; } foreach (int index in UtilityCore.RandomRange(0, flattened.Length, changeCount)) { // Mutate the value double newValue = MutateUtility.Mutate(flattened[index].Item3, args.DefaultFactor); // Cap it if (newValue < 0) { newValue = 0; } else if (newValue > 1) { newValue = 1; } // Store the mutated value flattened[index] = Tuple.Create(flattened[index].Item1, flattened[index].Item2, newValue); } if (shouldNormalize) { flattened = Normalize(flattened); } return new ThrusterMap(flattened, map.AllThrusters, map.BotToken); }
public static ThrusterMap Mutate(ThrusterMap map, bool shouldNormalize = true) { // Modify 2% of the properties // Each property should drift up to 10% each direction (the values get capped between 0 and 100%) MutateUtility.MuateArgs args = new MutateUtility.MuateArgs(false, .02, new MutateUtility.MuateFactorArgs(MutateUtility.FactorType.Distance, .1)); return Mutate(map, args); }