-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Loading the configuration outside of the functions #67 #69
Loading the configuration outside of the functions #67 #69
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #69 +/- ##
===========================================
+ Coverage 99.3% 99.42% +0.11%
===========================================
Files 5 5
Lines 433 520 +87
===========================================
+ Hits 430 517 +87
Misses 3 3
Continue to review full report at Codecov.
|
mocks/mocks.py
Outdated
@@ -21,12 +21,17 @@ | |||
from mocks.utils import load_yaml_config | |||
|
|||
|
|||
def load_configuration(config_location='config_example.yaml'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on Anthony's advice, I tried to create a dictionary and pass it through other functions instead of kwargs.
So the user should load the configuration first and pass it through other functions.
I hope this is what I was meant to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is pointless, the effect of having this is just to create an alternative name for mocks.utils.yaml_load_config
. You could achieve exactly the same effect with from mock.utils import load_yaml_config as load_configuration
, but there's no advantage in doing that, either.
What I suggested was creating a main function that would load the configuration and then call all of the other functions. That would be useful, because then the user would be able to create simulated images from simulation data without having to work out what order to call all of these functions in, or write their own code to so.
The reason why we originally suggesting splitting up the one big function you started with into a series of smaller, self contained functions wasn't that we thought that there shouldn't be a function that does the whole thing. The reason for splitting it up into multiple functions was to make the code easier to understand, test, maintain and reuse. There should still be a function that does the whole thing, it's just that instead of a great big block of code it should consist mostly of calls to the other functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some inconsistencies among the functions of mocks.py
. For instance functions scale_light_by_distance
and prepare_mocks
both use the distance to the galaxy to scale its light.
The first step to create the main function is making all functions consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After making the functions consistent, the jupyter-notebook examples should be updated as the base for the main function that calls other functions and does all the steps to create the mock image.
These notebooks could be used to create the main function.
mocks/tests/conftest.py
Outdated
config['Omega_Lambda'] = 0.727 | ||
config['Omega_0'] = 0.273 | ||
config['redshift'] = 0 | ||
return config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not want to load the configuration file for tests other than the one which is testing the mocks.load_configuration
so I created this fixture
. It is too long and that makes me think maybe I should try something else, so I will appreciate any advice about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use configuration files for all the tests? It would be simpler than this.
What you do is have a test fixture that loads the config and returns it (either here or in tests/test_mocks.py
), then have a test use the fixture and check that it loaded OK, e.g.
from mock.utils import load_yaml_config
@pytest.fixture(scope='package')
def configuration():
return load_yaml_config()
and
def test_load_configuration(configuration):
assert configuration['data_path'] == 'sim_data/cl19.fits'
assert configuration['galaxy_distance'] == 10.
 assert isinstance(configuration, dict)
The first test to use the configuration
fixture will trigger the call to load_yaml_config
, and if that fails with an exception then the exception will be passed up to the test, and it will fail (as it should). If it passes then the rest of the tests can run using the same configuration
fixture to access the test config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied your suggestion, but is that alright that the function utils.load_yaml_config
is tested twice? Once here and another time in test_utils.py
@lspitler @AnthonyHorton @wtgee |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, seems like I didn't successful explain my suggestion about where to load the configuration. See comments.
mocks/mocks.py
Outdated
@@ -21,12 +21,17 @@ | |||
from mocks.utils import load_yaml_config | |||
|
|||
|
|||
def load_configuration(config_location='config_example.yaml'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is pointless, the effect of having this is just to create an alternative name for mocks.utils.yaml_load_config
. You could achieve exactly the same effect with from mock.utils import load_yaml_config as load_configuration
, but there's no advantage in doing that, either.
What I suggested was creating a main function that would load the configuration and then call all of the other functions. That would be useful, because then the user would be able to create simulated images from simulation data without having to work out what order to call all of these functions in, or write their own code to so.
The reason why we originally suggesting splitting up the one big function you started with into a series of smaller, self contained functions wasn't that we thought that there shouldn't be a function that does the whole thing. The reason for splitting it up into multiple functions was to make the code easier to understand, test, maintain and reuse. There should still be a function that does the whole thing, it's just that instead of a great big block of code it should consist mostly of calls to the other functions.
mocks/tests/conftest.py
Outdated
config['Omega_Lambda'] = 0.727 | ||
config['Omega_0'] = 0.273 | ||
config['redshift'] = 0 | ||
return config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use configuration files for all the tests? It would be simpler than this.
What you do is have a test fixture that loads the config and returns it (either here or in tests/test_mocks.py
), then have a test use the fixture and check that it loaded OK, e.g.
from mock.utils import load_yaml_config
@pytest.fixture(scope='package')
def configuration():
return load_yaml_config()
and
def test_load_configuration(configuration):
assert configuration['data_path'] == 'sim_data/cl19.fits'
assert configuration['galaxy_distance'] == 10.
 assert isinstance(configuration, dict)
The first test to use the configuration
fixture will trigger the call to load_yaml_config
, and if that fails with an exception then the exception will be passed up to the test, and it will fail (as it should). If it passes then the rest of the tests can run using the same configuration
fixture to access the test config.
@AnthonyHorton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good step in right direction, but use of config versus function arguments is still patchy, and you have parsing of parameters spread around several functions instead of in one parse_config
function.
mocks/mocks.py
Outdated
@@ -108,26 +110,31 @@ def scale_light_by_distance(particle_positions, | |||
return particle_positions.value, particle_values | |||
|
|||
|
|||
def prepare_mocks(config_location='config_example.yaml', | |||
def prepare_mocks(config, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is doing 4 unrelated things. I would suggest splitting this into at least three functions.
The first function fully parses the config, i.e. loads the config file with mocks.utils.load_yaml_config
, then goes through all the parameters (not just galaxy_coordinates
& observation_time
as now) and applies any keyword arguments that override the config file values, gets the remaining values from the config file, adds default values for any parameters that are missing, and does any required unit conversions, etc. This function returns the fully populated, validated config dict for all the other functions to use. The other functions then don't have to do any fiddling around with overriding values, default values, unit conversions, etc.
The second function takes the config as its only argument, loads the simulation data, and returns it. This would be the place to do 32 bit to 64 bit float type conversion, when required.
The calls to compute_total_mass
and compute_apparent_ABmag
also don't belong here. You could create a 3rd function that calls them, using values from config
, and returns the results, or you could do those calls in the main function.
The general principle here is that a function should do one clearly defined thing, the thing that the function name implies that it does. Adding other, unrelated things to a function makes it much harder to understand the software.
mocks/mocks.py
Outdated
kwargs.get('target_galaxy_comoving_depth', | ||
config['target_galaxy_comoving_depth']) | ||
config['target_galaxy_comoving_depth'] = ensure_unit( | ||
config['target_galaxy_comoving_depth'], u.Mpc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't do all this stuff in this function, it doesn't belong here. Do it in a parse_config
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AnthonyHorton
Yes, I will. I just referenced your previous comment in another issue to start creating some functions, including parse_config
, in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a clue for me for creating the parse_config
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just put it in the right place to begin with? It's not off topic for this PR, which is about improving config loading. Seems to just be creating more work to write the code in one place in one PR, then immediately move it somewhere else in another one. You'll have to modify a bunch of tests twice if you do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all redundant now that it's being done in parse_config
. You should remove it from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AnthonyHorton
The function parse_config
and its tests are created. But it is not used by any other function yet.
@@ -0,0 +1,49 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After creating the parse_config
function, more cosmological parameters are added to config_examle.yaml
and since the new function should be tested in different ways, the added cosmological parameters are different to the default cosmology, WMAP9
. These different parameters cause changes in the result of many functions in test_mocks.py
. To avoid huge changes in the tests, I added this file with the same values that config_example.yaml
used to have.
However, I am not very happy with the name that I have chosen for it, maybe config_old_example.yaml
makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed given my comments below.
sim_pc_pixel: 170. | ||
|
||
# The baryonic mass of simulation particles (1e5 * Msol/h). | ||
particle_baryonic_mass_sim: 9.6031355 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I forgot it again!
@@ -0,0 +1,43 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is added for testing the parse_config
file when there is no cosmological parameter in the configuration file provided. The function is supposed to use the default cosmology in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed given my comments below.
T_CMB0: | ||
Neff: | ||
m_nu: | ||
redshift: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔢
@@ -0,0 +1,54 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has been added to test the capability of the parse_config
function when there are defined cosmological parameters in the configuration file but they have no value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve of the thoroughness, but I'm not sure this is entirely necessary. Seems like a strange thing for the user to do.
mocks/mocks.py
Outdated
try: | ||
assert config['Omega_b'] | ||
except AssertionError: | ||
config['Omega_b'] = default_cosmology.Ob0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make the code works for the Dark Matter only simulations, I think maybe I should change the lines that are parsing baryonic matter density as follow:
try:
config['Omega_b'] = kwargs.get('Omega_b',
config['Omega_b'])
except KeyError:
config['Omega_b'] = default_cosmology.Ob0
try:
assert config['Omega_b']
except AssertionError:
if config['Omega_b'] is not 0:
config['Omega_b'] = default_cosmology.Ob0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a general point, if you want to distinguish between something being None
, or 0
, or some other value which evaluates to False
(e.g. an empty list
) then you can test for those directly, e.g. if config['Omega_b'] is None:
or if config['Omega_b'] == 0:
, etc.
As I noted in comments above I really don't think you need to allow for keys without values in the config file so you don't need this added complexity at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of the reason for parsing the configuration in a dedicated function is to ensure all the other functions use a consistent set of configuration parameters. Consequently you should remove the option to override configuration parameters with keyword argument from the other functions. parse_config
should be only place to do that.
Also, nested .get()
is a better way to implement the 'keyword arg, or config file, or default value' pattern than the forced exceptions you are using.
You can also simplify things by dropping the requirement to be able to have keys in the config file with no value. I don't know why you would need to be able to do that.
config_directory/config_example.yaml
Outdated
# The resolution of the simulation(parsec / pixel). | ||
sim_pc_pixel: 170 | ||
sim_pc_pixel: 170 | ||
|
||
# The baryonic mass of simulation particles (1e5 * Msol/h). | ||
particle_baryonic_mass_sim: 9.6031355 | ||
|
||
# Cosmological parameters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might include a comment about which cosmology these example parameters correspond too, e.g. WMAP9.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of them are the parameters that have been used in the simulation and I just have chosen the others randomly. I intentionally wanted them to be different from WMAP9 for tests purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different from WMAP9 for tests purposes
that sounds like a test of astropy.cosmology
not huntsman-mocks
, which shouldn't care about what cosmology is needed.
Are you sure you need this test?
@@ -0,0 +1,54 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve of the thoroughness, but I'm not sure this is entirely necessary. Seems like a strange thing for the user to do.
mocks/mocks.py
Outdated
config = utils.load_yaml_config(config_file) | ||
# Coordinates of the target galaxy. | ||
config['galaxy_coordinates'] = kwargs.get('galaxy_coordinates', | ||
config['galaxy_coordinates']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want default values for any of these parameters, in case they aren't in either the config file or kwargs
? Can do this using the following pattern:
config[<parameter_name>] = kwargs.get(<parameter_name>,
config.get(<parameter_name>,
<default value>))
Might make sense to do this for parameters you don't expect to be changed by the user very often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I choose any particular coordinate as the default or just something like what exists in the config file would work?
mocks/mocks.py
Outdated
kwargs.get('target_galaxy_comoving_depth', | ||
config['target_galaxy_comoving_depth']) | ||
config['target_galaxy_comoving_depth'] = ensure_unit( | ||
config['target_galaxy_comoving_depth'], u.Mpc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all redundant now that it's being done in parse_config
. You should remove it from here.
@AnthonyHorton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments given. In retrospect, the discussion about cosmology and related tests might have been better suited for a separate PR as its separate to the way the config is loaded. But it seems it has merged here, so let's treat this PR as both finalising the expected cosmology and dealing with how to load the config.
@@ -0,0 +1,43 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed given my comments below.
@@ -0,0 +1,49 @@ | |||
# It is here as an example, and if the user wants to change the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed given my comments below.
config_directory/config_example.yaml
Outdated
# The resolution of the simulation(parsec / pixel). | ||
sim_pc_pixel: 170 | ||
sim_pc_pixel: 170 | ||
|
||
# The baryonic mass of simulation particles (1e5 * Msol/h). | ||
particle_baryonic_mass_sim: 9.6031355 | ||
|
||
# Cosmological parameters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different from WMAP9 for tests purposes
that sounds like a test of astropy.cosmology
not huntsman-mocks
, which shouldn't care about what cosmology is needed.
Are you sure you need this test?
@lspitler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good @bazkiaei I think this is ready after you address my remaining comments.
@@ -23,6 +23,118 @@ | |||
from mocks.utils import load_yaml_config | |||
|
|||
|
|||
def parse_config(config_file='config_example.yaml', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this should move into utils
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use some advice from @AnthonyHorton and @wtgee about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bazkiaei I like this definition:
Looks to me like /utils is a place where you can place small snippets you can use throughout the application. Small functions to build bigger things with.
from erikras/react-redux-universal-hot-example#808
You designed parse_config
to be used specifically for mocks.py
, so I guess it makes sense.
This is scope creep on the PR, so let's close this PR without changing this, but create a new Issue to decide where all mocks.py
functions should go -- other ones I think meet the definition I copied in above. Or better yet add it to the discussion about creating a Class for Mocks: #63
@AnthonyHorton your request for review is blocking this PR - can you Approve when you get a chance? I'm happy with @bazkiaei changes based upon your comments. |
…t to the `config_example.yamle`.
OK, looks good now. LGTM. |
The goal of this PR is to make the code not to load the configuration more than once.