From aa4cd608010073d4e8db320b04a70df096012837 Mon Sep 17 00:00:00 2001 From: Alexandros Evangelou Date: Thu, 27 Aug 2026 16:46:14 +0300 Subject: [PATCH 1/5] Add lab_mice schema and weight workflow This change adds the lab_mice DataJoint schema and wires it into the global configuration and schema loader. It also exposes the schema through the logger and includes the new mouse colony tables needed for animal identity, weights, transfers, surgery, and handling records. In addition, the startup UI now includes a dedicated weight entry screen for logging animal weights. --- src/ethopy/config.py | 1 + src/ethopy/core/mice.py | 233 ++++++++++++++++++++++++++++++++++++++ src/ethopy/setup_db.py | 1 + src/ethopy/utils/start.py | 38 +++++++ 4 files changed, 273 insertions(+) create mode 100644 src/ethopy/core/mice.py 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/mice.py b/src/ethopy/core/mice.py new file mode 100644 index 0000000..e943066 --- /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','constantina','sofia','eleanna','ilianna','zoi','mariap','marsa','asimenia','Other','Available','none','dimitra','nassi','alex','nikos','andreas','odysseas','anastasios','athanasios','paschalis','rania') # 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/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( "", From 5c9618c2410052455e98cfcca5e921f43cdf5f3b Mon Sep 17 00:00:00 2001 From: Alexandros Evangelou Date: Thu, 27 Aug 2026 16:46:33 +0300 Subject: [PATCH 2/5] add lab_mice virtual module --- src/ethopy/core/logger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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() From a20d96f368a89a6dcb05d3d9621c58392cad0c7e Mon Sep 17 00:00:00 2001 From: Alexandros Evangelou Date: Thu, 27 Aug 2026 16:49:05 +0300 Subject: [PATCH 3/5] remove owners in the enum --- src/ethopy/core/mice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ethopy/core/mice.py b/src/ethopy/core/mice.py index e943066..281447f 100644 --- a/src/ethopy/core/mice.py +++ b/src/ethopy/core/mice.py @@ -44,7 +44,7 @@ class Mice(dj.Manual): 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','constantina','sofia','eleanna','ilianna','zoi','mariap','marsa','asimenia','Other','Available','none','dimitra','nassi','alex','nikos','andreas','odysseas','anastasios','athanasios','paschalis','rania') # mouse's owner + 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 From c333b5cbf189e7461549a186fd6455193c143a0c Mon Sep 17 00:00:00 2001 From: Alexandros Evangelou Date: Fri, 28 Aug 2026 12:53:04 +0300 Subject: [PATCH 4/5] add abort button in calibrate --- src/ethopy/experiments/calibrate.py | 57 ++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/ethopy/experiments/calibrate.py b/src/ethopy/experiments/calibrate.py index 666ab56..bbeabcf 100644 --- a/src/ethopy/experiments/calibrate.py +++ b/src/ethopy/experiments/calibrate.py @@ -151,6 +151,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 +217,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 +225,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 +247,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 +265,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 +298,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( From e57d9bd51fc214b7b53d3f8aa8ed6a8d24dd3808 Mon Sep 17 00:00:00 2001 From: Alexandros Evangelou Date: Mon, 31 Aug 2026 12:28:59 +0300 Subject: [PATCH 5/5] remove pygame.quit() remove pygame.quit() on cleanup to avoid segmentation segmentation fault when try built PyWelcome after a cleanup --- src/ethopy/experiments/calibrate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ethopy/experiments/calibrate.py b/src/ethopy/experiments/calibrate.py index bbeabcf..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}")