The imsi developer guide
Important
This section of the documentation is under active development. Information contained herein is likely sparse and subject to change. Expect placeholders ahead. Readers and developers beware!
imsi is a comprehensive python package. As such, developers are able to modify and adapt the source code to their needs (as permitted through the imsi-license). This section provides additional information for those interested in developing imsi.
Contributing
Bugs and feature requests
If you find something broken in imsi or have a request for a feature, please open an issue in the source repository.
Standards
Under development
Testing
imsi includes unit and integration tests located in the top-level tests directory.
The tests follow pytest standards and setup settings can be configured through tests/.env.test, which sets environment variables used during execution.
These tests focus on common use cases of imsi and its underlying functions. They are not exhaustive, and ongoing development is needed to keep them aligned with code changes. Their purpose is to reduce the need for manual testing and to complement the bit-identity checks performed in CanESM.
Installing test dependencies
From the top-level of the imsi repository, install the test requirements:
pip install -e .[tests]
Running all tests
coverage run -m pytest tests/
Running a specific test
To avoid waiting for longer test suites to complete, you can run an individual test:
coverage run -m pytest tests/path/to/test/test_file.py::specific_test_name
Coverage reports
To inspect test coverage, run:
coverage report
coverage html
For more details, see the Coverage documentation <https://coverage.readthedocs.io/en/latest/>_.
When to run and write tests
All contributors are responsible for keeping imsi tests up to date. Follow these rules whenever you make changes:
Run tests frequently
Run the full test suite (with coverage) whenever you modify the codebase.
Always run tests as part of your merge request into main to confirm no breaking changes are introduced.
Write or update tests
Add tests whenever you:
Introduce new functionality.
Fix a bug (write a regression test to prevent it from reappearing).
Modify existing behavior (update tests only if the change is intentional).
Ensure your tests capture both expected behavior and common failure modes.
Before opening a merge request
Run coverage run -m pytest tests/.
Verify that all tests pass.
Check test coverage with coverage report or coverage html.
Confirm that any new code paths are covered by tests.
Following this checklist ensures imsi stays reliable, reduces manual testing, and speeds up code review.
Extending configuration
hooks
An experiment run is set up through various internal setup and configuration commands in imsi. For certain modelling platforms or base configurations, a developer may want to execute additional steps that alter or augment actions in imsi. For instance, a developer may want to add a step that calls a separate utility after the imsi config commands complete.
These types of additional steps can be added through a “hook”, which is a
dynamically loaded and conditionally executed python function within the
imsi package. A hook is defined within and executed through the
config_hooks_* files under /imsi/shell_interface:
# directory structure under /imsi
...
├── shell_interface
│ ├── ...
│ ├── config_hooks_collection_config.yaml # conditions (constraints)
│ ├── config_hooks_collection.py # functions (hooks)
│ ├── config_hooks_manager.py # dynamically calls the hooks
│ └── ...
...
As development of a modelling platform under imsi expands, managing control flow
can become unweildy. Developers can avoid having to write
these complex logic blocks into imsi code directly by instead defining a
hook as a function (in config_hooks_collection.py) and a set of
conditions (in config_hooks_collection_config.yaml).
About hooks
Individual hooks (functions) are stored in config_hooks_collection.py. The functions
are named using the convention:
def {imsi_step}_{description_of_function}(configuration: Configuration):
...
While the naming convention is useful for organization, it is left to the developer to name and call these functions appropriately.
A hook should only be run if certain constraints (conditions) within the
imsi configuration (stored within the Configuration object) are met.
The hook and corresponding conditions are defined in the
config_hooks_collection_config.yaml file (the “config yaml”), as
structured lists of key-value pairs. The key-value pairs must match the
structure of the Configuration dictionary. Multiple hooks can be
defined under the same “hook set” (recommended naming as an imsi step):
config_hooks: # required
stepname: # step/hook set - a list of hooks follows
- run: stepname_do_a # function name
constraints:
subconfigname: # conditions that must be
condition_a: 1 # met for function to run
condition_b:
keyb: "on"
Note
Currently, constraints are limited to only performing the operations “equal” for key-value pairs and “logical and” across multiple key-value pairs.
Again, while the constraints define when a hook should run,
it is left to the developer to make sure that the constraints are actually
checked beforehand. Instead of writing the checks into the function itself,
the main way to do this is to invoke the call_hooks() function as
a “wrapper” that will call all the hooks for the requested step (set). That is:
config_hooks_manager.call_hooks(configuration, step)
where configuration is the imsi Configuration object, and step
corresponds to the set of hooks defined in the config yaml.
Caution
Hooks require advanced knowledge of the imsi package and any files produced by imsi as part of the modelling platform implemented. Developers should only implement hooks when absolutely necessary and ensure that they do not compromise imsi’s functionality.
Example: making a new hook
For example, suppose you’d like to implement a hook that writes a
file to the /config folder of an experiment folder as part of imsi’s
setup:
# config_hooks_collection.py
def setup_write_exp_info(configuration):
"""Write config information to file in /config folder"""
path = os.path.join(configuration.setup_params.work_dir, "config")
source_id = configuration.source_id
runid = configuration.setup_params.runid
with open(os.path.join(path, "custom_output_file.txt"), "w") as f:
f.write("# custom output\n")
f.write(f"source_id={source_id}\n")
f.write(f"runid={runid}\n")
f.write(os.linesep)
Suppose you want to ensure that this function only runs if the
source_id of the experiment is set to "ModelA1-2".
The constraints for this hook would then be:
# config_hooks_collection_config.yaml
config_hooks:
setup:
- run: setup_write_exp_info
constraints:
model:
source_id": "ModelA1-2"
Remember, the function name begins with setup_*, indicating that
this should occurs after imsi’s setup has completed, but as the imsi
developer it is up to you to find the appropriate place to call this
hook set. The call would then be:
# Correct:
# Call of hooks using call_hooks()
# inside the appropriate imsi module/function
from imsi.utils.config_hooks_manager import call_hooks
# Other code, then where needed:
call_hooks(configuration, "setup")
# Wrong:
# Calling the hook directly will not automatically check
# if the constraints are met.
setup_write_exp_info(configuration)
Building on this example, if you wanted to add another hook to run at after the first, simply add it to the list under the same hook set:
# config_hooks_collection.py
def setup_another_hook(configuration):
print("hello", configuration.setup_params.parameters['runid'])
# config_hooks_collection_config.yaml
config_hooks:
setup:
- run: setup_write_exp_info
constraints:
model:
source_id": "ModelA1-2"
- run: setup_another_hook
constraints:
model:
source_id: "ModelA1-2"
You do not need to modify the call_hooks() call where it has been added
in the imsi code.
Building the docs
Under the main repo directory, is a .readthedocs.yml file that configures the documentation build process for Read the Docs. This file specifies the necessary settings and dependencies for building the documentation.
Building the docs offline
You can build a local version of imsi with the option to build the imsi docs with
pip install .[docs]
This will install sphinx and other packages required for building.
To build the docs, you can use the sphinx CLI included when you install the optional docs dependencies. E.g.:
sphinx-build -M html docs/ ~/public_html/docs