From 2934563cd9c6bff3498f5cd3e844ce0b3437bfb5 Mon Sep 17 00:00:00 2001 From: MichaelCoulter <37707865+MichaelCoulter@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:14:02 -0700 Subject: [PATCH 1/7] remove extra parameter for clusterless thresholder (#567) --- src/spyglass/spikesorting/spikesorting_sorting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spyglass/spikesorting/spikesorting_sorting.py b/src/spyglass/spikesorting/spikesorting_sorting.py index ede6a0ff1..4e2cd0c27 100644 --- a/src/spyglass/spikesorting/spikesorting_sorting.py +++ b/src/spyglass/spikesorting/spikesorting_sorting.py @@ -201,6 +201,7 @@ def make(self, key: dict): # need to remove tempdir and whiten from sorter_params sorter_params.pop("tempdir", None) sorter_params.pop("whiten", None) + sorter_params.pop("outputs", None) # Detect peaks for clusterless decoding detected_spikes = detect_peaks(recording, **sorter_params) From 59ed1754d98aae384df3f2f2c21644d9b4baa362 Mon Sep 17 00:00:00 2001 From: MichaelCoulter <37707865+MichaelCoulter@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:04:15 -0700 Subject: [PATCH 2/7] a few typos in lfp_artifact.py (#568) --- src/spyglass/lfp/v1/lfp_artifact.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spyglass/lfp/v1/lfp_artifact.py b/src/spyglass/lfp/v1/lfp_artifact.py index ce3ea502c..11c3baf78 100644 --- a/src/spyglass/lfp/v1/lfp_artifact.py +++ b/src/spyglass/lfp/v1/lfp_artifact.py @@ -96,7 +96,7 @@ def make(self, key): ).fetch1("artifact_params") artifact_detection_algorithm = artifact_params[ - "ripple_detection_algorithm" + "artifact_detection_algorithm" ] artifact_detection_params = artifact_params[ "artifact_detection_algorithm_params" @@ -121,11 +121,13 @@ def make(self, key): # set up a name for no-artifact times using recording id # we need some name here for recording_name key["artifact_removed_interval_list_name"] = "_".join( + [ key["nwb_file_name"], key["target_interval_list_name"], "LFP", key["artifact_params_name"], "artifact_removed_valid_times", + ] ) LFPArtifactRemovedIntervalList.insert1(key, replace=True) From 4e152225ef89265431698176908712f52609dfb2 Mon Sep 17 00:00:00 2001 From: Eric Denovellis Date: Fri, 30 Jun 2023 16:49:07 -0700 Subject: [PATCH 3/7] Fix linting --- src/spyglass/lfp/v1/lfp_artifact.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spyglass/lfp/v1/lfp_artifact.py b/src/spyglass/lfp/v1/lfp_artifact.py index 11c3baf78..3985707e7 100644 --- a/src/spyglass/lfp/v1/lfp_artifact.py +++ b/src/spyglass/lfp/v1/lfp_artifact.py @@ -122,11 +122,11 @@ def make(self, key): # we need some name here for recording_name key["artifact_removed_interval_list_name"] = "_".join( [ - key["nwb_file_name"], - key["target_interval_list_name"], - "LFP", - key["artifact_params_name"], - "artifact_removed_valid_times", + key["nwb_file_name"], + key["target_interval_list_name"], + "LFP", + key["artifact_params_name"], + "artifact_removed_valid_times", ] ) From 692f9fa5c291cd1b8c2134894ee6b7323f4df9f3 Mon Sep 17 00:00:00 2001 From: Chris Brozdowski Date: Wed, 5 Jul 2023 14:05:38 -0500 Subject: [PATCH 4/7] Improved Merge Table documentation (#565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restructure for mkdocs * Minor docstring edits for mkdocs * Adjust installer docs * Permit json config * Update docstrings for mkdocs. Add images * Minor fixes. Rename publish action. Add changelog notes * hard wrap changes, markdownlint * blackify * Fix version cmd. Minor doc wording * mkdocstrings require empty inits to identify submodules * 🤦‍♂️ Publish docs CI/CD `main`->`master` * Edit `get_part`; Add `merge_fetch` * Spelling fixes * Refactor restrict_parts. Adjust mkdocs nav. * Docs adjust for Merge tables * Fix `merge_get_part` * Use hatch for docs version * Update changelog * Typo * Spellcheck config * Normalize restriction/classmeth. Add notes on why * Typos * Docs publish on tag * Edit changelog: Add links, patch version bump * 🧪 Test gh-actions debug * get-part multi-source flag * Add mutual exclusivity flag from pos branch --- .github/workflows/publish-docs.yml | 7 +- CHANGELOG.md | 29 ++- docs/build-docs.sh | 2 +- docs/mkdocs.yml | 28 ++- docs/src/api/make_pages.py | 5 +- docs/src/images/merge_diagram.png | Bin 0 -> 98482 bytes docs/src/misc/merge_tables.md | 92 +++++-- pyproject.toml | 4 +- src/spyglass/lfp/lfp_merge.py | 4 +- src/spyglass/position/v1/dlc_reader.py | 2 +- src/spyglass/utils/dj_merge_tables.py | 325 +++++++++++++++++++------ src/spyglass/utils/nwb_helper_fn.py | 4 +- 12 files changed, 372 insertions(+), 130 deletions(-) create mode 100644 docs/src/images/merge_diagram.png diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index ff8ce2e19..a7a31758e 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,11 +1,8 @@ name: Publish docs on: - pull_request: - branches: - - master - types: - - closed push: + tags: # See PEP 440 for valid version format + - "*.*.*" # For docs bump, use X.X.XaX branches: - test_branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b563ef54..2b8aafc57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Change Log -## 0.4.1 (Unreleased) +## [0.4.1] (June 30, 2023) -- Add mkdocs automated deployment +- Add mkdocs automated deployment. #527, #537, #549, #551 +- Add class for Merge Tables. #556, #564, #565 -## 0.4.0 (May 22, 2023) +## [0.4.0] (May 22, 2023) - Updated call to `spikeinterface.preprocessing.whiten` to use dtype np.float16. #446, @@ -32,7 +33,7 @@ - Updated `environment_position.yml`. #502 - Renamed `FirFilter` class to `FirFilterParameters`. #512 -## 0.3.4 (March 30, 2023) +## [0.3.4] (March 30, 2023) - Fixed error in spike sorting pipeline referencing the "probe_type" column which is no longer accessible from the `Electrode` table. #437 @@ -43,18 +44,28 @@ - Fixed inconsistency between capitalized/uncapitalized versions of "Intan" for DataAcquisitionAmplifier and DataAcquisitionDevice.adc_circuit. #430, #438 -## 0.3.3 (March 29, 2023) +## [0.3.3] (March 29, 2023) - Fixed errors from referencing the changed primary key for `Probe`. #429 -## 0.3.2 (March 28, 2023) +## [0.3.2] (March 28, 2023) - Fixed import of `common_nwbfile`. #424 -## 0.3.1 (March 24, 2023) +## [0.3.1] (March 24, 2023) - Fixed import error due to `sortingview.Workspace`. #421 -## 0.3.0 (March 24, 2023) +## [0.3.0] (March 24, 2023) -To be added. +- Refactor common for non Frank Lab data, allow file-based mods #420 +- Allow creation and linkage of device metadata from YAML #400 +- Move helper functions to utils directory #386 + +[0.4.1]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.4.1 +[0.4.0]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.4.0 +[0.3.4]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.3.4 +[0.3.3]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.3.3 +[0.3.2]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.3.2 +[0.3.1]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.3.1 +[0.3.0]: https://github.com/LorenFrankLab/spyglass/releases/tag/0.3.0 diff --git a/docs/build-docs.sh b/docs/build-docs.sh index 946699256..5a16d162a 100644 --- a/docs/build-docs.sh +++ b/docs/build-docs.sh @@ -11,7 +11,7 @@ cp -r ./notebooks/ ./docs/src/ cp -r ./notebook-images ./docs/src/notebooks # Get major version -FULL_VERSION=$(python -c "from spyglass import __version__; print(__version__)") +FULL_VERSION=$(hatch version) # Most recent tag export MAJOR_VERSION="${FULL_VERSION%.*}" echo "$MAJOR_VERSION" diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 5526be38c..d4e8a5b19 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -15,9 +15,10 @@ theme: logo: images/FrankLab.png favicon: images/Spyglass.svg features: - - toc.integrate - - navigation.sections - - navigation.expand + - toc.follow + # - navigation.expand # CBroz1: removed bc long tutorial list hides rest + # - toc.integrate + # - navigation.sections - navigation.top - navigation.instant # saves loading time - 1 browser page - navigation.tracking # even with above, changes URL by section @@ -25,6 +26,7 @@ theme: - search.suggest - search.share - navigation.footer + - content.code.copy palette: - media: "(prefers-color-scheme: light)" scheme: auto @@ -47,6 +49,11 @@ nav: - Home: installation/index.md - Local: installation/local.md - Productions: installation/production.md + - Miscellaneous: + - FigURL: misc/figurl_views.md + - Session Groups: misc/session_groups.md + - Insert Data: misc/insert_data.md + - Merge Tables: misc/merge_tables.md - Tutorials: - Introduction: notebooks/00_intro.ipynb - Spike Sorting: notebooks/01_spikesorting.ipynb @@ -67,10 +74,6 @@ nav: - Spyglass Kachery Setup: notebooks/Spyglass_kachery_setup.ipynb - API Reference: api/ # defer to gen-files + literate-nav - How to Contribute: contribute.md - - Miscellaneous: - - FigURL: misc/figurl_views.md - - Session Groups: misc/session_groups.md - - Insert Data: misc/insert_data.md - Change Log: CHANGELOG.md - Copyright: LICENSE.md @@ -87,6 +90,7 @@ plugins: canonical_version: latest css_dir: stylesheets - mkdocstrings: + enabled: true # Set to false to reduce build time default_handler: python handlers: python: @@ -95,9 +99,6 @@ plugins: group_by_category: false line_length: 80 docstring_style: numpy - selection: - docstring_style: numpy - enabled: true # Set to false to reduce build time watch: - src/spyglass/ - literate-nav: @@ -117,6 +118,13 @@ markdown_extensions: - tables - toc: permalink: true + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences extra: generator: false # Disable watermark diff --git a/docs/src/api/make_pages.py b/docs/src/api/make_pages.py index d4d8f83dc..8dcd37c1d 100644 --- a/docs/src/api/make_pages.py +++ b/docs/src/api/make_pages.py @@ -1,12 +1,13 @@ """Generate the api pages and navigation. """ -import mkdocs_gen_files from pathlib import Path +import mkdocs_gen_files + nav = mkdocs_gen_files.Nav() for path in sorted(Path("src").glob("**/*.py")): - if path.stem == "__init__": + if path.stem == "__init__" or "cython" in path.stem: continue with mkdocs_gen_files.open(f"api/{path.with_suffix('')}.md", "w") as f: module_path = ".".join([p for p in path.with_suffix("").parts]) diff --git a/docs/src/images/merge_diagram.png b/docs/src/images/merge_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..72b0fc8c08afea29f838129425922bbda2ada54d GIT binary patch literal 98482 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

n|)Qx!1Z8=X__yapYS}WMtwaPg`B{ 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%0DAyc-RZ}p#IPrM`g?PC2Q7h67A*e>%Dd@6X5 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 list: """Returns a list of parts with restrictions applied. Parameters --------- restriction: str, optional - Restriction to apply to the merged view. Default True, no restrictions. + Restriction to apply to the parts. Default True, no restrictions. as_objects: bool, optional Default True. Return part tables as objects return_empties: bool, optional Default True. Return empty part tables + add_invalid_restrict: bool, optional + Default True. Include part for which the restriction is invalid. Returns ------ list list of datajoint tables, parts of Merge Table """ - if not dj.conn.connection.dependencies._loaded: - dj.conn.connection.dependencies.load() # Otherwise parts returns none + + cls._ensure_dependencies_loaded() if not restriction: restriction = True # Normalize restriction to sql string - restriction = make_condition(cls(), restriction, set()) + restr_str = make_condition(cls(), restriction, set()) parts_all = cls.parts(as_objects=True) # If the restriction makes ref to a source, we only want that part - if isinstance(restriction, str) and cls()._reserved_sk in restriction: + if ( + not return_empties + and isinstance(restr_str, str) + and f"`{cls()._reserved_sk}`" in restr_str + ): parts_all = [ part for part in parts_all if from_camel_case( - restriction.split(f'`{cls()._reserved_sk}`="')[-1].split( - '"' - )[0] + restr_str.split(f'`{cls()._reserved_sk}`="')[-1].split('"')[ + 0 + ] ) # Only look at source part table in part.full_table_name ] + if isinstance(restriction, dict): # restr by source already done above + _ = restriction.pop(cls()._reserved_sk, None) # won't work for str + # If a dict restriction has all invalid keys, it is treated as True + if not add_invalid_restrict: + parts_all = [ # so exclude tables w/ nonmatching attrs + p + for p in parts_all + if all([k in p.heading.names for k in restriction.keys()]) + ] parts = [] for part in parts_all: try: parts.append(part.restrict(restriction)) except DataJointError: # If restriction not valid on given part - parts.append(part) + if add_invalid_restrict: + parts.append(part) if not return_empties: parts = [p for p in parts if len(p)] @@ -110,6 +134,7 @@ def _merge_restrict_parents( restriction: str = True, as_objects: bool = True, return_empties: bool = True, + add_invalid_restrict: bool = True, ) -> list: """Returns a list of part parents with restrictions applied. @@ -119,22 +144,29 @@ def _merge_restrict_parents( Parameters --------- restriction: str, optional - Restriction to apply to the merged view. Default True, no restrictions. + Restriction to apply to the returned parent. Default True, no + restrictions. as_objects: bool, optional Default True. Return part tables as objects return_empties: bool, optional Default True. Return empty part tables + add_invalid_restrict: bool, optional + Default True. Include part for which the restriction is invalid. Returns ------ list list of datajoint tables, parents of parts of Merge Table """ + # .restict(restriction) does not work on returned part FreeTable + # & part.fetch below restricts parent to entries in merge table part_parents = [ - parent # Below, restricting parent to info from restricted part + parent & part.fetch(*part.heading.secondary_attributes, as_dict=True) for part in cls()._merge_restrict_parts( - restriction=restriction, return_empties=return_empties + restriction=restriction, + return_empties=return_empties, + add_invalid_restrict=add_invalid_restrict, ) for parent in part.parents(as_objects=True) # ID respective parents if cls().table_name not in parent.full_table_name # Not merge table @@ -145,9 +177,7 @@ def _merge_restrict_parents( return part_parents @classmethod - def _merge_repr( - cls, restriction: str = True, **kwargs - ) -> dj.expression.Union: + def _merge_repr(cls, restriction: str = True) -> dj.expression.Union: """Merged view, including null entries for columns unique to one part table. Parameters @@ -162,7 +192,11 @@ def _merge_repr( parts = [ cls() * p # join with master to include sec key (i.e., 'source') - for p in cls._merge_restrict_parts(restriction=restriction) + for p in cls._merge_restrict_parts( + restriction=restriction, + add_invalid_restrict=False, + return_empties=False, + ) ] primary_attrs = list( @@ -191,7 +225,9 @@ def _merge_repr( return query @classmethod - def _merge_insert(cls, rows: list, **kwargs) -> None: + def _merge_insert( + cls, rows: list, mutual_exclusvity=True, **kwargs + ) -> None: """Insert rows into merge table, ensuring db integrity and mutual exclusivity Parameters @@ -207,6 +243,7 @@ def _merge_insert(cls, rows: list, **kwargs) -> None: If entry already exists, mutual exclusivity errors If data doesn't exist in part parents, integrity error """ + cls._ensure_dependencies_loaded() try: for r in iter(rows): @@ -220,43 +257,61 @@ def _merge_insert(cls, rows: list, **kwargs) -> None: master_entries = [] parts_entries = {p: [] for p in parts} for row in rows: - key = {} - for part in parts: - master = part.parents(as_objects=True)[-1] + key = {} # empty to-be-inserted key + for part in parts: # check each part + part_parent = part.parents(as_objects=True)[-1] part_name = to_camel_case(part.table_name.split("__")[-1]) - if master & row: - if not key: - key = (master & row).fetch1("KEY") - master_pk = { - cls()._reserved_pk: dj.hash.key_hash(key), - } - parts_entries[part].append({**master_pk, **key}) - master_entries.append( - {**master_pk, cls()._reserved_sk: part_name} - ) - else: + if part_parent & row: # if row is in part parent + if key and mutual_exclusvity: # if key from other part raise ValueError( "Mutual Exclusivity Error! Entry exists in more " + f"than one table - Entry: {row}" ) + keys = (part_parent & row).fetch("KEY") # get pk + if len(keys) > 1: + raise ValueError( + "Ambiguous entry. Data has mult rows in" + + f"{part_name}:\n\tData:{row}\n\t{keys}" + ) + master_pk = { # make uuid + cls()._reserved_pk: dj.hash.key_hash(keys[0]), + } + parts_entries[part].append({**master_pk, **keys[0]}) + master_entries.append( + {**master_pk, cls()._reserved_sk: part_name} + ) + if not key: raise ValueError( "Non-existing entry in any of the parent tables - Entry: " + f"{row}" ) - # 1. nullcontext() allows use within `make` but decreases reliability - # 2. cls.connection.transaction is more reliable but throws errors if - # used within another transaction, i.e. in `make` - - with nullcontext(): # TODO: ensure this block within transaction + with cls._safe_context(): super().insert(cls(), master_entries, **kwargs) for part, part_entries in parts_entries.items(): part.insert(part_entries, **kwargs) @classmethod - def insert(cls, rows: list, **kwargs): + def _safe_context(cls): + """Return transaction if not already in one.""" + return ( + cls.connection.transaction + if not cls.connection.in_transaction + else nullcontext() + ) + + @classmethod + def _ensure_dependencies_loaded(cls) -> None: + """Ensure connection dependencies loaded. + + Otherwise parts returns none + """ + if not dj.conn.connection.dependencies._loaded: + dj.conn.connection.dependencies.load() + + def insert(self, rows: list, mutual_exclusvity=True, **kwargs): """Merges table specific insert Ensuring db integrity and mutual exclusivity @@ -265,6 +320,8 @@ def insert(cls, rows: list, **kwargs): --------- rows: List[dict] An iterable where an element is a dictionary. + mutual_exclusvity: bool + Check for mutual exclusivity before insert. Default True. Raises ------ @@ -274,12 +331,14 @@ def insert(cls, rows: list, **kwargs): If entry already exists, mutual exclusivity errors If data doesn't exist in part parents, integrity error """ - cls._merge_insert(rows, **kwargs) + self._merge_insert(rows, mutual_exclusvity=mutual_exclusvity, **kwargs) @classmethod def merge_view(cls, restriction: str = True): """Prints merged view, including null entries for unique columns. + Note: To handle this Union as a table-like object, use `merge_resrict` + Parameters --------- restriction: str, optional @@ -300,7 +359,7 @@ def merge_html(cls, restriction: str = True): @classmethod def merge_restrict(cls, restriction: str = True) -> dj.U: - """Given a restriction string, return a merged view with restriction applied. + """Given a restriction, return a merged view with restriction applied. Example ------- @@ -361,7 +420,6 @@ def merge_delete_parent( kwargs: dict Additional keyword arguments for DataJoint delete. """ - part_parents = cls._merge_restrict_parents( restriction=restriction, as_objects=True, return_empties=False ) @@ -369,32 +427,60 @@ def merge_delete_parent( if dry_run: return part_parents - super().delete(cls(), **kwargs) - for part_parent in part_parents: - super().delete(part_parent, **kwargs) + with cls._safe_context(): + super().delete(cls(), **kwargs) + for part_parent in part_parents: + super().delete(part_parent, **kwargs) - def fetch_nwb(self, *attrs, **kwargs): - part_parents = self._merge_restrict_parents( - restriction=self.restriction, return_empties=False + @classmethod + def fetch_nwb( + cls, restriction: str = True, multi_source=False, *attrs, **kwargs + ): + """Return the AnalysisNwbfile file linked in the source. + + Parameters + ---------- + restriction: str, optional + Restriction to apply to parents before running fetch. Default none. + multi_source: bool + Return from multiple parents. Default False. + """ + part_parents = cls._merge_restrict_parents( + restriction=restriction, + return_empties=False, + add_invalid_restrict=False, ) - if len(part_parents) == 1: - return fetch_nwb( - part_parents[0], - (AnalysisNwbfile, "analysis_file_abs_path"), - *attrs, - **kwargs, - ) - else: + if not multi_source and len(part_parents) != 1: raise ValueError( - f"{len(part_parents)} possible sources found in Merge Table" - + part_parents + f"{len(part_parents)} possible sources found in Merge Table:" + + " and ".join([p.full_table_name for p in part_parents]) ) + nwbs = [] + for part_parent in part_parents: + nwbs.extend( + fetch_nwb( + part_parent, + (AnalysisNwbfile, "analysis_file_abs_path"), + *attrs, + **kwargs, + ) + ) + return nwbs + + @classmethod def merge_get_part( - self, restriction: str = True, join_master: bool = False + cls, + restriction: str = True, + join_master: bool = False, + restrict_part=True, + multi_source=False, ) -> dj.Table: - """Retrieve part table from a restricted Merge table + """Retrieve part table from a restricted Merge table. + + Note: unlike other Merge Table methods, returns the native table, not + a FreeTable Parameters ---------- @@ -402,7 +488,17 @@ def merge_get_part( Optional restriction to apply before determining part to return. Default True. join_master: bool - Default False. Joint part with Merge master to show uuid and source + Join part with Merge master to show source field. Default False. + restrict_part: bool + Apply restriction to part. Default True. If False, return the + native part table. + multi_source: bool + Return multiple parts. Default False. + + Returns + ------ + Union[dj.Table, List[dj.Table]] + Native part table(s) of Merge. If `multi_source`, returns list. Example ------- @@ -412,31 +508,92 @@ def merge_get_part( Raises ------ ValueError - If multiple sources are found, lists and suggests restricting + If multiple sources are found, but not expected lists and suggests + restricting """ - # Note: `get_part_table`->`merge_get_part` to keep prefix on methods - sources = self.restrict(restriction).fetch(self._reserved_sk) + sources = [ + to_camel_case(n.split("__")[-1].strip("`")) # friendly part name + for n in cls._merge_restrict_parts( + restriction=restriction, + as_objects=False, + return_empties=False, + add_invalid_restrict=False, + ) + ] - if len(sources) != 1: + if not multi_source and len(sources) != 1: raise ValueError( f"Found multiple potential parts: {sources}\n\t" - + "Try adding a restriction before invoking `get_part`." + + "Try adding a restriction before invoking `get_part`.\n\t" + + "Or permitting multiple sources with `multi_source=True`." + ) + + parts = [ + getattr(cls, source)().restrict(restriction) + if restrict_part # Re-apply restriction or don't + else getattr(cls, source)() + for source in sources + ] + if join_master: + parts = [cls * part for part in parts] + + return parts if multi_source else parts[0] + + @classmethod + def merge_get_parent( + cls, + restriction: str = True, + join_master: bool = False, + multi_source=False, + ) -> dj.FreeTable: + """Returns a list of part parents with restrictions applied. + + Rather than part tables, we look at parents of those parts, the source + of the data, and only the rows that have keys inserted in the merge + table. + + Parameters + ---------- + restriction: str + Optional restriction to apply before determining parent to return. + Default True. + join_master: bool + Default False. Join part with Merge master to show uuid and source + + Returns + ------ + dj.FreeTable + Parent of parts of Merge Table as FreeTable. + """ + + part_parents = cls._merge_restrict_parents( + restriction=restriction, + as_objects=True, + return_empties=False, + add_invalid_restrict=False, + ) + + if not multi_source and len(sources) != 1: + raise ValueError( + f"Found multiple potential parents: {part_parents}\n\t" + + "Try adding a string restriction when invoking `get_parent`." + + "Or permitting multiple sources with `multi_source=True`." ) - if False: # Original: & here does nothing bc no unique pk on master - return getattr(self, sources[0]) & self + if join_master: + part_parents = [cls * part for part in parts] - if join_master: # Alt: Master * Part shows source - return self * getattr(self, sources[0]) - else: # Current default aligns with func name - return getattr(self, sources[0])() + return part_parents if multi_source else part_parents[0] @classmethod - def merge_fetch(cls, *attrs, **kwargs) -> list: + def merge_fetch(self, restriction: str = True, *attrs, **kwargs) -> list: """Perform a fetch across all parts. If >1 result, return as a list. Parameters ---------- + restriction: str + Optional restriction to apply before determining parent to return. + Default True. attrs, kwargs arguments passed to DataJoint `fetch` call @@ -446,8 +603,11 @@ def merge_fetch(cls, *attrs, **kwargs) -> list: Table contents, with type determined by kwargs """ results = [] - parts = cls()._merge_restrict_parts( - restriction=cls._restriction, return_empties=False + parts = self()._merge_restrict_parts( + restriction=restriction, + as_objects=True, + return_empties=False, + add_invalid_restrict=False, ) for part in parts: @@ -463,9 +623,22 @@ def merge_fetch(cls, *attrs, **kwargs) -> list: # for recarray, pd.DataFrame, or dict, and fetched contents differ if # attrs or "KEY" called. Intercept format, merge, and then transform? + if not results: + print( + "No merge_fetch results.\n\t" + + "If not restriction, try: `M.merge_fetch(True,'attr')\n\t" + + "If restricting by source, use dict: " + + "`M.merge_fetch({'source':'X'})" + ) return results[0] if len(results) == 1 else results +_Merge = Merge + +# Underscore as class name avoids errors when this included in a Diagram +# Aliased because underscore otherwise excludes from API docs. + + def delete_downstream_merge( table: dj.Table, restriction: str = True, dry_run=True, **kwargs ) -> list: @@ -489,6 +662,8 @@ def delete_downstream_merge( List[Tuple[dj.Table, dj.Table]] Entries in merge/part tables downstream of table input. """ + restriction = AndList((table.restriction, restriction)) + if not restriction: restriction = True diff --git a/src/spyglass/utils/nwb_helper_fn.py b/src/spyglass/utils/nwb_helper_fn.py index 58cf6a076..58b9b4696 100644 --- a/src/spyglass/utils/nwb_helper_fn.py +++ b/src/spyglass/utils/nwb_helper_fn.py @@ -63,13 +63,15 @@ def get_nwb_file(nwb_file_path): def get_config(nwb_file_path): """Return a dictionary of config settings for the given NWB file. If the file does not exist, return an empty dict. + Parameters ---------- nwb_file_path : str Absolute path to the NWB file. + Returns ------- - d : dict + dict Dictionary of configuration settings loaded from the corresponding YAML file """ if nwb_file_path in __configs: # load from cache if exists From 8f3ca2ad0992321f21f9b6813229eec929bd6274 Mon Sep 17 00:00:00 2001 From: Eric Denovellis Date: Mon, 10 Jul 2023 11:55:43 -0700 Subject: [PATCH 5/7] Bump python version required --- environment.yml | 2 +- environment_position.yml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 7a8c2775f..d7420426f 100644 --- a/environment.yml +++ b/environment.yml @@ -5,7 +5,7 @@ channels: - franklab - edeno dependencies: - - python>=3.8,<3.10 + - python>=3.9,<3.10 - jupyterlab>=3.* - pydotplus - dask diff --git a/environment_position.yml b/environment_position.yml index ef2b725a4..f06cc2348 100644 --- a/environment_position.yml +++ b/environment_position.yml @@ -15,7 +15,7 @@ channels: - franklab - edeno dependencies: - - python>=3.8, <3.10 + - python>=3.9, <3.10 - jupyterlab>=3.* - pydotplus>=2.0.* - libgcc diff --git a/pyproject.toml b/pyproject.toml index 213a3db14..ab9c073bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "spyglass-neuro" description = "Neuroscience data analysis framework for reproducible research" readme = "README.md" -requires-python = ">=3.8,<3.10" +requires-python = ">=3.9,<3.10" license = { file = "LICENSE" } authors = [ { name = "Loren Frank", email = "loren.frank@ucsf.edu" }, @@ -78,7 +78,7 @@ test = [ "kachery-cloud", ] docs = [ - "hatch", # Get version from env + "hatch", # Get version from env "mike", # Docs versioning "mkdocs", # Docs core "mkdocs-exclude", # Docs exclude files From 3c7e842f6d52a115b7f58f955f1ab30bfff5d24d Mon Sep 17 00:00:00 2001 From: Daniel Gramling <57648931+dpeg22@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:13:05 -0700 Subject: [PATCH 6/7] Update Position pipeline merge structure (#569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restructure for mkdocs * Minor docstring edits for mkdocs * Adjust installer docs * Permit json config * Update docstrings for mkdocs. Add images * Minor fixes. Rename publish action. Add changelog notes * hard wrap changes, markdownlint * blackify * Fix version cmd. Minor doc wording * mkdocstrings require empty inits to identify submodules * 🤦‍♂️ Publish docs CI/CD `main`->`master` * Edit `get_part`; Add `merge_fetch` * Spelling fixes * Refactor restrict_parts. Adjust mkdocs nav. * Docs adjust for Merge tables * Fix `merge_get_part` * Use hatch for docs version * Update changelog * Typo * Spellcheck config * merge CBroz1/spyglass master (#1) * Restructure for mkdocs * Minor docstring edits for mkdocs * Adjust installer docs * Permit json config * Update docstrings for mkdocs. Add images * Minor fixes. Rename publish action. Add changelog notes * hard wrap changes, markdownlint * blackify * Fix version cmd. Minor doc wording * mkdocstrings require empty inits to identify submodules * 🤦‍♂️ Publish docs CI/CD `main`->`master` * Edit `get_part`; Add `merge_fetch` * Spelling fixes * Refactor restrict_parts. Adjust mkdocs nav. * Docs adjust for Merge tables * Fix `merge_get_part` * Use hatch for docs version * Update changelog * Typo * Spellcheck config --------- Co-authored-by: CBroz1 Co-authored-by: CBroz1 * adopted Merge class to PositionOutput * WIP: dict/str ristrict consistency * Normalize restriction/classmeth. Add notes on why * Typos * Docs publish on tag * Edit changelog: Add links, patch version bump * 🧪 Test gh-actions debug * get-part multi-source flag * Add mutual exclusivity flag * Add mutual exclusivity flag from pos branch * small updates * Some refactoring suggestions (#3) * Some refactoring suggestions * Add support for restricting entry by part name * update insert to Merge * update insert to and fetch1_dataframe from merge * remove unused import * add comments for future changes --------- Co-authored-by: CBroz1 Co-authored-by: CBroz1 --- src/spyglass/position/position_merge.py | 387 ++++-------------- .../position/v1/position_dlc_selection.py | 105 +++-- .../position/v1/position_trodes_position.py | 19 +- src/spyglass/utils/dj_merge_tables.py | 35 +- 4 files changed, 166 insertions(+), 380 deletions(-) diff --git a/src/spyglass/position/position_merge.py b/src/spyglass/position/position_merge.py index 035745b8d..5a21b8134 100644 --- a/src/spyglass/position/position_merge.py +++ b/src/spyglass/position/position_merge.py @@ -1,19 +1,14 @@ import functools as ft import os from pathlib import Path -from typing import Dict import datajoint as dj import numpy as np import pandas as pd from tqdm import tqdm as tqdm -from ..common.common_interval import IntervalList -from ..common.common_nwbfile import AnalysisNwbfile -from ..common.common_position import ( - IntervalPositionInfo as CommonIntervalPositionInfo, -) -from ..utils.dj_helper_fn import fetch_nwb +from ..common.common_position import IntervalPositionInfo as CommonPos +from ..utils.dj_merge_tables import _Merge from .v1.dlc_utils import check_videofile, get_video_path, make_video from .v1.position_dlc_pose_estimation import DLCPoseEstimationSelection from .v1.position_dlc_selection import DLCPosV1 @@ -21,25 +16,19 @@ schema = dj.schema("position_merge") -_valid_data_sources = ["DLC", "Trodes", "Common"] - @schema -class PositionOutput(dj.Manual): +class PositionOutput(_Merge): """ Table to identify source of Position Information from upstream options (e.g. DLC, Trodes, etc...) To add another upstream option, a new Part table - should be added in the same syntax as DLCPos and TrodesPos and - - Note: all part tables need to be named using the source+"Pos" convention - i.e. if the source='DLC', then the table is DLCPos + should be added in the same syntax as DLCPos and TrodesPos. """ definition = """ - -> IntervalList - source: varchar(40) - version: int - position_id: int + merge_id : uuid + --- + source: varchar(32) --- """ @@ -50,22 +39,10 @@ class DLCPosV1(dj.Part): definition = """ -> PositionOutput - -> DLCPosV1 --- - -> AnalysisNwbfile - position_object_id : varchar(80) - orientation_object_id : varchar(80) - velocity_object_id : varchar(80) + -> DLCPosV1 """ - def fetch_nwb(self, *attrs, **kwargs): - return fetch_nwb( - self, - (AnalysisNwbfile, "analysis_file_abs_path"), - *attrs, - **kwargs, - ) - class TrodesPosV1(dj.Part): """ Table to pass-through upstream Trodes Position Tracking information @@ -73,22 +50,10 @@ class TrodesPosV1(dj.Part): definition = """ -> PositionOutput - -> TrodesPosV1 --- - -> AnalysisNwbfile - position_object_id : varchar(80) - orientation_object_id : varchar(80) - velocity_object_id : varchar(80) + -> TrodesPosV1 """ - def fetch_nwb(self, *attrs, **kwargs): - return fetch_nwb( - self, - (AnalysisNwbfile, "analysis_file_abs_path"), - *attrs, - **kwargs, - ) - class CommonPos(dj.Part): """ Table to pass-through upstream Trodes Position Tracking information @@ -96,119 +61,14 @@ class CommonPos(dj.Part): definition = """ -> PositionOutput - -> CommonIntervalPositionInfo --- - -> AnalysisNwbfile - position_object_id : varchar(80) - orientation_object_id : varchar(80) - velocity_object_id : varchar(80) + -> CommonPos """ - def fetch_nwb(self, *attrs, **kwargs): - return fetch_nwb( - self, - (AnalysisNwbfile, "analysis_file_abs_path"), - *attrs, - **kwargs, - ) - - def insert1(self, key, params: Dict = None, **kwargs): - """Overrides insert1 to also insert into specific part table. - - Parameters - ---------- - key : Dict - key specifying the entry to insert - params : Dict, optional - A dictionary containing all table entries - not specified by the parent table (PosMerge) - """ - assert ( - key["source"] in _valid_data_sources - ), f"source needs to be one of {_valid_data_sources}" - position_id = key.get("position_id", None) - if position_id is None: - key["position_id"] = ( - dj.U().aggr(self & key, n="max(position_id)").fetch1("n") or 0 - ) + 1 - else: - id = (self & key).fetch("position_id") - if len(id) > 0: - position_id = max(id) + 1 - else: - position_id = max(0, position_id) - key["position_id"] = position_id - super().insert1(key, **kwargs) - source = key["source"] - if source in ["Common"]: - table_name = f"{source}Pos" - else: - version = key["version"] - table_name = f"{source}PosV{version}" - part_table = getattr(self, table_name) - # TODO: The parent table to refer to is hard-coded here, expecting it to be the second - # Table in the definition. This could be more flexible. - if params: - table_query = ( - dj.FreeTable(dj.conn(), full_table_name=part_table.parents()[1]) - & key - & params - ) - else: - table_query = ( - dj.FreeTable(dj.conn(), full_table_name=part_table.parents()[1]) - & key - ) - if any( - "head" in col - for col in list(table_query.fetch().dtype.fields.keys()) - ): - ( - analysis_file_name, - position_object_id, - orientation_object_id, - velocity_object_id, - ) = table_query.fetch1( - "analysis_file_name", - "head_position_object_id", - "head_orientation_object_id", - "head_velocity_object_id", - ) - else: - ( - analysis_file_name, - position_object_id, - orientation_object_id, - velocity_object_id, - ) = table_query.fetch1( - "analysis_file_name", - "position_object_id", - "orientation_object_id", - "velocity_object_id", - ) - part_table.insert1( - { - **key, - "analysis_file_name": analysis_file_name, - "position_object_id": position_object_id, - "orientation_object_id": orientation_object_id, - "velocity_object_id": velocity_object_id, - **params, - }, - ) - - def fetch_nwb(self, *attrs, **kwargs): - source = self.fetch1("source") - if source in ["Common"]: - table_name = f"{source}Pos" - else: - version = self.fetch1("version") - table_name = f"{source}PosV{version}" - part_table = getattr(self, table_name) & self - return part_table.fetch_nwb() - def fetch1_dataframe(self): - nwb_data = self.fetch_nwb()[0] + # proj replaces operator restriction to enable + # (TableName & restriction).fetch1_dataframe() + nwb_data = self.fetch_nwb(self.proj())[0] index = pd.Index( np.asarray(nwb_data["position"].get_spatial_series().timestamps), name="time", @@ -285,7 +145,6 @@ class PositionVideoSelection(dj.Manual): plot_id : int plot : varchar(40) # Which position info to overlay on video file --- - position_ids : mediumblob output_dir : varchar(255) # directory where to save output video """ @@ -323,11 +182,15 @@ class PositionVideo(dj.Computed): """ def make(self, key): - assert key["plot"] in ["DLC", "Trodes", "Common", "All"] + raise NotImplementedError("work in progress -DPG") + + plot = key.get("plot") + if plot not in ["DLC", "Trodes", "Common", "All"]: + raise ValueError(f"Plot {key['plot']} not supported") + # CBroz: I was told only tests should `assert`, code should `raise` + M_TO_CM = 100 - output_dir, position_ids = (PositionVideoSelection & key).fetch1( - "output_dir", "position_ids" - ) + output_dir = (PositionVideoSelection & key).fetch1("output_dir") print("Loading position data...") # raw_position_df = ( @@ -337,122 +200,27 @@ def make(self, key): # "interval_list_name": key["interval_list_name"], # } # ).fetch1_dataframe() + query = { "nwb_file_name": key["nwb_file_name"], "interval_list_name": key["interval_list_name"], } - if key["plot"] == "DLC": - assert position_ids["dlc_position_id"] - pos_df = ( - PositionOutput() - & { - **query, - "source": "DLC", - "position_id": position_ids["dlc_position_id"], - } - ).fetch1_dataframe() - elif key["plot"] == "Trodes": - assert position_ids["trodes_position_id"] - pos_df = ( - PositionOutput() - & { - **query, - "source": "Trodes", - "position_id": position_ids["trodes_position_id"], - } - ).fetch1_dataframe() - elif key["plot"] == "Common": - assert position_ids["common_position_id"] - pos_df = ( - PositionOutput() - & { - **query, - "source": "Common", - "position_id": position_ids["common_position_id"], - } - ).fetch1_dataframe() - elif key["plot"] == "All": + merge_entries = { + "DLC": PositionOutput.DLCPosV1 & query, + "Trodes": PositionOutput.TrodesPosV1 & query, + "Common": PositionOutput.CommonPos & query, + } + + position_mean_dict = {} + if plot == "All": # Check which entries exist in PositionOutput merge_dict = {} - if "dlc_position_id" in position_ids: - if ( - len( - PositionOutput() - & { - **query, - "source": "DLC", - "position_id": position_ids["dlc_position_id"], - } - ) - > 0 - ): - dlc_df = ( - ( - PositionOutput() - & { - **query, - "source": "DLC", - "position_id": position_ids["dlc_position_id"], - } - ) - .fetch1_dataframe() - .drop(columns=["velocity_x", "velocity_y", "speed"]) - ) - merge_dict["DLC"] = dlc_df - if "trodes_position_id" in position_ids: - if ( - len( - PositionOutput() - & { - **query, - "source": "Trodes", - "position_id": position_ids["trodes_position_id"], - } + for source, entries in merge_entries.items(): + if entries: + merge_dict[source] = entries.fetch1_dataframe().drop( + columns=["velocity_x", "velocity_y", "speed"] ) - > 0 - ): - trodes_df = ( - ( - PositionOutput() - & { - **query, - "source": "Trodes", - "position_id": position_ids[ - "trodes_position_id" - ], - } - ) - .fetch1_dataframe() - .drop(columns=["velocity_x", "velocity_y", "speed"]) - ) - merge_dict["Trodes"] = trodes_df - if "common_position_id" in position_ids: - if ( - len( - PositionOutput() - & { - **query, - "source": "Common", - "position_id": position_ids["common_position_id"], - } - ) - > 0 - ): - common_df = ( - ( - PositionOutput() - & { - **query, - "source": "Common", - "position_id": position_ids[ - "common_position_id" - ], - } - ) - .fetch1_dataframe() - .drop(columns=["velocity_x", "velocity_y", "speed"]) - ) - merge_dict["Common"] = common_df + pos_df = ft.reduce( lambda left, right,: pd.merge( left[1], @@ -463,15 +231,33 @@ def make(self, key): ), merge_dict.items(), ) - print("Loading video data...") - epoch = ( - int( - key["interval_list_name"] - .replace("pos ", "") - .replace(" valid times", "") + position_mean_dict = { + source: { + "position": np.asarray( + pos_df[[f"position_x_{source}", f"position_y_{source}"]] + ), + "orientation": np.asarray( + pos_df[[f"orientation_{source}"]] + ), + } + for source in merge_dict.keys() + } + else: + if plot == "DLC": + # CBroz - why is this extra step needed for DLC? + pos_df_key = merge_entries[plot].fetch1(as_dict=True) + pos_df = (PositionOutput & pos_df_key).fetch1_dataframe() + elif plot in ["Trodes", "Common"]: + pos_df = merge_entries[plot].fetch1_dataframe() + + position_mean_dict[plot]["position"] = np.asarray( + pos_df[["position_x", "position_y"]] ) - + 1 - ) + position_mean_dict[plot]["orientation"] = np.asarray( + pos_df[["orientation"]] + ) + + print("Loading video data...") ( video_path, @@ -479,20 +265,24 @@ def make(self, key): meters_per_pixel, video_time, ) = get_video_path( - {"nwb_file_name": key["nwb_file_name"], "epoch": epoch} + { + "nwb_file_name": key["nwb_file_name"], + "epoch": int( + "".join(filter(str.isdigit, key["interval_list_name"])) + ) + + 1, + } ) video_dir = os.path.dirname(video_path) + "/" video_frame_col_name = [ col for col in pos_df.columns if "video_frame_ind" in col - ] - video_frame_inds = ( - pos_df[video_frame_col_name[0]].astype(int).to_numpy() - ) - if key["plot"] in ["DLC", "All"]: - temp_key = (PositionOutput.DLCPosV1 & key).fetch1("KEY") - video_path = (DLCPoseEstimationSelection & temp_key).fetch1( - "video_path" - ) + ][0] + video_frame_inds = pos_df[video_frame_col_name].astype(int).to_numpy() + if plot in ["DLC", "All"]: + video_path = ( + DLCPoseEstimationSelection + & (PositionOutput.DLCPosV1 & key).fetch1("KEY") + ).fetch1("video_path") else: video_path = check_videofile( video_dir, key["output_dir"], video_filename @@ -506,28 +296,7 @@ def make(self, key): # centroids = {'red': np.asarray(raw_position_df[['xloc', 'yloc']]), # 'green': np.asarray(raw_position_df[['xloc2', 'yloc2']])} - position_mean_dict = {} - if key["plot"] in ["DLC", "Trodes", "Common"]: - position_mean_dict[key["plot"]]["position"] = np.asarray( - pos_df[["position_x", "position_y"]] - ) - position_mean_dict[key["plot"]]["orientation"] = np.asarray( - pos_df[["orientation"]] - ) - elif key["plot"] == "All": - position_mean_dict = { - source: { - "position": np.asarray( - pos_df[[f"position_x_{source}", f"position_y_{source}"]] - ), - "orientation": np.asarray( - pos_df[[f"orientation_{source}"]] - ), - } - for source in merge_dict.keys() - } - position_time = np.asarray(pos_df.index) - cm_per_pixel = meters_per_pixel * M_TO_CM + print("Making video...") make_video( @@ -535,10 +304,10 @@ def make(self, key): video_frame_inds, position_mean_dict, video_time, - position_time, + np.asarray(pos_df.index), processor="opencv", output_video_filename=output_video_filename, - cm_to_pixels=cm_per_pixel, + cm_to_pixels=meters_per_pixel * M_TO_CM, disable_progressbar=False, ) self.insert1(key) diff --git a/src/spyglass/position/v1/position_dlc_selection.py b/src/spyglass/position/v1/position_dlc_selection.py index b48ee6be4..c72cb9931 100644 --- a/src/spyglass/position/v1/position_dlc_selection.py +++ b/src/spyglass/position/v1/position_dlc_selection.py @@ -1,9 +1,11 @@ +import copy from pathlib import Path import datajoint as dj import numpy as np import pandas as pd import pynwb +from datajoint.utils import to_camel_case from tqdm import tqdm as tqdm from ...common.common_nwbfile import AnalysisNwbfile @@ -52,59 +54,60 @@ class DLCPosV1(dj.Computed): """ def make(self, key): + orig_key = copy.deepcopy(key) key["pose_eval_result"] = self.evaluate_pose_estimation(key) - position_nwb_data = (DLCCentroid & key).fetch_nwb()[0] - orientation_nwb_data = (DLCOrientation & key).fetch_nwb()[0] - position_object = position_nwb_data["dlc_position"].spatial_series[ - "position" - ] - velocity_object = position_nwb_data["dlc_velocity"].time_series[ - "velocity" - ] - video_frame_object = position_nwb_data["dlc_velocity"].time_series[ - "video_frame_ind" - ] - orientation_object = orientation_nwb_data[ - "dlc_orientation" - ].spatial_series["orientation"] + + pos_nwb = (DLCCentroid & key).fetch_nwb()[0] + ori_nwb = (DLCOrientation & key).fetch_nwb()[0] + + pos_obj = pos_nwb["dlc_position"].spatial_series["position"] + vel_obj = pos_nwb["dlc_velocity"].time_series["velocity"] + vid_frame_obj = pos_nwb["dlc_velocity"].time_series["video_frame_ind"] + ori_obj = ori_nwb["dlc_orientation"].spatial_series["orientation"] + position = pynwb.behavior.Position() orientation = pynwb.behavior.CompassDirection() velocity = pynwb.behavior.BehavioralTimeSeries() + position.create_spatial_series( - name=position_object.name, - timestamps=np.asarray(position_object.timestamps), - conversion=position_object.conversion, - data=np.asarray(position_object.data), - reference_frame=position_object.reference_frame, - comments=position_object.comments, - description=position_object.description, + name=pos_obj.name, + timestamps=np.asarray(pos_obj.timestamps), + conversion=pos_obj.conversion, + data=np.asarray(pos_obj.data), + reference_frame=pos_obj.reference_frame, + comments=pos_obj.comments, + description=pos_obj.description, ) + orientation.create_spatial_series( - name=orientation_object.name, - timestamps=np.asarray(orientation_object.timestamps), - conversion=orientation_object.conversion, - data=np.asarray(orientation_object.data), - reference_frame=orientation_object.reference_frame, - comments=orientation_object.comments, - description=orientation_object.description, + name=ori_obj.name, + timestamps=np.asarray(ori_obj.timestamps), + conversion=ori_obj.conversion, + data=np.asarray(ori_obj.data), + reference_frame=ori_obj.reference_frame, + comments=ori_obj.comments, + description=ori_obj.description, ) + velocity.create_timeseries( - name=velocity_object.name, - timestamps=np.asarray(velocity_object.timestamps), - conversion=velocity_object.conversion, - unit=velocity_object.unit, - data=np.asarray(velocity_object.data), - comments=velocity_object.comments, - description=velocity_object.description, + name=vel_obj.name, + timestamps=np.asarray(vel_obj.timestamps), + conversion=vel_obj.conversion, + unit=vel_obj.unit, + data=np.asarray(vel_obj.data), + comments=vel_obj.comments, + description=vel_obj.description, ) + velocity.create_timeseries( - name=video_frame_object.name, - unit=video_frame_object.unit, - timestamps=np.asarray(video_frame_object.timestamps), - data=np.asarray(video_frame_object.data), - description=video_frame_object.description, - comments=video_frame_object.comments, + name=vid_frame_obj.name, + unit=vid_frame_obj.unit, + timestamps=np.asarray(vid_frame_obj.timestamps), + data=np.asarray(vid_frame_obj.data), + description=vid_frame_obj.description, + comments=vid_frame_obj.comments, ) + # Add to Analysis NWB file key["analysis_file_name"] = AnalysisNwbfile().create( key["nwb_file_name"] @@ -124,23 +127,15 @@ def make(self, key): nwb_file_name=key["nwb_file_name"], analysis_file_name=key["analysis_file_name"], ) - self.insert1(key) - from ..position_merge import PositionOutput - key["source"] = "DLC" - key["version"] = 1 - dlc_key = key.copy() - del dlc_key["pose_eval_result"] - key["interval_list_name"] = f"pos {key['epoch']-1} valid times" - valid_fields = PositionOutput().fetch().dtype.fields.keys() - entries_to_delete = [ - entry for entry in key.keys() if entry not in valid_fields - ] - for entry in entries_to_delete: - del key[entry] + from ..position_merge import PositionOutput - PositionOutput().insert1(key=key, params=dlc_key, skip_duplicates=True) + part_name = to_camel_case(self.table_name.split("__")[-1]) + # TODO: The next line belongs in a merge table function + PositionOutput._merge_insert( + [orig_key], part_name=part_name, skip_duplicates=True + ) def fetch_nwb(self, *attrs, **kwargs): return fetch_nwb( diff --git a/src/spyglass/position/v1/position_trodes_position.py b/src/spyglass/position/v1/position_trodes_position.py index a31aa158d..68f43526e 100644 --- a/src/spyglass/position/v1/position_trodes_position.py +++ b/src/spyglass/position/v1/position_trodes_position.py @@ -1,3 +1,4 @@ +import copy import os from pathlib import Path @@ -8,6 +9,7 @@ import pandas as pd import pynwb import pynwb.behavior +from datajoint.utils import to_camel_case from position_tools import ( get_angle, get_centriod, @@ -106,6 +108,7 @@ class TrodesPosV1(dj.Computed): """ def make(self, key): + orig_key = copy.deepcopy(key) print(f"Computing position for: {key}") key["analysis_file_name"] = AnalysisNwbfile().create( key["nwb_file_name"] @@ -215,19 +218,13 @@ def make(self, key): AnalysisNwbfile().add(key["nwb_file_name"], key["analysis_file_name"]) self.insert1(key) + from ..position_merge import PositionOutput - key["source"] = "Trodes" - key["version"] = 1 - trodes_key = key.copy() - valid_fields = PositionOutput().fetch().dtype.fields.keys() - entries_to_delete = [ - entry for entry in key.keys() if entry not in valid_fields - ] - for entry in entries_to_delete: - del key[entry] - PositionOutput().insert1( - key=key, params=trodes_key, skip_duplicates=True + part_name = to_camel_case(self.table_name.split("__")[-1]) + # TODO: The next line belongs in a merge table function + PositionOutput._merge_insert( + [orig_key], part_name=part_name, skip_duplicates=True ) @staticmethod diff --git a/src/spyglass/utils/dj_merge_tables.py b/src/spyglass/utils/dj_merge_tables.py index 9f3188a76..2a79ece4a 100644 --- a/src/spyglass/utils/dj_merge_tables.py +++ b/src/spyglass/utils/dj_merge_tables.py @@ -132,6 +132,7 @@ def _merge_restrict_parts( def _merge_restrict_parents( cls, restriction: str = True, + parent_name: str = None, as_objects: bool = True, return_empties: bool = True, add_invalid_restrict: bool = True, @@ -146,6 +147,8 @@ def _merge_restrict_parents( restriction: str, optional Restriction to apply to the returned parent. Default True, no restrictions. + parent_name: str, optional + CamelCase name of the parent. as_objects: bool, optional Default True. Return part tables as objects return_empties: bool, optional @@ -171,6 +174,12 @@ def _merge_restrict_parents( for parent in part.parents(as_objects=True) # ID respective parents if cls().table_name not in parent.full_table_name # Not merge table ] + if parent_name: + part_parents = [ + p + for p in part_parents + if from_camel_case(parent_name) in p.full_table_name + ] if not as_objects: part_parents = [p.full_table_name for p in part_parents] @@ -226,7 +235,7 @@ def _merge_repr(cls, restriction: str = True) -> dj.expression.Union: @classmethod def _merge_insert( - cls, rows: list, mutual_exclusvity=True, **kwargs + cls, rows: list, part_name: str = None, mutual_exclusvity=True, **kwargs ) -> None: """Insert rows into merge table, ensuring db integrity and mutual exclusivity @@ -234,6 +243,8 @@ def _merge_insert( --------- rows: List[dict] An iterable where an element is a dictionary. + part: str, optional + CamelCase name of the part table Raises ------ @@ -254,15 +265,22 @@ def _merge_insert( raise TypeError('Input "rows" must be a list of dictionaries') parts = cls._merge_restrict_parts(as_objects=True) + if part_name: + parts = [ + p + for p in parts + if from_camel_case(part_name) in p.full_table_name + ] + master_entries = [] parts_entries = {p: [] for p in parts} for row in rows: - key = {} # empty to-be-inserted key + keys = [] # empty to-be-inserted key for part in parts: # check each part part_parent = part.parents(as_objects=True)[-1] part_name = to_camel_case(part.table_name.split("__")[-1]) if part_parent & row: # if row is in part parent - if key and mutual_exclusvity: # if key from other part + if keys and mutual_exclusvity: # if key from other part raise ValueError( "Mutual Exclusivity Error! Entry exists in more " + f"than one table - Entry: {row}" @@ -271,7 +289,7 @@ def _merge_insert( keys = (part_parent & row).fetch("KEY") # get pk if len(keys) > 1: raise ValueError( - "Ambiguous entry. Data has mult rows in" + "Ambiguous entry. Data has mult rows in " + f"{part_name}:\n\tData:{row}\n\t{keys}" ) master_pk = { # make uuid @@ -282,7 +300,7 @@ def _merge_insert( {**master_pk, cls()._reserved_sk: part_name} ) - if not key: + if not keys: raise ValueError( "Non-existing entry in any of the parent tables - Entry: " + f"{row}" @@ -632,6 +650,13 @@ def merge_fetch(self, restriction: str = True, *attrs, **kwargs) -> list: ) return results[0] if len(results) == 1 else results + @classmethod + def merge_populate(source: str, key=None): + raise NotImplementedError( + "CBroz: In the future, this command will support executing " + + "part_parent `make` and then inserting all entries into Merge" + ) + _Merge = Merge From efae4ba4f4dd843b0d1681dd349b3eae9497e395 Mon Sep 17 00:00:00 2001 From: Eric Denovellis Date: Mon, 10 Jul 2023 15:26:47 -0700 Subject: [PATCH 7/7] Add position linearization (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restructure for mkdocs * Minor docstring edits for mkdocs * Adjust installer docs * Permit json config * Update docstrings for mkdocs. Add images * Minor fixes. Rename publish action. Add changelog notes * hard wrap changes, markdownlint * blackify * Fix version cmd. Minor doc wording * mkdocstrings require empty inits to identify submodules * 🤦‍♂️ Publish docs CI/CD `main`->`master` * Add linearization * Update src/spyglass/position_linearization/v1/linearization.py Co-authored-by: Daniel Gramling <57648931+dpeg22@users.noreply.github.com> * Use master table name instead * Edit `get_part`; Add `merge_fetch` * Spelling fixes * Refactor restrict_parts. Adjust mkdocs nav. * Docs adjust for Merge tables * Fix `merge_get_part` * Use hatch for docs version * Update changelog * Typo * Spellcheck config * merge CBroz1/spyglass master (#1) * Restructure for mkdocs * Minor docstring edits for mkdocs * Adjust installer docs * Permit json config * Update docstrings for mkdocs. Add images * Minor fixes. Rename publish action. Add changelog notes * hard wrap changes, markdownlint * blackify * Fix version cmd. Minor doc wording * mkdocstrings require empty inits to identify submodules * 🤦‍♂️ Publish docs CI/CD `main`->`master` * Edit `get_part`; Add `merge_fetch` * Spelling fixes * Refactor restrict_parts. Adjust mkdocs nav. * Docs adjust for Merge tables * Fix `merge_get_part` * Use hatch for docs version * Update changelog * Typo * Spellcheck config --------- Co-authored-by: CBroz1 Co-authored-by: CBroz1 * adopted Merge class to PositionOutput * WIP: dict/str ristrict consistency * Normalize restriction/classmeth. Add notes on why * Typos * Docs publish on tag * Edit changelog: Add links, patch version bump * 🧪 Test gh-actions debug * get-part multi-source flag * Add mutual exclusivity flag * Add mutual exclusivity flag from pos branch * small updates * add _Merge inheritance * Some refactoring suggestions (#3) * Some refactoring suggestions * Add support for restricting entry by part name * update insert to Merge * update insert to merge and merge fetch1_dataframe * update insert to and fetch1_dataframe from merge * change indent, add skip_dup arg to merge insertion * Remove node picker * Remove comment --------- Co-authored-by: CBroz1 Co-authored-by: Daniel Gramling <57648931+dpeg22@users.noreply.github.com> Co-authored-by: CBroz1 Co-authored-by: dpeg22 --- .../position_linearization/__init__.py | 3 + .../position_linearization_merge.py | 30 +++ .../position_linearization/v1/__init__.py | 7 + .../v1/linearization.py | 187 ++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 src/spyglass/position_linearization/__init__.py create mode 100644 src/spyglass/position_linearization/position_linearization_merge.py create mode 100644 src/spyglass/position_linearization/v1/__init__.py create mode 100644 src/spyglass/position_linearization/v1/linearization.py diff --git a/src/spyglass/position_linearization/__init__.py b/src/spyglass/position_linearization/__init__.py new file mode 100644 index 000000000..49103ec16 --- /dev/null +++ b/src/spyglass/position_linearization/__init__.py @@ -0,0 +1,3 @@ +from spyglass.position_linearization.position_linearization_merge import ( + LinearizedPositionOutput, +) diff --git a/src/spyglass/position_linearization/position_linearization_merge.py b/src/spyglass/position_linearization/position_linearization_merge.py new file mode 100644 index 000000000..68f9d063a --- /dev/null +++ b/src/spyglass/position_linearization/position_linearization_merge.py @@ -0,0 +1,30 @@ +import datajoint as dj + +from spyglass.position_linearization.v1.linearization import ( + LinearizedPositionV1, +) # noqa F401 + +from ..utils.dj_merge_tables import _Merge + +schema = dj.schema("position_linearization_merge") + + +@schema +class LinearizedPositionOutput(_Merge): + definition = """ + merge_id: uuid + --- + source: varchar(32) + """ + + class LinearizedPositionV1(dj.Part): + definition = """ + -> LinearizedPositionOutput + --- + -> LinearizedPositionV1 + """ + + def fetch1_dataframe(self): + return self.fetch_nwb(self.proj())[0]["linearized_position"].set_index( + "time" + ) diff --git a/src/spyglass/position_linearization/v1/__init__.py b/src/spyglass/position_linearization/v1/__init__.py new file mode 100644 index 000000000..ec3dffc65 --- /dev/null +++ b/src/spyglass/position_linearization/v1/__init__.py @@ -0,0 +1,7 @@ +from spyglass.position_linearization.v1.linearization import ( + LinearizationParameters, + LinearizationSelection, + LinearizedPositionV1, + NodePicker, + TrackGraph, +) diff --git a/src/spyglass/position_linearization/v1/linearization.py b/src/spyglass/position_linearization/v1/linearization.py new file mode 100644 index 000000000..7f30be8c9 --- /dev/null +++ b/src/spyglass/position_linearization/v1/linearization.py @@ -0,0 +1,187 @@ +import copy +import datajoint as dj +from datajoint.utils import to_camel_case +import numpy as np +from track_linearization import ( + get_linearized_position, + make_track_graph, + plot_graph_as_1D, + plot_track_graph, +) + +from spyglass.common.common_nwbfile import AnalysisNwbfile +from spyglass.position.position_merge import PositionOutput +from spyglass.utils.dj_helper_fn import fetch_nwb + +schema = dj.schema("position_linearization_v1") + + +@schema +class LinearizationParameters(dj.Lookup): + """Choose whether to use an HMM to linearize position. This can help when + the eucledian distances between separate arms are too close and the previous + position has some information about which arm the animal is on.""" + + definition = """ + linearization_param_name : varchar(80) # name for this set of parameters + --- + use_hmm = 0 : int # use HMM to determine linearization + # How much to prefer route distances between successive time points that are closer to the euclidean distance. Smaller numbers mean the route distance is more likely to be close to the euclidean distance. + route_euclidean_distance_scaling = 1.0 : float + sensor_std_dev = 5.0 : float # Uncertainty of position sensor (in cm). + # Biases the transition matrix to prefer the current track segment. + diagonal_bias = 0.5 : float + """ + + +@schema +class TrackGraph(dj.Manual): + """Graph representation of track representing the spatial environment. + Used for linearizing position.""" + + definition = """ + track_graph_name : varchar(80) + ---- + environment : varchar(80) # Type of Environment + node_positions : blob # 2D position of track_graph nodes, shape (n_nodes, 2) + edges: blob # shape (n_edges, 2) + linear_edge_order : blob # order of track graph edges in the linear space, shape (n_edges, 2) + linear_edge_spacing : blob # amount of space between edges in the linear space, shape (n_edges,) + """ + + def get_networkx_track_graph(self, track_graph_parameters=None): + if track_graph_parameters is None: + track_graph_parameters = self.fetch1() + return make_track_graph( + node_positions=track_graph_parameters["node_positions"], + edges=track_graph_parameters["edges"], + ) + + def plot_track_graph(self, ax=None, draw_edge_labels=False, **kwds): + """Plot the track graph in 2D position space.""" + track_graph = self.get_networkx_track_graph() + plot_track_graph( + track_graph, ax=ax, draw_edge_labels=draw_edge_labels, **kwds + ) + + def plot_track_graph_as_1D( + self, + ax=None, + axis="x", + other_axis_start=0.0, + draw_edge_labels=False, + node_size=300, + node_color="#1f77b4", + ): + """Plot the track graph in 1D to see how the linearization is set up.""" + track_graph_parameters = self.fetch1() + track_graph = self.get_networkx_track_graph( + track_graph_parameters=track_graph_parameters + ) + plot_graph_as_1D( + track_graph, + edge_order=track_graph_parameters["linear_edge_order"], + edge_spacing=track_graph_parameters["linear_edge_spacing"], + ax=ax, + axis=axis, + other_axis_start=other_axis_start, + draw_edge_labels=draw_edge_labels, + node_size=node_size, + node_color=node_color, + ) + + +@schema +class LinearizationSelection(dj.Lookup): + definition = """ + -> PositionOutput + -> TrackGraph + -> LinearizationParameters + --- + """ + + +@schema +class LinearizedPositionV1(dj.Computed): + """Linearized position for a given interval""" + + definition = """ + -> LinearizationSelection + --- + -> AnalysisNwbfile + linearized_position_object_id : varchar(40) + """ + + def make(self, key): + orig_key = copy.deepcopy(key) + print(f"Computing linear position for: {key}") + + position_nwb = PositionOutput.fetch_nwb(key)[0] + key["analysis_file_name"] = AnalysisNwbfile().create( + key["nwb_file_name"] + ) + position = np.asarray( + position_nwb["position"].get_spatial_series().data + ) + time = np.asarray( + position_nwb["position"].get_spatial_series().timestamps + ) + + linearization_parameters = ( + LinearizationParameters() + & {"linearization_param_name": key["linearization_param_name"]} + ).fetch1() + track_graph_info = ( + TrackGraph() & {"track_graph_name": key["track_graph_name"]} + ).fetch1() + + track_graph = make_track_graph( + node_positions=track_graph_info["node_positions"], + edges=track_graph_info["edges"], + ) + + linear_position_df = get_linearized_position( + position=position, + track_graph=track_graph, + edge_spacing=track_graph_info["linear_edge_spacing"], + edge_order=track_graph_info["linear_edge_order"], + use_HMM=linearization_parameters["use_hmm"], + route_euclidean_distance_scaling=linearization_parameters[ + "route_euclidean_distance_scaling" + ], + sensor_std_dev=linearization_parameters["sensor_std_dev"], + diagonal_bias=linearization_parameters["diagonal_bias"], + ) + + linear_position_df["time"] = time + + # Insert into analysis nwb file + nwb_analysis_file = AnalysisNwbfile() + + key["linearized_position_object_id"] = nwb_analysis_file.add_nwb_object( + analysis_file_name=key["analysis_file_name"], + nwb_object=linear_position_df, + ) + + nwb_analysis_file.add( + nwb_file_name=key["nwb_file_name"], + analysis_file_name=key["analysis_file_name"], + ) + + self.insert1(key) + + from ..position_linearization_merge import LinearizedPositionOutput + + part_name = to_camel_case(self.table_name.split("__")[-1]) + + LinearizedPositionOutput._merge_insert( + [orig_key], part_name=part_name, skip_duplicates=True + ) + + def fetch_nwb(self, *attrs, **kwargs): + return fetch_nwb( + self, (AnalysisNwbfile, "analysis_file_abs_path"), *attrs, **kwargs + ) + + def fetch1_dataframe(self): + return self.fetch_nwb()[0]["linearized_position"].set_index("time")