From 4a28c65fc1d46d5357755bc44a1e1b58bdd922bd Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:00:47 +0100 Subject: [PATCH 1/3] Added test_with_retries macro to repeat GA tests. Some tests may fail due to the randomness in the solutions --- Cargo.lock | 10 +++++++++ Cargo.toml | 2 +- optirustic-macros/Cargo.toml | 12 ++++++++++ optirustic-macros/src/lib.rs | 35 ++++++++++++++++++++++++++++++ optirustic/Cargo.toml | 1 + optirustic/src/algorithms/nsga2.rs | 29 ++++++++++++++----------- 6 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 optirustic-macros/Cargo.toml create mode 100644 optirustic-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4268b82..f854fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "hv-fonseca-et-al-2006-sys", "hv-wfg-sys", "log", + "optirustic-macros", "ordered-float", "plotters", "rand", @@ -734,6 +735,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "optirustic-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ordered-float" version = "4.2.1" diff --git a/Cargo.toml b/Cargo.toml index 4fbc4bf..59607b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["optirustic", "hv-fonseca-et-al-2006-sys", "hv-wfg-sys"] +members = ["optirustic", "optirustic-macros", "hv-fonseca-et-al-2006-sys", "hv-wfg-sys"] default-members = ["optirustic", "hv-fonseca-et-al-2006-sys", "hv-wfg-sys"] # Run test with optimisation to speed up tests solving optimisation problems. diff --git a/optirustic-macros/Cargo.toml b/optirustic-macros/Cargo.toml new file mode 100644 index 0000000..bb624cf --- /dev/null +++ b/optirustic-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "optirustic-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "*", features = ["full"] } +quote = "*" +proc-macro2 = "*" \ No newline at end of file diff --git a/optirustic-macros/src/lib.rs b/optirustic-macros/src/lib.rs new file mode 100644 index 0000000..3628693 --- /dev/null +++ b/optirustic-macros/src/lib.rs @@ -0,0 +1,35 @@ +use proc_macro::TokenStream; + +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +/// An attribute macro to repeat a test `n` times until the test passes. The test passes if it does +/// not panic once, it fails if it panics `n` times. +#[proc_macro_attribute] +pub fn test_with_retries(attrs: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let tries = attrs + .to_string() + .parse::() + .expect("Attr must be an int"); + + let expanded = quote! { + #[test] + fn #fn_name() { + #input_fn + for i in 1..=#tries { + let result = std::panic::catch_unwind(|| { #fn_name() }); + + if result.is_ok() { + return; + } + + if i == #tries { + std::panic::resume_unwind(result.unwrap_err()); + } + }; + } + }; + expanded.into() +} diff --git a/optirustic/Cargo.toml b/optirustic/Cargo.toml index c507519..4ce1f64 100644 --- a/optirustic/Cargo.toml +++ b/optirustic/Cargo.toml @@ -14,6 +14,7 @@ rayon = "1.10.0" env_logger = "0.11.3" chrono = { version = "0.4.38", features = ["serde"] } ordered-float = "4.2.0" +optirustic-macros = { path = "../optirustic-macros" } hv-fonseca-et-al-2006-sys = { path = "../hv-fonseca-et-al-2006-sys" } hv-wfg-sys = { path = "../hv-wfg-sys" } plotters = { version = "0.3.6", optional = true } diff --git a/optirustic/src/algorithms/nsga2.rs b/optirustic/src/algorithms/nsga2.rs index a47a5b8..64fae99 100644 --- a/optirustic/src/algorithms/nsga2.rs +++ b/optirustic/src/algorithms/nsga2.rs @@ -9,10 +9,10 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; use crate::algorithms::{Algorithm, ExportHistory, StoppingConditionType}; +use crate::core::utils::{argsort, get_rng, vector_max, vector_min, Sort}; use crate::core::{ Individual, Individuals, IndividualsMut, OError, Population, Problem, VariableValue, }; -use crate::core::utils::{argsort, get_rng, Sort, vector_max, vector_min}; use crate::operators::{ Crossover, CrowdedComparison, Mutation, PolynomialMutation, PolynomialMutationArgs, Selector, SimulatedBinaryCrossover, SimulatedBinaryCrossoverArgs, TournamentSelector, @@ -47,7 +47,7 @@ pub struct NSGA2Arg { /// Instead of initialising the population with random variables, see the initial population /// with the variable values from a JSON files exported with this tool. This option lets you /// restart the evolution from a previous generation; you can use any history file (exported - /// when the field `export_history`) or the file exported when the stopping condition was reached. + /// when the field `export_history`) or the file exported when the stopping condition was reached. pub resume_from_file: Option, /// The seed used in the random number generator (RNG). You can specify a seed in case you want /// to try to reproduce results. NSGA2 is a stochastic algorithm that relies on a RNG at @@ -441,13 +441,14 @@ impl Algorithm for NSGA2 { &self.args } } + #[cfg(test)] mod test_sorting { use float_cmp::assert_approx_eq; use crate::algorithms::NSGA2; - use crate::core::{Individuals, ObjectiveDirection, VariableValue}; use crate::core::utils::individuals_from_obj_values_dummy; + use crate::core::{Individuals, ObjectiveDirection, VariableValue}; #[test] /// Test the crowding distance algorithm (not enough points). @@ -669,7 +670,9 @@ mod test_sorting { } #[cfg(test)] mod test_problems { - use crate::algorithms::{Algorithm, MaxGeneration, NSGA2, NSGA2Arg, StoppingConditionType}; + use optirustic_macros::test_with_retries; + + use crate::algorithms::{Algorithm, MaxGeneration, NSGA2Arg, StoppingConditionType, NSGA2}; use crate::core::builtin_problems::{ SCHProblem, ZTD1Problem, ZTD2Problem, ZTD3Problem, ZTD4Problem, }; @@ -677,7 +680,8 @@ mod test_problems { const BOUND_TOL: f64 = 1.0 / 1000.0; const LOOSE_BOUND_TOL: f64 = 0.1; - #[test] + + #[test_with_retries(3)] /// Test problem 1 from Deb et al. (2002). Optional solution x in [0; 2] fn test_sch_problem() { let problem = SCHProblem::create().unwrap(); @@ -703,7 +707,7 @@ mod test_problems { } } - #[test] + #[test_with_retries(3)] /// Test the ZTD1 problem from Deb et al. (2002) with 30 variables. Solution x1 in [0; 1] and /// x2 to x30 = 0. The exact solutions are tested using a strict and loose bounds. fn test_ztd1_problem() { @@ -750,7 +754,7 @@ mod test_problems { } } - #[test] + #[test_with_retries(3)] /// Test the ZTD2 problem from Deb et al. (2002) with 30 variables. Solution x1 in [0; 1] and /// x2 to x30 = 0. The exact solutions are tested using a strict and loose bounds. fn test_ztd2_problem() { @@ -802,7 +806,7 @@ mod test_problems { } } - #[test] + #[test_with_retries(3)] /// Test the ZTD3 problem from Deb et al. (2002) with 30 variables. Solution x1 in [0; 1] and /// x2 to x30 = 0. The exact solutions are tested using a strict and loose bounds. fn test_ztd3_problem() { @@ -854,15 +858,13 @@ mod test_problems { } } - #[test] + #[test_with_retries(3)] /// Test the ZTD4 problem from Deb et al. (2002) with 30 variables. Solution x1 in [0; 1] and /// x2 to x10 = 0. The exact solutions are tested using a strict and loose bounds. fn test_ztd4_problem() { let number_of_individuals: usize = 10; - let problem = ZTD4Problem::create(number_of_individuals).unwrap(); let args = NSGA2Arg { number_of_individuals, - // this may take longer to converge stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(3000)), crossover_operator_options: None, mutation_operator_options: None, @@ -871,7 +873,8 @@ mod test_problems { resume_from_file: None, seed: Some(1), }; - let mut algo = NSGA2::new(problem, args).unwrap(); + let problem = ZTD4Problem::create(number_of_individuals).unwrap(); + let mut algo = NSGA2::new(problem, args.clone()).unwrap(); algo.run().unwrap(); let results = algo.get_results(); @@ -908,7 +911,7 @@ mod test_problems { } } - #[test] + #[test_with_retries(3)] /// Test the ZTD6 problem from Deb et al. (2002) with 30 variables. Solution x1 in [0; 1] and /// x2 to x10 = 0. The exact solutions are tested using a strict and loose bounds. fn test_ztd6_problem() { From c8bc446cfbdcccbfea97dec706a6e682342e3337 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Sat, 3 Aug 2024 13:37:13 +0100 Subject: [PATCH 2/3] Added as_algorithm_args and impl_algorithm_trait_items macros to handle algorithm arguments and trait implementation --- optirustic-macros/src/lib.rs | 256 ++++++++++++++++++++++++- optirustic/src/algorithms/algorithm.rs | 8 +- optirustic/src/algorithms/nsga2.rs | 83 +------- 3 files changed, 265 insertions(+), 82 deletions(-) diff --git a/optirustic-macros/src/lib.rs b/optirustic-macros/src/lib.rs index 3628693..669fd26 100644 --- a/optirustic-macros/src/lib.rs +++ b/optirustic-macros/src/lib.rs @@ -1,10 +1,10 @@ use proc_macro::TokenStream; - use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use syn::parse::Parser; +use syn::{parse_macro_input, DeriveInput, ItemFn}; /// An attribute macro to repeat a test `n` times until the test passes. The test passes if it does -/// not panic once, it fails if it panics `n` times. +/// not panic at least once, it fails if it panics `n` times. #[proc_macro_attribute] pub fn test_with_retries(attrs: TokenStream, item: TokenStream) -> TokenStream { let input_fn = parse_macro_input!(item as ItemFn); @@ -33,3 +33,253 @@ pub fn test_with_retries(attrs: TokenStream, item: TokenStream) -> TokenStream { }; expanded.into() } + +/// Register new fields on a struct that contains algorithm options. This macro adds: +/// - the Serialize, Deserialize, Clone traits to the structure to make it serialisable and +/// de-serialisable. +/// - add the following fields: stopping_condition ([`StoppingConditionType`]), parallel (`bool`) +/// and export_history (`Option`). +#[proc_macro_attribute] +pub fn as_algorithm_args(_attrs: TokenStream, input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + match &mut ast.data { + syn::Data::Struct(ref mut struct_data) => { + if let syn::Fields::Named(fields) = &mut struct_data.fields { + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The condition to use when to terminate the algorithm. + pub stopping_condition: StoppingConditionType + }) + .expect("Cannot add `stopping_condition` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// Whether the objective and constraint evaluation in [`Problem::evaluator`] should run + /// using threads. If the evaluation function takes a long time to run and return the updated + /// values, it is advisable to set this to `true`. This defaults to `true`. + pub parallel: Option + }) + .expect("Cannot add `parallel` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The options to configure the individual's history export. When provided, the algorithm will + /// save objectives, constraints and solutions to a file each time the generation increases by + /// a given step. This is useful to track convergence and inspect an algorithm evolution. + pub export_history: Option + }) + .expect("Cannot add `export_history` field"), + ); + } + + let expand = quote! { + use crate::algorithms::{StoppingConditionType, ExportHistory}; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Clone)] + #ast + }; + expand.into() + } + _ => unimplemented!("`as_algorithm_args` can only be used on structs"), + } +} + +/// This macro adds the following private fields to the struct defining an algorithm: +/// `problem`, `number_of_individuals`, `population`, `generation`,`stopping_condition`, +/// `start_time`, `export_history` and `parallel`. +/// +/// It also implements the `Display` trait. +/// +#[proc_macro_attribute] +pub fn as_algorithm(_attrs: TokenStream, input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; + + match &mut ast.data { + syn::Data::Struct(ref mut struct_data) => { + if let syn::Fields::Named(fields) = &mut struct_data.fields { + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The problem being solved. + problem: Arc + }) + .expect("Cannot add `problem` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The number of individuals to use in the population. + number_of_individuals: usize + }) + .expect("Cannot add `number_of_individuals` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The population with the solutions. + population: Population + }) + .expect("Cannot add `population` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The evolution step. + generation: usize + }) + .expect("Cannot add `generation` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The stopping condition. + stopping_condition: StoppingConditionType + }) + .expect("Cannot add `stopping_condition` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The time when the algorithm started. + start_time: Instant + }) + .expect("Cannot add `start_time` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The configuration struct to export the algorithm history. + export_history: Option + }) + .expect("Cannot add `export_history` field"), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// Whether the evaluation should run using threads + parallel: bool + }) + .expect("Cannot add `parallel` field"), + ); + } + + let expand = quote! { + use std::time::Instant; + use std::sync::Arc; + use crate::core::{Problem, Population}; + + #ast + + impl Display for #name { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name().as_str()) + } + } + }; + expand.into() + } + _ => unimplemented!("`as_algorithm` can only be used on structs"), + } +} + +/// This macro adds common items when the `Algorithm` trait is implemented for a new algorithm +/// struct. This adds the following items: `Algorithm::name()`, `Algorithm::stopping_condition()` +/// `Algorithm::start_time()`, `Algorithm::problem()`, `Algorithm::population()`, +/// `Algorithm::generation()` and `Algorithm::export_history()`. +/// +#[proc_macro_attribute] +pub fn impl_algorithm_trait_items(attrs: TokenStream, input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as syn::ItemImpl); + let name = if let syn::Type::Path(tp) = &*ast.self_ty { + tp.path.clone() + } else { + unimplemented!("Token not supported") + }; + let arg_type = syn::punctuated::Punctuated::::parse_terminated + .parse(attrs) + .expect("Cannot parse argument type"); + + let mut new_items = vec![ + syn::parse::( + quote!( + fn stopping_condition(&self) -> &StoppingConditionType { + &self.stopping_condition + } + ) + .into(), + ) + .expect("Failed to parse `name` item"), + syn::parse::( + quote!( + fn name(&self) -> String { + stringify!(#name).to_string() + } + ) + .into(), + ) + .expect("Failed to parse `name` item"), + syn::parse::( + quote!( + fn start_time(&self) -> &Instant { + &self.start_time + } + ) + .into(), + ) + .expect("Failed to parse `start_time` item"), + syn::parse::( + quote!( + fn problem(&self) -> Arc { + self.problem.clone() + } + ) + .into(), + ) + .expect("Failed to parse `problem` item"), + syn::parse::( + quote!( + fn population(&self) -> &Population { + &self.population + } + ) + .into(), + ) + .expect("Failed to parse `population` item"), + syn::parse::( + quote!( + fn export_history(&self) -> Option<&ExportHistory> { + self.export_history.as_ref() + } + ) + .into(), + ) + .expect("Failed to parse `export_history` item"), + syn::parse::( + quote!( + fn generation(&self) -> usize { + self.generation + } + ) + .into(), + ) + .expect("Failed to parse `export_history` item"), + syn::parse::( + quote!( + fn algorithm_options(&self) -> &#arg_type { + &self.args + } + ) + .into(), + ) + .expect("Failed to parse `algorithm_options` item"), + ]; + + ast.items.append(&mut new_items); + let expand = quote! { #ast }; + expand.into() +} diff --git a/optirustic/src/algorithms/algorithm.rs b/optirustic/src/algorithms/algorithm.rs index bc24b30..621074d 100644 --- a/optirustic/src/algorithms/algorithm.rs +++ b/optirustic/src/algorithms/algorithm.rs @@ -1,13 +1,13 @@ -use std::{fmt, fs}; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; +use std::{fmt, fs}; use log::{debug, info}; use rayon::prelude::*; -use serde::{Deserialize, Serialize}; use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use crate::algorithms::{StoppingCondition, StoppingConditionType}; use crate::core::{Individual, IndividualExport, OError, Population, Problem, ProblemExport}; @@ -88,7 +88,7 @@ impl ExportHistory { /// * `generation_step`: export the algorithm data each time the generation counter in a genetic // algorithm increases by the provided step. /// * `destination`: serialise the algorithm history and export the results to a JSON file in - /// the given folder. + /// the given folder. /// /// returns: `Result` pub fn new(generation_step: usize, destination: &PathBuf) -> Result { @@ -409,7 +409,7 @@ pub trait Algorithm: Display { /// * `problem`: The problem. /// * `name`: The algorithm name. /// * `expected_individuals`: The number of individuals to expect in the file. If this does not - /// match the population size, being used in the algorithm, an error is thrown. + /// match the population size, being used in the algorithm, an error is thrown. /// * `file`: The path to the JSON file exported from this library. /// /// returns: `Result` diff --git a/optirustic/src/algorithms/nsga2.rs b/optirustic/src/algorithms/nsga2.rs index 64fae99..ab98a1f 100644 --- a/optirustic/src/algorithms/nsga2.rs +++ b/optirustic/src/algorithms/nsga2.rs @@ -1,26 +1,21 @@ use std::fmt::{Display, Formatter}; use std::ops::Rem; use std::path::PathBuf; -use std::sync::Arc; -use std::time::Instant; -use log::{debug, info}; -use rand::RngCore; -use serde::{Deserialize, Serialize}; - -use crate::algorithms::{Algorithm, ExportHistory, StoppingConditionType}; +use crate::algorithms::Algorithm; use crate::core::utils::{argsort, get_rng, vector_max, vector_min, Sort}; -use crate::core::{ - Individual, Individuals, IndividualsMut, OError, Population, Problem, VariableValue, -}; +use crate::core::{Individual, Individuals, IndividualsMut, OError, VariableValue}; use crate::operators::{ Crossover, CrowdedComparison, Mutation, PolynomialMutation, PolynomialMutationArgs, Selector, SimulatedBinaryCrossover, SimulatedBinaryCrossoverArgs, TournamentSelector, }; use crate::utils::fast_non_dominated_sort; +use log::{debug, info}; +use optirustic_macros::{as_algorithm, as_algorithm_args, impl_algorithm_trait_items}; +use rand::RngCore; /// Input arguments for the NSGA2 algorithm. -#[derive(Serialize, Deserialize, Clone)] +#[as_algorithm_args] pub struct NSGA2Arg { /// The number of individuals to use in the population. This must be a multiple of `2`. pub number_of_individuals: usize, @@ -34,16 +29,6 @@ pub struct NSGA2Arg { /// divided by the number of real variables in the problem (i.e., each variable will have the /// same probability of being mutated). pub mutation_operator_options: Option, - /// The condition to use when to terminate the algorithm. - pub stopping_condition: StoppingConditionType, - /// Whether the objective and constraint evaluation in [`Problem::evaluator`] should run - /// using threads. If the evaluation function takes a long time to run and return the updated - /// values, it is advisable to set this to `true`. This defaults to `true`. - pub parallel: Option, - /// The options to configure the individual's history export. When provided, the algorithm will - /// save objectives, constraints and solutions to a file each time the generation increases by - /// a given step. This is useful to track convergence and inspect an algorithm evolution. - pub export_history: Option, /// Instead of initialising the population with random variables, see the initial population /// with the variable values from a JSON files exported with this tool. This option lets you /// restart the evolution from a previous generation; you can use any history file (exported @@ -75,13 +60,8 @@ pub struct NSGA2Arg { /// ```rust #[doc = include_str!("../../examples/nsga2_zdt1.rs")] /// ``` +#[as_algorithm] pub struct NSGA2 { - /// The number of individuals to use in the population. - number_of_individuals: usize, - /// The population with the solutions. - population: Population, - /// The problem being solved. - problem: Arc, /// The operator to use to select the individuals for reproduction. selector_operator: TournamentSelector, /// The operator to use to generate a new children by recombining the variables of parent @@ -90,28 +70,12 @@ pub struct NSGA2 { crossover_operator: SimulatedBinaryCrossover, /// The operator to use to mutate the variables of an individual. mutation_operator: PolynomialMutation, - /// The evolution step. - generation: usize, - /// The stopping condition. - stopping_condition: StoppingConditionType, - /// The time when the algorithm started. - start_time: Instant, - /// The configuration struct to export the algorithm history. - export_history: Option, - /// Whether the evaluation should run using threads - parallel: bool, /// The seed to use. rng: Box, /// The algorithm options args: NSGA2Arg, } -impl Display for NSGA2 { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(self.name().as_str()) - } -} - impl NSGA2 { /// Initialise the NSGA2 algorithm. /// @@ -286,6 +250,7 @@ impl NSGA2 { } /// Implementation of Section IIIC of the paper. +#[impl_algorithm_trait_items(NSGA2Arg)] impl Algorithm for NSGA2 { /// This assesses the initial random population and sets the individual's ranks and crowding /// distance needed in [`self.evolve`]. @@ -408,38 +373,6 @@ impl Algorithm for NSGA2 { self.generation += 1; Ok(()) } - - fn generation(&self) -> usize { - self.generation - } - - fn name(&self) -> String { - "NSGA2".to_string() - } - - fn start_time(&self) -> &Instant { - &self.start_time - } - - fn stopping_condition(&self) -> &StoppingConditionType { - &self.stopping_condition - } - - fn population(&self) -> &Population { - &self.population - } - - fn problem(&self) -> Arc { - self.problem.clone() - } - - fn export_history(&self) -> Option<&ExportHistory> { - self.export_history.as_ref() - } - - fn algorithm_options(&self) -> &NSGA2Arg { - &self.args - } } #[cfg(test)] From c4677701e692e7a8eaf7fb872d9a88addde3938a Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Sat, 3 Aug 2024 13:37:30 +0100 Subject: [PATCH 3/3] Fixed example path in ref point --- optirustic/src/utils/reference_points.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optirustic/src/utils/reference_points.rs b/optirustic/src/utils/reference_points.rs index 4bcf658..fb6b4c1 100644 --- a/optirustic/src/utils/reference_points.rs +++ b/optirustic/src/utils/reference_points.rs @@ -40,7 +40,7 @@ fn binomial_coefficient(mut n: u64, k: u64) -> u64 { /// /// # Example /// ```rust -#[doc = include_str!("../../examples/reference_points.rs")] +#[doc = include_str!("reference_points.rs")] /// ``` pub struct DasDarren1998 { /// The number of problem objectives.