How to maintain YAML configurations without duplication¶
Goal
Create and maintain YAML configuration files that share a common base while avoiding duplication of configuration code.
This guide presents three options to define a base YAML configuration file and extend it to create case-specific variants, such as development, production, or testing. Each option is illustrated with an example that starts from a base MATLAB simulation configuration and extends it into a high-precision configuration, inheriting from the base while overriding specific parameters.
Prerequisites:
MATLAB R2019b or later.
yamlpackage v1.6.0, installed and added to the path.Intermediate familiarity with YAML syntax.
Options summary:
Option |
Scalability |
Use this is you want… |
|---|---|---|
Low |
to keep everything in a single config file |
|
Medium |
to have separate config files |
|
High |
to have complex inheritance chains between config files |
YAML merge keys¶
How to implement¶
Create a single YAML file to store all configuration cases.
Define the shared base configuration in that file.
Use YAML anchors and merge keys to derive the remaining configurations in that file.
Load the file with
yaml.loadFile().Select the required configuration from the loaded data.
Example¶
Create a single YAML, config.yaml, to store all the configuration cases. Define the shared configuration in that file in the default field. Use YAML anchors (&default and &solver_options) and merge keys (<<: *default and <<: *solver_options) to derive base and high-precicion configurations in base and high_precision fields.
1default: &default
2 odefun: exponential_decay
3 tspan: [0, 10]
4 y0: 1
5 solver: ode45
6 solver_options: &solver_options
7 RelTol: 1.0e-3
8 AbsTol: 1.0e-6
9 MaxStep: 0.1
10 InitialStep: 0.01
11
12base:
13 <<: *default
14
15high_precision:
16 <<: *default
17 solver_options:
18 <<: *solver_options
19 RelTol: 1.0e-6
20 AbsTol: 1.0e-9
Load the config.yaml with yaml.loadFile() and select the required configuration from the loaded data:
config = yaml.loadFile("config.yaml", "ConvertToArray", true);
% Load base configuration.
cfg = config.base;
% Load high-precision configuration.
cfg = config.high_precision;
Implicit composition¶
How to implement¶
Create a YAML file that defines the base configuration.
Put all shared configuration values in that file.
Create a separate YAML file for each configuration case.
In each case-specific file, define only the values that differ from the base.
Select an external tool or implement a custom function to load the base configuration and apply the case-specific overrides.
Load the required configuration using that tool or function.
Example¶
Create a YAML file, base.yaml, with the base configuration values.
1odefun: exponential_decay
2tspan: [0, 10]
3y0: 1
4solver: ode45
5solver_options:
6 RelTol: 1.0e-3
7 AbsTol: 1.0e-6
8 MaxStep: 0.1
9 InitialStep: 0.01
Create a separate YAML file, high_precision.yaml, defining only the values that differ from the base.yaml:
1solver_options:
2 RelTol: 1.0e-6
3 AbsTol: 1.0e-9
Implement the following custom funtion, mergeStructs(), to override the base configuration with the high-precision configuration.
1function [result] = mergeStructs(override, base)
2%MERGESTRUCTS Recursively merge two structures.
3%
4% RESULT = MERGESTRUCTS(OVERRIDE, BASE) returns a structure where fields
5% in OVERRIDE replace those in BASE and nested structs are also merged
6% recursively.
7
8 result = base;
9
10 for fn = fieldnames(override)'
11 field = fn{1};
12
13 if isfield(result, field) && isstruct(result.(field)) ...
14 && isstruct(override.(field))
15 % Recursively merge nested structs.
16 result.(field) = mergeStructs(override.(field), result.(field));
17 else
18 % Override or add new field.
19 result.(field) = override.(field);
20 end
21 end
22end
Load the required configuration using yaml.loadFile() and mergeStructs():
% For loading base configuration.
cfg = yaml.loadFile("base.yaml", "ConvertToArray", true);
% For loading high-precision configuration.
base = yaml.loadFile("base.yaml", "ConvertToArray", true);
high_precision = yaml.loadFile("high_precision.yaml", "ConvertToArray", true);
cfg = mergeStructs(high_precision, base);
Explicit composition¶
How to implement¶
Create a separate YAML file for each base configuration.
Define all values for each base configuration case in its file.
Create a separate YAML file for each extended configuration.
In each extended configuration file, explicitly specify which base configuration it extends and define only the values that differ from that base.
Implement a custom function to load configurations, taking extensions into account.
Load the required configuration using this custom function.
Example¶
Create a YAML file, base.yaml, and define the base configuration:
1odefun: exponential_decay
2tspan: [0, 10]
3y0: 1
4solver: ode45
5solver_options:
6 RelTol: 1.0e-3
7 AbsTol: 1.0e-6
8 MaxStep: 0.1
9 InitialStep: 0.01
Create a YAML file, high_precision.yaml, define it is a extension of base.yaml writing extends: base.yaml and define the only the values that differ from base.yaml.
1extends: base.yaml
2solver_options:
3 RelTol: 1.0e-6
4 AbsTol: 1.0e-9
Implement the custom function, loadSimulationConfig(), to load a configuration file taking extensions into account.
1function [config] = loadSimulationConfig(filename)
2%LOADSIMULATIONCONFIG Load a simulation configuration from a YAML file.
3%
4% CONFIG = LOADSIMULATIONCONFIG(FILENAME) loads the YAML configuration
5% stored in FILENAME. If the configuration contains an 'extends' field,
6% the parent configuration is loaded and merged recursively.
7
8 % Load the configuration from YAML file.
9 config = yaml.loadFile(filename, "ConvertToArray", true);
10
11 % Merge with parent config if 'extends' is specified
12 if isfield(config, "extends")
13 parentConfig = loadSimulationConfig(config.extends);
14 config = rmfield(config, "extends");
15 config = mergeStructs(config, parentConfig);
16 end
17end
Load the required configuration file with loadSimulationConfig():
% Load base configuration.
cfg = loadSimulationConfig("base.yaml");
% Load high-precision configuration.
cfg = loadSimulationConfig("high_precision.yaml");
Appendix¶
Here is the ODE function definition:
1function dydt = exponential_decay(t, y)
2%EXPONENTIAL_DECAY Simple first-order ODE.
3%
4% dydt = EXPONENTIAL_DECAY(t, y) returns the time derivative of y
5% for the equation: dy/dt = -k*y, where k is the decay rate.
6
7 k = 0.5;
8 dydt = -k*y;
9end
Here is how to run a simulation with the loaded configuration:
% Use the selected configuration for the simulator.
solver = str2func(cfg.solver);
odefun = str2func(cfg.odefun);
[t, y] = solver(odefun, cfg.tspan, cfg.y0, cfg.solver_options);