Skip to content
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

[WIP] GUI hdf5 parameter loading #722

Closed
wants to merge 43 commits into from

Conversation

gtdang
Copy link
Collaborator

@gtdang gtdang commented Feb 28, 2024

Add support for loading connectivity and drive parameters from an hdf5 saved network object.

closes #708

@gtdang gtdang changed the title Gui hdf5 io [WIP] GUI hdf5 parameter loading Feb 28, 2024
@gtdang
Copy link
Collaborator Author

gtdang commented Feb 28, 2024

@ntolley @jasmainak
Hit a blocker on this PR. The GUI operates on a self.param attribute that is a dict of a parameter json.

I've gotten to the point of being able to read in the hdf5 file as a Network object and now I need to get it into the same dict format of self.param so I can update the GUI's parameter state.

Is there a way to create a param dict/json from a Network object? If not I'll need to come up with a way to map all of a network's attributes to the correct key-value pairs.

@ntolley
Copy link
Contributor

ntolley commented Mar 1, 2024

@gtdang unfortunately I think the latter option is necessary. The reason we developed the hdf5 format is precisely because we didn't have a way to output HNN-core network models into a .param file. The ultimate goal is to break ties with the param format entirely, and instead operate on the dict used to store the network.

@ntolley
Copy link
Contributor

ntolley commented Mar 1, 2024

Can you point me to a line where this crops up? It may be more expedient for me to figure out that mapping since I have a good sense of what attributes in the Network object map to .param values.

@gtdang
Copy link
Collaborator Author

gtdang commented Mar 5, 2024

@ntolley
Sorry it took some time to respond. There isn't a single place where parameter mapping necessarily appears. To get a better sense of how the GUI utilizes the param format I've mapped the flow of the params attribute in the image below.

The GUI functions are often initializing a Network object with the params attribute and then uses the Network object in another function to display the encoded Connections and Drives. I suppose if we wanted to do away with the params attribute we could just load the HDF5 Network to the GUI and pass that around. The function that would need to be reworked is the add_drive_tab and extract_drive_specs_from_hnn_params functions, which require the param dictionary format.

hnn-gui-params-flow

@gtdang
Copy link
Collaborator Author

gtdang commented Mar 6, 2024

@ntolley Sorry it took some time to respond. There isn't a single place where parameter mapping necessarily appears. To get a better sense of how the GUI utilizes the param format I've mapped the flow of the params attribute in the image below.

The GUI functions are often initializing a Network object with the params attribute and then uses the Network object in another function to display the encoded Connections and Drives. I suppose if we wanted to do away with the params attribute we could just load the HDF5 Network to the GUI and pass that around. The function that would need to be reworked is the add_drive_tab and extract_drive_specs_from_hnn_params functions, which require the param dictionary format.

hnn-gui-params-flow

One solution to support both hdf5 and the param file is:

  • Add a new class attribute to flag if the params are in a Network or param format (self.is_param)
  • Add a new class attribute to save a Network from hdf5 (self.network)
  1. In the diagram above, any function that "Initializes Network with self.params" will we rewritten to handle either param dict or a Network object. Probably will add self.is_param flag as an argument and logic to either initialize a Network from params dict or use self.network.
  2. Add a new function extract_drive_specs_from_hnn_network

@jasmainak
Copy link
Collaborator

@gtdang the more complexity you add, the more maintenance work and the harder for a future contributor ... I think you guys need to take a judgement call whether to support hdf5 or param or both and whether for read/write or both. My plan was to phase out params by outputting hdf5 always (never params/json) but allowing to read json for backwards compatibility ... for a while.

@gtdang
Copy link
Collaborator Author

gtdang commented Mar 6, 2024

@gtdang the more complexity you add, the more maintenance work and the harder for a future contributor ... I think you guys need to take a judgement call whether to support hdf5 or param or both and whether for read/write or both. My plan was to phase out params by outputting hdf5 always (never params/json) but allowing to read json for backwards compatibility ... for a while.

Totally agree with keeping complexity at a minimum. I'm curious about the shift to hdf5. Is the plan for the Network to only be instantiated by an hdf5 read-in? Seems like a chicken and egg situation where you need a Network to write to hdf5 yet need to init a Network with an hdf5.

Is the idea that users are not likely create & edit their own hdf5 Network and are always starting with pre-constructed Networks? Do we have a recommendations for hdf5 viewers/editors for those that want to edit params outside of the the API?

@jasmainak
Copy link
Collaborator

We don't want to encourage users to edit params outside the API ... they are on their own if they do so.

In the past, we had the params being passed around willy-nilly in different functions and it was hard to keep track what was happening. Modifying the Network only through the API allows us to validate the parameters while also ensuring that they are consistent ... for example start/end time of the drives cannot be outside the simulation start/end time.

Hdf5 was chosen as it's a simple hierarchical format that allows you to translate python objects into a binary format while also being future-proof (unlike pickling). Users should always start with a template network that we provide and make changes through the API. They can then save the network and load it back but they cannot make arbitrary changes to the network object.

@gtdang gtdang requested a review from hollandjg March 6, 2024 19:20
Copy link
Collaborator

@hollandjg hollandjg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! I've got a few comments – a couple of potential simplifications.

Comment on lines 73 to 80
output_filenames = [Path(output_dir, f.name.split('.')[0])
for f in file_list]

# Conversion
[convert_to_hdf5(file, outfile)
for (file, outfile) in zip(file_list, output_filenames)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it might be simpler just as a for loop over for file in file_list:.

Comment on lines 44 to 45
print(f"Failed to retrieve contents. Status code: "
f"{response.status_code}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this raise an exception instead?
You could use this pattern: https://stackoverflow.com/a/24531618
... which might simplify your code a bit.

Comment on lines 683 to 686
def _convert_to_path(value):
if isinstance(value, str):
value = Path(value)
return value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this function? Could you just do Path(value) wherever you use this function? Or do you rely on it not throwing an error and just returning None if you give it, say, an integer?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking further, it looks like you might pass it a Path instead. Path is meant to be idempotent, so that should still be fine.

Path throws an error unless you give it a string or a Pathlike-object (something with a __fspath__ method which returns a path on your system).

Comment on lines 709 to 713
_validate_type(params_fname, (str, Path), 'params_fname')
_validate_type(out_fname, (str, Path), 'out_fname')

params_fname = _convert_to_path(params_fname)
out_fname = _convert_to_path(out_fname)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you just do:

Suggested change
_validate_type(params_fname, (str, Path), 'params_fname')
_validate_type(out_fname, (str, Path), 'out_fname')
params_fname = _convert_to_path(params_fname)
out_fname = _convert_to_path(out_fname)
params_fname = pathlib.Path(params_fname)
out_fname = pathlib.Path(out_fname)

That'll validate the types of the variables (and throw the same TypeError if they're incompatible) and make your Path objects.

net_hdf5 = read_network(outpath)
assert net_hdf5 == net_params

# Check if writing with no extension will add one
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Check if writing with no extension will add one
# Check that writing with no extension will add one

# Check if writing with no extension will add one
outpath_no_ext = Path(tmp_path, 'default_no_ext')
convert_to_hdf5(params_base_fname, outpath_no_ext)
assert outpath_no_ext.with_suffix('.hdf5').exists()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also include the comment in the assert statement, so if it fails you get a more useful error message

Suggested change
assert outpath_no_ext.with_suffix('.hdf5').exists()
assert outpath_no_ext.with_suffix('.hdf5').exists(), "Writing with no extension adds one"

def test_convert_to_hdf5_bad_type():
"""Tests type validation in convert_to_hdf5 function"""
good_path = hnn_core_root
path_str = good_path.__str__()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use the str function rather than the dunder method here – it should do the same thing and seems a bit more standard than calling the dunder method directly (though I can't find a good reference for that – just some posts on StackExchange).

Suggested change
path_str = good_path.__str__()
path_str = str(good_path)

@@ -0,0 +1,93 @@
"""Script converting legacy param and json parameter files to hdf5.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this PR still relevant. I see: https://github.com/jonescompneurolab/hnn-core/tree/master/dev_scripts already in main ? Or does it need a rebase?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's currently held up by #733. Will be rebased after that one is merged.

gtdang added 27 commits April 3, 2024 16:44
…oved _connectivity_to_list_of_dicts function and call _conn_to_dict directly in write_network.
* Changed to load files into memoryview instead of bytes as this is the format consistent with ipywidgets documentation and was not working for hdf5.
* Added different mimetypes for different types of files.
@gtdang
Copy link
Collaborator Author

gtdang commented Apr 22, 2024

Closing this because hdf5 files in the current format are too large. We are working on a different solution for reading/writing outputs.

@gtdang gtdang closed this Apr 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GUI loading params from hdf5
4 participants