Finding self-intersections of a closed curve represented by an interpolation function

Finding the intersections

You can use use Graphics`Mesh`FindIntersections (see Implementation of Balaban's Line intersection algorithm in Mathematica for example) either on the plot or on the points stored in the InterpolatingFunctions in sol:

With

curvatureConst = -3.5;

there are five points of intersection:

Mathematica graphics

Here are the two methods:

intersections = Graphics`Mesh`FindIntersections[
  Line@Transpose[{x["ValuesOnGrid"], y["ValuesOnGrid"]} /. First[sol]]
  ]
intersections = Graphics`Mesh`FindIntersections[plot]
(*
  {{-0.043451, -0.990455}, {0.173594, 0.976447}, {0.386241, -1.02621},
   {0.998533, 1.03024}, {1.87042, -0.508015}}
  {{-0.0433839, -0.990405}, {0.173527, 0.976393}, {0.386279, -1.02621},
   {0.998525, 1.03019}, {1.87043, -0.508032}}
*)

They are slightly different due to different sample points. It does not really matter, since we will use these to get the approximate times the solution sol passes through/near them and the solve for more precise points.

Roughly approximating the times of intersection

Each of the points in sol is associated with a time. We just need to find the nearest points on each curve and get their corresponding times. There are a couple of pitfalls that I'll leave to those who need a more robust solution. If there is a multiple intersection (three or more segments of the curve), there will be multiple times to solve for; there code in the next section should handle this situation. One should beware that these methods are approximate, so if the multiplicity of the intersection is $n$ lines, then expect ${}_nC_2 = n(n-1)/2$ points of intersection that will be nearly the same. However, if another segment of the curve passes very close to a point of intersection but not through it, the code might capture a time associated with it; such a time will probably lead to a spurious solution (or possibly an error).

We can use Nearest to find the times; below nf is the NearestFunction that the times associated with the points nearest its argument. In the form nf[pt, {All, rad}], it will return the times for all points in sol that are within a distance rad of pt. These times will form clusters belonging to each branch of the curve that passes by or through pt. FindClusters will help sort them out. The first time in each cluster will correspond to the point on each branch in sol that is closest to pt. Now we don't want the radius rad to be too small or we might not capture a point in sol on one of the branches; on the other hand, we don't want it to be too big or we'll capture points on branches that pass close to, but do not pass through a point of intersection. Some manual adjustment may be necessary. We can determine how far apart points in sol are and set the distance to be a little bigger than the maximum. Satisfying the other condition won't always be possible, and I will leave that to be dealt with on a case by case basis.

rad = 1.05 Max[
    Norm /@ Differences@
      Transpose[{x["ValuesOnGrid"], y["ValuesOnGrid"]} /. First[sol]]
    ];
nf = Nearest[
   Transpose[{x["ValuesOnGrid"], y["ValuesOnGrid"]} /. First[sol]] -> 
    First[y["Coordinates"] /. First[sol]]];
approxtimes = First /@ FindClusters[#] & /@ nf[intersections, {All, rad}]
(*
  {{14.2283, 12.6884}, {0.175879, 1.74677}, {12.2531, 10.6247},
   {4.38008, 2.62039}, {8.69479, 6.90757}}
*)

Solving for the intersections

For simple intersections (of two branches), FindRoot is sufficient. The following returns the points of intersection and the times the curve passes through each.

MapThread[
 Function[{pt, t0},
  {{x[t], y[t]} /. # /. First@sol, #} &@
     FindRoot[{x[t], y[t]} == {x[s], y[s]} /. First[sol], {t, #1}, {s, #2}] & @@ t0],
 {intersections, approxtimes}
 ]
(*
  {{{-0.0434545, -0.990458}, {t -> 14.2286, s -> 12.6877}},
   {{0.173592, 0.976449}, {t -> 0.175734, s -> 1.74863}},
   {{0.38622, -1.02623}, {t -> 12.2491, s -> 10.6203}},
   {{0.998544, 1.03025}, {t -> 4.38009, s -> 2.62287}},
   {{1.87043, -0.508036}, {t -> 8.69794, s -> 6.9039}}}
*)

For intersections of higher multiplicity, minimizing the sum of squares of the differences with FindMinimum will yield the points of intersections and times. One could add a check that the minimum is in fact close to zero to check for spurious solutions.

MapThread[
 Function[{pt, t0},
  {{x[t[1]], y[t[1]]} /. # /. First@sol, #} &@ Last[
        FindMinimum @@ {
          Total[Differences[({x[#], y[#]} & /@ #)]^2, 2] /. 
           First[sol], Sequence @@ Transpose[{##}]
          }] & @@ {Array[t, Length[#]], #} &@t0],
 {intersections, approxtimes}
 ]

To illustrate the issues with multiple intersections, consider the 3-petal rosette:

sol = NDSolve[
 {{x'[t], y'[t]} == D[{Cos[3 t] Cos[t], Cos[3 t] Sin[t]}, t], {x[0], y[0]} == {1, 0}},
 {x, y}, {t, 0, Pi}];

The procedure above yields three approximate points of intersection:

intersections
(*  {{-0.00221279, 0.00330872}, {-0.0021968, -0.00154454}, {0.00171799, 0.000895285}}  *)

With FindMinimum they reduce to the same point (approximately the origin):

{{{4.47922*10^-7, 2.79*10^-7}, {t[1] -> 0.523599, t[2] -> 2.61799, t[3] -> 1.5708}},
 {{4.47922*10^-7, 2.79*10^-7}, {t[1] -> 0.523599, t[2] -> 2.61799, t[3] -> 1.5708}},
 {{4.47922*10^-7, 2.79*10^-7}, {t[1] -> 0.523599, t[2] -> 2.61799, t[3] -> 1.5708}}}

Return to initial position

You can use WhenEvent to determine when the system returns to its initial position. Since it may not return exactly, one has to use one's judgement about how close to the initial position will be counted as a "return." The way I'm proposing is to monitor one variable. When it returns to its initial value, check whether the other variables have returned to their initial values.

We define the initial conditions ics and measure the return with EuclideanDistance:

ics = {x[0], x'[0], y[0], y'[0]} == {xInit, xInitVel, yInit, yInitVel}
return = EuclideanDistance @@ ics /. h_[0] :> h[t]}

The one has to pick a value to compare return to; below I used 10^-6.

curvatureConst = -2.2598969107936748290654804804944433271884918212890625;
ics = {x[0], x'[0], y[0], y'[0]} == {xInit, xInitVel, yInit, yInitVel};
With[{return = EuclideanDistance @@ ics /. h_[0] :> h[t]},
  sol = NDSolve[{k == curvatureConst, (x'[t])^2 + (y'[t])^2 == 1, ics,
     WhenEvent[x[t] == xInit, If[return < 10^-6, "StopIntegration", {}]]},
    {x, y}, {t, maxTime}]
  ];
plot = ParametricPlot @@ {{x[t], y[t]} /. First@sol, 
   Flatten[{t, x["Domain"] /. sol}]}

Mathematica graphics

We can see that integration was stopped at t == 8.13255:

x["Domain"] /. sol
(*  {{0., 8.13255}}  *)

Note the documentation might lead you to believe that the following would be equivalent to the WhenEvent above:

WhenEvent[x[t] == xInit && return < 10^-6, "StopIntegration"]

But at the step taken, the value of return needs be less than 10^-6. In fact, it is greater than 10^-3. So you would have to use something like

WhenEvent[x[t] == xInit && return < 10^-2, "StopIntegration"]

to detect the return. The event location procedure finds the time when x[t] == xInit occurs, and it happens that the value of return at that point is actually about 4*10^-8. But if it were only slightly less than 10^-2, then integration would be stopped. Thus approach I used originally gives somewhat finer control.


For

curvatureConst = -2.25

the equations in the Question yield

enter image description here

An intersection can be found from

First@FindRoot[{(x[t] - x[t2]) /. sol, (y[t] - y[t2]) /. sol}, {{t, 8}, {t2, 10.5}}]
(* t -> 7.8869 *)
Flatten[{x[t], y[t]} /. sol /. %]
(* {-0.0330813, 0.693441} *)

Of course, there are multiple intersections, and which is obtained depends on the initial guess given to FindRoot.

To find where a curve closes, consider the curve displayed in the Question.

FindRoot[((x[t] - x[0]) /. sol)^2 + ((y[t] - y[0]) /. sol)^2, {t, 8}]
(* {t -> 8.13255} *)
Flatten[{x[t], y[t]} /. sol /. %]
(* {-6.30735*10^-8, 1.} *)