ic-analysis 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ """IntelliCage Analysis Toolkit.
2
+
3
+ The package provides reusable utilities to read IntelliCage exports, define
4
+ script-level experiment and subject metadata, compute behavior metrics, and
5
+ create publication-oriented plots.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from . import metrics as metrics
14
+ from .experiment import IntelliCageExperiment as Experiment
15
+ from .loader import CohortData, load_cohort_data
16
+ from .metadata import ExperimentMetadata, PhaseMetadata, SubjectMetadata, SubjectRegistry
17
+ from .subjects import create_subjects_yaml_template, load_subjects_yaml
18
+
19
+ metric = metrics
20
+
21
+ __all__ = [
22
+ "CohortData",
23
+ "Experiment",
24
+ "ExperimentMetadata",
25
+ "PhaseMetadata",
26
+ "SubjectMetadata",
27
+ "SubjectRegistry",
28
+ "create_subjects_yaml_template",
29
+ "experiment",
30
+ "load_subjects_yaml",
31
+ "load_cohort_data",
32
+ "metric",
33
+ "metrics"]
34
+ __version__ = "0.1.0"
35
+
36
+ def experiment(
37
+ *,
38
+ EXPERIMENT: ExperimentMetadata | dict[str, Any],
39
+ PHASES: dict[int | str, PhaseMetadata | dict[str, Any]] | None = None,
40
+ SUBJECTS: SubjectRegistry | dict[str | int, SubjectMetadata | dict[str, Any]] | None = None) -> Experiment:
41
+ """Build a generic IntelliCage experiment object from user-script metadata.
42
+
43
+ This is the recommended public entry point after ``import ic_analysis as
44
+ ic``. It validates experiment-level metadata, phase definitions, and the
45
+ subject registry, creates the configured results folder, and returns an
46
+ :class:`ic_analysis.experiment.IntelliCageExperiment` object ready for
47
+ ``load()``, ``prepare_analysis()``, and modular plotting calls.
48
+
49
+ :param EXPERIMENT: Experiment metadata as an
50
+ :class:`ExperimentMetadata` instance or a dictionary. Required keys for
51
+ dictionaries are ``name``, ``root_data_path``, ``results_data_path``,
52
+ and ``group_names``. The optional ``mouse_day`` dictionary can define
53
+ ``{"start": "06:00", "end": "18:00"}``.
54
+ :param PHASES: Phase definitions as a mapping from phase number to
55
+ :class:`PhaseMetadata` or dictionary. Default is ``None`` when phases
56
+ are embedded in ``EXPERIMENT``. Each phase can define ``short_name``,
57
+ ``long_name``, ``folder_name``, ``color``, and
58
+ ``scheduled_start_hour``.
59
+ :param SUBJECTS: Subject definitions as a
60
+ :class:`SubjectRegistry` or dictionary. No default is allowed: the
61
+ toolkit deliberately analyzes only explicitly declared animals.
62
+ :returns: A configured :class:`ic_analysis.experiment.IntelliCageExperiment`.
63
+ :raises ValueError: If phases, groups, or subjects are missing or invalid.
64
+ """
65
+
66
+ experiment_metadata = _coerce_experiment_metadata(EXPERIMENT, PHASES)
67
+ if SUBJECTS is None:
68
+ raise ValueError("`SUBJECTS` must define the animals included in the analysis.")
69
+ if isinstance(SUBJECTS, SubjectRegistry):
70
+ subjects = SUBJECTS
71
+ else:
72
+ subjects = SubjectRegistry.from_mapping(SUBJECTS)
73
+ experiment_metadata.results_data_path.mkdir(parents=True, exist_ok=True)
74
+ return Experiment(experiment_metadata, subjects)
75
+
76
+ def _coerce_experiment_metadata(
77
+ experiment_value: ExperimentMetadata | dict[str, Any],
78
+ phases_value: dict[int | str, PhaseMetadata | dict[str, Any]] | None) -> ExperimentMetadata:
79
+ """Normalize public ``ic.experiment`` metadata inputs."""
80
+
81
+ if isinstance(experiment_value, ExperimentMetadata):
82
+ return experiment_value
83
+ data = dict(experiment_value)
84
+ phases = phases_value or data.pop("phases", None) or data.pop("PHASES", None)
85
+ if phases is None:
86
+ raise ValueError("`PHASES` must define at least one experiment phase.")
87
+ mouse_day = data.pop("mouse_day", None) or data.pop("MouseDay", None)
88
+ if isinstance(mouse_day, dict):
89
+ if "start" in mouse_day and "mouse_day_start_time" not in data:
90
+ data["mouse_day_start_time"] = mouse_day["start"]
91
+ if "end" in mouse_day and "mouse_day_end_time" not in data:
92
+ data["mouse_day_end_time"] = mouse_day["end"]
93
+ normalized_phases = _normalize_phase_definitions(phases)
94
+ phase_colors = data.get("phase_colors") or {
95
+ int(number): phase.get("color")
96
+ for number, phase in normalized_phases.items()
97
+ if isinstance(phase, dict) and phase.get("color") is not None}
98
+ group_names = data.get("group_names") or data.get("groups") or data.get("GROUPS")
99
+ if group_names is None:
100
+ raise ValueError("`EXPERIMENT` must define `group_names`.")
101
+ return ExperimentMetadata(
102
+ name=str(data.get("name") or data.get("experiment_name") or data.get("ExperimentName")),
103
+ root_data_path=Path(data.get("root_data_path") or data.get("root") or data.get("data_path")),
104
+ results_data_path=Path(data.get("results_data_path") or data.get("results") or data.get("output_path")),
105
+ phases=normalized_phases,
106
+ group_names=list(group_names),
107
+ group_colors=data.get("group_colors") or {},
108
+ phase_colors=phase_colors,
109
+ optional_phase_numbers=set(data.get("optional_phase_numbers") or []),
110
+ mouse_day_start_hour=float(data.get("mouse_day_start_hour", 6.0)),
111
+ awake_duration_hours=float(data.get("awake_duration_hours", 12.0)),
112
+ mouse_day_start_time=data.get("mouse_day_start_time"),
113
+ mouse_day_end_time=data.get("mouse_day_end_time"),
114
+ experiment_day0_start_hour=data.get("experiment_day0_start_hour"),
115
+ schedule_anchor_phase_number=data.get("schedule_anchor_phase_number"))
116
+
117
+ def _normalize_phase_definitions(
118
+ phases: dict[int | str, PhaseMetadata | dict[str, Any]]) -> dict[int, PhaseMetadata | dict[str, Any]]:
119
+ """Normalize public phase definitions to integer keys."""
120
+
121
+ normalized: dict[int, PhaseMetadata | dict[str, Any]] = {}
122
+ for phase_number, phase in phases.items():
123
+ normalized[int(phase_number)] = phase
124
+ return normalized