num_opt/
optimizer.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! Calculates the cost of a curve, and performs gradient descent to optimize it

use rand::Rng;

use crate::{
    hermite,
    my_float::{Fpt, MyFloat},
    physics::{self, g_force_is_safe, info::use_sigfigs, linalg::MyVector3, tol},
    point,
};

pub struct CostHistoryPoint {
    pub delta_cost: Fpt,
    pub x: Fpt,
    pub y: Fpt,
    pub z: Fpt,
    pub safe: bool,
}

/// Given initial state and curve, calculates the total cost of the curve
///
/// This requires a physics simulation be run, with the paramer `initial` being
/// the starting physical state of the object, including properties like
/// position, velocity, and other parameters.
///
/// Return a valid cost as a `Option<Fpt>`, or return `None` if the cost
/// can't be computed
/// - happens when the coaster gets stuck
///
/// ### Implementation Details
///
/// `let mut phys = initial;` copies the initial physical state into a mutable
/// variable phys, which evolves as the simulation progresses
///
/// `phys.cost()` computes the total cost of the curve, if cost is `Nan`, it
/// returns `None` to indicate an invalid calculation
pub fn cost_v2<T: MyFloat>(
    initial: physics::PhysicsStateV3<T>,
    curve: &hermite::Spline<T>,
    step: Fpt,
) -> Option<Fpt> {
    cost_v2_with_history(initial, curve, step, None).0
}

pub fn cost_v2_with_history<T: MyFloat>(
    initial: physics::PhysicsStateV3<T>,
    curve: &hermite::Spline<T>,
    step: Fpt,
    min_dist_between_points: Option<Fpt>,
) -> (Option<Fpt>, Vec<CostHistoryPoint>) {
    let mut phys = initial;
    let mut hist = vec![];
    let mut ppos: Option<MyVector3<T>> = None;
    let mut pcost = 0.0;
    let mut safe = true;
    while phys.step(&T::from_f(step), curve).is_some() {
        safe =
            safe && g_force_is_safe(phys.additional_info().up_gs, phys.additional_info().side_gs);
        if let Some(min_dist) = min_dist_between_points {
            let cp = phys.x().clone().inner();
            if ppos
                .clone()
                .is_none_or(|p| (cp.clone() - p).magnitude() > min_dist)
            {
                // add to history
                let ccost = phys.cost().to_f();
                hist.push(CostHistoryPoint {
                    x: cp.x.to_f(),
                    y: cp.y.to_f(),
                    z: cp.z.to_f(),
                    delta_cost: ccost - pcost,
                    safe,
                });
                pcost = ccost;
                ppos = Some(cp);
                safe = true;
            }
        }
    }
    if phys.cost().is_nan() {
        (None, hist)
    } else {
        (Some(phys.cost().to_f()), hist)
    }
}

/// Performs one step of GD to minimize the cost of the curve (gradients
/// calculated using secant approximation)  
///
/// Returns: the original cost, or `None` if this calculation failed
///
/// - `initial`: starting physics state
/// - `curve`: The Hermite spline to optimize
/// - `points`: Array of control points for the spline
/// - `lr`: Determines the step size for gradient descent
pub fn optimize_v2<T: MyFloat>(
    initial: &physics::PhysicsStateV3<T>,
    curve: &hermite::Spline<T>,
    points: &mut [point::Point<T>],
    lr: Fpt,
) -> Option<Fpt> {
    const NUDGE_DIST: Fpt = 0.0001; // small step size for derivative approximation
    if let Some(curr) = cost_v2(initial.clone(), curve, 0.05) {
        //let mut deriv: Vec<Vec<Option<T>>> = vec![];
        let controls = points.to_vec();

        const SKIP_CHANCE: f64 = 0.0;

        let c = |i: usize| {
            let mut controls = controls.clone();
            let orig = controls[i].clone();
            // Generate all possible variations of the derivatives for this
            // control point
            let nudged = orig.nudged(T::from_f(NUDGE_DIST));
            let mut sublist = vec![];
            // For each control point, compute the gradient of the cost function
            // with respect to its derivatives.
            for np in nudged {
                if rand::thread_rng().gen_bool(SKIP_CHANCE) {
                    //log::debug!("Skipping trial for point {i}");
                    sublist.push(None);
                    continue;
                }
                //log::debug!("trial for point {i}");
                controls[i] = np;
                let params = hermite::Spline::<T>::new(&controls);
                let new_cost = cost_v2(initial.clone(), &params, 0.05);
                // Store the calculated gradients for this control point.
                sublist.push(new_cost.map(|c| T::from_f((c - curr) / NUDGE_DIST)));
            }
            sublist
        };

        use rayon::prelude::*;

        let mut deriv = (1..controls.len())
            .into_par_iter()
            .map(c)
            .collect::<Vec<_>>();
        //let mut deriv = (1..controls.len()).into_iter().map(c).collect::<Vec<_>>();
        //(1..controls.len()).for_each(c);
        // get the largest gradient magnitude across all control points. if gradient value is too large, normalize all
        // gradients by dividing by the maximum magnitude for the smoothness.
        let mut max_deriv_mag = 0.0;
        for dlist in &deriv {
            for d in dlist {
                if let Some(d) = d {
                    if d.abs() > max_deriv_mag {
                        max_deriv_mag = d.abs().to_f();
                    }
                }
            }
        }
        if max_deriv_mag > 1.0 {
            for dlist in &mut deriv {
                for d in dlist {
                    *d = d.clone().map(|inner| inner / T::from_f(max_deriv_mag));
                }
            }
        }
        // adjust each control point's derivatives using the calculated gradients.
        let mut iter = points.iter_mut();
        iter.next();
        for (p, d) in iter.zip(deriv) {
            p.descend_derivatives(&d, T::from_f(lr));
        }
        Some(curr)
        // function concludes by returning the current cost, which helps track progress over multiple optimization iterations.
    } else {
        None
    }
}

pub fn optimize_v3<T: MyFloat>(
    initial: &physics::PhysicsStateV3<T>,
    curve: &hermite::Spline<T>,
    points: &mut [point::Point<T>],
    lr: Fpt,
) -> Option<Fpt> {
    if let Some(base) = cost_v2(initial.clone(), curve, 0.05) {
        // choose a point
        let i = rand::thread_rng().gen_range(1..points.len());
        // choose a parameter
        let j = rand::thread_rng().gen_range(3..=11);
        //let v = points[i].get_at_i(j);

        let mut adjust_amt = 0.1;

        let mut should_adjust = |i: usize, j: usize, amt: Fpt| {
            let v = points[i].get_at_i(j);
            points[i].set_at_i(j, v.clone() + T::from_f(amt));
            let new_curve = hermite::Spline::new(points);
            let new_cost = cost_v2(initial.clone(), &new_curve, 0.05);
            // undo change
            points[i].set_at_i(j, v);
            new_cost.is_some_and(|new_cost| new_cost < base)
        };

        loop {
            if adjust_amt.abs() < tol() {
                log::warn!("Optimizer failed to find better parameter value");
                break;
            }
            if should_adjust(i, j, adjust_amt) {
                let v = points[i].get_at_i(j);
                points[i].set_at_i(j, v.clone() + T::from_f(adjust_amt));
                break;
            } else if adjust_amt < 0.0 {
                adjust_amt *= -0.1;
                log::debug!("New adjust mag: {}", use_sigfigs(&adjust_amt));
            } else {
                adjust_amt *= -1.0;
            }
        }

        Some(base)
    } else {
        None
    }
}