Skip to content

Commit 6627225

Browse files
authored
Merge pull request #1063 from EnergySystemsModellingLab/coding-style-docs
Add docs about coding style/architecture and point AI agents at it
2 parents 3c60bad + dc63383 commit 6627225

File tree

4 files changed

+200
-1
lines changed

4 files changed

+200
-1
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
"editor.formatOnSave": false
1616
},
1717
"editor.formatOnSave": true,
18-
"editor.rulers": [100] // default max_width for rustfmt
18+
"editor.rulers": [100], // default max_width for rustfmt
19+
"chat.useAgentsMdFile": true
1920
}

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# AGENTS.md
2+
3+
## Code style and architecture
4+
5+
- When generating or reviewing code, please consult the guidelines in
6+
`docs/developer_guide/architecture_quickstart.md`
7+
- If adding a new feature or fixing a bug that was present in the last release of MUSE2, add a note
8+
to `docs/release_notes/upcoming.md`
9+
- Prefer UK spelling in code and documentation
10+
11+
For Rust code:
12+
13+
- Prefer `use` imports to fully qualified paths

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Developer Guide](developer_guide/README.md)
1818
- [Setting up your development environment](developer_guide/setup.md)
1919
- [Building and developing MUSE2](developer_guide/coding.md)
20+
- [Architecture and coding style](developer_guide/architecture_quickstart.md)
2021
- [Developing the documentation](developer_guide/docs.md)
2122
- [API documentation](./api/muse2/README.md)
2223
- [Release notes](release_notes/README.md)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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

Comments
 (0)