Skip to content

Commit a410d76

Browse files
committed
Improvements to debug plot utility
1 parent 3dc37fb commit a410d76

16 files changed

Lines changed: 273 additions & 332 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ To run the tests, use `cargo test --release`.
2121
The release flag helps with performance since the simulation is very slow in debug mode.
2222
Some of the tests are capable of producing plots using the [`plotters`](https://github.com/plotters-rs/plotters) crate.
2323
This can be enabled with the `plotters` feature flag, for example by running the tests as `cargo test --release --features "plotters"`.
24-
The resulting plots can be found in the `target` directory.
24+
The output can be found in the `target` directory of the respective crate.
2525
On Linux this feature requires some additional dependencies [as documented in the plotters readme](https://github.com/plotters-rs/plotters#dependencies).
2626

2727

@@ -93,9 +93,9 @@ For actually contributing to this repository please follow these steps:
9393

9494
4. **Wait and see if the build pipeline passes.** This automatic process ensures that the project still compiles successfully and all tests work as expected. Don't worry if this step fails initially, you can keep updating your branch with new commits until all checks have passed.
9595

96-
5. **Sign the Contributor License Agreement**. If this is your first contribution to VirtualBow, you will be asked to sign our [Contributor License Agreement](https://cla-assistant.io/bow-simulation/virtualbow) (CLA). This document clarifies the rights to your contribution and ensures that we can use your contribution(s) for our purposes. Our CLA is adapted from the [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf) with only minor changes, so it is a fairly standard license agreement.
96+
5. **Sign the Contributor License Agreement**. If this is your first contribution to VirtualBow, you will be asked to sign our [Contributor License Agreement](https://cla-assistant.io/bow-simulation/virtualbow) (CLA). This document clarifies the rights to your contribution and ensures that we can use your contribution(s) for our purposes. Our CLA is adapted from the [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf) with only minor changes.
9797

98-
6. **You are almost done!** Your changes will now be reviewed for merging as soon as possible.
98+
6. **Almost done!** Your changes will now be reviewed for merging as soon as possible.
9999

100100
# License
101101

solver/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ spec_math = "0.1.6"
5656
assert2 = "0.3.16"
5757
test_each_file = "0.3.6"
5858
cbindgen = "0.29.2"
59-
indexmap = "2.12.0"
59+
indexmap = "2.12.0"
60+
num-traits = "0.2.19"

solver/virtualbow/tests/test_example_bows.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ fn check_modal_properties(plotter: &mut Plotter, model: &BowModel) {
3737
assert_abs_diff_eq!(modes[0].zeta, model.damping.damping_ratio_limbs, epsilon=1e-5); // TODO: Can this be made more accurate?
3838

3939
for (i, mode) in modes.iter().enumerate() {
40-
plotter.add_point((i as f64, mode.omega), (i as f64, 0.0), "Modal Frequency", "Mode [-]", "Omega [1/s]");
41-
plotter.add_point((i as f64, mode.zeta), (i as f64, 0.0), "Modal Damping", "Mode [-]", "Zeta [-]");
40+
plotter.add_point("Modal Frequency", "Mode [-]", "Omega [1/s]", "Actual", (i, mode.omega));
41+
plotter.add_point("Modal Damping", "Mode [-]", "Zeta [-]", "Actual", (i, mode.zeta));
4242
}
4343
}
4444

@@ -512,14 +512,14 @@ fn check_dynamic_state_properties(plotter: &mut Plotter, model: &BowModel, outpu
512512

513513
// Todo: Find a way to check dynamics, i.e. Force = Mass x Acceleration
514514

515-
plotter.add_point((time, damping_energy_limbs), (time, 0.0), "Damping energy limbs", "Time [s]", "Energy [J]");
516-
plotter.add_point((time, damping_energy_string), (time, 0.0), "Damping energy string", "Time [s]", "Energy [J]");
515+
plotter.add_point("Damping energy limbs", "Time [s]", "Energy [J]", "Actual", (time, damping_energy_limbs));
516+
plotter.add_point("Damping energy string", "Time [s]", "Energy [J]", "Actual", (time, damping_energy_string));
517517

518-
plotter.add_point((time, damping_power_limbs), (time, 0.0), "Damping power limbs", "Time [s]", "Energy [J]");
519-
plotter.add_point((time, damping_power_string), (time, 0.0), "Damping power string", "Time [s]", "Energy [J]");
518+
plotter.add_point("Damping power limbs", "Time [s]", "Energy [J]", "Actual", (time, damping_power_limbs));
519+
plotter.add_point("Damping power string", "Time [s]", "Energy [J]", "Actual", (time, damping_power_string));
520520

521521
let total_energy = elastic_energy_limbs + elastic_energy_string + kinetic_energy_limbs + kinetic_energy_string + kinetic_energy_arrow + damping_energy_limbs + damping_energy_string;
522-
plotter.add_point((time, total_energy), (time, 0.0), "Total energy", "Time [s]", "Energy [J]");
522+
plotter.add_point("Total energy", "Time [s]", "Energy [J]", "Actual", (time, total_energy));
523523
}
524524
}
525525

solver/virtualbow_fem/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ iter_num_tools.workspace = true
2222
approx.workspace = true
2323
serde.workspace = true
2424
indexmap.workspace = true
25+
num-traits.workspace = true
2526

2627
[dev-dependencies]
2728
assert_matches.workspace = true
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pub mod plotter;
2-
pub mod plotter2;
32
pub mod syschecks;
43
pub mod sections;
54
pub mod curves;

solver/virtualbow_fem/src/testutils/plotter.rs

Lines changed: 85 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,52 @@
1-
use std::collections::HashMap;
2-
use itertools::chain;
1+
use indexmap::IndexMap;
2+
use num_traits::ToPrimitive;
33

44
// Utility for creating simple comparison plots for tests without having to aggregate the data manually.
55
// Create an instance and add points for various plots as needed. When the instance goes out of scope,
66
// the plot images are written to the target directory, named after the currently running test.
77
// Inspired by https://github.com/fabianboesiger/debug-plotter
88

9+
// TODO: Replace plotter module with plotter2
10+
911
pub struct Plotter {
10-
plots: HashMap<PlotInfo, PlotData>
12+
plots: IndexMap<String, PlotData> // IndexMap preserves order of insertion
1113
}
1214

1315
impl Plotter {
1416
pub fn new() -> Self {
1517
Self {
16-
plots: HashMap::new()
18+
plots: Default::default()
1719
}
1820
}
1921

20-
pub fn add_point(&mut self, point: (f64, f64), point_ref: (f64, f64), name: &str, x_label: &str, y_label: &str) {
21-
self.data_mut(name, x_label, y_label).add_point(point, point_ref);
22+
pub fn add_points<const N: usize, X, Y>(&mut self, title: &str, xlabel: &str, ylabel: &str, points: [(&str, X, Y); N])
23+
where X: ToPrimitive + Copy,
24+
Y: ToPrimitive + Copy
25+
{
26+
for i in 0..N {
27+
let (series, x, y) = points[i];
28+
self.data_mut(title, xlabel, ylabel, series, i).add_point((x, y)); // Color according to index
29+
}
2230
}
2331

24-
pub fn add_points<I1, I2>(&mut self, points: I1, points_ref: I2, name: &str, x_label: &str, y_label: &str)
25-
where I1: IntoIterator<Item=(f64, f64)>, I2: IntoIterator<Item=(f64, f64)>
32+
pub fn add_point<X, Y>(&mut self, title: &str, xlabel: &str, ylabel: &str, series: &str, point: (X, Y))
33+
where X: ToPrimitive,
34+
Y: ToPrimitive
2635
{
27-
self.data_mut(name, x_label, y_label).add_points(points, points_ref);
36+
self.data_mut(title, xlabel, ylabel, series, 0).add_point(point); // Color zero
2837
}
2938

30-
fn data_mut(&mut self, name: &str, x_label: &str, y_label: &str) -> &mut PlotData {
31-
let info = PlotInfo {
32-
name: name.into(),
33-
x_label: x_label.into(),
34-
y_label: y_label.into()
35-
};
39+
fn data_mut(&mut self, title: &str, xlabel: &str, ylabel: &str, series: &str, color: usize) -> &mut SeriesData {
40+
let plot = self.plots.entry(title.into()).or_insert(PlotData {
41+
series: Default::default(),
42+
xlabel: xlabel.into(),
43+
ylabel: ylabel.into(),
44+
});
3645

37-
self.plots.entry(info).or_default()
46+
plot.series.entry(series.into()).or_insert(SeriesData {
47+
points: Vec::new(),
48+
color,
49+
})
3850
}
3951
}
4052

@@ -66,89 +78,107 @@ impl Drop for Plotter {
6678
}
6779
}
6880

69-
#[derive(Hash, Eq, PartialEq)]
70-
struct PlotInfo {
71-
name: String,
72-
x_label: String,
73-
y_label: String
81+
#[allow(dead_code)]
82+
struct PlotData {
83+
series: IndexMap<String, SeriesData>, // IndexMap preserves order of insertion
84+
xlabel: String,
85+
ylabel: String,
7486
}
7587

76-
#[derive(Default)]
77-
struct PlotData {
88+
#[allow(dead_code)]
89+
struct SeriesData {
7890
points: Vec<(f64, f64)>,
79-
points_ref: Vec<(f64, f64)>,
91+
color: usize
8092
}
8193

82-
#[allow(dead_code)]
83-
impl PlotData {
84-
fn add_point(&mut self, point: (f64, f64), point_ref: (f64, f64)) {
85-
self.points.push(point);
86-
self.points_ref.push(point_ref);
94+
impl SeriesData {
95+
fn add_point<X, Y>(&mut self, point: (X, Y))
96+
where X: ToPrimitive,
97+
Y: ToPrimitive
98+
{
99+
let x = point.0.to_f64().unwrap();
100+
let y = point.1.to_f64().unwrap();
101+
self.points.push((x, y));
87102
}
88103

89-
fn add_points<I1, I2>(&mut self, points: I1, points_ref: I2)
90-
where I1: IntoIterator<Item=(f64, f64)>, I2: IntoIterator<Item=(f64, f64)>
91-
{
92-
self.points.extend(points);
93-
self.points_ref.extend(points_ref);
104+
fn x_min(&self) -> f64 {
105+
self.points.iter().map(|p| p.0).fold(f64::NAN, f64::min)
106+
}
107+
108+
fn x_max(&self) -> f64 {
109+
self.points.iter().map(|p| p.0).fold(f64::NAN, f64::max)
94110
}
95111

112+
fn y_min(&self) -> f64 {
113+
self.points.iter().map(|p| p.1).fold(f64::NAN, f64::min)
114+
}
115+
116+
fn y_max(&self) -> f64 {
117+
self.points.iter().map(|p| p.1).fold(f64::NAN, f64::max)
118+
}
119+
}
120+
121+
#[allow(dead_code)]
122+
impl PlotData {
96123
fn x_min(&self) -> f64 {
97-
chain!(&self.points, &self.points_ref).map(|pt| pt.0).fold(f64::NAN, f64::min)
124+
self.series.values().map(|s| s.x_min()).fold(f64::NAN, f64::min)
98125
}
99126

100127
fn x_max(&self) -> f64 {
101-
chain!(&self.points, &self.points_ref).map(|pt| pt.0).fold(f64::NAN, f64::max)
128+
self.series.values().map(|s| s.x_max()).fold(f64::NAN, f64::max)
102129
}
103130

104131
fn y_min(&self) -> f64 {
105-
chain!(&self.points, &self.points_ref).map(|pt| pt.1).fold(f64::NAN, f64::min)
132+
self.series.values().map(|s| s.y_min()).fold(f64::NAN, f64::min)
106133
}
107134

108135
fn y_max(&self) -> f64 {
109-
chain!(&self.points, &self.points_ref).map(|pt| pt.1).fold(f64::NAN, f64::max)
136+
self.series.values().map(|s| s.y_max()).fold(f64::NAN, f64::max)
110137
}
111138
}
112139

113140
// Actually creates the plot file from the given info and data (only if the optional "plotters" dependency is enabled)
114141
// The output directory is determined from the name of the current thread, which is named after the test method
115142
#[cfg(feature = "plotters")]
116-
fn create_plot(output_path: &str, info: &PlotInfo, data: &PlotData) {
143+
fn create_plot(output_path: &str, title: &str, data: &PlotData) {
117144
use plotters::backend::BitMapBackend;
118145
use plotters::chart::ChartBuilder;
119146
use plotters::drawing::IntoDrawingArea;
120-
use plotters::element::PathElement;
121147
use plotters::series::LineSeries;
122-
use plotters::style::WHITE;
123-
use plotters::style::BLACK;
124-
use plotters::style::BLUE;
125-
use plotters::style::RED;
148+
use plotters::element::PathElement;
149+
use plotters::style::{BLACK, WHITE, BLUE, RED, MAGENTA, CYAN, GREEN, YELLOW};
126150

127-
let file_path = format!("{}/{}.png", output_path, info.name.to_lowercase().replace(" ", "_"));
151+
let file_path = format!("{}/{}.png", output_path, title.to_lowercase().replace(" ", "_"));
128152
let root_area = BitMapBackend::new(&file_path, (1200, 800)).into_drawing_area();
129153
root_area.fill(&WHITE).unwrap();
130154

155+
156+
let colors = [BLUE, RED, MAGENTA, CYAN, GREEN, YELLOW];
157+
let color_from_index = |index: usize| {
158+
let idx = index % colors.len();
159+
colors[idx]
160+
};
161+
131162
let mut ctx = ChartBuilder::on(&root_area)
132163
.margin(30)
133164
.x_label_area_size(30)
134165
.y_label_area_size(60)
135-
.caption(&info.name, 20)
166+
.caption(&title, 20)
136167
.build_cartesian_2d(data.x_min()..data.x_max(), data.y_min()..data.y_max())
137168
.unwrap();
138169

139170
ctx.configure_mesh()
140-
.x_desc(&info.x_label)
141-
.y_desc(&info.y_label)
171+
.x_desc(&data.xlabel)
172+
.y_desc(&data.ylabel)
142173
.draw()
143174
.unwrap();
144175

145-
ctx.draw_series(LineSeries::new(data.points.iter().copied(), BLUE)).unwrap()
146-
.label("Actual")
147-
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
148-
149-
ctx.draw_series(LineSeries::new(data.points_ref.iter().copied(), RED)).unwrap()
150-
.label("Reference")
151-
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
176+
for (name, series) in &data.series {
177+
let color = color_from_index(series.color);
178+
ctx.draw_series(LineSeries::new(series.points.iter().copied(), color)).unwrap()
179+
.label(name)
180+
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
181+
}
152182

153183
ctx.configure_series_labels()
154184
.border_style(BLACK)

0 commit comments

Comments
 (0)