Example #1
0
        public void T03_Constrained()
        {
            // a constrained minimization problem using a barrier function to minimize x^2 subject to x > 5
            MinimumBracket bracket = Minimize.BracketInward(new BarrierFunction(1), 5 + (5.0 / 2) * IEEE754.DoublePrecision, 100, 100).First();
            double         value;

            Assert.AreEqual(5, Minimize.Brent(new BarrierFunction(1e-10), bracket, out value), 1.2e-7);
            Assert.AreEqual(25, value, 1.2e-6);

            // test a constrained minimization problem using various methods
            ConstrainedMinimizer minimizer;

            double[] point;
            foreach (ConstraintEnforcement method in (ConstraintEnforcement[])Enum.GetValues(typeof(ConstraintEnforcement)))
            {
                // test the same problem using the ConstrainedMinimizer
                point = new double[2] {
                    5, 8
                };
                minimizer = new ConstrainedMinimizer(new ConstraintTestFunction())
                {
                    ConstraintEnforcement = method
                };
                minimizer.SetBounds(0, 1, double.PositiveInfinity);
                minimizer.SetBounds(1, 3, 9);
                value = minimizer.Minimize(point);
                switch (method)
                {
                case ConstraintEnforcement.InverseBarrier: // ~ 220 function calls and 180 gradient evaluations
                    Assert.AreEqual(9, value, 1.4e-8);
                    Assert.AreEqual(1, point[0], 2.4e-10);
                    Assert.AreEqual(3, point[1], 4.1e-10);
                    break;

                case ConstraintEnforcement.LinearPenalty: // ~ 870 function calls and 120 gradient evaluations
                    Assert.AreEqual(9, value, 4.4e-11);
                    Assert.AreEqual(1, point[0], 2.5e-12);
                    Assert.AreEqual(3, point[1], 2.5e-12);
                    break;

                case ConstraintEnforcement.LogBarrier: // ~ 140 function calls and 130 gradient evaluations
                    Assert.AreEqual(9, value, 5.1e-9);
                    Assert.AreEqual(1, point[0], 6e-12);
                    Assert.AreEqual(3, point[1], 1.7e-11);
                    break;

                case ConstraintEnforcement.QuadraticPenalty: // ~ 670 function calls and 170 gradient evaluations
                    Assert.AreEqual(9, value, 9e-9);
                    Assert.AreEqual(1, point[0], 9e-10);
                    Assert.AreEqual(3, point[1], 4e-10);
                    break;

                default: throw new NotImplementedException();
                }
            }

            // test the constrained minimizer with a tricky problem -- finding the minimum of the rosenbrock banana constrained to an
            // arbitrary line that doesn't pass through the minimum of the original problem
            minimizer = new ConstrainedMinimizer(new RosenbrockBanana());
            minimizer.AddConstraint(new LineConstraint(-1.5, -1.5, 0, 1.5, 0));
            point = new double[2] {
                -1.2, 2
            };
            value = minimizer.Minimize(point);
            Assert.AreEqual(2.4975, value, 2e-9);
            Assert.AreEqual(-0.57955689989313142185, point[0], 8e-10);
            Assert.AreEqual(0.34088620021373715629, point[1], 1e-9);
        }
Example #2
0
        bool UpdateSolution()
        {
            double?speed = null, time = null, aob = null, radius = null;

            if (!string.IsNullOrEmpty(txtSpeed.Text.Trim()))
            {
                double value;
                if (!TryParseSpeed(txtSpeed.Text, unitSystem, out value))
                {
                    ShowInvalidSpeed(txtSpeed.Text);
                    goto invalidData;
                }
                speed = value;
            }

            if (!string.IsNullOrEmpty(txtTime.Text.Trim()))
            {
                TimeSpan timeSpan;
                bool     relative;
                if (!TryParseTime(txtTime.Text, out timeSpan, out relative))
                {
                    ShowInvalidTime(txtTime.Text, false, false);
                    goto invalidData;
                }
                time = timeSpan.TotalSeconds;
            }

            if (!string.IsNullOrEmpty(txtAoB.Text.Trim()))
            {
                double value;
                if (!TryParseAngle(txtAoB.Text.Trim(), out value))
                {
                    ShowInvalidAngle(txtAoB.Text);
                    goto invalidData;
                }
                aob = value;
            }

            if (!string.IsNullOrEmpty(txtRadius.Text.Trim()))
            {
                double value;
                if (!TryParseLength(txtRadius.Text, unitSystem, out value))
                {
                    ShowInvalidLength(txtRadius.Text);
                    goto invalidData;
                }
                if (value != 0)
                {
                    radius = value;
                }
            }

            if (speed.HasValue && time.HasValue)
            {
                lblSolution.Text = "Remove speed or time.";
                return(false);
            }

            if (aob.HasValue && !radius.HasValue)
            {
                lblSolution.Text = "Using AoB requires a radius.";
                return(false);
            }

            if (radius.HasValue && !aob.HasValue)
            {
                lblSolution.Text = "Using a radius requires AoB.";
                return(false);
            }

            Point2  targetPt;
            Vector2 targetVel;
            double  targetCourse;

            if (target != null)
            {
                targetPt     = target.Position;
                targetVel    = target.GetEffectiveVelocity();
                targetCourse = target.Direction;
            }
            else
            {
                double bearing, range;
                if (string.IsNullOrEmpty(txtBearing.Text))
                {
                    lblSolution.Text = "Enter a target bearing.";
                    return(false);
                }
                else if (!TryParseAngle(txtBearing.Text, out bearing))
                {
                    ShowInvalidAngle(txtBearing.Text);
                    goto invalidData;
                }

                if (string.IsNullOrEmpty(txtRange.Text))
                {
                    lblSolution.Text = "Enter a target range.";
                    return(false);
                }
                else if (!TryParseLength(txtRange.Text, unitSystem, out range))
                {
                    ShowInvalidLength(txtRange.Text);
                    goto invalidData;
                }

                targetPt = new Vector2(0, range).Rotate(-bearing).ToPoint();

                double targetSpeed;
                if (string.IsNullOrEmpty(txtTargetSpeed.Text))
                {
                    lblSolution.Text = "Enter a target speed.";
                    return(false);
                }
                else if (!TryParseSpeed(txtTargetSpeed.Text, unitSystem, out targetSpeed))
                {
                    ShowInvalidSpeed(txtTargetSpeed.Text);
                    goto invalidData;
                }

                if (targetSpeed == 0)
                {
                    targetCourse = 0;
                }
                else if (string.IsNullOrEmpty(txtCourse.Text))
                {
                    lblSolution.Text = "Enter a target course.";
                    return(false);
                }
                else if (!TryParseAngle(txtCourse.Text, out targetCourse))
                {
                    ShowInvalidAngle(txtCourse.Text);
                    goto invalidData;
                }

                targetVel = new Vector2(0, targetSpeed).Rotate(-targetCourse);
            }

            // if AoB was specified, then we're actually trying to intercept a single point on the radius circle, so make that are target point
            // and use the standard point intercept algorithm
            if (aob.HasValue)
            {
                targetPt += new Vector2(0, radius.Value).Rotate(-(targetCourse + aob.Value));
            }

            // if we've already satisfied the intercept criteria...
            Vector2 o = unit == null ? new Vector2(targetPt) : targetPt - unit.Position;

            if (o.LengthSqr <= (radius.HasValue && !aob.HasValue ? radius.Value * radius.Value : 0))
            {
                lblSolution.Text = "You are already there.";
                return(false);
            }

            // if the target is not moving, any speed will work, so we'll just arbitrarily head there at 10 units of speed
            if (targetVel.LengthSqr == 0)
            {
                Solution         = o.GetNormal(MB.ConvertFromUnit(10, MB.GetAppropriateSpeedUnit(unitSystem)));
                InterceptPoint   = targetPt;
                lblSolution.Text = ManeuveringBoard.GetAngleString(MB.SwapBearing(o.Angle)) + " (target stationary)";
                return(true);
            }

            // if we have a single target point (i.e. the target itself, or one point on the radius circle), the intercept formula basically
            // consists in solving a quadratic formula. if we're at P and the target is at T with velocity V, then we know that the interception
            // point is at T+V*t, where t is time. if we take the vector from P to the intersection point (T+V*t-P), then the distance is
            // |T+V*t-P|. dividing by the speed s gives us the time: t = |(T+V*t-P)|/s. so s*t = |T+V*t-P|. squaring both sides, replacing T-P
            // with the helper O (i.e. translating P to the origin), and expanding the vector, we get (s*t)^2 = (Ox+Vx*t)^2 + (Oy+Vy*t)^2
            if (!time.HasValue) // if the user didn't specify the time of intercept... (if they did, the problem's solved already)
            {
                if (!speed.HasValue)
                {
                    // if the user specified no information, there are an infinite number of solutions. we'll choose the one that requires
                    // approximately the lowest intercept speed. if we solve the quadratic equation for speed instead of time, we get
                    // s = sqrt((Ox + Vx*t)^2 + (Oy + Vy*t)^2) / t. we need to minimize this equation where s >= 0 and t >= 0. there's probably
                    // some way to do this analytically, but it seems complicated, so we'll just minimize it numerically. we'll represent the time
                    // TODO: check out the inverse of this function. it may be more sloped and easier to optimize?
                    // TODO: consider a better method of rescaling (to get all the numbers about the same magnitude)
                    Func <double, double> speedFunc = t =>
                    {
                        if (t <= 0)
                        {
                            return(double.NaN);
                        }
                        t *= 3600; // we'll scale the time from seconds to hours because the minimizer is a bit more stable when params are around 1.0
                        double x = o.X + targetVel.X * t, y = o.Y + targetVel.Y * t;
                        return(Math.Sqrt(x * x + y * y) / t);
                    };
                    Func <double, double> derivative = t =>
                    {
                        if (t <= 0)
                        {
                            return(double.NaN);
                        }
                        t *= 3600;
                        double x = o.X + targetVel.X * t, y = o.Y + targetVel.Y * t;
                        return(-(o.X * x + o.Y * y) / (t * t * Math.Sqrt(x * x + y * y)));
                    };
                    ConstrainedMinimizer minimizer = new ConstrainedMinimizer(new DifferentiableMDFunction(speedFunc, derivative));
                    // the function tends to be very flat near the minimum, so it's hard to find it exactly. increase the gradient tolerance to
                    // prevent it from failing as often. we're going to be rounding the answer anyway, so it needn't be exact
                    minimizer.GradientTolerance = 1e-6;
                    // constrain the solution to be non-negative
                    minimizer.AddConstraint(new DifferentiableMDFunction(t => - speedFunc(t), t => - derivative(t)));
                    minimizer.SetBounds(0, 0, double.MaxValue); // constrain t to be non-negative
                    try
                    {
                        double[] point    = new double[] { 1 };
                        double   minSpeed = minimizer.Minimize(point);
                        // now we want to round the speed up to the next unit, to make it look nicer. we also want to increase the speed by a small
                        // amount because the time seems to increase without bound as the speed nears the minimum. for instance, a difference of
                        // 0.01 kn near the minimum might increase the time by 5 hours. speeds near the minimum also render the math below very
                        // inaccurate due to mismatching precision. so we'll add about a knot to the speed as well as round it up
                        SpeedUnit speedUnit = MB.GetAppropriateSpeedUnit(unitSystem);
                        speed = MB.ConvertFromUnit(Math.Ceiling(MB.ConvertToUnit(minSpeed + 0.5, speedUnit)), speedUnit); // 0.5 m/s ~= 1 kn
                    }
                    catch (MinimumNotFoundException)
                    {
                        lblSolution.Text = "Try setting parameters.";
                        return(false);
                    }
                }

                // if know the intercept speed, we take the above equation and factor out time. we end up with:
                // t^2(Vx^2 + Vy^2 - s^2) + t*(2Ox*Vx + 2Oy*Vy) + Ox^2 + Oy^2 = 0. if we take A=(Vx^2 + Vy^2 - s^2), B=2(Ox*Vx + Oy*Vy), and
                // C=Ox^2 + Oy^2, then we have the quadratic A*t^2 + B*t + C = 0 which we can solve with the quadratic formula.
                // t = (-B +/- sqrt(B^2 - 4AC)) / 2A (and we can remove a factor of 2). if the discriminant is negative, there is no solution.
                // otherwise, we take whichever solution gives the smallest non-negative time
                double A = targetVel.X * targetVel.X + targetVel.Y * targetVel.Y - speed.Value * speed.Value, B = o.X * targetVel.X + o.Y * targetVel.Y, C = o.X * o.X + o.Y * o.Y;

                // if A = 0, then the speeds are identical, and we get division by zero solving the quadratic. but if A = 0 then we just have
                // B*t + C = 0 or t = -C/B. we know B can't be zero because we checked the relevant cases above
                if (A == 0)
                {
                    double t = -C / (2 * B); // we have to multiply B by 2 because we removed a factor of two above
                    if (t >= 0)
                    {
                        time = t;
                    }
                    else
                    {
                        goto noSolution;
                    }
                }
                else
                {
                    double discriminant = B * B - A * C;
                    if (discriminant < 0)
                    {
                        goto noSolution;
                    }
                    double root = Math.Sqrt(discriminant), time1 = (-B + root) / A, time2 = (-B - root) / A;
                    if (time1 >= 0 && time1 <= time2)
                    {
                        time = time1;
                    }
                    else if (time2 >= 0)
                    {
                        time = time2;
                    }
                    else
                    {
                        goto noSolution;
                    }
                }
            }

            // now that we know the time of intercept, we can calculate the intercept point and get the velocity we'd need to get there.
            // the intercept point is T+V*t. the intercept vector is T+V*t-P = O+V*t. this vector has a length equal to the distance, but we
            // want a length equal to the speed, so divide by time to get speed (e.g. 10 km in 2 hour = 5km/hour). but (O+V*t)/t = O/t+V
            Solution       = o / time.Value + targetVel;
            InterceptPoint = targetPt + targetVel * time.Value;
            haveSolution   = true;

            lblSolution.Text = ManeuveringBoard.GetAngleString(MB.SwapBearing(Solution.Angle)) + " at " +
                               MB.GetSpeedString(Solution.Length, unitSystem) + " for " + GetTimeString(time.Value);
            return(true);

noSolution:
            lblSolution.Text = "No intercept is possible.";
            return(false);

invalidData:
            lblSolution.Text = "Invalid data.";
            return(false);
        }