Icarus
Vehicle Simulation as a Transformable Computational Graph, built on Vulcan and Janus
Loading...
Searching...
No Matches
Linearizer.hpp
Go to the documentation of this file.
1#pragma once
2
14
15#include <icarus/core/Error.hpp>
18
19// Include SymbolicSimulatorCore and AutoDiff here (outside namespace)
20// to avoid namespace resolution issues
22#include <janus/math/AutoDiff.hpp>
23
24#include <Eigen/Dense>
25#include <memory>
26#include <string>
27#include <vector>
28
29namespace icarus {
30// Forward declaration - Simulator is defined in sim/Simulator.hpp
31class Simulator;
32} // namespace icarus
33
34namespace icarus::staging {
35
36// Forward declaration
38
39// =============================================================================
40// Linearizer Interface
41// =============================================================================
42
47 public:
48 virtual ~Linearizer() = default;
49
59 virtual ::icarus::staging::LinearModel Compute(::icarus::Simulator &sim,
60 const LinearizationConfig &config) = 0;
61};
62
63// =============================================================================
64// FiniteDifferenceLinearizer
65// =============================================================================
66
76 public:
77 struct Options {
78 double step_size{1e-7};
79 };
80
82 explicit FiniteDifferenceLinearizer(Options opts) : opts_(std::move(opts)) {}
83
85 const LinearizationConfig &config) override;
86
87 private:
88 Options opts_;
89};
90
91// =============================================================================
92// SymbolicLinearizer
93// =============================================================================
94
102 public:
104 const LinearizationConfig &config) override;
105};
106
107// =============================================================================
108// Factory
109// =============================================================================
110
114inline std::unique_ptr<Linearizer> CreateLinearizer(bool symbolic_enabled) {
115 if (symbolic_enabled) {
116 return std::make_unique<SymbolicLinearizer>();
117 }
118 return std::make_unique<FiniteDifferenceLinearizer>();
119}
120
121} // namespace icarus::staging
122
123// =============================================================================
124// Implementation
125// =============================================================================
126
128
129namespace icarus::staging {
130
131inline ::icarus::staging::LinearModel
134 model.state_names = config.states;
135 model.input_names = config.inputs;
136 model.output_names = config.outputs;
137
138 const int nx = static_cast<int>(config.states.size());
139 const int nu = static_cast<int>(config.inputs.size());
140 const int ny = static_cast<int>(config.outputs.size());
141 const double h = opts_.step_size;
142 const double t = sim.Time();
143
144 if (nx == 0) {
145 throw ConfigError("Linearization requires at least one state");
146 }
147
148 // Store operating point
149 model.x0.resize(nx);
150 for (int i = 0; i < nx; ++i) {
151 model.x0(i) = sim.Peek(config.states[i]);
152 }
153 model.u0.resize(nu);
154 for (int i = 0; i < nu; ++i) {
155 model.u0(i) = sim.Peek(config.inputs[i]);
156 }
157 model.t0 = t;
158 // Get full state vector and its size
159 Eigen::VectorXd state_backup = sim.GetState();
160 const auto n_states_total = state_backup.size();
161
162 // Resolve mapping from config.states names to full-state indices
163 // This uses the DataDictionary to find the actual state positions
164 std::vector<int> state_indices;
165 state_indices.reserve(nx);
166 auto dict = sim.GetDataDictionary();
167
168 for (const auto &state_name : config.states) {
169 bool found = false;
170 // Search through components to find state offset
171 Eigen::Index current_offset = 0;
172 for (const auto &comp_entry : dict.components) {
173 // Check if this component owns the state by matching the prefix
174 if (state_name.rfind(comp_entry.name + ".", 0) == 0) {
175 // Found the component - need to find which state index within it
176 // States are typically ordered: x, y, z, vx, vy, vz, etc.
177 // We look for the local state name after the component prefix
178 std::string local_state = state_name.substr(comp_entry.name.size() + 1);
179
180 // Get component state size from outputs that look like state variables
181 std::size_t comp_state_size = 0;
182 std::vector<std::string> state_like_outputs;
183 for (const auto &output : comp_entry.outputs) {
184 // State variables are typically position, velocity, attitude, etc.
185 std::string local = output.name.substr(comp_entry.name.size() + 1);
186 if (local.find("position") != std::string::npos ||
187 local.find("velocity") != std::string::npos ||
188 local.find("attitude") != std::string::npos ||
189 local.find("omega") != std::string::npos ||
190 local.find(".x") != std::string::npos ||
191 local.find(".y") != std::string::npos ||
192 local.find(".z") != std::string::npos) {
193 state_like_outputs.push_back(local);
194 comp_state_size++;
195 }
196 }
197
198 // Find position of this state within component's states
199 for (std::size_t i = 0; i < state_like_outputs.size(); ++i) {
200 if (local_state == state_like_outputs[i]) {
201 state_indices.push_back(static_cast<int>(current_offset + i));
202 found = true;
203 break;
204 }
205 }
206 if (found)
207 break;
208 }
209 // Estimate component state size based on typical 6DOF patterns
210 // This is a heuristic - components with position/velocity have 6 states
211 std::size_t estimated_state_size = 0;
212 for (const auto &output : comp_entry.outputs) {
213 std::string local = output.name.substr(comp_entry.name.size() + 1);
214 if (local.find("position") != std::string::npos ||
215 local.find("velocity") != std::string::npos) {
216 estimated_state_size = 6; // 3 position + 3 velocity
217 break;
218 }
219 }
220 current_offset += static_cast<Eigen::Index>(estimated_state_size);
221 }
222
223 if (!found) {
224 // Fallback: try direct registry lookup by name pattern
225 // Assume states are named like "Component.position.x" and map to indices
226 // based on the position in config.states list
227 state_indices.push_back(static_cast<int>(state_indices.size()));
228 }
229 }
230
231 // If mapping failed, warn and fall back to sequential indices
232 // This matches SymbolicLinearizer's fallback behavior
233 bool mapping_failed = false;
234 for (int i = 0; i < nx; ++i) {
235 if (state_indices[i] >= static_cast<int>(n_states_total)) {
236 mapping_failed = true;
237 break;
238 }
239 }
240 if (mapping_failed) {
241 sim.GetLogger().Log(
243 "[LIN] FiniteDifferenceLinearizer: State index mapping produced out-of-bounds indices. "
244 "Falling back to sequential indices. This may produce incorrect Jacobian results.");
245 // Guard: ensure we have enough states in the simulator to populate all indices
246 if (n_states_total < static_cast<Eigen::Index>(nx)) {
247 throw ConfigError("FiniteDifferenceLinearizer: Cannot linearize - simulator has only " +
248 std::to_string(n_states_total) +
249 " states but linearization config requires " + std::to_string(nx) +
250 " states.");
251 }
252 state_indices.clear();
253 for (int i = 0; i < nx; ++i) {
254 state_indices.push_back(i);
255 }
256 }
257
258 // Compute A = df/dx (central differences on derivatives)
259 model.A.resize(nx, nx);
260
261 for (int j = 0; j < nx; ++j) {
262 int full_idx = state_indices[j]; // Index in full state vector
263
264 Eigen::VectorXd state_plus = state_backup;
265 Eigen::VectorXd state_minus = state_backup;
266
267 state_plus(full_idx) += h;
268 state_minus(full_idx) -= h;
269
270 // Forward perturbation
271 sim.SetState(state_plus);
272 Eigen::VectorXd xdot_plus = sim.ComputeDerivatives(t);
273
274 // Backward perturbation
275 sim.SetState(state_minus);
276 Eigen::VectorXd xdot_minus = sim.ComputeDerivatives(t);
277
278 // Central difference - extract only the rows corresponding to selected states
279 Eigen::VectorXd col(nx);
280 for (int i = 0; i < nx; ++i) {
281 int row_idx = state_indices[i];
282 col(i) = (xdot_plus(row_idx) - xdot_minus(row_idx)) / (2.0 * h);
283 }
284 model.A.col(j) = col;
285 }
286
287 // Restore state
288 sim.SetState(state_backup);
289
290 // Compute B = df/du
291 model.B.resize(nx, nu);
292 for (int j = 0; j < nu; ++j) {
293 double u0_j = sim.Peek(config.inputs[j]);
294
295 // Forward perturbation
296 sim.Poke(config.inputs[j], u0_j + h);
297 Eigen::VectorXd xdot_plus = sim.ComputeDerivatives(t);
298
299 // Backward perturbation
300 sim.Poke(config.inputs[j], u0_j - h);
301 Eigen::VectorXd xdot_minus = sim.ComputeDerivatives(t);
302
303 // Central difference - extract only the rows corresponding to selected states
304 Eigen::VectorXd col(nx);
305 for (int i = 0; i < nx; ++i) {
306 int row_idx = state_indices[i];
307 col(i) = (xdot_plus(row_idx) - xdot_minus(row_idx)) / (2.0 * h);
308 }
309 model.B.col(j) = col;
310
311 // Restore
312 sim.Poke(config.inputs[j], u0_j);
313 }
314
315 // Helper: get outputs
316 auto get_y = [&]() {
317 Eigen::VectorXd y(ny);
318 for (int i = 0; i < ny; ++i) {
319 y(i) = sim.Peek(config.outputs[i]);
320 }
321 return y;
322 };
323
324 // Compute C = dg/dx
325 model.C.resize(ny, nx);
326 if (ny > 0) {
327 for (int j = 0; j < nx; ++j) {
328 int full_idx = state_indices[j]; // Index in full state vector
329
330 Eigen::VectorXd state_plus = state_backup;
331 Eigen::VectorXd state_minus = state_backup;
332
333 state_plus(full_idx) += h;
334 state_minus(full_idx) -= h;
335
336 // Forward perturbation
337 sim.SetState(state_plus);
338 sim.ComputeDerivatives(t); // Update outputs
339 Eigen::VectorXd y_plus = get_y();
340
341 // Backward perturbation
342 sim.SetState(state_minus);
343 sim.ComputeDerivatives(t);
344 Eigen::VectorXd y_minus = get_y();
345
346 // Central difference
347 model.C.col(j) = (y_plus - y_minus) / (2.0 * h);
348 }
349
350 // Restore state
351 sim.SetState(state_backup);
352 }
353
354 // Compute D = dg/du
355 model.D.resize(ny, nu);
356 if (ny > 0 && nu > 0) {
357 for (int j = 0; j < nu; ++j) {
358 double u0_j = sim.Peek(config.inputs[j]);
359
360 // Forward perturbation
361 sim.Poke(config.inputs[j], u0_j + h);
362 sim.ComputeDerivatives(t);
363 Eigen::VectorXd y_plus = get_y();
364
365 // Backward perturbation
366 sim.Poke(config.inputs[j], u0_j - h);
367 sim.ComputeDerivatives(t);
368 Eigen::VectorXd y_minus = get_y();
369
370 // Central difference
371 model.D.col(j) = (y_plus - y_minus) / (2.0 * h);
372
373 // Restore
374 sim.Poke(config.inputs[j], u0_j);
375 }
376 }
377
378 // Restore final state
379 sim.SetState(state_backup);
380
381 return model;
382}
383
384// =============================================================================
385// SymbolicLinearizer Implementation
386// =============================================================================
387
388inline ::icarus::staging::LinearModel
390 using Scalar = janus::SymbolicScalar;
391
393 model.state_names = config.states;
394 model.input_names = config.inputs;
395 model.output_names = config.outputs;
396
397 const int nx = static_cast<int>(config.states.size());
398 const int nu = static_cast<int>(config.inputs.size());
399 const int ny = static_cast<int>(config.outputs.size());
400
401 if (nx == 0) {
402 throw ConfigError("SymbolicLinearizer: requires at least one state");
403 }
404
405 // Store operating point from numeric simulator
406 model.t0 = sim.Time();
407 model.x0.resize(nx);
408 for (int i = 0; i < nx; ++i) {
409 model.x0(i) = sim.Peek(config.states[i]);
410 }
411 model.u0.resize(nu);
412 for (int i = 0; i < nu; ++i) {
413 model.u0(i) = sim.Peek(config.inputs[i]);
414 }
415
416 // Create symbolic simulator
417 SymbolicSimulatorCore sym_sim(sim.GetConfig());
418
419 const std::size_t n_states_total = sym_sim.GetStateSize();
420
421 // Create symbolic variables - MUST use sym_vec_pair to get pure symbolic MX
422 auto [x_vec, x_mx] = janus::sym_vec_pair("x", static_cast<int>(n_states_total));
423 Scalar t_sym = janus::sym("t");
424
425 // Create symbolic inputs
426 std::vector<Scalar> u_elements;
427 Scalar u_mx;
428 if (nu > 0) {
429 auto [u_v, u_m] = janus::sym_vec_pair("u", nu);
430 for (int i = 0; i < nu; ++i) {
431 u_elements.push_back(u_v(i));
432 }
433 u_mx = u_m;
434 }
435
436 // Set symbolic state and time
437 sym_sim.SetState(x_vec);
438 sym_sim.SetTime(t_sym);
439
440 // Apply symbolic inputs
441 for (int i = 0; i < nu; ++i) {
442 sym_sim.SetSignal(config.inputs[i], u_elements[i]);
443 }
444
445 // Compute derivatives symbolically
446 JanusVector<Scalar> xdot_vec = sym_sim.ComputeDerivatives();
447
448 // Find indices of selected states in global state vector using state bindings
449 std::vector<int> state_indices;
450 state_indices.reserve(nx);
451 const auto &bindings = sym_sim.GetStateBindings();
452 for (int j = 0; j < nx; ++j) {
453 bool found = false;
454 for (std::size_t bi = 0; bi < bindings.size(); ++bi) {
455 if (config.states[j] == bindings[bi].name) {
456 state_indices.push_back(static_cast<int>(bi));
457 found = true;
458 break;
459 }
460 }
461 if (!found) {
462 // Name not found - will use fallback below
463 }
464 }
465
466 // If we couldn't match by name, assume states are in order of the state vector
467 if (state_indices.size() != static_cast<std::size_t>(nx)) {
468 // Warn about fallback behavior - this could produce incorrect results
469 // if state ordering doesn't match config expectations
470 sim.GetLogger().Log(
472 "[LIN] SymbolicLinearizer: Could not match " +
473 std::to_string(nx - static_cast<int>(state_indices.size())) +
474 " state name(s) to state layout. Falling back to index-based mapping. "
475 "This may produce incorrect Jacobian results.");
476 // Guard: ensure we have enough states in the symbolic simulator to populate all indices
477 if (n_states_total < static_cast<std::size_t>(nx)) {
478 throw ConfigError(
479 "SymbolicLinearizer: Cannot linearize - symbolic simulator has only " +
480 std::to_string(n_states_total) + " states but linearization config requires " +
481 std::to_string(nx) + " states.");
482 }
483 state_indices.clear();
484 for (int i = 0; i < nx; ++i) {
485 state_indices.push_back(i);
486 }
487 }
488
489 // Build symbolic xdot vector for selected states
490 std::vector<Scalar> xdot_selected;
491 xdot_selected.reserve(nx);
492 for (int idx : state_indices) {
493 xdot_selected.push_back(xdot_vec(idx));
494 }
495 Scalar xdot_mx = Scalar::vertcat(xdot_selected);
496
497 // Compute A = df/dx using symbolic Jacobian
498 // Use the full x_mx symbol (pure MX) for differentiation, then extract relevant columns
499 Scalar A_full_sym = janus::jacobian({xdot_mx}, {x_mx});
500
501 // Compute B = df/du
502 Scalar B_sym;
503 if (nu > 0) {
504 B_sym = janus::jacobian({xdot_mx}, {u_mx});
505 }
506
507 // Build symbolic outputs
508 std::vector<Scalar> y_elements;
509 y_elements.reserve(ny);
510 for (const auto &output_name : config.outputs) {
511 if (sym_sim.HasSignal(output_name)) {
512 y_elements.push_back(sym_sim.GetSignal(output_name));
513 } else {
514 throw ConfigError("SymbolicLinearizer: Unknown output signal '" + output_name + "'");
515 }
516 }
517
518 Scalar C_full_sym, D_sym;
519 if (ny > 0) {
520 Scalar y_mx = Scalar::vertcat(y_elements);
521
522 // Compute C = dg/dx (full Jacobian)
523 C_full_sym = janus::jacobian({y_mx}, {x_mx});
524
525 // Compute D = dg/du
526 if (nu > 0) {
527 D_sym = janus::jacobian({y_mx}, {u_mx});
528 }
529 }
530
531 // Create janus::Functions for evaluation
532 std::vector<janus::SymbolicArg> all_inputs;
533 all_inputs.push_back(t_sym);
534 all_inputs.push_back(x_mx);
535 if (nu > 0) {
536 all_inputs.push_back(u_mx);
537 }
538
539 janus::Function A_full_func("A_full", all_inputs, {A_full_sym});
540
541 // Build full state vector for evaluation:
542 // - Start with the full operating-point state from the simulator
543 // - Override selected state positions with model.x0 values using state_indices
544 Eigen::VectorXd full_state = Eigen::VectorXd::Zero(static_cast<Eigen::Index>(n_states_total));
545
546 // Get the full operating-point state from simulator
547 Eigen::VectorXd sim_state = sim.GetState();
548 for (Eigen::Index k = 0;
549 k < std::min(sim_state.size(), static_cast<Eigen::Index>(n_states_total)); ++k) {
550 full_state(k) = sim_state(k);
551 }
552
553 // Map model.x0 values into their correct positions using state_indices
554 for (int j = 0; j < nx; ++j) {
555 int k = state_indices[j]; // Full state index for selected state j
556 full_state(k) = model.x0(j);
557 }
558
559 // Evaluate A matrix (full) and extract selected columns
560 // Call function with properly typed arguments: (t0, x, [u])
561 std::vector<Eigen::MatrixXd> A_full_result;
562 if (nu > 0) {
563 A_full_result = A_full_func(model.t0, full_state, model.u0);
564 } else {
565 A_full_result = A_full_func(model.t0, full_state);
566 }
567
568 model.A.resize(nx, nx);
569 for (int i = 0; i < nx; ++i) {
570 for (int j = 0; j < nx; ++j) {
571 int col_idx = state_indices[j]; // Map to full state column
572 model.A(i, j) = A_full_result[0](i, col_idx);
573 }
574 }
575
576 // Evaluate B matrix
577 if (nu > 0) {
578 janus::Function B_func("B", all_inputs, {B_sym});
579 auto B_result = B_func(model.t0, full_state, model.u0);
580 model.B.resize(nx, nu);
581 for (int i = 0; i < nx; ++i) {
582 for (int j = 0; j < nu; ++j) {
583 model.B(i, j) = B_result[0](i, j);
584 }
585 }
586 } else {
587 model.B.resize(nx, 0);
588 }
589
590 // Evaluate C matrix (full) and extract selected columns
591 if (ny > 0) {
592 janus::Function C_full_func("C_full", all_inputs, {C_full_sym});
593 std::vector<Eigen::MatrixXd> C_full_result;
594 if (nu > 0) {
595 C_full_result = C_full_func(model.t0, full_state, model.u0);
596 } else {
597 C_full_result = C_full_func(model.t0, full_state);
598 }
599 model.C.resize(ny, nx);
600 for (int i = 0; i < ny; ++i) {
601 for (int j = 0; j < nx; ++j) {
602 int col_idx = state_indices[j]; // Map to full state column
603 model.C(i, j) = C_full_result[0](i, col_idx);
604 }
605 }
606 } else {
607 model.C.resize(0, nx);
608 }
609
610 // Evaluate D matrix
611 if (ny > 0 && nu > 0) {
612 janus::Function D_func("D", all_inputs, {D_sym});
613 auto D_result = D_func(model.t0, full_state, model.u0);
614 model.D.resize(ny, nu);
615 for (int i = 0; i < ny; ++i) {
616 for (int j = 0; j < nu; ++j) {
617 model.D(i, j) = D_result[0](i, j);
618 }
619 }
620 } else {
621 model.D.resize(ny, nu);
622 model.D.setZero();
623 }
624
625 return model;
626}
627
628} // namespace icarus::staging
Consolidated error handling for Icarus.
Simulator and subsystem configuration structs.
Top-level simulation coordinator.
Core types for staging subsystem (trim, linearization, symbolic).
Lightweight symbolic simulator for graph extraction.
Configuration/parsing errors with optional file context.
Definition Error.hpp:185
void Log(LogLevel level, const std::string &message)
Log raw message.
Definition MissionLogger.hpp:584
Top-level simulation coordinator.
Definition Simulator.hpp:70
double Peek(const std::string &name) const
Read a signal value by name.
Definition Simulator.hpp:190
Eigen::VectorXd GetState() const
Get current state vector.
Definition Simulator.hpp:1097
const SimulatorConfig & GetConfig() const
Get simulator configuration.
Definition Simulator.hpp:300
void Poke(const std::string &name, double value)
Write a signal value by name.
Definition Simulator.hpp:211
Eigen::VectorXd ComputeDerivatives(double t)
Compute derivatives for current state at time t.
Definition Simulator.hpp:1101
void SetState(const Eigen::VectorXd &X)
Set state vector.
Definition Simulator.hpp:1099
MissionLogger & GetLogger()
Get the mission logger.
Definition Simulator.hpp:216
double Time() const
Get current simulation time (MET - derived from epoch).
Definition Simulator.hpp:143
DataDictionary GetDataDictionary() const
Get data dictionary for the simulation.
Definition Simulator.hpp:1191
FiniteDifferenceLinearizer()
Definition Linearizer.hpp:81
FiniteDifferenceLinearizer(Options opts)
Definition Linearizer.hpp:82
::icarus::staging::LinearModel Compute(::icarus::Simulator &sim, const LinearizationConfig &config) override
Compute linear model at current operating point.
Definition Linearizer.hpp:132
Abstract linearizer interface.
Definition Linearizer.hpp:46
virtual ~Linearizer()=default
virtual::icarus::staging::LinearModel Compute(::icarus::Simulator &sim, const LinearizationConfig &config)=0
Compute linear model at current operating point.
Linearizer using symbolic Jacobians.
Definition Linearizer.hpp:101
::icarus::staging::LinearModel Compute(::icarus::Simulator &sim, const LinearizationConfig &config) override
Compute linear model at current operating point.
Definition Linearizer.hpp:389
Lightweight symbolic simulator for graph extraction.
Definition SymbolicSimulatorCore.hpp:48
JanusVector< Scalar > ComputeDerivatives()
Compute derivatives symbolically.
Definition SymbolicSimulatorCore.hpp:101
void SetTime(Scalar t)
Set time (symbolic).
Definition SymbolicSimulatorCore.hpp:90
bool HasSignal(const std::string &name) const
Check if signal exists.
Definition SymbolicSimulatorCore.hpp:182
const std::vector< StateBinding< Scalar > > & GetStateBindings() const
Get state bindings (for introspection).
Definition SymbolicSimulatorCore.hpp:196
void SetState(const JanusVector< Scalar > &x)
Set state vector (symbolic).
Definition SymbolicSimulatorCore.hpp:80
void SetSignal(const std::string &name, Scalar value)
Write signal value (symbolic).
Definition SymbolicSimulatorCore.hpp:177
Scalar GetSignal(const std::string &name) const
Read signal value (symbolic).
Definition SymbolicSimulatorCore.hpp:170
std::size_t GetStateSize() const
Get total state size.
Definition SymbolicSimulatorCore.hpp:149
Definition Simulator.hpp:53
std::unique_ptr< Linearizer > CreateLinearizer(bool symbolic_enabled)
Create appropriate linearizer based on configuration.
Definition Linearizer.hpp:114
Definition AggregationTypes.hpp:13
@ Warning
Potential issues.
Definition Console.hpp:40
Linearization configuration.
Definition SimulatorConfig.hpp:200
std::vector< std::string > states
State variables (for A matrix rows/cols).
Definition SimulatorConfig.hpp:204
std::vector< std::string > inputs
Input variables (for B matrix cols).
Definition SimulatorConfig.hpp:207
std::vector< std::string > outputs
Output variables (for C matrix rows).
Definition SimulatorConfig.hpp:210
double step_size
Finite difference step size.
Definition Linearizer.hpp:78
Linear state-space model.
Definition LinearModel.hpp:31
std::vector< std::string > state_names
Definition LinearModel.hpp:37
Eigen::MatrixXd B
Input matrix (n_states x n_inputs).
Definition LinearModel.hpp:33
double t0
Definition LinearModel.hpp:44
Eigen::MatrixXd D
Feedthrough matrix (n_outputs x n_inputs).
Definition LinearModel.hpp:35
Eigen::MatrixXd C
Output matrix (n_outputs x n_states).
Definition LinearModel.hpp:34
Eigen::MatrixXd A
State matrix (n_states x n_states).
Definition LinearModel.hpp:32
Eigen::VectorXd u0
Definition LinearModel.hpp:43
Eigen::VectorXd x0
Operating point where linearization was performed.
Definition LinearModel.hpp:42
std::vector< std::string > input_names
Definition LinearModel.hpp:38
std::vector< std::string > output_names
Definition LinearModel.hpp:39