/*!
The file `main.od` contains the description of the outputs to be generated by executing the [`Output`][crate::experiments::Action::Output] action in an experiment.
see [`create_output`] for documentation on the configuration syntax of this file.
*/
use std::fs::{self,File};
use std::io::{Write};
use std::cmp::Ordering;
use std::process::Command;
use std::collections::{HashSet,BTreeMap};
use std::rc::Rc;
use std::fmt::Debug;
use std::path::PathBuf;
use crate::config_parser::{ConfigurationValue,Expr};
use crate::config::{self,evaluate,reevaluate,values_to_f32_with_count};
use crate::experiments::ExperimentFiles;
use crate::error::{Error,SourceLocation};
use crate::{get_git_id,get_version_number,source_location,match_object_panic,match_object,error};
/** Creates some output using an output description object as guide.
### Comma separated values
Creates a .csv with the given `fields` as columns.
```ignore
CSV
{
//The fields to be included as columns of the CSV file.
fields: [=configuration.traffic.pattern.legend_name,=configuration.traffic.load,=configuration.legend_name,=result.accepted_load],
//the name of the field to be generated
filename: "results.csv",
}
```
### Plots of data
See the reference of [Plotkind] for detailed information.
```ignore
Plots
{
//The file will contain a figure for each value of the selector. Each figure receives the filtered data.
//In this example each traffic pattern get its own figure.
selector: =configuration.traffic.pattern.legend_name,
//A list of [Plotkind]s with the information of what to draw in each figure.
//This example contains a simple chart plot, with a point for each value of `offered_load`, using that same value as abscissas (a.k.a. x axis) and the value of `accepted_load` in ordinates (a.k.a. y axis). These values are averaged respect to the other parameters, such as `random_seed`.
kind: [Plotkind{
parameter: =configuration.traffic.load,
abscissas: =configuration.traffic.load,
label_abscissas: "offered load",
ordinates: =result.accepted_load,
label_ordinates: "accepted load",
min_ordinate: 0.0,
max_ordinate: 1.0,
}],
//The value to use for both legend and lines to draw.
//In this example each combination of routing and base configuration gets a line.
legend: [=configuration.routing.legend_name,=configuration.legend_name],
//Prefix to use in texmporal files and similar. Must contain only simple characters and should be unique.
prefix: "throughput",
//The backend to actually draw the data. Only `Tikz` is supported. To execute the output action with this backend it is required a latex installation including the `pgfplots` latex package, which may be located at the `texlive-pictures` package of some linux distributions. Its temporal files are stored into a `tikz_tmp` directory, which may be inspected in case of errors.
backend: Tikz
{
//A generated file with latex code to generate the plots. Prepared to be inserted into another document; it is not an standalone file.
tex_filename: "throughput.tex",
//A pdf generated with the plots. Its source is actually in the `tikz_tmp` directory, which has some additional preambles than `tex_filename`.
pdf_filename: "throughput.pdf",
},
},
```
### Preprocessing of data
A `PreprocessArgMax` process the results and creates a file containing an array with the maximum
values of some expression together the value of an auxiliary expression.
The generated file is binary file containing a [ConfigurationValue::Array] with as many elements as experiments and in the `i`th entry contains `PreprocessedArgMax{argument:computed_argument_value,maximum_value:computed_maximum_value}`.
```ignore
//This example find where accepted load is maximum.
PreprocessArgMax
{
//The file to be generated
filename: "peak.cfg",
//We apply the maximum to each subset of the data with same value of the `selector`.
selector: [ =configuration.traffic.pattern.legend_name, =configuration.routing.legend_name , =configuration.legend_name ],
//The target expression to be maximized
target: =result.accepted_load,
//The auxiliary expression. Its value associated to the same entry that maximized the `target` value is stored.
argument: =configuration.traffic.load,
},
```
The generated file can be used by following output description.
The expression [FileExpression][evaluate] evaluates an expression into a file
and with `at{container:file_data,position:index}` we access the corresponding record,
as `index` is a variable with the experiment number.
For example, to use this `peak.cfg`.
```ignore
Plots
{
//Note the selector is a bit different, as the preprocessing included also the legend of this plot.
selector: [=configuration.traffic.pattern.legend_name,],
kind: [Plotkind{
parameter: =configuration.traffic.load,
abscissas: =configuration.traffic.load,
// --- get greatest worst server with average close to peak.
// we read the stored value and compare it with the accepted load plus an epsilon.
ordinates: =if{
condition: lt{
first:FileExpression
{
filename: "peak.cfg",
expression:at{container:file_data,position:index}
}.maximum_value,
second:add{first:result.accepted_load, second:0.02}
},
true_expression: result.server_percentile0.accepted_load,
false_expression: 0,
},
label_abscissas: "offered load",
label_ordinates: "conditional load",
}],
legend: [=configuration.routing.legend_name,=configuration.legend_name],
prefix: "salud",
backend: Tikz
{
tex_filename: "salud.tex",
pdf_filename: "salud.pdf",
},
},
```
*/
pub fn create_output(description: &ConfigurationValue, environment: &mut OutputEnvironment)
-> Result<(),Error>
{
if let &ConfigurationValue::Object(ref name, ref _attributes) = description
{
match name.as_ref()
{
"CSV" =>
{
println!("Creating a CSV...");
return create_csv(description,environment);
},
"Plots" =>
{
println!("Creating a plot...");
return create_plots(description,environment);
},
"PreprocessArgMax" =>
{
println!("Creating a file with ArgMax preprocessing...");
return create_preprocess_arg_max(description,environment);
},
_ => return Err(Error::ill_formed_configuration(source_location!(),description.clone()).with_message(format!("unrecognized output description object {}",name))),
};
}
else
{
return Err(Error::ill_formed_configuration(source_location!(),description.clone()).with_message("Output description is not an object.".to_string() ));
};
}
/**
Encapsulation of the information we have when trying to build output files. Like
* For which instances on the main.cfg do we have results?
* The paths employed.
* Are we trying something weird like feeding from a CSV without the normal main.cfg? yet to implement.
*/
#[derive(Debug)]
pub struct OutputEnvironment<'a>
{
/// UPDATE: Contains triplets `( experiment_index, experiment, result )`.
results: Vec<OutputEnvironmentEntry>,
///The total number of experiment entries in the main.cfg.
total_experiments: usize,
///The files associated to the experiment.
files: &'a ExperimentFiles,
selector_map: EnumeratedMap<ConfigurationValue>,
legend_map: EnumeratedMap<ConfigurationValue>,
/// When not None, only generate targets in the list.
pub targets: &'a Option<Vec<String>>,
}
#[derive(Debug)]
pub struct OutputEnvironmentEntry
{
experiment_index: usize,
pub experiment: Option<ConfigurationValue>,
pub result: Option<ConfigurationValue>,
pub csv: Option<ConfigurationValue>,
}
impl OutputEnvironmentEntry
{
pub fn new(experiment_index:usize) -> Self
{
OutputEnvironmentEntry{
experiment_index,
experiment:None,
result:None,
csv:None,
}
}
pub fn with_experiment(mut self,experiment:ConfigurationValue) -> Self
{
self.experiment=Some(experiment);
self
}
pub fn with_result(mut self,result:ConfigurationValue) -> Self
{
self.result=Some(result);
self
}
pub fn with_csv(mut self,csv:ConfigurationValue) -> Self
{
self.csv=Some(csv);
self
}
/// Much like config::combine ...
pub fn config(&self) -> ConfigurationValue
{
let mut attrs = vec![
(String::from("index"),ConfigurationValue::Number(self.experiment_index as f64)),
];
if let Some(configuration) = &self.experiment
{
attrs.push( (String::from("configuration"),configuration.clone()) );
}
if let Some(result) = &self.result
{
attrs.push( (String::from("result"),result.clone()) );
}
if let Some(csv) = &self.csv
{
attrs.push( (String::from("csv"),csv.clone()) );
}
ConfigurationValue::Object(String::from("Context"),attrs)
}
}
impl<'a> OutputEnvironment<'a>
{
pub fn new(results: Vec<OutputEnvironmentEntry>, total_experiments: usize, files: &'a ExperimentFiles, targets:&'a Option<Vec<String>>) -> OutputEnvironment<'a>
{
OutputEnvironment{
results,
total_experiments,
files,
selector_map: EnumeratedMap::default(),
legend_map: EnumeratedMap::default(),
targets,
}
}
///Iterate over `ConfigurationValue`s with the context of each result.
///Just like `config::combine`.
pub fn iter(&self) -> OutputEnvironmentIterator
{
OutputEnvironmentIterator{
environment: self,
index:0,
}
}
///Returns the total of experiments in the configuration
pub fn total_experiments(&self) -> usize
{
self.total_experiments
}
///Return the number of experiments for which we have results.
pub fn available_results(&self) -> usize
{
self.results.len()
}
/// Apply f(self,experiment_index,config) to each entry.
pub fn map<F>(&mut self,mut f:F) -> Result<(),Error>
where F: FnMut(&mut OutputEnvironment,usize,ConfigurationValue)->Result<(),Error>
{
//for (index,(experiment_index,configuration,result)) in self.results.iter().enumerate()
for index in 0..self.results.len()
{
//let (experiment_index,configuration,result) = &self.results[index];
//let experiment_index = *experiment_index;
let experiment_index = self.results[index].experiment_index;
let context=self.results[index].config();
f(self,experiment_index,context)?;
}
Ok(())
}
}
pub struct OutputEnvironmentIterator<'a>
{
environment: &'a OutputEnvironment<'a>,
index: usize,
}
impl<'a> Iterator for OutputEnvironmentIterator<'a>
{
type Item = ConfigurationValue;
fn next(&mut self) -> Option<Self::Item>
{
if self.index<self.environment.results.len() {
//let (experiment_index,configuration,result) = &self.environment.results[self.index];
//let context=combine(*experiment_index,configuration,result);
let context = self.environment.results[self.index].config();
self.index+=1;
Some(context)
} else {
None
}
}
}
///Creates a csv file using filename and field given in `description`.
fn create_csv(description: &ConfigurationValue, environment:&mut OutputEnvironment)
-> Result<(),Error>
{
let mut fields=None;
let mut filename=None;
match_object_panic!(description,"CSV",value,
"fields" => match value
{
&ConfigurationValue::Array(ref a) => fields=Some(a.iter().map(|v|{
match v{
&ConfigurationValue::Expression(ref expr) => {
(format!("{expr}"), expr.clone())
},
&ConfigurationValue::Array(ref arr) => {
if arr.len() != 2
{
panic!("Each CSV header must be an Expression or an Array [Name,Expression].");
}
let h = arr[0].as_str().expect("bad value for fields");
let e = arr[1].as_expr().expect("bad value for fields");
(h.to_string(),e.clone())
},
_ => panic!("bad value for fields"),
}
}).collect::<Vec<(String,Expr)>>()),
_ => panic!("bad value for fields"),
}
"filename" => match value
{
&ConfigurationValue::Literal(ref s) => filename=Some(s.to_string()),
_ => panic!("bad value for filename ({:?})",value),
}
);
let fields=fields.expect("There were no fields");
let filename=filename.expect("There were no filename");
if let Some(targets) = environment.targets {
if !targets.contains(&filename) {
//TODO: perhaps we need a success type.
return Ok(());
}
};
println!("Creating CSV with name \"{}\"",filename);
let path = environment.files.get_outputs_path();
let output_path=path.join(filename);
let mut output_file=File::create(&output_path).expect("Could not create output file.");
//let header=fields.iter().map(|e|format!("{}",e)).collect::<Vec<String>>().join(", ");
let (headers,fields) : (Vec<_>,Vec<_>) = fields.into_iter().unzip();
let header = headers.join(", ");
writeln!(output_file,"{}",header).unwrap();
for context in environment.iter()
{
//let row=fields.iter().map(|e| format!("{}",evaluate(e,&context,&path)) ).collect::<Vec<String>>().join(", ");
//let row=fields.iter().map(|e| evaluate(e,&context,&path).expect("ERROR TO BE TRANSPOSED").to_csv_field() ).collect::<Vec<String>>().join(", ");
let row=fields.iter()
.map(|e| Ok(evaluate(e,&context,&path)?.to_csv_field()) )
.collect::<Result<Vec<String>,Error>>()?
.join(", ");
writeln!(output_file,"{}",row).unwrap();
}
Ok(())
}
///The raw `ConfigurationValue`s to be used in a plot. Before being averaged.
#[derive(PartialEq,PartialOrd,Debug)]
struct RawRecord
{
/// Index associated to the selector. To keep things in order.
selector_index: usize,
/// Index associated to the legend. To keep things in order.
legend_index: usize,
///The selector refers to some Figure
selector: ConfigurationValue,
///The legend refers to the line inside a Figure
legend: ConfigurationValue,
///The parameter refers to some point in a line, for which the average is being made.
parameter: ConfigurationValue,
///The value in the abscissas (a.k.a., x-axis).
abscissa: ConfigurationValue,
///The value in the ordinates (a.k.a., x-axis).
ordinate: ConfigurationValue,
///The value of the upper whisker in box plots.
upper_whisker: Option<ConfigurationValue>,
///The value of the bottom whisker in box plots.
bottom_whisker: Option<ConfigurationValue>,
///The value of the upmost part of the box in box plots.
upper_box_limit: Option<ConfigurationValue>,
///The value of the lowest part of the box in box plots.
bottom_box_limit: Option<ConfigurationValue>,
///The value of the middle line in the box in box plots.
box_middle: Option<ConfigurationValue>,
///The git_id of the binary that produced the simulation.
git_id: ConfigurationValue,
///The version of the binary the produced the simulation.
version_number: ConfigurationValue,
}
///The `f32`-averaged values to be used in a plot.
#[derive(Debug)]
struct AveragedRecord
{
///The selector refers to some Figure
selector: ConfigurationValue,
///The legend refers to the line inside a Figure
legend: ConfigurationValue,
///The parameter refers to some point in a line, for which the average is being made.
//It does not seem to be needed at the present moment.
parameter: ConfigurationValue,
///The average value and standard deviation in the abscissas (a.k.a., x-axis).
abscissa: (Option<f32>,Option<f32>),
///The average value and standard deviation in the ordinates (a.k.a., x-axis).
ordinate: (Option<f32>,Option<f32>),
///Keep the original value if it is shared by all the averaged.
shared_abscissa: Option<ConfigurationValue>,
///The value of the upper whisker in box plots.
upper_whisker: Option<f32>,
///The value of the bottom whisker in box plots.
bottom_whisker: Option<f32>,
///The value of the upmost part of the box in box plots.
upper_box_limit: Option<f32>,
///The value of the lowest part of the box in box plots.
bottom_box_limit: Option<f32>,
///The value of the middle line in the box in box plots.
box_middle: Option<f32>,
///Set of involved `git_id`s.
version_set: HashSet<String>,
}
/**
A description of a specific plot (axis and data).
## Chart plot
```ignore
Plotkind{
//A point will be generated for each value of parameter.
parameter: =configuration.traffic.load,
//The averaged value of abscissas is used as the x coordinate.
abscissas: =configuration.traffic.load,
//A text to use as label of the x axix.
label_abscissas: "offered load",
//The averaged value of ordinates is used as the y coordinate.
ordinates: =result.accepted_load,
//A text to use as label of the y axix.
label_ordinates: "accepted load",
//To enforce a minimum value of the y axis.
min_ordinate: 0.0,
//To enforce a maximum value of the y axis.
max_ordinate: 1.0,
//To enforce a minimum value of the x axis.
//min_abscissa
//To enforce a maximum value of the x axis.
//max_abscissa
}
```
The options `ordinate` or `histogram` can be used instead of the `ordinates` to use an [ConfigurationValue::Expression] that evaluates to an array. The abscissas will be the first naturals in such case.
## Bar plot
Just add `bar: true` to make the plot to have bars instead of lines with marks.
## Whisker and box plots
A Box plot with whiskers can be built by defining the value `upper_box_limit` and the related ones.
```ignore
//This example assumes `statistics_server_percentiles: [0,25,50,75,100],` to be included in the `main.cfg`.
Plotkind{
parameter: =configuration.traffic.pattern.legend_name,
abscissas: =configuration.traffic.pattern.legend_name,
label_abscissas: "traffic pattern",
ordinates: =result.accepted_load,
label_ordinates: "throughput",
//Example with the upper whisker representing the greatest value of accepted among all servers.
upper_whisker: =result.server_percentile100.accepted_load,
//Similarly the whisker downwards uses the minimum value of the accepted load among the servers.
bottom_whisker: =result.server_percentile0.accepted_load,
//For the box limits it is common to use Q1 and Q3 quartiles.
upper_box_limit: =result.server_percentile75.accepted_load,
bottom_box_limit: =result.server_percentile25.accepted_load,
//The middle line commonly represents the median value.
box_middle: =result.server_percentile50.accepted_load,
min_ordinate: 0.0,
}
```
**/
#[derive(Debug)]
pub struct Plotkind<'a>
{
parameter: Option<&'a ConfigurationValue>,
abscissas: Option<&'a ConfigurationValue>,
ordinates: Option<&'a ConfigurationValue>,
histogram: Option<&'a ConfigurationValue>,
array: Option<&'a ConfigurationValue>,
label_abscissas: String,
label_ordinates: String,
min_ordinate: Option<f32>,
max_ordinate: Option<f32>,
min_abscissa: Option<f32>,
max_abscissa: Option<f32>,
///Whether to be a bar plot
bar: bool,
///Apply an expression after averaging.
///It can be used to normalize relative to another line.
/// ordinate_post_expression: =div{first:average,second:at{container:all,position:4}}
ordinate_post_expression: Option<Expr>,
//parameters used in box plots.
upper_whisker: Option<&'a ConfigurationValue>,
bottom_whisker: Option<&'a ConfigurationValue>,
upper_box_limit: Option<&'a ConfigurationValue>,
bottom_box_limit: Option<&'a ConfigurationValue>,
box_middle: Option<&'a ConfigurationValue>,
/// Raw string for the backend. Use with care.
raw: Option<String>,
}
#[derive(Debug)]
struct PlotData
{
data: Vec<AveragedRecord>,
}
impl PlotData
{
pub fn with_capacity(capacity:usize) -> PlotData
{
PlotData{
data: Vec::with_capacity(capacity)
}
}
pub fn push(&mut self, record:AveragedRecord)
{
self.data.push(record);
}
pub fn len(&self) -> usize
{
self.data.len()
}
}
impl<'a> Plotkind<'a>
{
fn new(description: &'a ConfigurationValue)->Plotkind<'a>
{
let mut parameter=None;
let mut abscissas=None;
let mut ordinates=None;
let mut histogram=None;
let mut array=None;
let mut label_abscissas=None;
let mut label_ordinates=None;
let mut min_ordinate=None;
let mut max_ordinate=None;
let mut min_abscissa=None;
let mut max_abscissa=None;
let mut bar=false;
let mut upper_whisker=None;
let mut bottom_whisker=None;
let mut upper_box_limit=None;
let mut bottom_box_limit=None;
let mut box_middle=None;
let mut ordinate_post_expression=None;
let mut raw=None;
match_object_panic!(description,"Plotkind",value,
"parameter" => parameter=Some(value),
"abscissas" => abscissas=Some(value),
"ordinates" => ordinates=Some(value),
"histogram" => histogram=Some(value),
"array" => array=Some(value),
"label_abscissas" => label_abscissas=Some(value.as_str().unwrap_or_else(|_|panic!("bad value for label_abscissas ({:?})",value)).to_string()),
"label_ordinates" => label_ordinates=Some(value.as_str().unwrap_or_else(|_|panic!("bad value for label_ordinates ({:?})",value)).to_string()),
"min_ordinate" => min_ordinate=Some(value.as_f64().expect("bad value for min_ordinate") as f32),
"max_ordinate" => max_ordinate=Some(value.as_f64().expect("bad value for max_ordinate") as f32),
"min_abscissa" => min_abscissa=Some(value.as_f64().expect("bad value for min_abscissa") as f32),
"max_abscissa" => max_abscissa=Some(value.as_f64().expect("bad value for max_abscissa") as f32),
"bar" => bar=value.as_bool().expect("bad value for bar"),
"ordinate_post_expression" => match &value
{
&ConfigurationValue::Expression(e) => ordinate_post_expression = Some(e.clone()),
_ => panic!("bad value for ordinate_post_expression"),
}
"upper_whisker" => upper_whisker=Some(value),
"bottom_whisker" => bottom_whisker=Some(value),
"upper_box_limit" => upper_box_limit=Some(value),
"bottom_box_limit" => bottom_box_limit=Some(value),
"box_middle" => box_middle=Some(value),
"raw" => raw=Some(value.as_str().expect("bad value for raw").to_string()),
);
//let parameter=parameter.expect("There were no parameter");
//let abscissas=abscissas.expect("There were no abscissas");
//let ordinates=ordinates.expect("There were no ordinates");
let label_abscissas=label_abscissas.expect("There were no label_abscissas");
let label_ordinates=label_ordinates.expect("There were no label_ordinates");
Plotkind{
parameter,
abscissas,
ordinates,
histogram,
array,
label_abscissas,
label_ordinates,
min_ordinate,
max_ordinate,
min_abscissa,
max_abscissa,
bar,
ordinate_post_expression,
upper_whisker,
bottom_whisker,
upper_box_limit,
bottom_box_limit,
box_middle,
raw,
}
}
}
///Create plots according to a `Plots` object.
fn create_plots(description: &ConfigurationValue, environment:&mut OutputEnvironment)
-> Result<(),Error>
{
let mut selector=None;
let mut legend=None;
let mut backend=None;
let mut prefix=None;
let mut kind:Option<Vec<Plotkind>>=None;
match_object!(description,"Plots",value,
"selector" => selector=Some(value),
"legend" => legend=Some(value),
"backend" => backend=Some(value),
"kind" => kind = Some(value.as_array()?.iter().map(Plotkind::new).collect()),
"prefix" => prefix=Some(value.as_str()?.to_string()),
);
let selector = selector.ok_or_else(||description.ill("There were no selector"))?;
let legend=legend.ok_or_else(||description.ill("There were no legend"))?;
let kind=kind.ok_or_else(||description.ill("There were no kind"))?;
let backend=backend.ok_or_else(||description.ill("There were no backend"))?;
let prefix=prefix.unwrap_or_else(||"noprefix".to_string());
let outputs_path = environment.files.get_outputs_path();
println!("Creating plots");
let mut avgs:Vec<PlotData>=Vec::with_capacity(kind.len());
//let git_id_expr = Expr::Ident("git_id".to_string());
let git_id_expr = Expr::Member( Rc::new(Expr::Ident("result".to_string())) , "git_id".to_string() );
let version_number_expr = Expr::Member( Rc::new(Expr::Ident("result".to_string())) , "version_number".to_string() );
//let mut selector_map = EnumeratedMap::default();
//let mut legend_map = EnumeratedMap::default();
//let mut selector_map = environment.selector_map;
//for context in environment.iter()
environment.map(|environment,_index,context|
{
//A first pass to populate maps
let selector=reevaluate(selector,&context,&outputs_path)?;
let legend=reevaluate(legend,&context,&outputs_path)?;
environment.selector_map.insert( &selector );
environment.legend_map.insert( &legend );
Ok(())
})?;
for pk in kind.iter()
{
println!("averaging plot {:?}",pk);
//Each plot should be a map? `plot` with plot[legend_value][abscissa_value]=(ordinate_value,abscissa_deviation,ordinate_deviation)
//And there should be a plot for each value of selector. Should this be a list or another map?
//But first we compute (selector,legend_value,abscissa_value,ordinate_value) for each result.
let mut records=Vec::with_capacity(environment.available_results());
let array = if let Some(ref data) = pk.histogram { Some(data) } else { pk.array.as_ref() };
let boxplot:bool = pk.upper_box_limit.is_some();
//if let Some(histogram)=pk.histogram
if let Some(data)=array
{
//for context in environment.iter()
environment.map(|environment,_index,context|
{
let histogram_values=reevaluate(data,&context,&outputs_path)?;
let selector=reevaluate(selector,&context,&outputs_path)?;
let legend=reevaluate(legend,&context,&outputs_path)?;
let git_id = evaluate(&git_id_expr,&context,&outputs_path)?;
let version_number = evaluate(&version_number_expr,&context,&outputs_path)?;
let selector_index = environment.selector_map.insert( &selector );
let legend_index = environment.legend_map.insert( &legend );
let abscissa_array = pk.abscissas.map(|cv|reevaluate(cv,&context,&outputs_path)).transpose()?;
match histogram_values
{
ConfigurationValue::Array(ref l)=>
{
//let total:f64 = l.iter().map(|cv|match cv{
// ConfigurationValue::Number(x) => x,
// _ => panic!("adding an array of non-numbers for a histogram"),
//}).sum();
let factor:Option<f64> = if pk.histogram.is_some()
{
let total:f64 = l.iter().map(|cv|match cv{
ConfigurationValue::Number(x) => x,
_ => panic!("adding an array of non-numbers for a histogram"),
}).sum();
Some(1f64 / total)
} else { None };
for (h_index,h_value) in l.iter().enumerate()
{
let ordinate=if let &ConfigurationValue::Number(amount)=h_value
{
if let Some(factor)=factor
{
ConfigurationValue::Number(amount * factor)
}
else
{
ConfigurationValue::Number(amount)
}
}
else
{
panic!("A histogram count/array value should be a number (instead of {})",h_value);
};
let abscissa = match abscissa_array
{
None => ConfigurationValue::Number(h_index as f64),
Some(ConfigurationValue::Array(ref x)) => x[h_index].clone(),
_ => panic!("The abscissa is not an array when using array to build the ordinates"),
};
let record=RawRecord{
selector_index,
legend_index,
selector:selector.clone(),
legend:legend.clone(),
parameter: abscissa.clone(),
abscissa,
//ordinate: h_value.clone(),
ordinate,
upper_whisker:None,
bottom_whisker:None,
upper_box_limit:None,
bottom_box_limit:None,
box_middle:None,
git_id: git_id.clone(),
version_number: version_number.clone(),
};
records.push(record);
}
}
ConfigurationValue::None => println!("WARNING: ignoring null histogram/array."),
_ => panic!("histogram/array from non-Array"),
}
Ok(())
})?;
}
else
{
//for context in environment.iter()
environment.map(|environment,_index,context|
{
let selector=reevaluate(selector,&context,&outputs_path)?;
let legend=reevaluate(legend,&context,&outputs_path)?;
let selector_index = environment.selector_map.insert( &selector );
let legend_index = environment.legend_map.insert( &legend );
let record=RawRecord{
selector_index,
legend_index,
selector,
legend,
parameter:reevaluate(pk.parameter.unwrap(),&context,&outputs_path)?,
abscissa:reevaluate(pk.abscissas.unwrap(),&context,&outputs_path)?,
ordinate: pk.ordinates.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?.unwrap_or_default(),
upper_whisker: pk.upper_whisker.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?,
bottom_whisker: pk.bottom_whisker.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?,
upper_box_limit: pk.upper_box_limit.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?,
bottom_box_limit: pk.bottom_box_limit.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?,
box_middle: pk.box_middle.map(|v|reevaluate(v,&context,&outputs_path)).transpose()?,
git_id: evaluate(&git_id_expr,&context,&outputs_path).unwrap_or_default(),
version_number: evaluate(&version_number_expr,&context,&outputs_path).unwrap_or_default(),
};
//println!("{:?}",record);
records.push(record);
Ok(())
})?;
}
records.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less));
//println!("ordered as");
//for record in records.iter()
//{
// println!("{:?}",record);
//}
let mut averaged=PlotData::with_capacity(records.len());
let mut index=0;
while index<records.len()
{
//let &(ref selector_value,ref legend_value,ref parameter_value,_,_)=&records[index];
let &RawRecord{selector:ref selector_value,legend:ref legend_value,parameter:ref parameter_value,..}=&records[index];
let mut current_abscissas:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_ordinates:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_upper_whiskers:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_bottom_whiskers:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_upper_box_limits:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_bottom_box_limits:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut current_box_middles:Vec<ConfigurationValue>=Vec::with_capacity(records.len());
let mut version_set : HashSet<String> = HashSet::new();
while index<records.len() && records[index].selector==*selector_value && records[index].legend==*legend_value && records[index].parameter==*parameter_value
{
//let &(_,_,_,ref abscissa_value,ref ordinate_value) = &records[index];
let &RawRecord{abscissa:ref abscissa_value,ordinate:ref ordinate_value,
upper_whisker: ref upper_whisker_value,
bottom_whisker: ref bottom_whisker_value,
upper_box_limit: ref upper_box_limit_value,
bottom_box_limit: ref bottom_box_limit_value,
box_middle: ref box_middle_value,
git_id: ref git_value,
version_number: ref version_value,..} = &records[index];
current_abscissas.push(abscissa_value.clone());
current_ordinates.push(ordinate_value.clone());
if boxplot
{
upper_whisker_value.as_ref().map(|v|current_upper_whiskers.push(v.clone()));
bottom_whisker_value.as_ref().map(|v|current_bottom_whiskers.push(v.clone()));
upper_box_limit_value.as_ref().map(|v|current_upper_box_limits.push(v.clone()));
bottom_box_limit_value.as_ref().map(|v|current_bottom_box_limits.push(v.clone()));
box_middle_value.as_ref().map(|v|current_box_middles.push(v.clone()));
}
index+=1;
//if let ConfigurationValue::Literal(ref git_str)=git_value
//{
// version_set.insert(git_str.to_string());
//}
let used_git = if let ConfigurationValue::Literal(ref git_str)=git_value {
if git_str.len()>2 { Some(git_str.as_ref()) }
else { None }
} else { None };
let used_version = if let ConfigurationValue::Literal(ref version_str) = version_value {
if version_str.len()>1 { Some(version_str.as_ref()) }
else { None }
} else { None };
let version = version_string(used_git,used_version);
version_set.insert(version);
}
let averaged_record = AveragedRecord{selector:selector_value.clone(),legend:legend_value.clone(),parameter:parameter_value.clone(),abscissa:standard_deviation(¤t_abscissas),ordinate:standard_deviation(¤t_ordinates),shared_abscissa:shared_element(&mut current_abscissas.iter()).cloned(),
upper_whisker:if boxplot {standard_deviation(¤t_upper_whiskers).0} else {None},
bottom_whisker:if boxplot {standard_deviation(¤t_bottom_whiskers).0} else {None},
upper_box_limit:if boxplot {standard_deviation(¤t_upper_box_limits).0} else {None},
bottom_box_limit:if boxplot {standard_deviation(¤t_bottom_box_limits).0} else {None},
box_middle:if boxplot {standard_deviation(¤t_box_middles).0} else {None},
version_set};
if let Some(x) = averaged_record.abscissa.0
{
if let Some(xmin) = pk.min_abscissa
{
if xmin > x
{
continue;
}
}
if let Some(xmax) = pk.max_abscissa
{
if xmax < x
{
continue;
}
}
}
averaged.push(averaged_record);
}
if let Some(expression)=&pk.ordinate_post_expression
{
//If a post expression is required make another pass.
let mut selection_map : BTreeMap<String,Vec<usize>> = BTreeMap::new();
for (index,record) in averaged.data.iter().enumerate()
{
let &AveragedRecord{selector:ref selector_value,parameter:ref parameter_value,..}=record;
//ConfigurationValue cannot implement Ord...
let key = (selector_value,parameter_value);
let key = format!("{:?}",key);
match selection_map.get_mut(&key)
{
Some(ref mut values) =>
{
values.push(index);
}
None =>
{
selection_map.insert(key,vec![index]);
}
};
}
for (key,indices) in selection_map
{
//let collection : Vec<f64> = averaged[record_index..collection_end].iter().map(|r|r.ordinate.0.unwrap() as f64).collect();
let collection : Vec<f64> = indices.iter().map(|&index|averaged.data[index].ordinate.0.unwrap() as f64).collect();
println!("key is {} values are {:?}",key,&collection);
let collection_cv = ConfigurationValue::Array(collection.iter().map(|&x|ConfigurationValue::Number(x)).collect());
for index in indices
{
let record = &mut averaged.data[index];
let ordinate = record.ordinate.0.unwrap();
let context=ConfigurationValue::Object(String::from("Context"),vec![
(String::from("average"),ConfigurationValue::Number(ordinate as f64)),
(String::from("all"),collection_cv.clone()),
]);
match evaluate(expression,&context,&outputs_path)?
{
ConfigurationValue::Number(new_ordinate) =>
{
dbg!(index,ordinate,new_ordinate);
record.ordinate.0 = Some(new_ordinate as f32);
//The variance is no longer valid.
record.ordinate.1 = None;
}
_ => panic!(),
}
}
}
//--
//let mut record_index=0;
//while record_index<averaged.len()
//{
// let mut collection_end=record_index;
// {
// let &AveragedRecord{selector:ref selector_value,parameter:ref parameter_value,..}=&averaged[record_index];
// while collection_end<averaged.len() && averaged[collection_end].selector==*selector_value && averaged[collection_end].parameter==*parameter_value
// {
// collection_end+=1;
// }
// }
// println!("collection is {:?}",&averaged[record_index..collection_end]);
// record_index=collection_end;
//}
}
//println!("averaged as {averaged:?}");
avgs.push(averaged);
}
if let &ConfigurationValue::Object(ref name, ref _attributes) = backend
{
match name.as_ref()
{
//"Tikz" => return tikz_backend(backend,avgs,kind,(environment.results.len(),environment.total_experiments),prefix,environment.files),
"Tikz" => return tikz_backend(backend,avgs,kind,environment,prefix),
_ => panic!("unrecognized backend object {}",name),
};
}
else
{
panic!("backend is not an object.");
};
}
///Rewrites text into Latex code that output that text.
pub fn latex_protect_text(text:&str) -> String
{
text.chars().map(|c|match c{
'_' => "\\_".to_string(),
'%' => "\\%".to_string(),
x => format!("{}",x),
}).collect::<String>()
}
///Make a command name trying to include all of `text`, replacing all non-alphabetic characters.
fn latex_make_command_name(text:&str) -> String
{
text.chars().map(|c|match c{
'a'..='z' | 'A'..='Z' => format!("{}",c),
'0' => "R".to_string(),
'1' => "RI".to_string(),
'2' => "RII".to_string(),
'3' => "RIII".to_string(),
'4' => "RIV".to_string(),
'5' => "RV".to_string(),
'6' => "RVI".to_string(),
'7' => "RVII".to_string(),
'8' => "RVIII".to_string(),
'9' => "RIX".to_string(),
'_' => "u".to_string(),
':' => "c".to_string(),
_ => "x".to_string(),
}).collect::<String>()
}
// ///Make text appropriate for tikz symbolic coordinates.
// ///Because reasons like boxplots, I have moved to use numerical coordinates with textual tick labels, so this becomes obsolete.
// fn latex_make_symbol(text:&str) -> String
// {
// text.trim().chars().map(|c|match c{
// '\n' => " ".to_string(),
// '%' => "\\%".to_string(),
// ',' => "s".to_string(),
// '[' | ']' => "b".to_string(),
// '{' | '}' => "c".to_string(),
// x => format!("{}",x),
// }).collect::<String>()
// }
///Draw a plot using the tikz backend.
///`backend`: contains options to the backend
///`averages[kind_index][point_index]`: contains the data to be plotted. The data is ordered by selector, which is not an index.
///`kind`: the configuration of the plots
///`amount_experiments`: (experiments_with_results, total) of the experiments
///`files`: An ExperimentFiles struct with the information on where to generate each thing.
fn tikz_backend(backend: &ConfigurationValue, averages: Vec<PlotData>, kind:Vec<Plotkind>, environment:&mut OutputEnvironment, prefix:String)
-> Result<(),Error>
{
let mut tex_filename=None;
let mut pdf_filename=None;
match_object!(backend,"Tikz",value,
"tex_filename" => tex_filename = Some(value.as_str()?.to_string()),
"pdf_filename" => pdf_filename = Some(value.as_str()?.to_string()),
);
let tex_filename=tex_filename.ok_or_else(||backend.ill("There were no tex_filename"))?;
let pdf_filename=pdf_filename.ok_or_else(||backend.ill("There were no pdf_filename"))?;
if let Some(targets) = environment.targets {
if !targets.contains(&tex_filename) && !targets.contains(&pdf_filename) {
//TODO: perhaps we need a success type.
return Ok(());
}
};
let outputs_path = environment.files.get_outputs_path();
let root = environment.files.root.clone().unwrap();
let tex_path=&outputs_path.join(tex_filename);
println!("Creating {:?}",tex_path);
let mut tex_file=File::create(&tex_path).expect("Could not create tex file.");
let mut tikz=String::new();
let ymin:Vec<String>=kind.iter().map(|kd| match kd.min_ordinate{
None => String::new(),
Some(x) => format!("ymin={},",x),
}).collect();
let ymax:Vec<String>=kind.iter().map(|kd| match kd.max_ordinate{
None => String::new(),
Some(x) => format!("ymax={},",x),
}).collect();
let xmin:Vec<String>=kind.iter().map(|kd| match kd.min_abscissa{
None => String::new(),
Some(x) => format!("xmin={},",x),
}).collect();
let xmax:Vec<String>=kind.iter().map(|kd| match kd.max_abscissa{
None => String::new(),
Some(x) => format!("xmax={},",x),
}).collect();
//let mut xsymbols : Vec<String> = vec![];
//for kind in averages.iter()
//{
// for average in kind.iter()
// {
// if average.abscissa.0.is_none()
// {
// if let Some(ref symbol) = average.shared_abscissa
// {
// let str_symbol: String = format!("{}",symbol);
// if !xsymbols.contains(&str_symbol)
// {
// xsymbols.push(str_symbol);
// }
// }
// }
// }
//}
let mut all_legend_tex_id_vec:Vec<String> = Vec::new();
let mut all_legend_tex_id_set:HashSet<String> = HashSet::new();
let mut all_legend_tex_entry:HashSet<String> = HashSet::new();
let folder=root.canonicalize().expect("path does not have canonical form").file_name().expect("could not get name of the root folder").to_str().unwrap().to_string();
let folder_id = latex_make_command_name(&folder);
//while index<averaged.len()
//let mut figure_index=0;
let mut all_git_ids: HashSet<String> = HashSet::new();
let mut offsets:Vec<usize>=vec![0;kind.len()];//to keep track of the offset as progressing in selectors.
let tmp_path=root.join("tikz_tmp");
// latex_jobs=[(X,Y)], where Y should be regenerated if X changes.
let mut latex_jobs : Vec<(PathBuf,PathBuf,String)> = vec![];
//We try to make a figure for each selector. Then in each figure we make a tikzpicture for each PlotKind.
'figures: loop
{
let mut wrote=0;//amount of plotkinds already written. We use this as end condition.
let mut tracked_selector_value=None;
let mut figure_tikz=String::new();
for kind_index in 0..kind.len()
{
//println!("averages.len()={}",averages.len());
//println!("averages[{}].len()={}",kind_index,averages[kind_index].len());
if offsets[kind_index]>=averages[kind_index].len()
{
//There are not any points left.
//continue;
break 'figures;
}
let kaverages=&averages[kind_index].data;
let koffset=&mut offsets[kind_index];
let kd=&kind[kind_index];
let selector_value=&kaverages[*koffset].selector;
let selector_value_to_use = if let Some(value)=tracked_selector_value
{
if selector_value != value
{
println!("warning: missing data");
//continue;
break 'figures;
}
value
}
else
{
tracked_selector_value = Some(selector_value);
selector_value
};
if wrote==0
{
let selectorname=latex_make_command_name(&tracked_selector_value.unwrap().to_string());
figure_tikz.push_str(&format!(r#"
\begin{{experimentfigure}}%
%\begin{{center}}
\centering%
\tikzpicturedependsonfile{{externalized-plots/external-{folder_id}-{prefix}-selector{selectorname}-kind0.md5}}%
\tikzsetnextfilename{{externalized-legends/legend-{folder_id}-{prefix}-{selectorname}}}%
\pgfplotslegendfromname{{legend-{folder_id}-{prefix}-{selectorname}}}\\"#,selectorname=selectorname,prefix=prefix,folder_id=folder_id));
let dependency_str = format!("external-{folder_id}-{prefix}-selector{selectorname}-kind0.pdf");
//let target_str = format!("legend-{folder_id}-{prefix}-{selectorname}");
let target_str = format!("externalized-legends/legend-{folder_id}-{prefix}-{selectorname}");
let dependency = tmp_path.join("externalized-plots").join(dependency_str);
//let target = tmp_path.join("externalized-legends").join(target_str);
let target = tmp_path.join(&target_str);
latex_jobs.push( (dependency,target,target_str) );
}
wrote+=1;
let mut pre_plots=String::new();
let mut raw_plots_data = vec![];
//let mut raw_plots=String::new();
let mut good_plots=0;
let mut symbols:Vec<String> = vec![];
let boxplot:bool = kd.upper_box_limit.is_some();
while *koffset<kaverages.len() && *selector_value_to_use==kaverages[*koffset].selector
{
let legend_value=&kaverages[*koffset].legend;
let legend_tex_entry= latex_protect_text(&legend_value.to_string());
let legend_tex_id = latex_make_command_name(&legend_value.to_string());
//all_legend_tex_id.insert(legend_tex_id.clone());//XXX Can we avoid the clone when not necessary?
if !all_legend_tex_id_set.contains(&legend_tex_id)
{
all_legend_tex_id_set.insert(legend_tex_id.clone());
all_legend_tex_id_vec.push(legend_tex_id.clone());
}
//XXX this line can be improved
let legend_index = all_legend_tex_id_vec.iter().enumerate().find_map(|(index,s)|if s==&legend_tex_id {Some(index)} else {None}).unwrap();
all_legend_tex_entry.insert(format!("\\def\\{}text{{{}}}\n",legend_tex_id,legend_tex_entry));
//all_legend_tex_entry.insert(format!("\\expandafter\\def\\csname {}text\\endcsname{{{}}}\n",legend_tex_id,legend_tex_entry));
//let mut current_raw_plot=String::new();
let mut current_raw_plot_data = Vec::new();
let mut drawn_points=0;
// to_draw elements are of the form (x value, y value, x deviation, y deviation)
let mut to_draw:Vec<(f32,f32,f32,f32)> = Vec::with_capacity(kaverages.len());
// symbolic_to_draw are of the form (x name, y value, y deviation)
let mut symbolic_to_draw: Vec<(String,f32,f32)> = Vec::with_capacity(kaverages.len());
// to box elements are of the form (x value, top whisker, bottom whisker, top box, bottom box, middle mox, mark)
let mut to_box:Vec<(f32,Option<f32>,Option<f32>,Option<f32>,Option<f32>,Option<f32>,Option<f32>)> = Vec::with_capacity(kaverages.len());
while *koffset<kaverages.len() && *selector_value_to_use==kaverages[*koffset].selector && *legend_value==kaverages[*koffset].legend
{
let (abscissa_average,abscissa_deviation)=kaverages[*koffset].abscissa;
let (ordinate_average,ordinate_deviation)=kaverages[*koffset].ordinate;
if boxplot
{
let mut x : f32 = if let Some(xvalue) = abscissa_average { xvalue } else
{
let symbol = kaverages[*koffset].shared_abscissa.as_ref().expect("no abscissa found");
let str_symbol: String = latex_protect_text(&format!("{}",symbol));
if let Some(symbol_index) = symbols.iter().enumerate().find_map(|(index,s)|if s==&str_symbol {Some(index)} else {None})
{
symbol_index as f32
} else {
symbols.push(str_symbol);
(symbols.len()-1) as f32
}
};
x += -0.3 + legend_index as f32*0.1;
to_box.push( (x,
kaverages[*koffset].upper_whisker,
kaverages[*koffset].bottom_whisker,
kaverages[*koffset].upper_box_limit,
kaverages[*koffset].bottom_box_limit,
kaverages[*koffset].box_middle,
kaverages[*koffset].ordinate.0,
) );
drawn_points+=1;
} else {
if let (Some(x),Some(y))=(abscissa_average,ordinate_average)
{
let abscissa_deviation=abscissa_deviation.unwrap_or(0f32);
let ordinate_deviation=ordinate_deviation.unwrap_or(0f32);
to_draw.push( (x,y,abscissa_deviation,ordinate_deviation) );
drawn_points+=1;
} else if let (Some(symbol),Some(y)) = (kaverages[*koffset].shared_abscissa.as_ref(),ordinate_average)
{
let ordinate_deviation=ordinate_deviation.unwrap_or(0f32);
let str_symbol: String = latex_protect_text(&format!("{}",symbol));
symbolic_to_draw.push( (str_symbol,y,ordinate_deviation) );
drawn_points+=1;
}
}
for git_id in kaverages[*koffset].version_set.iter()
{
all_git_ids.insert(git_id.clone());
}
*koffset+=1;
}
let mut drawn_in_range=0;
if drawn_points>=1
{
let cmp = | x:&f32, y:&f32 | if x==y { std::cmp::Ordering::Equal } else if x<y {std::cmp::Ordering::Less} else {std::cmp::Ordering::Greater};
let to_draw_x_min = if let Some(x)=kd.min_abscissa
{
Some(x)
}
else
{
//to_draw.iter().map(|t|t.0).min_by(cmp)
to_draw.iter().map(|t|t.0).chain( to_box.iter().map(|t|t.0) ).min_by(cmp)
};
let to_draw_x_max = if let Some(x)=kd.max_abscissa
{
Some(x)
}
else
{
//to_draw.iter().map(|t|t.0).max_by(cmp)
to_draw.iter().map(|t|t.0).chain( to_box.iter().map(|t|t.0) ).max_by(cmp)
};
let to_draw_y_min = if let Some(y)=kd.min_ordinate
{
y
}
else
{
//to_draw.iter().map(|t|t.1).min_by(cmp).expect("no points")
//to_draw.iter().map(|t|t.1).chain( symbolic_to_draw.iter().map(|t|t.1) ).min_by(cmp).expect("no points")
to_draw.iter().map(|t|t.1).chain( symbolic_to_draw.iter().map(|t|t.1) ).
chain( to_box.iter().filter_map(|t|t.1) ).
chain( to_box.iter().filter_map(|t|t.2) ).
min_by(cmp).expect("no points")
};
let to_draw_y_max = if let Some(y) = kd.max_ordinate
{
y
}
else
{
//to_draw.iter().map(|t|t.1).max_by(cmp).expect("no points")
to_draw.iter().map(|t|t.1).chain( symbolic_to_draw.iter().map(|t|t.1) ).
chain( to_box.iter().filter_map(|t|t.1) ).
chain( to_box.iter().filter_map(|t|t.2) ).
max_by(cmp).expect("no points")
};
//let x_range = to_draw_x_max - to_draw_x_min;
let x_range = if let (Some(a),Some(b)) = (to_draw_x_max , to_draw_x_min) { Some(a - b) } else { None };
let y_range = to_draw_y_max - to_draw_y_min;
for (x,y,dx,dy) in to_draw
{
//if dx.abs()*20f32 > x_range || dy.abs()*20f32 > y_range
if x_range.map_or(true, |xr|dx.abs()*20f32>xr) || dy.abs()*20f32 > y_range
{
//current_raw_plot.push_str(&format!("({},{}) +- ({},{})",x,y,dx,dy));
current_raw_plot_data.push(format!("({},{}) +- ({},{})",x,y,dx,dy));
}
else
{
//current_raw_plot.push_str(&format!("({},{})",x,y));
current_raw_plot_data.push(format!("({},{})",x,y));
}
if to_draw_x_min.unwrap()<=x && x<=to_draw_x_max.unwrap() && to_draw_y_min<=y && y<=to_draw_y_max
{
drawn_in_range+=1;
}
}
for (symbol,y,dy) in symbolic_to_draw
{
//if !symbols.contains(&symbol)
//{
// symbols.push(symbol);
//}
let symbol_index= if let Some(symbol_index) = symbols.iter().enumerate().find_map(|(index,s)|if *s==symbol {Some(index)} else {None})
{
symbol_index as f32
} else {
symbols.push(symbol);
(symbols.len()-1) as f32
};
if let Some(xlimit) = to_draw_x_min
{
if symbol_index < xlimit
{
continue;
}
}
if let Some(xlimit) = to_draw_x_max
{
if symbol_index >= xlimit
{
continue;
}
}
if dy.abs()*20f32 > y_range
{
//current_raw_plot.push_str(&format!("({},{}) +- ({},{})",symbol_index,y,0,dy));
current_raw_plot_data.push(format!("({},{}) +- ({},{})",symbol_index,y,0,dy));
}
else
{
//current_raw_plot.push_str(&format!("({},{})",symbol_index,y));
current_raw_plot_data.push(format!("({},{})",symbol_index,y));
}
if to_draw_y_min<=y && y<=to_draw_y_max
{
drawn_in_range+=1;
}
}
for (x,uw,bw,ub,bb,mb,mark) in to_box
{
//current_raw_plot.push_str(&format!("\\addplot[{}boxplot,boxplot prepared={{draw position={},\n",&legend_tex_id,x));
let mut data = format!("\\addplot[{}boxplot,boxplot prepared={{draw position={},\n",&legend_tex_id,x);
if let Some(value)=uw
{
data.push_str(&format!("\tupper whisker={},",value));
}
if let Some(value)=bw
{
data.push_str(&format!("\tlower whisker={},",value));
}
if let Some(value)=ub
{
data.push_str(&format!("\tupper quartile={},",value));
}
if let Some(value)=bb
{
data.push_str(&format!("\tlower quartile={},",value));
}
if let Some(value)=mb
{
data.push_str(&format!("\tmedian={},",value));
}
if let Some(value)=mark
{
data.push_str(&format!("\taverage={},",value));
}
data.push_str("}] coordinates {};\n");
current_raw_plot_data.push(data);
}
}
if boxplot
{
//raw_plots.push_str(¤t_raw_plot);
if kind_index==0
{
pre_plots.push_str(r"\addlegendimage{");
pre_plots.push_str(&legend_tex_id);
pre_plots.push_str(r"bar}\addlegendentry{\");
pre_plots.push_str(&legend_tex_id);
pre_plots.push_str("text}\n");
}
raw_plots_data.push( (String::new(), current_raw_plot_data, String::new(),String::from("\n")) );
} else {
let mut before = String::new();
let mut after = String::new();
before.push_str(r"\addplot[");
before.push_str(&legend_tex_id);
if kd.bar
{
before.push_str("bar");
}
if drawn_in_range > 20
{
let mark_period = drawn_in_range/10;
before.push_str(&format!(",mark repeat={}",mark_period));
}
before.push_str(r"] coordinates{");
//raw_plots.push_str(¤t_raw_plot);
if kind_index==0
{
after.push_str(r"};\addlegendentry{\");
}
else
{
after.push_str(r"};%\addlegendentry{\");
}
after.push_str(&legend_tex_id);
after.push_str("text}\n");// '\addlegendentry{}' does not use a semicolon
raw_plots_data.push( (before, current_raw_plot_data, after, String::from(" ")) );
}
if ((boxplot || kd.bar) && drawn_points>=1) || drawn_points>1
{
good_plots+=1;
}
}
let raw_plots : String = raw_plots_data.into_iter().map(|(before_plot,raw_plot_data,after_plot,separator)|
format!("{}{}{}",before_plot,raw_plot_data.join(&separator),after_plot),
).collect();
if good_plots==0 { figure_tikz.push_str("skipped bad plot.\\\\"); continue; }
//\begin{{tikzpicture}}[baseline,trim left=(left trim point),trim axis right,remember picture]
//\path (yticklabel cs:0) ++(-1pt,0pt) coordinate (left trim point);
let selectorcode=latex_make_command_name(&selector_value_to_use.to_string());
let selectorname=latex_protect_text(&selector_value_to_use.to_string());
let tikzname=format!("{}-{}-selector{}-kind{}",folder_id,prefix,selectorcode,kind_index);
let mut axis = "axis";
let mut extra = "".to_string();
if !symbols.is_empty()
{
axis = "symbolic";
let first_tick = kd.min_abscissa.map(|x|x as usize).unwrap_or(0);
let last_tick = match kd.max_abscissa
{
Some(x) => (x as usize).min(symbols.len()) -1,
None => symbols.len()-1,
};
//let symbolic_coords = symbols[first_tick..=last_tick].join(",");
let symbolic_coords = symbols[first_tick..=last_tick].iter().map(|tick|format!("{{{}}}",tick)).collect::<Vec<_>>().join(",");
extra += &format!("xtick={{{firsttick},...,{lasttick}}}, xticklabels = {{{symbolic_coords}}},",firsttick=first_tick,lasttick=last_tick,symbolic_coords=symbolic_coords);
//extra += &format!("xtick={{0,...,{lastsymbol}}}, xticklabels = {{{symbolic_coords}}},",lastsymbol=symbols.len()-1,symbolic_coords=symbolic_coords);
//if boxplot
//{
// let symbolic_coords = symbols.join(",");
// extra += &format!("xtick={{0,...,{lastsymbol}}}, xticklabels = {{{symbolic_coords}}},",lastsymbol=symbols.len()-1,symbolic_coords=symbolic_coords);
//} else {
// axis="symbolic";
// let symbolic_coords = symbols.join(",");
// extra += &format!("symbolic x coords={{{symbolic_coords}}}, xtick = {{{symbolic_coords}}},",symbolic_coords=symbolic_coords);
//}
}
if kd.bar
{
extra += "ybar,bar width=3pt,enlarge x limits=0.2,";
}
if boxplot
{
extra += "width=500pt,area legend,boxplot={draw direction=y,box extend=0.08,every average/.style={/tikz/mark=*}},"
}
if let Some(ref raw_style) = kd.raw
{
extra = extra + raw_style + ",";
}
figure_tikz.push_str(&format!(r#"
\tikzsetnextfilename{{externalized-plots/external-{tikzname}}}%
\begin{{tikzpicture}}[baseline,remember picture]
\begin{{axis}}[
automatically generated {axis},{extra}
{kind_index_style},
{legend_to_name},
title={{{selectorname}}},
%%ybar interval=0.6,
{ymin_string}{ymax_string}{xmin_string}{xmax_string}%
%%enlargelimits=false,
ymajorgrids=true,
yminorgrids=true,
xmajorgrids=true,
mark options=solid,
minor y tick num=4,
xlabel={{{xlabel_string}}},
ylabel={{{ylabel_string}}},
%%legend style={{at={{(1.05,1.0)}},anchor=north west}},
%%legend style={{opacity=0.7,at={{(0.99,0.99)}},anchor=north east}},
%%legend style={{at={{(0.00,1.01)}},anchor=south west,font=\scriptsize}},legend columns=3,transpose legend,legend cell align=left,
%%legend style={{at={{(0.00,1.01)}},anchor=south west,font=\scriptsize}},legend columns=2,legend cell align=left,
%%every x tick label/.append style={{anchor=base,yshift=-7}},
]
{pre_plots}{plots_string} \end{{axis}}
%\pgfresetboundingbox\useasboundingbox (y label.north west) (current axis.north east) ($(current axis.outer north west)!(current axis.north east)!(current axis.outer north east)$);
\end{{tikzpicture}}%{selectorname} - {kind_index}"#,tikzname=tikzname,kind_index_style=if kind_index==0{"first kind,"} else {"posterior kind,"},axis=axis,extra=extra,ymin_string=ymin[kind_index],ymax_string=ymax[kind_index],xmin_string=xmin[kind_index],xmax_string=xmax[kind_index],xlabel_string=latex_protect_text(&kd.label_abscissas),ylabel_string=latex_protect_text(&kd.label_ordinates),pre_plots=pre_plots,plots_string=raw_plots,legend_to_name=if kind_index==0{format!("legend to name=legend-{}-{}-{}",folder_id,prefix,selectorcode)}else{"".to_string()}));
if kind_index < kind.len()-1 {
figure_tikz.push_str("\n\t\\kindseparator%");
}
}
if wrote==0
{
break;
}
let selector_tex_caption=Some(latex_protect_text(&tracked_selector_value.unwrap().to_string()));
figure_tikz.push_str(&format!(r#"
%\end{{center}}
\caption{{\captionprologue {caption}}}%
\label{{fig:PLACEHOLDER}}%
\end{{experimentfigure}}
"#,caption=selector_tex_caption.unwrap()));
tikz.push_str(&figure_tikz);
//figure_index+=1;
}
let amount_string=
{
let done = environment.available_results();
let total = environment.total_experiments();
if done==total {format!("all {} done",done)} else {format!("{} of {}",done,total)}
};
let version=version_string(Some(get_git_id()),Some(get_version_number()));
let title=format!("{}/{} ({})",folder,pdf_filename,amount_string);
let header=format!("\\tiny {}:{} ({})\\\\pdflatex on \\today\\\\version={}",latex_protect_text(&folder),latex_protect_text(&pdf_filename),amount_string,latex_protect_text(&version));
let shared_prelude=format!(r#"
%% -- common pgfplots prelude --
\newenvironment{{experimentfigure}}{{\begin{{figure}}[H]\tikzexternalenable}}{{\tikzexternaldisable\end{{figure}}}}
%\newenvironment{{experimentfigure}}{{\begin{{figure*}}}}{{\end{{figure*}}}}
\newcommand{{\kindseparator}}{{\hskip 0ex{{}}}}
\pgfplotsset{{compat=newest}}
\makeatletter
%required for fill plus pattern on boxplot
\tikzset{{nomorepostaction/.code=\let\tikz@postactions\pgfutil@empty}}
\makeatother
\pgfplotsset{{minor grid style={{dashed,very thin, color=blue!15}}}}
\pgfplotsset{{major grid style={{very thin, color=black!30}}}}
\pgfplotsset{{
automatically generated axis/.style={{
%default: height=207pt, width=240pt. 240:207 ~~ 7:6
%height=115pt,%may fit 3figures with 1 line caption
height=105pt,%may fit 3figures with 2 line caption
width=174pt,
scaled ticks=false,
xticklabel style={{font=\tiny,/pgf/number format/.cd, fixed,/tikz/.cd}},% formattin ticks' labels
yticklabel style={{font=\tiny,/pgf/number format/.cd, fixed,/tikz/.cd}},% formattin ticks' labels
x label style={{at={{(ticklabel cs:0.5, -5pt)}},name={{x label}},anchor=north,font=\scriptsize}},
y label style={{at={{(ticklabel cs:0.5, -5pt)}},name={{y label}},anchor=south,font=\scriptsize}},
}},
automatically generated symbolic/.style={{
height=105pt,
width=500pt,
xticklabel style={{font=\tiny,rotate=90}},
yticklabel style={{font=\tiny,/pgf/number format/.cd, fixed,/tikz/.cd}},% formattin ticks' labels
x label style={{at={{(ticklabel cs:0.5, -5pt)}},name={{x label}},anchor=north,font=\scriptsize}},
y label style={{at={{(ticklabel cs:0.5, -5pt)}},name={{y label}},anchor=south,font=\scriptsize}},
}},
first kind/.style={{
%The first axis on each line of plots
%legend style={{overlay,at={{(0.50,1.05)}},anchor=south,font=\scriptsize,fill=none}},
%legend style={{at={{(0.00,1.01)}},anchor=south west,font=\scriptsize,fill=none}},
%legend style={{at={{($(axis description cs:0.00,1.01)!(current page.center)!(axis description cs:1.00,1.01)$)}},anchor=south,font=\scriptsize,fill=none}},
legend style={{font=\scriptsize,fill=none}},
legend columns=2,legend cell align=left,
}},
posterior kind/.style={{
%Axis following the first on each line of plots
%legend style={{at={{(0.50,1.05)}},overlay,anchor=south,font=\tiny,fill=none}},
legend style={{draw=none}},
}},
}}
\def\timetickcode{{%
%\pgfkeys{{/pgf/fpu}}%
\pgfkeys{{/pgf/fpu,/pgf/fpu/output format=fixed}}%
%\pgfkeys{{/pgf/fpu=true}}%
\pgfmathparse{{\tick}}%
%\typeout{{tick=\tick, math=\pgfmathresult}}%
\edef\tmp{{\pgfmathresult}}%
%\pgfmathsetmacro\xseconds{{Mod(\tmp,60)}}%
%\pgfkeys{{/pgf/fpu=false}}%
%\pgfmathtruncatemacro\xseconds{{Mod(\tmp,60)}}%
%\pgfkeys{{/pgf/fpu=true}}%
%\typeout{{total seconds=\tmp}}%
%\pgfmathparse{{\tmp/60}}\typeout{{tmp/60=\pgfmathresult}}%
%\pgfmathparse{{floor(\tmp/60)}}\typeout{{floor(tmp/60)=\pgfmathresult}}%
%\pgfmathparse{{round(\tmp/60)}}\typeout{{round(tmp/60)=\pgfmathresult}}%
%\pgfmathparse{{int(\tmp/60)}}\typeout{{int(tmp/60)=\pgfmathresult}}%
\pgfmathtruncatemacro\seconds{{\tmp-60*floor(\tmp/60)}}%
%\typeout{{truncated seconds=\seconds}}%
\pgfmathtruncatemacro\tmp{{(\tmp - \seconds)/60}}%
%\typeout{{total minutes=\tmp}}%
%\pgfmathtruncatemacro\minutes{{Mod(\tmp,60)}}%
\pgfmathtruncatemacro\minutes{{\tmp-60*floor(\tmp/60)}}%
%\typeout{{truncated minutes=\seconds}}%
\pgfmathtruncatemacro\tmp{{(\tmp - \minutes)/60}}%
%\typeout{{total hours=\tmp}}%
%\pgfmathtruncatemacro\hours{{Mod(\tmp,24)}}%
\pgfmathtruncatemacro\hours{{\tmp-24*floor(\tmp/24)}}%
%\typeout{{truncated hours=\hours}}%
\pgfmathtruncatemacro\days{{(\tmp - \hours)/24}}%
%{{\tiny\days-\hours:\minutes:\seconds}}%
\ifnum\days=0%
{{\tiny\hours:\minutes:\seconds}}%
\else%
{{\tiny\days-\hours:\minutes}}%
\fi%
%{{\tiny\tick}}%
}}
\def\memorytickcode{{%
\pgfkeys{{/pgf/fpu,/pgf/fpu/output format=fixed}}%
%\pgfmathprintnumber[sci,sci zerofill,precision=1]{{\tick}}%
\pgfmathtruncatemacro\unitcase{{log2(\tick+0.001)/10+1.1}}%We add a byte to keep the log2 sensible.
%\pgfmathfloatifflags{{}}
\pgfmathparse{{\tick / pow(1024,\unitcase-1)}}%
\pgfmathprintnumber{{\pgfmathresult}}%
\ifcase\unitcase B%0
\or KB%1
\or MB%2
\or GB%3
\or TB%4
\else +1024,\pgfmathprintnumber{{unitcase}}B%
\fi%
}}
\tikzset{{
automatically generated plot/.style={{
%/pgfplots/error bars/.cd,error bar style={{ultra thick}},x dir=both, y dir=both,
/pgfplots/error bars/x dir=both,
/pgfplots/error bars/y dir=both,
/pgfplots/error bars/x explicit,
/pgfplots/error bars/y explicit,
/pgfplots/error bars/error bar style={{ultra thin,solid}},
/tikz/mark options={{solid}},
}},
automatically generated bar plot/.style={{
/pgfplots/error bars/y dir=both,
/pgfplots/error bars/y explicit,
}},
automatically generated boxplot/.style={{
%/pgfplots/boxplot={{
% box extend=0.1,
% every average/.style={{/tikz/mark=*}},
%}},
}},
%/pgf/images/aux in dpth=true,
x time ticks/.style={{
/pgfplots/scaled x ticks=false,
/pgfplots/xticklabel={{\timetickcode}},
}},
y time ticks/.style={{
/pgfplots/scaled y ticks=false,
/pgfplots/yticklabel={{\timetickcode}}
}},
x memory ticks from kilobytes/.style={{
/pgfplots/scaled x ticks=false,
/pgfplots/xticklabel={{\memorytickcode}}
}},
y memory ticks from kilobytes/.style={{
/pgfplots/scaled y ticks=false,
/pgfplots/yticklabel={{\memorytickcode}}
}},
}}"#);
let mut local_prelude=format!(r#"
%% -- experiment-local prelude
\newcommand\captionprologue{{X: }}
\newcommand\experimenttitle{{{title_string}}}
\newcommand\experimentheader{{{header_string}}}
"#,title_string=title,header_string=header);
//Look up the documentation of xcolor.
//yellow!50!black is too similar to olive.
//Try to select list containing a prime amount of elements.
let tikz_colors=["red","green","blue","black","violet","orange","lightgray","pink","olive","teal","purple"];
let tikz_pens=["solid","dashed","dotted","dash dot"];
//let tikz_marks=["o","square","triangle"];
let tikz_marks=["o","square","triangle","star","diamond","Mercedes star flipped"];
let tikz_patterns=["horizontal lines","grid","crosshatch","dots","north east lines","vertical lines"];
let tikz_fill_colors=["red!20","green!20","blue!20","black!20","violet!20","orange!20","lightgray!20","pink!20","olive!20","teal!20","purple!20"];
let mut color_index=0;
let mut pen_index=0;
let mut mark_index=0;
for legend_tex_id in all_legend_tex_id_vec
{
local_prelude.push_str(r"\tikzset{");
local_prelude.push_str(&legend_tex_id);
local_prelude.push_str(&format!("/.style={{automatically generated plot,{},{},mark={}}}}}\n",tikz_colors[color_index],tikz_pens[pen_index],tikz_marks[mark_index]));
local_prelude.push_str(r"\tikzset{");
local_prelude.push_str(&legend_tex_id);
local_prelude.push_str("bar");
local_prelude.push_str(&format!("/.style={{automatically generated bar plot,fill={},postaction={{pattern={}}},}}}}\n",tikz_fill_colors[color_index],tikz_patterns[mark_index]));
local_prelude.push_str(r"\tikzset{");
local_prelude.push_str(&legend_tex_id);
local_prelude.push_str("boxplot");
//local_prelude.push_str(&format!("/.style={{automatically generated boxplot,fill={},postaction={{pattern={}}},}}}}\n",tikz_fill_colors[color_index],tikz_patterns[mark_index]));
local_prelude.push_str(&format!("/.style={{automatically generated boxplot,fill={},every path/.style={{postaction={{nomorepostaction,pattern={}}},}}}}}}\n",tikz_fill_colors[color_index],tikz_patterns[mark_index]));
color_index+=1;
pen_index+=1;
mark_index+=1;
while color_index>=tikz_colors.len()
{
color_index-=tikz_colors.len();
//pen_index+=1;
//mark_index+=1;
}
while pen_index>=tikz_pens.len()
{
pen_index-=tikz_pens.len();
//mark_index+=1;
}
while mark_index>=tikz_marks.len()
{
mark_index-=tikz_marks.len();
}
}
for legend_tex_entry in all_legend_tex_entry
{
local_prelude.push_str(&legend_tex_entry);
}
//writeln!(tex_file,"{}",local_prelude).unwrap();
//writeln!(tex_file,"{}",tikz).unwrap();
writeln!(tex_file,r#"{shared_prelude}
\bgroup
{local_prelude}
%% -- henceafter the data
{data_string}
\egroup
"#,shared_prelude=shared_prelude,local_prelude=local_prelude,data_string=tikz).unwrap();
let pdf_path=&outputs_path.join(&pdf_filename);
println!("Creating {:?}",pdf_path);
if !tmp_path.is_dir()
{
fs::create_dir(&tmp_path).expect("Something went wrong when creating the tikz tmp directory.");
}
let tmp_path_externalized=tmp_path.join("externalized-plots");
if !tmp_path_externalized.is_dir()
{
fs::create_dir(&tmp_path_externalized).expect("Something went wrong when creating the tikz externalized-plots directory.");
}
let tmp_path_externalized_legends=tmp_path.join("externalized-legends");
if !tmp_path_externalized_legends.is_dir()
{
fs::create_dir(&tmp_path_externalized_legends).expect("Something went wrong when creating the tikz externalized-legends directory.");
}
//let main_cfg_contents=
//{
// //let cfg=root.join("main.cfg");
// //let mut cfg_file=File::open(&cfg).expect("main.cfg could not be opened");
// //let mut cfg_contents = String::new();
// //cfg_file.read_to_string(&mut cfg_contents).expect("something went wrong reading main.cfg");
// //cfg_contents
// environment.files.cfg_contents.clone().unwrap_or_else(||"There is no main.cfg".to_string())
//};
let main_cfg_formatted = if let Some(crate::config_parser::Token::Value(value)) = &environment.files.parsed_cfg {
//let text = value.format_terminal();
//format!("\\tt {}",latex_protect_text(&text)
// .replace("{","\\{")
// .replace("}","\\}")
// .replace("\n","\n\\newline ")
// .replace("\t","\\mbox{}\\hskip 1em "))
value.format_latex()
} else { format!("could not parse configuration") };
let all_git_formatted=
{
if all_git_ids.is_empty() {
"No git-id found.".to_string()
} else {
let core = all_git_ids.iter().map(|s|format!("\\item {}",latex_protect_text(s))).collect::<Vec<String>>().join("\n");
format!("The following versions used in the simulations.\\\\\\begin{{itemize}}\n{}\n\\end{{itemize}}",core)
}
};
let whole_tex=format!(r#"
\documentclass[a4paper, 12pt, fleqn]{{article}}
\usepackage[utf8]{{inputenc}}
\usepackage{{amsfonts}}
\usepackage{{amssymb}}
\usepackage{{amsmath}}
\usepackage{{graphicx}}
\usepackage{{amsthm}}
\usepackage{{color}}
%\usepackage[cm]{{fullpage}}
\usepackage[paper=a4paper,margin=1cm,includehead=true]{{geometry}}
\usepackage{{float}}
\usepackage{{tikz}}
\usepackage{{pgfplots}}
\usetikzlibrary{{calc,external,patterns}}
\usepgfplotslibrary{{statistics}}%for boxplots
\tikzexternaldisable
\tikzexternalize
%\tikzexternalize[prefix=externalized/]
\usepackage[bookmarks=true]{{hyperref}}
{shared_prelude}
{local_prelude}
\newcommand\autor{{Cantabrian Agile Modular Interconnection Open Simulator}}
\hypersetup{{
unicode=false,
pdftoolbar=true,
pdfmenubar=true,
pdffitwindow=true,
pdftitle={{\experimenttitle}},
pdfauthor={{\autor}},
pdfsubject={{}},
pdfnewwindow=true,
pdfkeywords={{}},
pdfpagemode=None,
colorlinks=false,
linkcolor=red,
citecolor=green,
filecolor=magenta,
urlcolor=cyan,
pdfborder={{0 0 0}},
}}
\begin{{document}}
\pagestyle{{myheadings}}
\markright{{\experimentheader}}
{data_string}
\clearpage\tiny
{git_ids}
{cfg_string}
\end{{document}}
"#,shared_prelude=shared_prelude,local_prelude=local_prelude,data_string=tikz,git_ids=all_git_formatted,cfg_string=main_cfg_formatted);
let tmpname=format!("{}-tmp",prefix);
let tmpname_tex=format!("{}.tex",&tmpname);
let tmpname_pdf=format!("{}.pdf",&tmpname);
let whole_tex_path=tmp_path.join(&tmpname_tex);
let mut whole_tex_file=File::create(&whole_tex_path).expect("Could not create whole tex temporal file.");
writeln!(whole_tex_file,"{}",whole_tex).unwrap();
let mut latex_jobs: Vec<(PathBuf,PathBuf,String,Option<std::time::SystemTime>)> = latex_jobs.into_iter().map(|(dependency,target,target_str)|{
let timestamp = dependency.metadata().ok().and_then(|metadata|metadata.modified().ok());
(dependency,target,target_str,timestamp)
}).collect();
for _ in 0..3
{
//With `remember picture` we need at least two passes.
//And externalize with legend to names seems to require three passes.
let _pdflatex=Command::new("pdflatex")
.current_dir(&tmp_path)
.arg("--shell-escape")
.arg(&tmpname_tex)
.output().map_err(|e|Error::command_not_found(source_location!(),"pdflatex".to_string(),e))?;
for (dependency,target,target_str,timestamp) in latex_jobs.iter_mut()
{
let new_timestamp = dependency.metadata().ok().and_then(|metadata|metadata.modified().ok());
let target_timestamp = target.metadata().ok().and_then(|metadata|metadata.modified().ok());
let regenerate = dependency.exists() && (!target.exists() || timestamp.is_none() || new_timestamp.is_none() || target_timestamp.is_none() || timestamp.unwrap()<new_timestamp.unwrap() || target_timestamp.unwrap()<new_timestamp.unwrap() );
if regenerate
{
//$ pdflatex -shell-escape -halt-on-error -interaction=batchmode -jobname "externalized-legends/legend-plzxaltxRIIRIIIRIIIxHammingRVIIIxRVIIIxRVIIIxtemporalxRVIVxdxaggregated-throughput-xdimensionxcomplementxreversex" "\def\tikzexternalrealjob{throughput-tmp}\input{throughput-tmp}"
let target_str = format!("\"{}\"",target_str);
let _pdflatex=Command::new("pdflatex")
.current_dir(&tmp_path)
.arg("--shell-escape")
.arg("-interaction=batchmode")
.arg("-jobname")
.arg(target_str)
.arg(format!(r"\def\tikzexternalrealjob{{{tmpname}}}\input{{{tmpname}}}"))
.output().map_err(|e|Error::command_not_found(source_location!(),"pdflatex".to_string(),e))?;
}
*timestamp = new_timestamp;
}
}
let filepath=tmp_path.join(tmpname_pdf);
//fs::copy(&filepath,&pdf_path).or_else(|err|Err(BackendError::CouldNotGenerateFile{filepath:filepath,io_error:Some(err)}))?;
fs::copy(&filepath,&pdf_path).map_err(|err|Error::could_not_generate_file(source_location!(),filepath,err))?;
Ok(())
}
/**
Create a "config" file as a function of the results.
For example to find out the offered load which gives maximum accepted load use the following. Assuming we want independent figures for each traffic pattern.
PreprocessArgMax{
filename: "peak_load.cfg",
selector: [=configuration.legend_name, =configuration.traffic.pattern.legend_name],
target: =results.accepted_load,
argument: =configuration.traffic.load,
}
**/
fn create_preprocess_arg_max(description: &ConfigurationValue, environment:&mut OutputEnvironment) -> Result<(),Error>
{
//File to be crated, inside "path/".
let mut filename = None;
//To define bins. Results with same selector are averaged together.
let mut selector = None;
//Expression to maximize.
let mut target = None;
//Expression to store.
let mut argument = None;
match_object!(description,"PreprocessArgMax",value,
"filename" => filename = Some(value.as_str()?.to_string()),
"selector" => selector=Some(value),
"target" => target=Some(value),
"argument" => argument=Some(value),
);
let filename = filename.ok_or_else(||description.ill("There were no filename"))?;
if let Some(targets) = environment.targets {
if !targets.contains(&filename) {
//TODO: perhaps we need a success type.
return Ok(());
}
};
let selector = selector.ok_or_else(||description.ill("There were no selector"))?;
let target = target.ok_or_else(||description.ill("There were no target"))?;
let argument = argument.ok_or_else(||description.ill("There were no argument"))?;
let outputs_path = environment.files.get_outputs_path();
// --- Evaluate the records
//records [ selector, argument, target value ]
let mut records : Vec< (ConfigurationValue, ConfigurationValue, f64 ) > = vec![];
for context in environment.iter()
{
let selector=reevaluate(selector,&context,&outputs_path)?;
let target=reevaluate(target,&context,&outputs_path)?;
let argument=reevaluate(argument,&context,&outputs_path)?;
match target
{
ConfigurationValue::Number(x) => records.push( (selector,argument,x) ),
_ => panic!("target {} cannot be evaluated into f64",target),
}
}
// --- Average the records
records.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less));
let mut averaged : Vec< (ConfigurationValue, ConfigurationValue, f64) > = vec![];
let mut index=0;
while index<records.len()
{
let mut same_index = index;
let mut count = 0;
let mut total = 0f64;
while same_index<records.len() && records[index].0==records[same_index].0 && records[index].1==records[same_index].1
{
count+=1;
total+=records[same_index].2;
same_index+=1;
}
averaged.push( (records[index].0.clone(),records[index].1.clone(),total/count as f64) );
index=same_index;
}
drop(records);
// --- Find the maximizing argument
let mut optimal : Vec< (ConfigurationValue, ConfigurationValue, f64) > = vec![];
index=0;
while index<averaged.len()
{
let mut same_index = index;
let mut best_index = index;
while same_index<averaged.len() && averaged[index].0==averaged[same_index].0
{
if averaged[same_index].2 > averaged[best_index].2
{
best_index = same_index;
}
same_index+=1;
}
optimal.push( averaged[best_index].clone() );
index=same_index;
}
drop(averaged);
// --- Build the file
let mut data : Vec<ConfigurationValue> = vec![ConfigurationValue::None;environment.total_experiments];
let index_expr = Expr::Ident("result".to_string());
for context in environment.iter()
{
let selector=reevaluate(selector,&context,&outputs_path)?;
let index : usize = optimal.iter().position(|r|r.0 == selector).unwrap_or_else(||panic!("did not found selector {}",selector));
let content = vec![
(String::from("argument"),optimal[index].1.clone()),
(String::from("maximum_value"),ConfigurationValue::Number(optimal[index].2))
];
let experiment_index : usize = evaluate(&index_expr,&context,&outputs_path)?.as_f64()? as usize;
data[experiment_index]= ConfigurationValue::Object(String::from("PreprocessedArgMax"), content);
}
let cfg = ConfigurationValue::Array(data);
let data_path=&outputs_path.join(filename);
let mut output = File::create(&data_path).expect("Could not create the data file.");
//writeln!(output,"{}",cfg).unwrap();
let binary_data = config::config_to_binary(&cfg).expect("error while serializing into binary");
output.write_all(&binary_data).expect("error happened when creating binary file");
Ok(())
}
/// Calculates the average and deviation of the values in a Vec.
fn standard_deviation(list:&Vec<ConfigurationValue>) -> (Option<f32>,Option<f32>)
{
let (list,_good_count,_none_count,other_count)=values_to_f32_with_count(list);
if other_count > 0
{
return (None,None);
}
match list.len()
{
0 => return (None,None),
1 => return (Some(list[0]),None),
_ => (),
}
let total:f32=list.iter().sum();
let average=total/list.len() as f32;
let sum:f32=list.iter().map(|v|{
let x= v-average;
x*x
}).sum();
//let deviation=((sum/(list.len()-1)) as f64).sqrt() as f32;
let deviation=
{
let x:f32=sum/(list.len()-1) as f32;
x.sqrt()
};
(Some(average),Some(deviation))
}
///Get a Some(x) if all elements are equal.
fn shared_element<I:Iterator>(iter:&mut I) -> Option<I::Item> where I::Item : PartialEq
{
if let Some( first ) = iter.next()
{
if iter.all(|x|x==first) { Some(first) } else { None }
} else {
None
}
}
/// A map over anything implementing Debug.
/// Uses `Debug::fmt` to get the keys. Elements with same debug representation will be considered equal to this map's effects.
#[derive(Debug,Default)]
struct EnumeratedMap<T:Debug>
{
///Map from `key.fmt` to offsets.
map: BTreeMap<String,usize>,
///Elements
data: Vec<T>
}
impl<T:Debug+Clone> EnumeratedMap<T>
{
/// Clone if necessary
pub fn insert(&mut self, element:&T)->usize
{
let key = format!("{:?}",element);
match self.map.get(&key)
{
Some(&x) => x,
None =>
{
let index=self.data.len();
self.data.push(element.clone());
self.map.insert(key,index);
index
}
}
}
}
/**
Combines an optional `git_id` with an optional `version_number` (generally from functions `get_git_id` and `get_version_number`)
to generate a version string to print.
**/
pub fn version_string(git_id:Option<&str>,version_number:Option<&str>) -> String
{
match (git_id,version_number)
{
(Some(git),Some(version)) => format!("{git}({version})"),
(None,Some(version)) => format!("caminos-lib-{version}"),
(Some(git),None) => format!("{git}"),
(None,None) => format!("no-version-found"),
}
}