|
| 1 | +# Architecture and coding style |
| 2 | + |
| 3 | +This document describes the overall architecture of the MUSE2 project, as well as the coding style |
| 4 | +used for Rust. This document is intended to help new contributors get started with the codebase more |
| 5 | +quickly rather than to be a set of prescriptions. |
| 6 | + |
| 7 | +## User interface and deployment |
| 8 | + |
| 9 | +MUSE2 is a command-line utility, designed to be built and run as a single, standalone executable |
| 10 | +file (called `muse2` on Unix platforms and `muse2.exe` on Windows). The normal way in which users |
| 11 | +will obtain MUSE2 is by downloading a pre-built binary for their platform (currently, Linux, Windows |
| 12 | +and macOS ARM binaries are provided). The user should not need to install any additional |
| 13 | +dependencies in order to use MUSE2. All assets (e.g. example models; see below) should be bundled |
| 14 | +into this executable file. |
| 15 | + |
| 16 | +For information on the command-line interface, see [the documentation][cli]. The [`clap`] crate is |
| 17 | +used to provide this interface. |
| 18 | + |
| 19 | +[cli]: ../command_line_help.md |
| 20 | +[`clap`]: https://docs.rs/clap/latest/clap/ |
| 21 | + |
| 22 | +## Using external crates |
| 23 | + |
| 24 | +With Rust it is easy to add external dependencies (called "crates") from [crates.io]. It is |
| 25 | +preferable to make use of external crates for additional required functionality rather than |
| 26 | +reimplementing this by hand. |
| 27 | + |
| 28 | +Some crates which we make heavy use of are: |
| 29 | + |
| 30 | +- [`anyhow`] - Ergonomic error handling in Rust (see below) |
| 31 | +- [`float-cmp`] - For approximate comparison of floating-point types |
| 32 | +- [`indexmap`] - Provides hash table and set types which preserve insertion order |
| 33 | +- [`itertools`] - Provides extra features for iterator types (consider using to simplify code using |
| 34 | + iterators) |
| 35 | + |
| 36 | +[crates.io]: https://crates.io/ |
| 37 | +[`anyhow`]: https://docs.rs/anyhow/ |
| 38 | +[`float-cmp`]: https://docs.rs/float-cmp/ |
| 39 | +[`indexmap`]: https://docs.rs/indexmap/ |
| 40 | +[`itertools`]: https://docs.rs/itertools/ |
| 41 | + |
| 42 | +## Error handling |
| 43 | + |
| 44 | +One of the distinctive features of the Rust programming language is its approach to error handling. |
| 45 | +There are two usual ways to signal that an error has occurred: either by returning it from a |
| 46 | +function like any other value (usually via the [`Result`] enum) or by "panicking", which usually |
| 47 | +results in termination of the program[^1]. See [the official docs] for more information. |
| 48 | + |
| 49 | +In the case of MUSE2, we use the [`anyhow`] crate for error handling, which provides some useful |
| 50 | +helpers for passing error messages up the call stack and attaching additional context. For any |
| 51 | +user-facing error (e.g. caused by a malformed input file), you should return an error wrapped in a |
| 52 | +`Result`, so that it can be logged. For purely developer-facing errors, such as functions called |
| 53 | +with bad arguments, you should instead [`panic!`]. Note that users should **NEVER** be able to |
| 54 | +trigger a panic in MUSE2 and any case where this happens should be treated as a bug. We use the |
| 55 | +[`human-panic`] crate to direct users to report the bug if a panic occurs in a release build. |
| 56 | + |
| 57 | +[^1]: Technically, panics [can be caught](https://doc.rust-lang.org/std/panic/fn.catch_unwind.html), |
| 58 | + but this is unusual and we don't do it in MUSE2 |
| 59 | + |
| 60 | +[`Result`]: https://doc.rust-lang.org/std/result/ |
| 61 | +[the official docs]: https://doc.rust-lang.org/book/ch09-00-error-handling.html |
| 62 | +[`panic!`]: https://doc.rust-lang.org/std/macro.panic.html |
| 63 | +[`human-panic`]: https://docs.rs/human-panic/ |
| 64 | + |
| 65 | +## Logging |
| 66 | + |
| 67 | +MUSE2 makes use of the [`log`] crate, which provides a number of macros for logging at different |
| 68 | +levels (e.g. `info!`, `warn!` etc.). This crate just provides the helper macros and does not provide |
| 69 | +a logging backend. For the backend, we use [`fern`], which deals with formatting and (in the case of |
| 70 | +simulation runs) writing to log files. A few simple commands print to the console directly without |
| 71 | +using the logging framework (e.g. `muse2 example list`), but for code run as part of model |
| 72 | +validation and simulation runs, you should use the `log` macros rather than printing to the console |
| 73 | +directly. Note that you should generally not use the `error!` macro directly, but should instead |
| 74 | +pass these errors up the call stack via `anyhow` (see above). |
| 75 | + |
| 76 | +Note that the log level is configurable at runtime; see [user guide][logging-docs] for details. |
| 77 | + |
| 78 | +## Writing tests |
| 79 | + |
| 80 | +This repository includes tests for many aspects of MUSE2's functionality (both unit tests and |
| 81 | +integration tests). These can be run with `cargo test`. All tests must pass for submitted code; this |
| 82 | +is enforced via a [GitHub Actions workflow][ci-workflow]. Newly added code should include tests, |
| 83 | +wherever feasible. Code coverage is tracked with [Codecov]. There is good documentation on how to |
| 84 | +write tests in Rust [in the Rust book][tests-docs]. |
| 85 | + |
| 86 | +You may wish to use [test fixtures] for your unit tests. While Rust's built-in testing framework |
| 87 | +does not support test fixtures directly, the [`rstest`] crate, which is already included as a |
| 88 | +dependency for MUSE2, provides this functionality. You should prefer adding test fixtures over |
| 89 | +copy-pasting the same data structures between different tests. For common data structures (e.g. |
| 90 | +commodities, assets etc.), there are fixtures for these already provided in [`fixture.rs`]. You |
| 91 | +should use these where possible rather than creating new fixtures. |
| 92 | + |
| 93 | +As the fixtures needed for many tests are potentially complicated, there are also helper macros for |
| 94 | +testing that validation/running fails/succeeds for modified versions of example models. For more |
| 95 | +information, see [`fixture.rs`]. As this method is likely to lead to terser code compared to using |
| 96 | +fixtures, it should be preferred for new tests. |
| 97 | + |
| 98 | +We check whether each of the bundled example models (see below) runs successfully to completion as |
| 99 | +regression tests. We also check that the output has not substantially changed (i.e. that the numbers |
| 100 | +in the outputs are within a tolerance), which helps catch accidental changes to the behaviour of |
| 101 | +MUSE2. Of course, often we _do_ want to change the behaviour of MUSE2 as the model evolves. In this |
| 102 | +case, you can regenerate test data by running: |
| 103 | + |
| 104 | +```sh |
| 105 | +just regenerate_test_data |
| 106 | +``` |
| 107 | + |
| 108 | +If you do so, please verify that the changes to the output files are at least roughly what was |
| 109 | +expected, before you commit these updated test files. |
| 110 | + |
| 111 | +[`log`]: https://docs.rs/log |
| 112 | +[`fern`]: https://docs.rs/fern |
| 113 | +[logging-docs]: ../user_guide.md#setting-the-log-level |
| 114 | +[ci-workflow]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/.github/workflows/cargo-test.yml |
| 115 | +[Codecov]: https://about.codecov.io/ |
| 116 | +[tests-docs]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html |
| 117 | +[test fixtures]: https://en.wikipedia.org/wiki/Test_fixture |
| 118 | +[`rstest`]: https://docs.rs/rstest |
| 119 | +[`fixture.rs`]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/src/fixture.rs |
| 120 | + |
| 121 | +## Example models |
| 122 | + |
| 123 | +MUSE2 provides a number of example models, to showcase its functionality and help users get started |
| 124 | +with creating their own. These models live in the [`examples`] folder of the repository and are also |
| 125 | +bundled with the MUSE2 executable ([see user guide for more detail][user-guide-example-models]). |
| 126 | + |
| 127 | +As these are intended as both a kind of documentation and templates, they should ideally be kept as |
| 128 | +simple as possible. |
| 129 | + |
| 130 | +If you add a new example model, please also add a regression test ([see here for an |
| 131 | +example][regression-test-example]). |
| 132 | + |
| 133 | +[`examples`]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/examples/ |
| 134 | +[user-guide-example-models]: https://energysystemsmodellinglab.github.io/MUSE2/user_guide.html#example-models |
| 135 | +[regression-test-example]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/tests/regression_muse1_default.rs |
| 136 | + |
| 137 | +## Unit types |
| 138 | + |
| 139 | +We define a number of types for units used commonly in MUSE2, such as activity, capacity etc. These |
| 140 | +are simple wrappers around `f64`s, but provide additional type safety, ensuring that the wrong types |
| 141 | +are not passed to functions, for example. Certain arithmetic operations involving types are also |
| 142 | +defined: for example, if you divide a variable of type [`Money`] by one of type [`Activity`], you |
| 143 | +get a result of type [`MoneyPerActivity`]. |
| 144 | + |
| 145 | +These types should be used in preference to plain `f64`s, where possible. For variables which are |
| 146 | +unitless, there is a [`Dimensionless`] type to make this explicit. |
| 147 | + |
| 148 | +For more information, consult [the documentation for the `units` module][units-module-docs]. |
| 149 | + |
| 150 | +[`Money`]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/units/struct.Money.html |
| 151 | +[`Activity`]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/units/struct.Activity.html |
| 152 | +[`MoneyPerActivity`]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/units/struct.MoneyPerActivity.html |
| 153 | +[`Dimensionless`]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/units/struct.Dimensionless.html |
| 154 | +[units-module-docs]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/units/index.html |
| 155 | + |
| 156 | +## Input and output files |
| 157 | + |
| 158 | +Input and output files for MUSE2 are either in [CSV] or [TOML] format. Users provide model |
| 159 | +definitions via a number of input files and the simulation results are written to files in an output |
| 160 | +folder. The code responsible for reading and validating input files and writing output files is in |
| 161 | +the [`input`][input-module] and [`output`][output-module], respectively. |
| 162 | + |
| 163 | +The file formats for MUSE2 input and output files are described [in the |
| 164 | +documentation][file-format-docs]. This documentation is generated from schema files ([JSON schemas] |
| 165 | +for TOML files and [table schemas] for CSV files); these schemas **MUST** be updated when the file |
| 166 | +format changes (i.e. when a field is added/removed/changed). (For details of how to generate this |
| 167 | +documentation locally, see [Developing the documentation].) |
| 168 | + |
| 169 | +When it comes to reading input files, we try to perform as much validation as possible within the |
| 170 | +input layer, so that we can provide users with detailed error messages, rather than waiting until |
| 171 | +errors in the input data become apparent in the simulation run (or, worse, are missed altogether!). |
| 172 | +A certain amount of type safety is given by the [`serde`] crate (e.g. checking that fields which |
| 173 | +should be integers are really integers), but we also carry out many other validation checks (e.g. |
| 174 | +checking that there is a producer for every required commodity in the first year). |
| 175 | + |
| 176 | +[CSV]: https://en.wikipedia.org/wiki/Comma-separated_values |
| 177 | +[TOML]: https://toml.io/en/ |
| 178 | +[input-module]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/input/index.html |
| 179 | +[output-module]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/output/index.html |
| 180 | +[file-format-docs]: https://energysystemsmodellinglab.github.io/MUSE2/file_formats/ |
| 181 | +[JSON schemas]: https://json-schema.org/ |
| 182 | +[table schemas]: https://specs.frictionlessdata.io/table-schema/ |
| 183 | +[`serde`]: https://serde.rs/ |
| 184 | +[Developing the documentation]: ./docs.md#documenting-file-formats |
0 commit comments