diff --git a/src/ethopy/config.py b/src/ethopy/config.py index 1eb2f0d..9825e04 100644 --- a/src/ethopy/config.py +++ b/src/ethopy/config.py @@ -59,6 +59,7 @@ def _set_defaults(self) -> None: "behavior": "lab_behavior", "interface": "lab_interface", "recording": "lab_recordings", + "mice": "lab_mice", }, "logging": { "level": "INFO", diff --git a/src/ethopy/core/logger.py b/src/ethopy/core/logger.py index 4249c99..d06994c 100755 --- a/src/ethopy/core/logger.py +++ b/src/ethopy/core/logger.py @@ -60,19 +60,21 @@ def _set_connection() -> None: behavior: The virtual module for behavior. interface: The virtual module for interface. recording: The virtual module for recording. + mice: The virtual module for the animal colony. public_conn: The connection object for public access. Returns: None """ - global experiment, stimulus, behavior, interface, recording, public_conn + global experiment, stimulus, behavior, interface, recording, mice, public_conn virtual_modules, public_conn = create_virtual_modules(SCHEMATA) experiment = virtual_modules["experiment"] stimulus = virtual_modules["stimulus"] behavior = virtual_modules["behavior"] recording = virtual_modules["recording"] interface = virtual_modules["interface"] + mice = virtual_modules["mice"] _set_connection() diff --git a/src/ethopy/core/mice.py b/src/ethopy/core/mice.py new file mode 100644 index 0000000..281447f --- /dev/null +++ b/src/ethopy/core/mice.py @@ -0,0 +1,233 @@ +"""Create tables for the lab_mice schema. + +Mirrors the animal colony schema hosted at `lab_mice`: animal identity, lines and +genotypes, weights, surgeries/implants and location transfers. Importing this +module declares any missing table in the schema mapped to ``mice`` in SCHEMATA. +""" + +import datajoint as dj + +from ethopy.core.logger import mice # noqa: F401 + + +@mice.schema +class Lines(dj.Manual): + """Basic mouse line info.""" + + definition = """ + # Basic mouse line info + line : varchar(100) # Mouse Line Abbreviation + --- + line_full : varchar(100) # full line name + rec_strain : varchar(20) # recipient strain + donor_strain : varchar(20) # donor strain + n=null : tinyint # minimum number of backcrosses to recipient strain + seq : varchar(5000) # sequence of transgene, if available + line_notes : varchar(4096) # other comments + line_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class Mice(dj.Manual): + """Basic mouse info.""" + + definition = """ + # Basic mouse info + animal_id : int # id number + --- + other_id='' : varchar(20) # alternative id number + dob=null : date # animal's date of birth + dow=null : date # animal's date of weaning + sex='unknown' : enum('M','F','unknown') # animal's sex + color='unknown' : enum('Black','Brown','White','unknown') # animal's color + line='' : varchar(255) # mouse line + genotype='unknown' : enum('homozygous','heterozygous','hemizygous','positive','negative','wild type','unknown') + ear_punch='unknown' : enum('None','R','L','RL','RR','LL','unknown') # animal's ear punch + owner='none' : enum('manolis','maria','emina','Other','Available','none') # mouse's owner + fluo_test='unknown' : enum('unknown','no','yes') # fluorescence test result + mouse_notes='' : varchar(4096) # other comments and distinguishing features + facility='unknown' : enum('TMF','Taub','Other','unknown') # animal's current facility + room='unknown' : enum('VK3','VH1','T014','T057','T086D','Other','unknown','T027') # animal's current room + rack=null : char(4) # animal's current rack + row='' : char(1) # animal's current row + mouse_ts=CURRENT_TIMESTAMP : timestamp # automatic + cage_id='' : varchar(100) # animal's current cage + usage='unknown' : enum('in use','available','euthanize','unknown') # availability + """ + + +@mice.schema +class Death(dj.Manual): + """Info about each mouse's death.""" + + definition = """ + # info about each mouse's death + -> Mice + --- + dod=null : date # date of death + death_notes='' : varchar(4096) # other comments + death_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class Founders(dj.Manual): + """Additional info about founder mice.""" + + definition = """ + # Additional info about founder mice + -> Mice + -> Lines + --- + source : varchar(100) # source of mouse (lab, company) + doa=null : date # date of arrival + founder_notes : varchar(4096) # other comments + founder_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class Genotypes(dj.Manual): + """Info about each mouse's genotype.""" + + definition = """ + # info about each mouse's genotype + -> Mice + -> Lines + --- + genotype='unknown' : enum('homozygous','heterozygous','hemizygous','positive','negative','wild type','unknown') # animal's genotype + genotype_notes=null : varchar(4096) # other comments + genotype_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class Parents(dj.Manual): + """Parent-child relationships between mice.""" + + definition = """ + # parent-child relationships between mice + -> Mice + parent_id : varchar(20) # id number of parent + --- + relation_notes='' : varchar(4096) # other comments + relation_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class Transfers(dj.Manual): + """Completed transfers.""" + + definition = """ + # completed transfers + -> Mice + dot : date # date of transfer + --- + from_owner='none' : enum('alex','manolis','Other','Available','none') # previous owner + to_owner='none' : enum('alex','manolis','Other','Available','none') # new owner + from_facility='unknown' : enum('TMF','Taub','Other','unknown') # animal's previous facility + to_facility='unknown' : enum('TMF','Taub','Other','unknown') # animal's new facility + from_room='unknown' : enum('VD4','T014','T057','T086D','Other','unknown','VK3','T027','VH1') # animal's previous room + to_room='unknown' : enum('VD4','T014','T057','T086D','Other','unknown','VK3','T027','VH1') # animal's new room + from_rack=null : char(4) # animal's previous rack + to_rack=null : char(4) # animal's new rack + from_row='' : char(1) # animal's previous row + to_row='' : char(1) # animal's new row + transfer_notes='' : varchar(4096) # other comments + transfer_ts=CURRENT_TIMESTAMP : timestamp # automatic + """ + + +@mice.schema +class MouseWeight(dj.Manual): + """Weight measurements, logged by the setup at session start.""" + + definition = """ + animal_id : int unsigned # id number + timestamp=CURRENT_TIMESTAMP : timestamp # timestamp of weight + --- + weight : double(5,2) # weight in grams + """ + + +@mice.schema +class GrowthCurve(dj.Lookup): + """Reference weight per age, sex and genotype.""" + + definition = """ + age : int # in weeks + gender : enum('male','female') + genotype : enum('C57BL/6J') + --- + weight=null : double # in grams + std=null : double # standard deviation + """ + + +@mice.schema +class SurgeryType(dj.Lookup): + """Surgery types.""" + + definition = """ + # Surgery types + surgery : varchar(16) # aim + --- + description='' : varchar(2048) # description + """ + + +@mice.schema +class Surgery(dj.Manual): + """Surgery information.""" + + definition = """ + # Surgery information + animal_id : smallint unsigned # animal id + timestamp : datetime # timestamp + --- + user_name : varchar(16) # user performing the surgery + -> SurgeryType + note=null : varchar(2048) # surgery notes + """ + + +@mice.schema +class Implants(dj.Manual): + """Implant information.""" + + definition = """ + animal_id : int unsigned # id number + doi : date # date of implantation + --- + experimenter='Other' : varchar(64) # name of experimenter + anesthesia='Other' : enum('isoflurane','ketamine/xylazine mix','Other') # anesthesia method + comments=null : varchar(100) + """ + + +@mice.schema +class Handling(dj.Manual): + """Handling sessions.""" + + definition = """ + animal_id : int unsigned # id number + timestamp : datetime # date of handling + --- + experimenter='Other' : varchar(64) # name of experimenter + type='Touch' : enum('Touch','Other') # handling method + comments=null : varchar(100) + """ + + +@mice.schema +class Person(dj.Manual): + """People in the lab.""" + + definition = """ + # people in the lab + person : varchar(12) # person's short name + --- + full_name : varchar(64) # person's full name + """ diff --git a/src/ethopy/experiments/calibrate.py b/src/ethopy/experiments/calibrate.py index 666ab56..94dac09 100644 --- a/src/ethopy/experiments/calibrate.py +++ b/src/ethopy/experiments/calibrate.py @@ -134,8 +134,9 @@ def cleanup(self): if hasattr(self, "menu"): self.menu.disable() + # Not pygame.quit(): it frees fonts still cached by + # pygame_menu, segfaulting the next menu built. pygame.display.quit() - pygame.quit() except Exception as e: log.warning(f"Error during pygame cleanup: {e}") @@ -151,6 +152,47 @@ def cleanup(self): except Exception as e: log.warning(f"Error updating logger status: {e}") + def _clear_menu(self): + """Clear the menu and re-add the always-available Abort button. + + Every step rebuilds the menu from scratch, so the button has to be + re-added after each clear. It floats, so it does not shift the layout + of the widgets added after it. + """ + self.menu.clear() + self.menu.add.button( + "Abort", + self.abort, + align=pygame_menu.locals.ALIGN_LEFT, + float=True, + padding=(5, 10, 5, 10), + background_color=(153, 0, 0), + font_size=25, + ).translate(650, 350) + + def abort(self): + """Stop the calibration immediately. + + Measurements already written by log_pulse_weight are left untouched; + only the remaining pulses and weight prompts are skipped. + """ + log.warning("Calibration aborted by user") + try: + self.menu.clear() + self.menu.add.label( + "Calibration aborted!", float=True, font_size=30 + ).translate(20, 80) + try: + self.menu.draw(self.screen) + pygame.display.flip() + time.sleep(2) + except pygame.error: + pass # Display might already be quit + except Exception as e: + log.warning(f"Error during abort: {e}") + finally: + self.stop = True + def exit(self): """exit _summary_ @@ -176,7 +218,7 @@ def exit(self): def create_pressure_menu(self): """The First menu in Calibration where is definde the air pressure in PSI""" - self.menu.clear() + self._clear_menu() self.button_input("Enter air pressure (PSI)", self.create_pulsenum_menu) def create_pulsenum_menu(self): @@ -184,7 +226,7 @@ def create_pulsenum_menu(self): self.pressure = self.curr self.curr = "" if self.cal_idx < len(self.session_params["pulsenum"]): - self.menu.clear() + self._clear_menu() self.menu.add.label( "Place zero-weighted pad under the port", float=True, font_size=30 ).translate(20, 80) @@ -206,7 +248,7 @@ def create_pulse_num(self): """ self.pulse = 0 msg = f"Pulse {self.pulse + 1}/{self.session_params['pulsenum'][self.cal_idx]}" - self.menu.clear() + self._clear_menu() pulses_label = self.menu.add.label( msg, float=True, @@ -224,6 +266,12 @@ def run_pulses(self, widget, menu): widget (_type_): The widget that uses the function menu (_type_): The current menu """ + # run() calls menu.update() and menu.draw() in the same iteration, so an + # Abort pressed during update would otherwise still deliver one more round + # of pulses when this draw callback fires. + if self.stop: + return + if self.pulse < self.session_params["pulsenum"][self.cal_idx]: self.msg = f"Pulse {self.pulse + 1}/{self.session_params['pulsenum'][self.cal_idx]}" log.info(f"\r{self.msg}") @@ -251,10 +299,10 @@ def run_pulses(self, widget, menu): def create_port_weight(self): """A menu with numpad for defining the water in every port""" - self.menu.clear() + self._clear_menu() cal_idx = self.cal_idx - 1 if self.session_params["save"]: - self.menu.clear() + self._clear_menu() if len(self.ports) != 0: if len(self.ports) != len(self.session_params["ports"]): self.log_pulse_weight( diff --git a/src/ethopy/setup_db.py b/src/ethopy/setup_db.py index df4afa5..64cbb40 100644 --- a/src/ethopy/setup_db.py +++ b/src/ethopy/setup_db.py @@ -280,6 +280,7 @@ def createschema() -> None: ("core/interface", "from ethopy.core.interface import *"), ("core/behavior", "from ethopy.core.behavior import *"), ("core/recordings", "from ethopy.core.recordings import *"), + ("core/mice", "from ethopy.core.mice import *"), ("stimuli", "from ethopy.stimuli import *"), ("behaviors", "from ethopy.behaviors import *"), ("experiments", "from ethopy.experiments import *"), diff --git a/src/ethopy/utils/start.py b/src/ethopy/utils/start.py index 70a07c6..06ce312 100644 --- a/src/ethopy/utils/start.py +++ b/src/ethopy/utils/start.py @@ -47,6 +47,7 @@ def __init__(self, logger) -> None: def setup_menus(self) -> None: self.animal_menu = self.create_animal() self.task_menu = self.create_task() + self.weight_menu = self.create_weight() self.main_menu = self.create_main() def mainloop(self) -> None: @@ -127,6 +128,15 @@ def create_main(self) -> "pygame_menu.Menu": background_color=(128, 128, 128), ).translate(630, 290) + menu.add.button( + "Weight", + self.weight_menu, + align=pygame_menu.locals.ALIGN_LEFT, + float=True, + padding=(10, 20, 10, 20), + background_color=(128, 128, 128), + ).translate(10, 310) + menu.add.button( "Power off", self.shutdown, @@ -167,6 +177,34 @@ def create_animal(self): return menu_animal + def create_weight(self): + menu_weight = pygame_menu.Menu( + "", + self.SCREEN_WIDTH, + self.SCREEN_HEIGHT, + center_content=False, + onclose=pygame_menu.events.EXIT, + theme=self.theme, + ) + menu_weight.add.label( + "Enter animal weight: ", + font_size=20, + ) + self.curr_weight = "" + menu_weight.add.vertical_margin(5) + self.weight_screen = menu_weight.add.label( + "", + background_color=None, + margin=(10, 0), + selectable=True, + selection_effect=None, + ) + menu_weight = self.add_num_pad( + menu_weight, self.log_animal_weight, self.weight_screen + ) + + return menu_weight + def create_task(self): menu_task = pygame_menu.Menu( "",