-
Notifications
You must be signed in to change notification settings - Fork 39
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
major update: add channel configuration file management #100
Conversation
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 these should not be on GitHub. We can have code that will generate these (maybe this is already generated by code)?
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.
Sure
software/control/utils.py
Outdated
def get_channel_color(self, channel): | ||
channel_info = CHANNEL_COLORS_MAP.get(extract_wavelength_from_config_name(channel), {"hex": 0xFFFFFF, "name": "gray"}) | ||
return channel_info["hex"] |
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.
Will all channels have gray color?
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.
Not very sure about this. It was in the old ConfigurationManager and I moved it to utils because at a few places we are passing the whole ConfigurationManager only to use get_channel_color and extract_wavelength
software/control/gui_hcs.py
Outdated
use_glass_top=USE_GLASS_TOP, | ||
look_for_cache=False, | ||
self.objectiveStore, | ||
self.acquisitionConfigurationManager.get_autofocus_configurations() |
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.
(mark this to look at - laserAutofocusController probably should not access self.acquisitionConfigurationManager)
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 left a bunch of comments. Some other observations:
- It looks like the new
ChannelConfigurationManager
is only ever used when there's aConfigurationManager
present. Instead of needing to pass around both, would it make sense for us to just put aAcquisitionConfigurations
ordict[ConfigTypeFlag, List[Configurations]]
attribute on theConfigurationManager
, and then just add a few helpers to that object for getting configs? - There are a few hard things about the codebase, but one of the hardest is the fact that we store related state in a bunch of separate places, and in some cases store the same state in multiple places. My suggestion is to get rid of all the
active_*
attributes, and just store theConfigTypeFlag
and currentConfiguration
in one place. Preferably together in a way that requires updating both if you're going to update 1. - Make all the xml read/write functions build the xml at the time of writing/parsing and eliminate all the intermediate xml tree storage. Just use xml as our serialization format.
for mode in self.config_xml_tree_root.iter("mode"): | ||
self.num_configurations += 1 | ||
self.configurations.append( | ||
class ChannelConfigurationManager: |
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 we considered this already and it doesn't make sense, but there's a pydantic <-> xml library ( https://pydantic-xml.readthedocs.io/en/latest/ ) that would let us use pydantic models for configuration, and handles writing / reading them from xml files for us.
This would be nice because:
- We'd get all the benefits of using pydantic models (IDEs understand them, some types, validation of values, consistency in our codebase)
- We don't need to manually serialize/deserialize the xml structure in code. This is really fragile and likely to break as we make changes!
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 nice. Will try this out
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.
@Alpaca233 can we get rid of "CHANNEL" config type?
software/control/core/core.py
Outdated
def set_profile_path(self, profile_path: Path) -> None: | ||
self.current_profile_path = profile_path | ||
|
||
def load_configurations(self, objective: str) -> None: |
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'm not sure of the right answer (or if there is one), but what led us to a directory structure + multiple xml files instead of 1 xml file containing all the configurations? The single file approach seems simpler (we only need to know 1 path), and easier to distribute (we distribute a single file, not a directory structure with many files). I guess one down side (maybe?) is that it could be a pretty big file, and picking and choosing only individual configurations might be trickier. But we're already requiring someone to modify xml if they aren't using our UI, so I'm not sure it's worse (if anything, they only need to deal with xml instead of a directory structure and files that need correct names).
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.
So in each objective folder there could be multiple files. Now we have channel configuration file (xml) and autofocus cache file (json). I guess this would be easier to find and make changes to them. Also may be easier to add an objective and another type of configuration file? This is @hongquanli 's idea so he may have a better answer.
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 have strong objective thoughts either way. My comment comes from a place of trying to simplify (it is simpler if we only need to keep track of a single datastructure (or a few), versus a few datastructures, a directory format, and a file naming convention).
Both can definitely work, though. And if we want heterogeneous serialization formats for some reason, then it definitely makes sense.
objective_path = self.current_profile_path / objective | ||
|
||
# Load spinning disk configurations if enabled | ||
if ENABLE_SPINNING_DISK_CONFOCAL: |
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.
Whenever possible, if we can de-couple from global configuration that'd be helpful. In this case, we could just check to see if confocal_conifgurations.xml
exists. If it does, load it. If it doesn't, don't (same for the widefield_
and channel_
files). Then anywhere that needs that configuration later on can check if the ChannelConfigurationManager
it is using has it or not (they'll already need to check if ENABLE_SPINNING_DISK_CONFOCAL
is True
there anyway).
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.
Will change this
self.save_configurations() | ||
def save_configurations(self, objective: str) -> None: | ||
"""Save channel configurations for a specific objective.""" | ||
if ENABLE_SPINNING_DISK_CONFOCAL: |
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.
Same here (and everywhere we check for ENABLE_SPINNING_DISK_CONFOCAL
in this class). Is there a disadvantage to just checking what config is active and writing that (regardless of the ENABLE_*
check)?
software/control/core/core.py
Outdated
self.active_channel_config = None | ||
self.active_config_xml_tree = None | ||
self.active_config_xml_tree_root = None | ||
self.active_config_flag = -1 # 0: channel, 1: confocal, 2: widefield |
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.
It'd be better to use an enum.Enum
class for this. In other words, something like this:
class ConfigTypeFlag(enum.Enum):
UNKNOWN = auto()
CHANNEL = auto()
CONFOCAL = auto()
WIDEFIELD = auto()
Then
self.active_config_flag = ConfigTypeFlag.UNKNOWN
This is better for a couple of reasons:
- We don't need to remember numbers, and don't use bare numbers elsewhere in code. The later is particularly important because if we use bare numbers (eg: 1), it's impossible to distinguish between different occurrences of 1 in our codebase without understanding all the context it is used it (does that 1 mean active_config_flag, or a quantity 1 of something, or some other flag?).
- IDEs and tools know how to handle the enum, so if we ever need to refactor it's really easy.
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.
Will change this
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.
While reviewing Hongquan's #100, we can across the fact that auto()
is a little dangerous. So instead I'd use:
class ConfigTypeFlag(enum.Enum):
UNKNOWN = "UNKNOWN"
CHANNEL = "CHANNEL"
CONFOCAL = "CONFOCAL"
WIDEFIELD = "WIDEFIELD"
for now.
@@ -165,3 +167,21 @@ def ensure_directory_exists(raw_string_path: str): | |||
path: pathlib.Path = pathlib.Path(raw_string_path) | |||
_log.debug(f"Making sure directory '{path}' exists.") | |||
path.mkdir(parents=True, exist_ok=True) | |||
|
|||
|
|||
def extract_wavelength_from_config_name(name): |
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 seems pretty fragile! If we depend on the wavelength for config, should it instead be a config attribute? It's a bit funky to store some of our config values in the object itself, and some in the name.
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.
Indeed. I think there were a few other cases where we need to extract an attribute from the name. We may need to update this config object
@@ -66,7 +66,8 @@ def __init__(self, title): | |||
def toggle_content(self, state): | |||
self.content_widget.setVisible(state) | |||
|
|||
|
|||
''' |
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 like a bunch of things are marked like this. I won't comment on them from now on, but make sure to delete or fix before merging!
software/control/widgets.py
Outdated
@@ -329,6 +330,71 @@ def apply_and_exit(self): | |||
self.close() | |||
|
|||
|
|||
class LaserAutofocusSettingsWidget(QDialog): | |||
def __init__(self, laser_af_controller): |
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.
It'd be nice to add the type annotation on laser_af_controller
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.
Will change this
software/control/widgets.py
Outdated
@@ -347,17 +413,12 @@ def __init__(self, xlight, config_manager=None): | |||
|
|||
self.disk_position_state = self.xlight.get_disk_position() | |||
|
|||
if self.config_manager is not None: |
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.
Normally for object presence checks, you can just do:
if self.config_manager:
# do things with config manager
What you've done works too, just the direct check is a bit less typing. It only breaks down if you have an object that modifies its __bool__
method or len method (see https://docs.python.org/3/reference/datamodel.html#object.__bool__ ). But it's odd for an object to do that if it's not a math/collection type of some sort.
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.
Will change this
software/control/widgets.py
Outdated
self.currentConfiguration = self.configurationManager.configurations[0] | ||
|
||
if self.configurationManager and self.channelConfigurationManager: | ||
self.currentConfiguration = self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] |
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.
Hmm it looks like we store our currentConfiguration
here. It's tricky to follow if we store the active config here, but also have an active channel stored on the channelConfigurationManager
, and the active objective elsewhere. Ideally, we'd just store the active configuration information in one place and use that to query information from other sources as needed.
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.
Also, is this [0]
always guaranteed safe?
software/control/core/core.py
Outdated
self.autofocus_configurations[objective] = {} | ||
self.autofocus_configurations[objective].update(updates) | ||
|
||
class ConfigurationManager(QObject): |
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 like it does not need to be a QObject
software/control/core/core.py
Outdated
def _get_available_profiles(self) -> List[str]: | ||
if not self.base_config_path.exists(): | ||
os.makedirs(self.base_config_path) | ||
os.makedirs(self.base_config_path / "default_profile") | ||
for objective in OBJECTIVES: | ||
os.makedirs(self.base_config_path / "default_profile" / objective) | ||
return [d.name for d in self.base_config_path.iterdir() if d.is_dir()] |
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 needs more documentations. What are the "available profiles"?
software/control/core/core.py
Outdated
@@ -4433,7 +4560,6 @@ def get_surface_grid(self, x_range, y_range, num_points=50): | |||
|
|||
|
|||
class LaserAutofocusController(QObject): |
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.
We should have a separate PR to update LaserAutofocusController (including make it use laserAFConfig base model)
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.
For this PR we can leave this class unchanged (revert the modifications). It would have been better to update LaserAutofocusController to make it use laserAFConfig before doing this PR.
software/control/core/core.py
Outdated
self.color = utils.get_channel_color(self.name) | ||
|
||
|
||
QMetaType.registerType(ChannelMode) |
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.
What does this do?
software/control/core/core.py
Outdated
class ChannelConfig(BaseXmlModel, tag='configurations'): | ||
"""Root configuration file model""" | ||
modes: List[ChannelMode] = element(tag='mode') |
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.
Add more description on what this is and how this is used?
software/control/core/core.py
Outdated
@@ -3690,7 +3690,8 @@ def load_configurations(self, objective: str) -> None: | |||
config_file = self.current_profile_path / objective / "laser_af_cache.json" |
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.
we should rename laser af cache
to laser af setting
software/control/core/core.py
Outdated
def update_laser_af_cache(self, objective: str, updates: Dict[str, Any]) -> None: | ||
if objective not in self.autofocus_configurations: | ||
self.autofocus_configurations[objective] = {} | ||
self.autofocus_configurations[objective].update(updates) | ||
self.autofocus_configurations[objective] = LaserAFConfig(**updates) | ||
else: | ||
config = self.autofocus_configurations[objective] | ||
self.autofocus_configurations[objective] = config.model_copy(update=updates) |
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 do the two cases need to be treated separately? Can we use self.autofocus_configurations[objective] = LaserAFConfig(**updates)
for both?
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.
LaserAFConfig(**updates) creates a new BaseModel object with default values if the laser af cache doesn't exist, and update the items as needed. model_copy update the current model loaded in memory. If we use LaserAFConfig(**updates) we will lose the information of the loaded model.
Tested in simulation mode
New file structure:
software/acquisition_configurations/default_configurations/4x/
configuration files:
channel_configurations.xml
confocal_configurations.xml (optional)
widefield_configuration.xml (optional)
laser_af_reference_plane.json (optional)