Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ For Rust code:
- Test function names should not be prefixed with `test_`
- Prefer using parameterised tests (using `rstest`) over separate ones where testing similar
functionality
- While this crate is a library, no one is using it as a library, so don't warn about breaking
changes to the external API
2 changes: 2 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
# The AssetRef type uses Rc<Asset> internally for shared ownership, but the hash
# implementation is carefully designed to be consistent regardless of interior mutability
ignore-interior-mutability = ["muse2::asset::AssetRef"]
# Things that should not be treated as identifiers. The ".." represents the default options.
doc-valid-idents = ["HiGHS", ".."]
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Setting up your development environment](developer_guide/setup.md)
- [Building and developing MUSE2](developer_guide/coding.md)
- [Architecture and coding style](developer_guide/architecture_quickstart.md)
- [Applying custom HiGHS options](developer_guide/custom_highs_options.md)
- [Developing the documentation](developer_guide/docs.md)
- [API documentation](./api/muse2/README.md)
- [Release notes](release_notes/README.md)
Expand Down
36 changes: 36 additions & 0 deletions docs/developer_guide/custom_highs_options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Applying custom HiGHS options

As part of development, you may wish to directly set custom options for the HiGHS solver. Note that
while some of these options will not affect results of simulations (e.g. to enable console logging
for HiGHS), as we cannot guarantee this for all options, in order to use this feature, you have to
set `please_give_me_broken_results = true` in your [`model.toml` file][model.toml].

You can change any of the options exposed by the HiGHS solver; for more information, see [the HiGHS
documentation][highs-opts].

You can set options to be applied to all optimisations, just dispatch or just appraisal.

Here is an example:

```toml
Comment thread
alexdewar marked this conversation as resolved.
please_give_me_broken_results = true
milestone_years = [2020, 2030, 2040]

# These options are applied to all optimisations
[highs.global_options]
# These two options are required to be enabled to log to console
log_to_console = true
Comment thread
alexdewar marked this conversation as resolved.
output_flag = true

# These ones are just applied to dispatch
[highs.dispatch_options]
# Increase to higher than default
primal_feasibility_tolerance = 10e-6

# These ones are just applied to appraisal
[highs.appraisal_options]
optimality_tolerance = 10e-6
```

[model.toml]: https://energysystemsmodellinglab.github.io/MUSE2/file_formats/input_files.html#model-parameters-modeltoml
[highs-opts]: https://ergo-code.github.io/HiGHS/stable/options/definitions/
4 changes: 3 additions & 1 deletion docs/release_notes/upcoming.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ready to be released, carry out the following steps:

## New features

<!-- TODO -->
- Users can now optionally pass [custom options][highs-opts-docs] to the HiGHS solver [#1276]

## Breaking changes

Expand All @@ -26,4 +26,6 @@ ready to be released, carry out the following steps:

- Fix misleading warning message for assets decommissioned before simulation start ([#1259])

[highs-opts-docs]: https://energysystemsmodellinglab.github.io/MUSE2/developer_guide/custom_highs_options.html
[#1259]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1259
[#1276]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1276
21 changes: 21 additions & 0 deletions schemas/input/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,26 @@ properties:
investment cycle. Changing the value of this parameter is potentially dangerous,
so it requires setting `please_give_me_broken_results` to true.
default: 1e-12
highs:
type: object
description: |
Used for setting custom HiGHS options. As this is unsafe, it requires setting
`please_give_me_broken_results` to true. For more information, see the relevant [section of
the developer documentation][highs-opts-docs].

[highs-opts-docs]: https://energysystemsmodellinglab.github.io/MUSE2/developer_guide/custom_highs_options.html
properties:
global_options:
type: object
description: HiGHS options applied to all optimisations
properties: {}
dispatch_options:
type: object
description: HiGHS options applied only to dispatch optimisations
properties: {}
appraisal_options:
type: object
description: HiGHS options applied only to appraisal optimisations
properties: {}

required: [milestone_years]
201 changes: 199 additions & 2 deletions src/model/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use crate::input::{
};
use crate::units::{Capacity, Dimensionless, Flow, MoneyPerFlow};
use anyhow::{Context, Result, ensure};
use itertools::Itertools;
use log::warn;
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use std::path::Path;
use std::sync::OnceLock;
use toml::Table;

const MODEL_PARAMETERS_FILE_NAME: &str = "model.toml";

Expand Down Expand Up @@ -63,7 +65,7 @@ fn set_dangerous_model_options_flag(enabled: bool) {
///
/// NOTE: If you add or change a field in this struct, you must also update the schema in
/// `schemas/input/model.yaml`.
#[derive(Debug, Deserialize, PartialEq)]
#[derive(Deserialize)]
#[serde(default)]
pub struct ModelParameters {
/// Milestone years
Expand Down Expand Up @@ -98,6 +100,12 @@ pub struct ModelParameters {
pub mothball_years: u32,
/// Absolute tolerance when checking if remaining demand is close enough to zero
pub remaining_demand_absolute_tolerance: Flow,
/// Options for the HiGHS solver.
///
/// For a full list of options, see [the HiGHS documentation].
///
/// [the HiGHS documentation]: https://ergo-code.github.io/HiGHS/stable/options/definitions/
pub highs: HighsOptions,
}

impl Default for ModelParameters {
Expand All @@ -117,7 +125,79 @@ impl Default for ModelParameters {
capacity_margin: Dimensionless(0.2),
mothball_years: 0,
remaining_demand_absolute_tolerance: DEFAULT_REMAINING_DEMAND_ABSOLUTE_TOLERANCE,
highs: HighsOptions::default(),
}
}
}

/// Defines the TOML table holding the sub-tables to define HiGHS options
#[derive(Default)]
pub struct HighsOptions {
/// HiGHS options applied to dispatch optimisation
pub dispatch_options: Table,
/// HiGHS options applied to appraisal optimisation
pub appraisal_options: Table,
}

impl<'de> Deserialize<'de> for HighsOptions {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Default, Deserialize)]
#[serde(default)]
#[allow(clippy::struct_field_names)]
struct RawHighsOptions {
global_options: Table,
dispatch_options: Table,
appraisal_options: Table,
}

let RawHighsOptions {
global_options,
mut dispatch_options,
mut appraisal_options,
} = RawHighsOptions::deserialize(deserializer)?;

let append_global_options = |options: &mut Table| {
for (option, value) in &global_options {
options
.entry(option.clone())
.or_insert_with(|| value.clone());
}
};
append_global_options(&mut dispatch_options);
append_global_options(&mut appraisal_options);

Ok(Self {
dispatch_options,
appraisal_options,
})
}
}

impl HighsOptions {
/// Log custom HiGHS options set by user, if any
fn log_options(&self) {
fn log_highs_options(name: &str, options: &Table) {
if options.is_empty() {
return;
}

let options_str = options
.iter()
.format_with("\n - ", |(opt, val), f| f(&format_args!("{opt} = {val}")))
.to_string();
warn!("Using custom HiGHS options for {name}:\n - {options_str}");
}

log_highs_options("dispatch", &self.dispatch_options);
log_highs_options("appraisal", &self.appraisal_options);
}

/// Check whether any options have been set
pub fn is_empty(&self) -> bool {
self.dispatch_options.is_empty() && self.appraisal_options.is_empty()
}
}

Expand Down Expand Up @@ -195,6 +275,20 @@ fn check_capacity_margin(value: Dimensionless) -> Result<()> {
Ok(())
}

/// Check the custom HiGHS options are valid.
///
/// Note that we cannot know whether the options specified exist and are of the correct type until
/// we attempt to use them. We could check for types that are never valid (e.g. an array), but as
/// we're checking later anyway, we don't bother.
fn check_highs_options(dangerous_options_enabled: bool, highs: &HighsOptions) -> Result<()> {
ensure!(
dangerous_options_enabled || highs.is_empty(),
"Cannot set custom HiGHS options without enabling {ALLOW_DANGEROUS_OPTION_NAME}"
);
Comment on lines +283 to +287

Ok(())
}

impl ModelParameters {
/// Read a model file from the specified directory.
///
Expand All @@ -215,6 +309,8 @@ impl ModelParameters {
.validate()
.with_context(|| input_err_msg(file_path))?;

model_params.highs.log_options();

Ok(model_params)
}

Expand Down Expand Up @@ -256,6 +352,8 @@ impl ModelParameters {
self.remaining_demand_absolute_tolerance,
)?;

check_highs_options(self.allow_dangerous_options, &self.highs)?;

Ok(())
}
}
Expand Down Expand Up @@ -320,6 +418,105 @@ mod tests {
assert_eq!(model_params.milestone_years, [2020, 2100]);
}

#[test]
fn model_params_deserialisation_copies_highs_global_options() {
let model_params: ModelParameters = toml::from_str(
"
milestone_years = [2020, 2100]

[highs.global_options]
output_flag = true

[highs.dispatch_options]
log_to_console = false
",
)
.unwrap();

assert_eq!(
model_params.highs.dispatch_options["output_flag"],
toml::Value::Boolean(true)
);
assert_eq!(
model_params.highs.dispatch_options["log_to_console"],
toml::Value::Boolean(false)
);
assert_eq!(
model_params.highs.appraisal_options["output_flag"],
toml::Value::Boolean(true)
);
}

#[test]
fn highs_options_deserialisation_copies_global_options() {
let highs: HighsOptions = toml::from_str(
"
[global_options]
output_flag = true
log_to_console = true

[dispatch_options]
primal_feasibility_tolerance = 1e-5

[appraisal_options]
optimality_tolerance = 1e-5
",
)
.unwrap();

assert_eq!(
highs.dispatch_options["output_flag"],
toml::Value::Boolean(true)
);
assert_eq!(
highs.dispatch_options["log_to_console"],
toml::Value::Boolean(true)
);
assert_eq!(
highs.appraisal_options["output_flag"],
toml::Value::Boolean(true)
);
assert_eq!(
highs.appraisal_options["log_to_console"],
toml::Value::Boolean(true)
);
}

#[test]
fn highs_options_deserialisation_preserves_specific_options() {
let highs: HighsOptions = toml::from_str(
"
[global_options]
output_flag = true
log_to_console = true

[dispatch_options]
output_flag = false

[appraisal_options]
log_to_console = false
",
)
.unwrap();

assert_eq!(
highs.dispatch_options["output_flag"],
toml::Value::Boolean(false)
);
assert_eq!(
highs.dispatch_options["log_to_console"],
toml::Value::Boolean(true)
);
assert_eq!(
highs.appraisal_options["output_flag"],
toml::Value::Boolean(true)
);
assert_eq!(
highs.appraisal_options["log_to_console"],
toml::Value::Boolean(false)
);
}

#[rstest]
#[case(1.0, true)] // Valid positive value
#[case(1e-10, true)] // Valid very small positive value
Expand Down
4 changes: 2 additions & 2 deletions src/simulation/investment/appraisal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,12 @@ fn calculate_lcox(
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let results = perform_optimisation(
model,
asset,
max_capacity,
commodity,
coefficients,
demand,
&model.time_slice_info,
highs::Sense::Minimise,
)?;

Expand Down Expand Up @@ -296,12 +296,12 @@ fn calculate_npv(
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let results = perform_optimisation(
model,
asset,
max_capacity,
commodity,
coefficients,
demand,
&model.time_slice_info,
highs::Sense::Maximise,
)?;

Expand Down
Loading
Loading