Repository: hed-standard/hed-python (language: python)

Issue:
Minor doc update



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 4752547b7..7b777ebde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,4 +119,5 @@ Thumbs.db
 Desktop.ini
 
 # Project specific
-schema_cache_test/
\ No newline at end of file
+schema_cache_test/
+hed_cache/
\ No newline at end of file
diff --git a/docs/source/Overview.md b/docs/source/Overview.md
deleted file mode 100644
index 557412552..000000000
--- a/docs/source/Overview.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Introduction to HED
-This document contains the specification for third generation HED or HED-3G. 
-It is meant for the implementers and users of HED tools. Other tutorials and tagging 
-guides are available to researchers using HED to annotate their data. 
-This document contains the specification for the first official release of HED-3G 
-(HED versions 8.0.0-xxx and above.) **When the term HED is used in this document, 
-it refers to third generation (HED-3G) unless explicitly stated otherwise.**
-
-The aspects of HED that are described in this document are supported or will soon
-be supported by validators and other tools and are available for immediate use by annotators. 
-The schema vocabulary can be viewed using an expandable
-[schema viewer](https://www.hedtags.org/display_hed.html).
-
-## Resources
-All HED-related source and documentation repositories are housed on the HED-standard 
-organization GitHub site, [https://github.com/hed-standard](https://github.com/hed-standard),
-which is maintained by the HED Working Group. HED development is open-source and
-community-based. Also see the official HED website [https://www.hedtags.org](https://www.hedtags.org)
-for a list of additional resources.
-
-## Contribute
-The HED Working Group invites those interested in HED to contribute to the development process. 
-Users are encouraged to use the *Issues* mechanism of the `hed-specification`
-repository on the GitHub `hed-standard` working group website: 
-[https://github.com/hed-standard/hed-specification/issues](https://github.com/hed-standard/hed-specification/issues)
-to ask for help or make suggestions. The HED discussion forum 
-[https://github.com/hed-standard/hed-specification/discussions](https://github.com/hed-standard/hed-specification/discussions) is maintained for in depth 
-discussions of HED issues and evolution.
diff --git a/docs/source/_templates/autosummary/class.rst b/docs/source/_templates/autosummary/class.rst
new file mode 100644
index 000000000..50efd957f
--- /dev/null
+++ b/docs/source/_templates/autosummary/class.rst
@@ -0,0 +1,34 @@
+{{ fullname | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ module }}.{{ objname }}
+   {% block methods %}
+   .. automethod:: __init__
+
+   {% if methods %}
+   .. rubric:: {{ _('Methods') }}
+
+   .. autosummary::
+   {% for item in methods %}
+      ~{{ name }}.{{ item }}
+   {%- endfor %}
+   {% endif %}
+   {% endblock %}
+
+   {% block attributes %}
+   {% if attributes %}
+   .. rubric:: {{ _('Attributes') }}
+
+   .. autosummary::
+   {% for item in attributes %}
+      ~{{ name }}.{{ item }}
+   {%- endfor %}
+   {% endif %}
+   {% endblock %}
+
+.. autoclass:: {{ module }}.{{ objname }}
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html
index 1659dba9a..1bf809ffd 100644
--- a/docs/source/_templates/layout.html
+++ b/docs/source/_templates/layout.html
@@ -9,6 +9,10 @@
       <span class="caption-text">HED resources:</span>
     </p>
     <ul>
+     <li class="toctree-li">
+        <a href="https://hed-examples.readthedocs.io/en/latest/HedDocumentationSummary.html">Documentation summary</a></li>
+      <li class="toctree-li"><a href="https://www.hedtags.org/display_hed.html">Schema viewer</a></li>
+      <li class="toctree-li"><a href="https://www.hedtags.org/display_hed_library.html">Library schema viewer</a></li>
       <li class="toctree-li"><a href="https://hed-specification.readthedocs.io/en/latest/index.html">Specification</a></li>
       <li class="toctree-li"><a href="https://hed-examples.readthedocs.io/en/latest/">Examples and tutorials</a></li>
       <li class="toctree-li"><a href="https://hedtools.ucsd.edu/hed">Online tools</a></li>
diff --git a/docs/source/api.rst b/docs/source/api.rst
index 8322b5b79..afafa7433 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -27,7 +27,7 @@ HED models
 
 .. currentmodule:: hed
 
-.. _calibration_ref:
+.. _calibration_ref1:
 
 
 HED schema handling
@@ -57,14 +57,18 @@ HED tools
    :toctree: generated/
 
    hed.tools.BidsDataset
+   hed.tools.BidsDatasetSummary
    hed.tools.BidsEventFile
    hed.tools.BidsEventFiles
    hed.tools.BidsFile
    hed.tools.BidsJsonFile
    hed.tools.BidsSidecarFile
+   hed.tools.BidsTsvDictionary
    hed.tools.BidsTsvFile
-   hed.tools.ColumnSummary
-   hed.tools.DatasetSummary
+   hed.tools.BidsTsvSummary
+
+   hed.tools.FileDictionary
+   hed.tools.HedLogger
    hed.tools.KeyMap
    hed.tools.KeyTemplate
    hed.tools.SummaryEntry
@@ -89,7 +93,7 @@ HED utilities
 .. currentmodule:: hed
 
 
-HED validation
+HED validators
 --------------
 
 .. autosummary:: hed.validator
@@ -101,4 +105,11 @@ HED validation
 
 .. currentmodule:: hed
 
-.. _calibration_ref:
\ No newline at end of file
+Modules
+--------------
+.. toctree::
+   :maxdepth: 6
+
+   generated/modules
+
+.. _calibration_ref2:
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 3e3d10fcd..229138018 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -27,7 +27,7 @@
 version = '0.0.1'
 release = '0.0.1'
 
-currentdir = os.path.abspath(os.path.dirname(__file__))
+currentdir = os.path.realpath(os.path.dirname(__file__))
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/source/generated/hed.models.BaseInput.rst b/docs/source/generated/hed.models.BaseInput.rst
index eb81579f3..c71e0161e 100644
--- a/docs/source/generated/hed.models.BaseInput.rst
+++ b/docs/source/generated/hed.models.BaseInput.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: BaseInput
-
+.. autoclass:: hed.models.BaseInput
    
    .. automethod:: __init__
 
@@ -49,4 +48,10 @@
       ~BaseInput.name
       ~BaseInput.worksheet_name
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.BaseInput
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.ColumnMapper.rst b/docs/source/generated/hed.models.ColumnMapper.rst
index 8f8fbae28..ebb6163cd 100644
--- a/docs/source/generated/hed.models.ColumnMapper.rst
+++ b/docs/source/generated/hed.models.ColumnMapper.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: ColumnMapper
-
+.. autoclass:: hed.models.ColumnMapper
    
    .. automethod:: __init__
 
@@ -29,4 +28,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.ColumnMapper
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.ColumnMetadata.rst b/docs/source/generated/hed.models.ColumnMetadata.rst
index 5974849d9..26031668c 100644
--- a/docs/source/generated/hed.models.ColumnMetadata.rst
+++ b/docs/source/generated/hed.models.ColumnMetadata.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: ColumnMetadata
-
+.. autoclass:: hed.models.ColumnMetadata
    
    .. automethod:: __init__
 
@@ -33,4 +32,10 @@
       ~ColumnMetadata.def_dict
       ~ColumnMetadata.hed_dict
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.ColumnMetadata
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.DefDict.rst b/docs/source/generated/hed.models.DefDict.rst
index 7ce137163..9d221f0f1 100644
--- a/docs/source/generated/hed.models.DefDict.rst
+++ b/docs/source/generated/hed.models.DefDict.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: DefDict
-
+.. autoclass:: hed.models.DefDict
    
    .. automethod:: __init__
 
@@ -21,4 +20,16 @@
 
    
    
-   
\ No newline at end of file
+   .. rubric:: Attributes
+
+   .. autosummary::
+   
+      ~DefDict.defs
+   
+   
+
+.. autoclass:: hed.models.DefDict
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.DefEntry.rst b/docs/source/generated/hed.models.DefEntry.rst
index 1e87eb738..abd7f615a 100644
--- a/docs/source/generated/hed.models.DefEntry.rst
+++ b/docs/source/generated/hed.models.DefEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: DefEntry
-
+.. autoclass:: hed.models.DefEntry
    
    .. automethod:: __init__
 
@@ -20,4 +19,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.DefEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.DefinitionMapper.rst b/docs/source/generated/hed.models.DefinitionMapper.rst
index d33216757..fccc73a4d 100644
--- a/docs/source/generated/hed.models.DefinitionMapper.rst
+++ b/docs/source/generated/hed.models.DefinitionMapper.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: DefinitionMapper
-
+.. autoclass:: hed.models.DefinitionMapper
    
    .. automethod:: __init__
 
@@ -25,4 +24,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.DefinitionMapper
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.EventsInput.rst b/docs/source/generated/hed.models.EventsInput.rst
index bf77770d7..52117c7bf 100644
--- a/docs/source/generated/hed.models.EventsInput.rst
+++ b/docs/source/generated/hed.models.EventsInput.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: EventsInput
-
+.. autoclass:: hed.models.EventsInput
    
    .. automethod:: __init__
 
@@ -53,4 +52,10 @@
       ~EventsInput.name
       ~EventsInput.worksheet_name
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.EventsInput
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.HedGroup.rst b/docs/source/generated/hed.models.HedGroup.rst
index 2ec520cef..25b75aaef 100644
--- a/docs/source/generated/hed.models.HedGroup.rst
+++ b/docs/source/generated/hed.models.HedGroup.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: HedGroup
-
+.. autoclass:: hed.models.HedGroup
    
    .. automethod:: __init__
 
@@ -17,23 +16,24 @@
       ~HedGroup.append
       ~HedGroup.cascade_mutable
       ~HedGroup.check_if_in_original_tags_and_groups
-      ~HedGroup.extract_tags_from_group
+      ~HedGroup.find_def_tags
+      ~HedGroup.find_exact_tags
       ~HedGroup.find_placeholder_tag
+      ~HedGroup.find_tags
       ~HedGroup.get_all_groups
       ~HedGroup.get_all_tags
       ~HedGroup.get_as_form
       ~HedGroup.get_as_long
       ~HedGroup.get_as_short
       ~HedGroup.get_direct_children
+      ~HedGroup.get_frozen
       ~HedGroup.get_original_hed_string
       ~HedGroup.groups
-      ~HedGroup.is_group
       ~HedGroup.lower
       ~HedGroup.make_tag_mutable
       ~HedGroup.remove_groups
       ~HedGroup.replace_tag
       ~HedGroup.tags
-      ~HedGroup.without_defs
    
    
 
@@ -43,6 +43,13 @@
 
    .. autosummary::
    
+      ~HedGroup.is_group
       ~HedGroup.span
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.HedGroup
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.HedInput.rst b/docs/source/generated/hed.models.HedInput.rst
index a0152d4a8..638fc321d 100644
--- a/docs/source/generated/hed.models.HedInput.rst
+++ b/docs/source/generated/hed.models.HedInput.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: HedInput
-
+.. autoclass:: hed.models.HedInput
    
    .. automethod:: __init__
 
@@ -49,4 +48,10 @@
       ~HedInput.name
       ~HedInput.worksheet_name
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.HedInput
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.HedString.rst b/docs/source/generated/hed.models.HedString.rst
index dc82dcc84..3badb9a14 100644
--- a/docs/source/generated/hed.models.HedString.rst
+++ b/docs/source/generated/hed.models.HedString.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: HedString
-
+.. autoclass:: hed.models.HedString
    
    .. automethod:: __init__
 
@@ -15,7 +14,7 @@
    
       ~HedString.__init__
       ~HedString.append
-      ~HedString.apply_ops
+      ~HedString.apply_funcs
       ~HedString.cascade_mutable
       ~HedString.check_if_in_original_tags_and_groups
       ~HedString.convert_to_canonical_forms
@@ -23,17 +22,20 @@
       ~HedString.convert_to_original
       ~HedString.convert_to_short
       ~HedString.create_from_other
-      ~HedString.extract_tags_from_group
+      ~HedString.find_def_tags
+      ~HedString.find_exact_tags
       ~HedString.find_placeholder_tag
+      ~HedString.find_tags
+      ~HedString.find_top_level_tags
       ~HedString.get_all_groups
       ~HedString.get_all_tags
       ~HedString.get_as_form
       ~HedString.get_as_long
       ~HedString.get_as_short
       ~HedString.get_direct_children
+      ~HedString.get_frozen
       ~HedString.get_original_hed_string
       ~HedString.groups
-      ~HedString.is_group
       ~HedString.lower
       ~HedString.make_tag_mutable
       ~HedString.remove_definitions
@@ -41,10 +43,8 @@
       ~HedString.replace_tag
       ~HedString.split_hed_string
       ~HedString.split_hed_string_into_groups
-      ~HedString.split_hed_string_return_tags
       ~HedString.tags
       ~HedString.validate
-      ~HedString.without_defs
    
    
 
@@ -56,6 +56,13 @@
    
       ~HedString.CLOSING_GROUP_CHARACTER
       ~HedString.OPENING_GROUP_CHARACTER
+      ~HedString.is_group
       ~HedString.span
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.HedString
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.HedTag.rst b/docs/source/generated/hed.models.HedTag.rst
index 18e3ffe43..1c9014b31 100644
--- a/docs/source/generated/hed.models.HedTag.rst
+++ b/docs/source/generated/hed.models.HedTag.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: HedTag
-
+.. autoclass:: hed.models.HedTag
    
    .. automethod:: __init__
 
@@ -53,4 +52,10 @@
       ~HedTag.unit_classes
       ~HedTag.value_classes
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.HedTag
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.OnsetMapper.rst b/docs/source/generated/hed.models.OnsetMapper.rst
index 7b66880f7..34dccee5c 100644
--- a/docs/source/generated/hed.models.OnsetMapper.rst
+++ b/docs/source/generated/hed.models.OnsetMapper.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: OnsetMapper
-
+.. autoclass:: hed.models.OnsetMapper
    
    .. automethod:: __init__
 
@@ -20,4 +19,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.OnsetMapper
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.Sidecar.rst b/docs/source/generated/hed.models.Sidecar.rst
index 99ccb7906..07efa32ef 100644
--- a/docs/source/generated/hed.models.Sidecar.rst
+++ b/docs/source/generated/hed.models.Sidecar.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.models
 
-.. autoclass:: Sidecar
-
+.. autoclass:: hed.models.Sidecar
    
    .. automethod:: __init__
 
@@ -27,4 +26,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.models.Sidecar
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.models.rst b/docs/source/generated/hed.models.rst
index b679db454..e93a8b521 100644
--- a/docs/source/generated/hed.models.rst
+++ b/docs/source/generated/hed.models.rst
@@ -60,6 +60,14 @@ hed.models.hed\_group module
    :undoc-members:
    :show-inheritance:
 
+hed.models.hed\_group\_base module
+----------------------------------
+
+.. automodule:: hed.models.hed_group_base
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 hed.models.hed\_input module
 ----------------------------
 
diff --git a/docs/source/generated/hed.schema.HedSchema.rst b/docs/source/generated/hed.schema.HedSchema.rst
index 0b705413c..301394d05 100644
--- a/docs/source/generated/hed.schema.HedSchema.rst
+++ b/docs/source/generated/hed.schema.HedSchema.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: HedSchema
-
+.. autoclass:: hed.schema.HedSchema
    
    .. automethod:: __init__
 
@@ -54,4 +53,10 @@
       ~HedSchema.value_classes
       ~HedSchema.version
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.HedSchema
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.HedSchemaEntry.rst b/docs/source/generated/hed.schema.HedSchemaEntry.rst
index 07b5a440f..3bbee27dd 100644
--- a/docs/source/generated/hed.schema.HedSchemaEntry.rst
+++ b/docs/source/generated/hed.schema.HedSchemaEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: HedSchemaEntry
-
+.. autoclass:: hed.schema.HedSchemaEntry
    
    .. automethod:: __init__
 
@@ -23,4 +22,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.HedSchemaEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.HedSchemaGroup.rst b/docs/source/generated/hed.schema.HedSchemaGroup.rst
index 2e5f0a872..1a71e060c 100644
--- a/docs/source/generated/hed.schema.HedSchemaGroup.rst
+++ b/docs/source/generated/hed.schema.HedSchemaGroup.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: HedSchemaGroup
-
+.. autoclass:: hed.schema.HedSchemaGroup
    
    .. automethod:: __init__
 
@@ -36,4 +35,10 @@
       ~HedSchemaGroup.valid_prefixes
       ~HedSchemaGroup.value_classes
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.HedSchemaGroup
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.HedSchemaSection.rst b/docs/source/generated/hed.schema.HedSchemaSection.rst
index d56a2a748..41c82e01c 100644
--- a/docs/source/generated/hed.schema.HedSchemaSection.rst
+++ b/docs/source/generated/hed.schema.HedSchemaSection.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: HedSchemaSection
-
+.. autoclass:: hed.schema.HedSchemaSection
    
    .. automethod:: __init__
 
@@ -24,4 +23,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.HedSchemaSection
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.HedTagEntry.rst b/docs/source/generated/hed.schema.HedTagEntry.rst
index 8b06f346a..acad0c741 100644
--- a/docs/source/generated/hed.schema.HedTagEntry.rst
+++ b/docs/source/generated/hed.schema.HedTagEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: HedTagEntry
-
+.. autoclass:: hed.schema.HedTagEntry
    
    .. automethod:: __init__
 
@@ -18,6 +17,7 @@
       ~HedTagEntry.attribute_has_property
       ~HedTagEntry.base_tag_has_attribute
       ~HedTagEntry.finalize_entry
+      ~HedTagEntry.get_fake_tag_entry
       ~HedTagEntry.has_attribute
       ~HedTagEntry.set_attribute_value
    
@@ -25,4 +25,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.HedTagEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.UnitClassEntry.rst b/docs/source/generated/hed.schema.UnitClassEntry.rst
index 034ad71bf..19e92c768 100644
--- a/docs/source/generated/hed.schema.UnitClassEntry.rst
+++ b/docs/source/generated/hed.schema.UnitClassEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: UnitClassEntry
-
+.. autoclass:: hed.schema.UnitClassEntry
    
    .. automethod:: __init__
 
@@ -24,4 +23,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.UnitClassEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.schema.UnitEntry.rst b/docs/source/generated/hed.schema.UnitEntry.rst
index 26507073c..9a2967b1c 100644
--- a/docs/source/generated/hed.schema.UnitEntry.rst
+++ b/docs/source/generated/hed.schema.UnitEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.schema
 
-.. autoclass:: UnitEntry
-
+.. autoclass:: hed.schema.UnitEntry
    
    .. automethod:: __init__
 
@@ -23,4 +22,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.schema.UnitEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsDataset.rst b/docs/source/generated/hed.tools.BidsDataset.rst
index 9fa3786f6..247d7ceca 100644
--- a/docs/source/generated/hed.tools.BidsDataset.rst
+++ b/docs/source/generated/hed.tools.BidsDataset.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsDataset
-
+.. autoclass:: hed.tools.BidsDataset
    
    .. automethod:: __init__
 
@@ -22,4 +21,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsDataset
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsDatasetSummary.rst b/docs/source/generated/hed.tools.BidsDatasetSummary.rst
new file mode 100644
index 000000000..a8fa7beb5
--- /dev/null
+++ b/docs/source/generated/hed.tools.BidsDatasetSummary.rst
@@ -0,0 +1,27 @@
+﻿hed.tools.BidsDatasetSummary
+============================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.BidsDatasetSummary
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~BidsDatasetSummary.__init__
+   
+   
+
+   
+   
+   
+
+.. autoclass:: hed.tools.BidsDatasetSummary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsEventFile.rst b/docs/source/generated/hed.tools.BidsEventFile.rst
index 70c37edae..0eece3342 100644
--- a/docs/source/generated/hed.tools.BidsEventFile.rst
+++ b/docs/source/generated/hed.tools.BidsEventFile.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsEventFile
-
+.. autoclass:: hed.tools.BidsEventFile
    
    .. automethod:: __init__
 
@@ -22,4 +21,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsEventFile
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsEventFiles.rst b/docs/source/generated/hed.tools.BidsEventFiles.rst
index c35d30c75..b103d753e 100644
--- a/docs/source/generated/hed.tools.BidsEventFiles.rst
+++ b/docs/source/generated/hed.tools.BidsEventFiles.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsEventFiles
-
+.. autoclass:: hed.tools.BidsEventFiles
    
    .. automethod:: __init__
 
@@ -22,4 +21,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsEventFiles
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsFile.rst b/docs/source/generated/hed.tools.BidsFile.rst
index 908c65caf..1ef1ac01e 100644
--- a/docs/source/generated/hed.tools.BidsFile.rst
+++ b/docs/source/generated/hed.tools.BidsFile.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsFile
-
+.. autoclass:: hed.tools.BidsFile
    
    .. automethod:: __init__
 
@@ -19,4 +18,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsFile
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsJsonFile.rst b/docs/source/generated/hed.tools.BidsJsonFile.rst
index 4ea19e67d..9d3c18599 100644
--- a/docs/source/generated/hed.tools.BidsJsonFile.rst
+++ b/docs/source/generated/hed.tools.BidsJsonFile.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsJsonFile
-
+.. autoclass:: hed.tools.BidsJsonFile
    
    .. automethod:: __init__
 
@@ -21,4 +20,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsJsonFile
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsSidecarFile.rst b/docs/source/generated/hed.tools.BidsSidecarFile.rst
index 90a056e9d..41108663a 100644
--- a/docs/source/generated/hed.tools.BidsSidecarFile.rst
+++ b/docs/source/generated/hed.tools.BidsSidecarFile.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsSidecarFile
-
+.. autoclass:: hed.tools.BidsSidecarFile
    
    .. automethod:: __init__
 
@@ -23,4 +22,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsSidecarFile
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsTsvDictionary.rst b/docs/source/generated/hed.tools.BidsTsvDictionary.rst
new file mode 100644
index 000000000..59e8f6c0f
--- /dev/null
+++ b/docs/source/generated/hed.tools.BidsTsvDictionary.rst
@@ -0,0 +1,46 @@
+﻿hed.tools.BidsTsvDictionary
+===========================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.BidsTsvDictionary
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~BidsTsvDictionary.__init__
+      ~BidsTsvDictionary.count_diffs
+      ~BidsTsvDictionary.create_split_dict
+      ~BidsTsvDictionary.iter_files
+      ~BidsTsvDictionary.iter_tsv_info
+      ~BidsTsvDictionary.key_diffs
+      ~BidsTsvDictionary.make_bids_file_dict
+      ~BidsTsvDictionary.make_file_dict
+      ~BidsTsvDictionary.make_key
+      ~BidsTsvDictionary.make_query
+      ~BidsTsvDictionary.match_query
+      ~BidsTsvDictionary.print_files
+      ~BidsTsvDictionary.split_dict_by_entity
+   
+   
+
+   
+   
+   .. rubric:: Attributes
+
+   .. autosummary::
+   
+      ~BidsTsvDictionary.file_list
+      ~BidsTsvDictionary.key_list
+   
+   
+
+.. autoclass:: hed.tools.BidsTsvDictionary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsTsvFile.rst b/docs/source/generated/hed.tools.BidsTsvFile.rst
index 15f5d1b05..6429eb3d1 100644
--- a/docs/source/generated/hed.tools.BidsTsvFile.rst
+++ b/docs/source/generated/hed.tools.BidsTsvFile.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: BidsTsvFile
-
+.. autoclass:: hed.tools.BidsTsvFile
    
    .. automethod:: __init__
 
@@ -21,4 +20,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.BidsTsvFile
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsTsvSummary.rst b/docs/source/generated/hed.tools.BidsTsvSummary.rst
new file mode 100644
index 000000000..81f37c177
--- /dev/null
+++ b/docs/source/generated/hed.tools.BidsTsvSummary.rst
@@ -0,0 +1,33 @@
+﻿hed.tools.BidsTsvSummary
+========================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.BidsTsvSummary
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~BidsTsvSummary.__init__
+      ~BidsTsvSummary.extract_sidecar_template
+      ~BidsTsvSummary.get_number_unique_values
+      ~BidsTsvSummary.make_combined_dicts
+      ~BidsTsvSummary.print
+      ~BidsTsvSummary.update
+      ~BidsTsvSummary.update_summary
+   
+   
+
+   
+   
+   
+
+.. autoclass:: hed.tools.BidsTsvSummary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.ColumnSummary.rst b/docs/source/generated/hed.tools.ColumnSummary.rst
deleted file mode 100644
index c56d02ff0..000000000
--- a/docs/source/generated/hed.tools.ColumnSummary.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-﻿hed.tools.ColumnSummary
-=======================
-
-.. currentmodule:: hed.tools
-
-.. autoclass:: ColumnSummary
-
-   
-   .. automethod:: __init__
-
-   
-   .. rubric:: Methods
-
-   .. autosummary::
-   
-      ~ColumnSummary.__init__
-      ~ColumnSummary.extract_sidecar_template
-      ~ColumnSummary.get_number_unique_values
-      ~ColumnSummary.print
-      ~ColumnSummary.update
-      ~ColumnSummary.update_dict
-   
-   
-
-   
-   
-   
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.DatasetSummary.rst b/docs/source/generated/hed.tools.DatasetSummary.rst
deleted file mode 100644
index d79f45ede..000000000
--- a/docs/source/generated/hed.tools.DatasetSummary.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-﻿hed.tools.DatasetSummary
-========================
-
-.. currentmodule:: hed.tools
-
-.. autoclass:: DatasetSummary
-
-   
-   .. automethod:: __init__
-
-   
-   .. rubric:: Methods
-
-   .. autosummary::
-   
-      ~DatasetSummary.__init__
-   
-   
-
-   
-   
-   
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.FileDictionary.rst b/docs/source/generated/hed.tools.FileDictionary.rst
new file mode 100644
index 000000000..7d91f726f
--- /dev/null
+++ b/docs/source/generated/hed.tools.FileDictionary.rst
@@ -0,0 +1,39 @@
+﻿hed.tools.FileDictionary
+========================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.FileDictionary
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~FileDictionary.__init__
+      ~FileDictionary.iter_files
+      ~FileDictionary.key_diffs
+      ~FileDictionary.make_file_dict
+      ~FileDictionary.make_key
+      ~FileDictionary.print_files
+   
+   
+
+   
+   
+   .. rubric:: Attributes
+
+   .. autosummary::
+   
+      ~FileDictionary.file_list
+      ~FileDictionary.key_list
+   
+   
+
+.. autoclass:: hed.tools.FileDictionary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.HedLogger.rst b/docs/source/generated/hed.tools.HedLogger.rst
new file mode 100644
index 000000000..34d44aec9
--- /dev/null
+++ b/docs/source/generated/hed.tools.HedLogger.rst
@@ -0,0 +1,31 @@
+﻿hed.tools.HedLogger
+===================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.HedLogger
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~HedLogger.__init__
+      ~HedLogger.add
+      ~HedLogger.get_log
+      ~HedLogger.get_log_keys
+      ~HedLogger.print_log
+   
+   
+
+   
+   
+   
+
+.. autoclass:: hed.tools.HedLogger
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.KeyMap.rst b/docs/source/generated/hed.tools.KeyMap.rst
index 04f3f02d1..b0430bacc 100644
--- a/docs/source/generated/hed.tools.KeyMap.rst
+++ b/docs/source/generated/hed.tools.KeyMap.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: KeyMap
-
+.. autoclass:: hed.tools.KeyMap
    
    .. automethod:: __init__
 
@@ -22,4 +21,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.KeyMap
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.KeyTemplate.rst b/docs/source/generated/hed.tools.KeyTemplate.rst
index 0b72ebd10..315763f65 100644
--- a/docs/source/generated/hed.tools.KeyTemplate.rst
+++ b/docs/source/generated/hed.tools.KeyTemplate.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: KeyTemplate
-
+.. autoclass:: hed.tools.KeyTemplate
    
    .. automethod:: __init__
 
@@ -24,4 +23,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.KeyTemplate
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.SummaryEntry.rst b/docs/source/generated/hed.tools.SummaryEntry.rst
index cf11a9983..14792b40e 100644
--- a/docs/source/generated/hed.tools.SummaryEntry.rst
+++ b/docs/source/generated/hed.tools.SummaryEntry.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: SummaryEntry
-
+.. autoclass:: hed.tools.SummaryEntry
    
    .. automethod:: __init__
 
@@ -22,4 +21,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.SummaryEntry
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.TagSummary.rst b/docs/source/generated/hed.tools.TagSummary.rst
index 0ca4a570d..74a57ecd7 100644
--- a/docs/source/generated/hed.tools.TagSummary.rst
+++ b/docs/source/generated/hed.tools.TagSummary.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.tools
 
-.. autoclass:: TagSummary
-
+.. autoclass:: hed.tools.TagSummary
    
    .. automethod:: __init__
 
@@ -20,4 +19,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.tools.TagSummary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.annotation.rst b/docs/source/generated/hed.tools.annotation.rst
index d15bd0011..444194043 100644
--- a/docs/source/generated/hed.tools.annotation.rst
+++ b/docs/source/generated/hed.tools.annotation.rst
@@ -12,18 +12,10 @@ hed.tools.annotation.annotation\_util module
    :undoc-members:
    :show-inheritance:
 
-hed.tools.annotation.column\_summary module
--------------------------------------------
-
-.. automodule:: hed.tools.annotation.column_summary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.dataset\_summary module
+hed.tools.annotation.file\_dictionary module
 --------------------------------------------
 
-.. automodule:: hed.tools.annotation.dataset_summary
+.. automodule:: hed.tools.annotation.file_dictionary
    :members:
    :undoc-members:
    :show-inheritance:
diff --git a/docs/source/generated/hed.tools.bids.rst b/docs/source/generated/hed.tools.bids.rst
index ba3919738..80ce88ade 100644
--- a/docs/source/generated/hed.tools.bids.rst
+++ b/docs/source/generated/hed.tools.bids.rst
@@ -12,6 +12,14 @@ hed.tools.bids.bids\_dataset module
    :undoc-members:
    :show-inheritance:
 
+hed.tools.bids.bids\_dataset\_summary module
+--------------------------------------------
+
+.. automodule:: hed.tools.bids.bids_dataset_summary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 hed.tools.bids.bids\_event\_file module
 ---------------------------------------
 
@@ -36,6 +44,14 @@ hed.tools.bids.bids\_file module
    :undoc-members:
    :show-inheritance:
 
+hed.tools.bids.bids\_file\_dictionary module
+--------------------------------------------
+
+.. automodule:: hed.tools.bids.bids_file_dictionary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 hed.tools.bids.bids\_json\_file module
 --------------------------------------
 
@@ -52,6 +68,14 @@ hed.tools.bids.bids\_sidecar\_file module
    :undoc-members:
    :show-inheritance:
 
+hed.tools.bids.bids\_tsv\_dictionary module
+-------------------------------------------
+
+.. automodule:: hed.tools.bids.bids_tsv_dictionary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 hed.tools.bids.bids\_tsv\_file module
 -------------------------------------
 
@@ -60,6 +84,14 @@ hed.tools.bids.bids\_tsv\_file module
    :undoc-members:
    :show-inheritance:
 
+hed.tools.bids.bids\_tsv\_summary module
+----------------------------------------
+
+.. automodule:: hed.tools.bids.bids_tsv_summary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/source/generated/hed.util.io_util.rst b/docs/source/generated/hed.util.io_util.rst
index af69d228e..4df3b8fd8 100644
--- a/docs/source/generated/hed.util.io_util.rst
+++ b/docs/source/generated/hed.util.io_util.rst
@@ -17,9 +17,8 @@
       generate_filename
       get_dir_dictionary
       get_file_list
+      get_filtered_list
       get_path_components
-      make_file_dict
-      make_key
       parse_bids_filename
    
    
diff --git a/docs/source/generated/hed.validator.HedValidator.rst b/docs/source/generated/hed.validator.HedValidator.rst
index 993161262..f3353508f 100644
--- a/docs/source/generated/hed.validator.HedValidator.rst
+++ b/docs/source/generated/hed.validator.HedValidator.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.validator
 
-.. autoclass:: HedValidator
-
+.. autoclass:: hed.validator.HedValidator
    
    .. automethod:: __init__
 
@@ -19,4 +18,10 @@
 
    
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.validator.HedValidator
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.validator.TagValidator.rst b/docs/source/generated/hed.validator.TagValidator.rst
index 18568a6f0..d0c859b8b 100644
--- a/docs/source/generated/hed.validator.TagValidator.rst
+++ b/docs/source/generated/hed.validator.TagValidator.rst
@@ -3,8 +3,7 @@
 
 .. currentmodule:: hed.validator
 
-.. autoclass:: TagValidator
-
+.. autoclass:: hed.validator.TagValidator
    
    .. automethod:: __init__
 
@@ -17,7 +16,6 @@
       ~TagValidator.check_capitalization
       ~TagValidator.check_count_tag_group_parentheses
       ~TagValidator.check_delimiter_issues_in_hed_string
-      ~TagValidator.check_duplicate_tags_exist
       ~TagValidator.check_for_invalid_extension_chars
       ~TagValidator.check_for_placeholder
       ~TagValidator.check_for_required_tags
@@ -54,4 +52,10 @@
       ~TagValidator.TAG_ALLOWED_CHARS
       ~TagValidator.pattern_doubleslash
    
-   
\ No newline at end of file
+   
+
+.. autoclass:: hed.validator.TagValidator
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 2973f4768..604d3aaff 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,7 +1,7 @@
 Hierarchical Event Descriptor (HED) Python Tools
 ================================================
 
-.. sidebar:: **Downloads**
+.. sidebar:: **Links**
 
    * `PDF docs <https://hed-python.readthedocs.io/_/downloads/en/latest/pdf/>`_
 
@@ -13,7 +13,7 @@ Note:  this is a work in progress. More information is coming.
    :maxdepth: 2
    :caption: Contents:
 
-   Overview.md
+   introduction.md
    user_guide.rst
 
 .. toctree::
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
index 02fa8d11f..f49a7dac5 100644
--- a/docs/source/introduction.rst
+++ b/docs/source/introduction.rst
@@ -11,23 +11,23 @@ Introduction to HED
 Why HED?
 ========
 
-.. topic:: **Why use HED?**
+.. topic:: Why use HED?
 
-HED (Hierarchical Event Descriptors) is an infrastructure and
-a controlled vocabulary that allows researchers to annotate
-their experimental data, especially events,
-so that tools
+   HED (Hierarchical Event Descriptors) is an infrastructure and
+   a controlled vocabulary that allows researchers to annotate
+   their experimental data, especially events,
+   so that tools can automatically use this information in analysis.
 
-  For more information on Hierarchical Event Descriptors (HED),
-visit https://hed-specification.readthedocs.io/en/latest/index.html or
-the
+For more information on using Hierarchical Event Descriptors (HED)
+visit `HED examples <https://hed-examples.readthedocs.io/en/latest/index.html>`_:
 
 
 Installing hedtools
 ===================
 Hedtools will be available soon on pypi, but in the meantime,
-you can install directly from the  `GitHub repository
-      <https://github.com/hed-standard/hed-python>`_::
+you can install directly from the
+`GitHub repository <https://github.com/hed-standard/hed-python>`_
+using the following command:
 
 >>> pip install git+https://github.com/hed-standard/hed-python.git
 
diff --git a/docs/source/models/index.rst b/docs/source/models/index.rst
index bc5216b85..ce9aabb1e 100644
--- a/docs/source/models/index.rst
+++ b/docs/source/models/index.rst
@@ -14,9 +14,11 @@ HED data models
 
 .. _loading_datasets:
 
-Loading BIDS datasets
+HED datasets
 ===========================================
 
+**This section is a placeholder and will be updated soon.**
+
 The :obj:`hed.models.BaseInput` class requires the path to a valid BIDS dataset::
 
     >>> from os.path import join
@@ -35,5 +37,4 @@ the file tree. By default, defined BIDS entities include things like "subject",
 Querying datasets
 ===========================================
 
-Pybids layouts can be queried according to a number of parameters, using
-grabbit's ``get`` functionality.
+**Coming***
\ No newline at end of file
diff --git a/docs/source/schema/index.rst b/docs/source/schema/index.rst
index c19e44200..a81eb2c30 100644
--- a/docs/source/schema/index.rst
+++ b/docs/source/schema/index.rst
@@ -14,26 +14,4 @@ HED vocabulary schema
 
 .. _loading_datasets:
 
-Loading BIDS datasets
-===========================================
-
-The :obj:`hed.schema.HedSchema` class requires the path to a valid BIDS dataset::
-
-    >>> from os.path import join
-    >>> from bids import BIDSLayout
-    >>> from bids.tests import get_test_data_path
-    >>> layout = BIDSLayout(join(get_test_data_path(), 'synthetic'))
-
-The ``BaseInput`` instance is a lightweight container for all of the files in the
-BIDS project directory. It automatically detects any BIDS entities found in the
-file paths, and allows us to perform simple but relatively powerful queries over
-the file tree. By default, defined BIDS entities include things like "subject",
-"session", "run", and "type".
-
-.. _querying_datasets:
-
-Querying datasets
-===========================================
-
-Pybids layouts can be queried according to a number of parameters, using
-grabbit's ``get`` functionality.
+**This section is a placeholder and will be updated soon.**
diff --git a/docs/source/tools/index.rst b/docs/source/tools/index.rst
index ce42d0a13..36511d569 100644
--- a/docs/source/tools/index.rst
+++ b/docs/source/tools/index.rst
@@ -14,26 +14,7 @@ HED analysis tools
 
 .. _loading_datasets:
 
-Loading BIDS datasets
+Analysis with HED
 ===========================================
 
-The :obj:`hed.models.BaseInput` class requires the path to a valid BIDS dataset::
-
-    >>> from os.path import join
-    >>> from bids import BIDSLayout
-    >>> from bids.tests import get_test_data_path
-    >>> layout = BIDSLayout(join(get_test_data_path(), 'synthetic'))
-
-The ``BaseInput`` instance is a lightweight container for all of the files in the
-BIDS project directory. It automatically detects any BIDS entities found in the
-file paths, and allows us to perform simple but relatively powerful queries over
-the file tree. By default, defined BIDS entities include things like "subject",
-"session", "run", and "type".
-
-.. _querying_datasets:
-
-Querying datasets
-===========================================
-
-Pybids layouts can be queried according to a number of parameters, using
-grabbit's ``get`` functionality.
+**This section is a placeholder and will be updated soon.**
diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst
index 617396d54..3f1f1a71e 100644
--- a/docs/source/user_guide.rst
+++ b/docs/source/user_guide.rst
@@ -7,7 +7,6 @@ HED tools user guide
 .. toctree::
    :numbered:
 
-   introduction.rst
    models/index.rst
    schema/index.rst
    tools/index.rst
diff --git a/docs/source/validator/index.rst b/docs/source/validator/index.rst
index d3e321f16..598b89a05 100644
--- a/docs/source/validator/index.rst
+++ b/docs/source/validator/index.rst
@@ -1,7 +1,7 @@
-.. _reports:
+
 
 =====================================================
-HED validators
+HED validation
 =====================================================
 
 .. contents:: **Contents**
@@ -12,28 +12,9 @@ HED validators
 
 .. currentmodule:: hed.validator
 
-.. _loading_datasets:
-
-Loading BIDS datasets
-===========================================
-
-The :obj:`hed.models.BaseInput` class requires the path to a valid BIDS dataset::
-
-    >>> from os.path import join
-    >>> from bids import BIDSLayout
-    >>> from bids.tests import get_test_data_path
-    >>> layout = BIDSLayout(join(get_test_data_path(), 'synthetic'))
-
-The ``BaseInput`` instance is a lightweight container for all of the files in the
-BIDS project directory. It automatically detects any BIDS entities found in the
-file paths, and allows us to perform simple but relatively powerful queries over
-the file tree. By default, defined BIDS entities include things like "subject",
-"session", "run", and "type".
 
-.. _querying_datasets:
 
-Querying datasets
+HED validation tools
 ===========================================
 
-Pybids layouts can be queried according to a number of parameters, using
-grabbit's ``get`` functionality.
+**This section is a placeholder and will be updated soon.**
diff --git a/hed/__init__.py b/hed/__init__.py
index daed7297a..df20a62f8 100644
--- a/hed/__init__.py
+++ b/hed/__init__.py
@@ -1,31 +1,26 @@
-# from hed.models.hed_string import HedString
-# from hed.models.hed_tag import HedTag
-# from hed.errors.error_reporter import get_printable_issue_string
-# from hed.errors.exceptions import HedFileError, HedExceptions
-#
-# from hed.models.base_input import BaseInput
-# from hed.models.hed_input import HedInput
-# from hed.models.events_input import EventsInput
-# from hed.models.sidecar import Sidecar
-#
-# from hed.schema.hed_schema import HedSchema
-# from hed.schema.hed_schema_group import HedSchemaGroup
-# from hed.schema.hed_schema_io import load_schema, load_schema_version
+from hed.models.hed_string import HedString
+from hed.models.hed_tag import HedTag
+from hed.errors.error_reporter import get_printable_issue_string
+from hed.errors.exceptions import HedFileError, HedExceptions
 
-# from hed.tools.data_utils import get_new_dataframe, make_info_dataframe, remove_quotes, reorder_columns, \
-#     separate_columns, make_key, get_key_hash, get_row_hash
-# from hed.tools.event_utils import add_columns, check_match, delete_columns, delete_rows_by_column, \
-#    replace_values
+from hed.models.base_input import BaseInput
+from hed.models.hed_input import HedInput
+from hed.models.events_input import EventsInput
+from hed.models.sidecar import Sidecar
 
-#from hed.validator.hed_validator import HedValidator
+from hed.schema.hed_schema import HedSchema
+from hed.schema.hed_schema_group import HedSchemaGroup
+from hed.schema.hed_schema_io import load_schema, load_schema_version
 
-# from hed import errors, schema, models, tools, util
+from hed.validator.hed_validator import HedValidator
 
-__all__ = [
-    "errors",
-    "models",
-    "schema",
-    "tools",
-    "util",
-    "validator"
-]
\ No newline at end of file
+from hed import errors, models, schema, tools, util, validator
+
+# __all__ = [
+#     "errors",
+#     "models",
+#     "schema",
+#     "tools",
+#     "util",
+#     "validator"
+# ]
diff --git a/hed/errors/error_messages.py b/hed/errors/error_messages.py
index d58d8322f..b305fa590 100644
--- a/hed/errors/error_messages.py
+++ b/hed/errors/error_messages.py
@@ -72,6 +72,11 @@ def val_error_duplicate_tag(tag):
     return f'Repeated tag - "{tag}"', {}
 
 
+@hed_error(ValidationErrors.HED_TAG_REPEATED_GROUP)
+def val_error_duplicate_group(group):
+    return f'Repeated group - "{group}"', {}
+
+
 @hed_error(ValidationErrors.HED_PARENTHESES_MISMATCH)
 def val_error_parentheses(opening_parentheses_count, closing_parentheses_count):
     return f'Number of opening and closing parentheses are unequal. '\
@@ -121,7 +126,7 @@ def val_error_extra_column(extra_column_name):
         "or identified in sidecars.", {}
 
 
-@hed_tag_error(ValidationErrors.HED_UNKNOWN_PREFIX)
+@hed_tag_error(ValidationErrors.HED_LIBRARY_UNMATCHED)
 def val_error_unknown_prefix(tag, unknown_prefix, known_prefixes):
     return f"Tag '{tag} has unknown prefix '{unknown_prefix}'.  Valid prefixes: {known_prefixes}", {}
 
@@ -140,7 +145,7 @@ def val_error_sidecar_key_missing(invalid_key, category_keys):
 def val_error_def_unmatched(tag):
     return f"A data-recording’s Def tag cannot be matched to definition.  Tag: '{tag}'", {}
 
-@hed_tag_error(ValidationErrors.HED_DEF_EXPAND_BAD)
+@hed_tag_error(ValidationErrors.HED_DEF_EXPAND_INVALID)
 def val_error_bad_def_expand(tag, actual_def, found_def):
     return f"A data-recording’s Def-expand tag does not match the given definition.  Tag: '{tag}'.  Actual Def: {actual_def}.  Found Def: {found_def}", {}
 
@@ -250,11 +255,10 @@ def sidecar_error_unknown_column(column_name):
            "Most likely the column definition in question needs a # sign to replace a number somewhere.", {}
 
 
-@hed_error(DefinitionErrors.WRONG_NUMBER_DEFINITION_TAGS, actual_code=ValidationErrors.HED_DEFINITION_INVALID)
-def def_error_wrong_def_tags(def_name, tag_list):
-    tag_list_strings = [str(tag) for tag in tag_list]
-    return f"Too many def tags found in definition for {def_name}.  Expected 1, also found: {tag_list_strings}", {}
-
+@hed_tag_error(DefinitionErrors.DEF_TAG_IN_DEFINITION, actual_code=ValidationErrors.HED_DEFINITION_INVALID)
+def def_error_def_tag_in_definition(tag, def_name):
+    return f"Invalid tag {tag} found in definition for {def_name}.  Def and Def-expand tags cannot be in definitions."\
+        , {}
 
 @hed_error(DefinitionErrors.WRONG_NUMBER_GROUP_TAGS, actual_code=ValidationErrors.HED_DEFINITION_INVALID)
 def def_error_wrong_group_tags(def_name, tag_list):
diff --git a/hed/errors/error_types.py b/hed/errors/error_types.py
index 772c48f63..5dfc83e77 100644
--- a/hed/errors/error_types.py
+++ b/hed/errors/error_types.py
@@ -24,11 +24,9 @@ class ValidationErrors:
     HED_CHARACTER_INVALID = 'HED_CHARACTER_INVALID'
     HED_COMMA_MISSING = 'HED_COMMA_MISSING'
     HED_DEF_UNMATCHED = "HED_DEF_UNMATCHED"
-    HED_DEF_EXPAND_BAD = "HED_DEF_EXPAND_BAD"
+    HED_DEF_EXPAND_INVALID = "HED_DEF_EXPAND_INVALID"
     HED_DEF_VALUE_INVALID = "HED_DEF_VALUE_INVALID"
     HED_DEFINITION_INVALID = "HED_DEFINITION_INVALID"
-    # HED_GENERIC_ERROR
-    # HED_GENERIC_WARNING
     # HED_LIBRARY_UNMATCHED
     HED_NODE_NAME_EMPTY = 'HED_NODE_NAME_EMPTY'
     HED_ONSET_OFFSET_ERROR = 'HED_ONSET_OFFSET_ERROR'
@@ -43,6 +41,7 @@ class ValidationErrors:
     HED_TAG_INVALID = "HED_TAG_INVALID"
     HED_TAG_NOT_UNIQUE = 'HED_TAG_NOT_UNIQUE'
     HED_TAG_REPEATED = 'HED_TAG_REPEATED'
+    HED_TAG_REPEATED_GROUP = 'HED_TAG_REPEATED_GROUP'
     HED_TAG_REQUIRES_CHILD = 'HED_TAG_REQUIRES_CHILD'
     HED_TILDES_UNSUPPORTED = 'HED_TILDES_UNSUPPORTED'
     HED_UNITS_INVALID = 'HED_UNITS_INVALID'
@@ -52,12 +51,11 @@ class ValidationErrors:
 
     HED_MISSING_COLUMN = "HED_MISSING_COLUMN"
     HED_UNKNOWN_COLUMN = "HED_UNKNOWN_COLUMN"
-    HED_UNKNOWN_PREFIX = "HED_UNKNOWN_PREFIX"
+    HED_LIBRARY_UNMATCHED = "HED_LIBRARY_UNMATCHED"
 
     # Below here shows what the given error maps to
     # HED_TAG_EMPTY
     HED_GROUP_EMPTY = 'emptyHedGroup'
-    # HED_CHARACTER_INVALID
     INVALID_TAG_CHARACTER = 'invalidTagCharacter'
 
     # These are all HED_TAG_INVALID
@@ -99,7 +97,7 @@ class SchemaWarnings:
 
 # These are all HED_DEFINITION_INVALID errors
 class DefinitionErrors:
-    WRONG_NUMBER_DEFINITION_TAGS = 'wrongNumberDefTags'
+    DEF_TAG_IN_DEFINITION = 'DEF_TAG_IN_DEFINITION'
     WRONG_NUMBER_GROUP_TAGS = 'wrongNumberGroupTags'
     WRONG_NUMBER_PLACEHOLDER_TAGS = 'wrongNumberPlaceholderTags'
     DUPLICATE_DEFINITION = 'duplicateDefinition'
diff --git a/hed/errors/exceptions.py b/hed/errors/exceptions.py
index 159f6590a..8781b9c67 100644
--- a/hed/errors/exceptions.py
+++ b/hed/errors/exceptions.py
@@ -9,17 +9,17 @@ class HedExceptions:
     INVALID_EXTENSION = 'invalidExtension'
 
     # These are actual schema issues, not that the file cannot be found or parsed
-    SCHEMA_HEADER_MISSING = 'schemaHeaderMissing'
-    SCHEMA_HEADER_INVALID = 'schemaHeaderInvalid'
+    SCHEMA_HEADER_MISSING = 'HED_SCHEMA_HEADER_INVALID'
+    HED_SCHEMA_HEADER_INVALID = 'HED_SCHEMA_HEADER_INVALID'
     BAD_HED_LIBRARY_NAME = 'badHedLibraryName'
-    BAD_HED_SEMANTIC_VERSION = 'BadHedSemanticVersion'
-    SCHEMA_START_MISSING = 'schemaStartMissing'
-    SCHEMA_END_INVALID = 'schemaEndMissing'
-    HED_END_INVALID = 'hedEndMissing'
+    HED_SCHEMA_VERSION_INVALID = 'HED_SCHEMA_VERSION_INVALID'
+    SCHEMA_START_MISSING = 'HED_WIKI_SEPARATOR_INVALID'
+    SCHEMA_END_INVALID = 'HED_WIKI_SEPARATOR_INVALID'
+    HED_END_INVALID = 'HED_WIKI_SEPARATOR_INVALID'
     INVALID_SECTION_SEPARATOR = 'invalidSectionSeparator'
 
     # This issue will contain a list of lines with issues.
-    HED_SCHEMA_WIKI_WARNINGS = 'HED_SCHEMA_WIKI_WARNINGS'
+    HED_WIKI_DELIMITERS_INVALID = 'HED_WIKI_DELIMITERS_INVALID'
     HED_SCHEMA_NODE_NAME_INVALID = 'HED_SCHEMA_NODE_NAME_INVALID'
 
     SCHEMA_DUPLICATE_PREFIX = 'schemaDuplicatePrefix'
@@ -31,7 +31,7 @@ def __init__(self, error_type, message, filename, issues=None):
         self.error_type = error_type
         self.message = message
         self.filename = filename
-        # only filled in when this lists multiple errors, such as the HED_SCHEMA_WIKI_WARNINGS
+        # only filled in when this lists multiple errors, such as the HED_WIKI_DELIMITERS_INVALID
         self.issues = issues
 
     def format_error_message(self, include_tabbing=True, return_string_only=False,
@@ -66,15 +66,15 @@ def format_error_message(self, include_tabbing=True, return_string_only=False,
                                             f"{message}.  '{filename}'",
             HedExceptions.CANNOT_PARSE_JSON: f"{error_prefix}Cannot parse json: {message}. '{filename}'",
             HedExceptions.SCHEMA_HEADER_MISSING: f"{error_prefix}{self.message}.  '{filename}'",
-            HedExceptions.SCHEMA_HEADER_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
+            HedExceptions.HED_SCHEMA_HEADER_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.BAD_HED_LIBRARY_NAME: f"{error_prefix}{self.message}.  '{filename}'",
-            HedExceptions.BAD_HED_SEMANTIC_VERSION: f"{error_prefix}{self.message}.  '{filename}'",
+            HedExceptions.HED_SCHEMA_VERSION_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.SCHEMA_START_MISSING: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.SCHEMA_END_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.HED_END_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.INVALID_SECTION_SEPARATOR: f"{error_prefix}{self.message}.  '{filename}'",
             HedExceptions.HED_SCHEMA_NODE_NAME_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
-            HedExceptions.HED_SCHEMA_WIKI_WARNINGS: f"{error_prefix}{self.message}.  '{filename}'",
+            HedExceptions.HED_WIKI_DELIMITERS_INVALID: f"{error_prefix}{self.message}.  '{filename}'",
         }
         default_error_message = f'{error_prefix}Internal Error'
         error_message = error_types.get(error_type, default_error_message)
diff --git a/hed/models/base_input.py b/hed/models/base_input.py
index b9f8fbafa..0fb42f615 100644
--- a/hed/models/base_input.py
+++ b/hed/models/base_input.py
@@ -267,6 +267,8 @@ def iter_raw(self, hed_ops=None, error_handler=None, **kwargs):
         ----------
         hed_ops : [func or HedOps] or func or HedOps
             A list of HedOps of funcs to apply to the hed strings before returning
+        error_handler: ErrorHandler
+            The error handler to use for context, uses a default one if none.
         kwargs:
             See models.hed_ops.translate_ops or the specific hed_ops for additional options
 
@@ -322,16 +324,16 @@ def iter_dataframe(self, mapper=None, return_row_dict=False, hed_ops=None, run_s
             mapper = self._mapper
 
         tag_funcs, string_funcs = self._translate_ops(hed_ops, run_string_ops_on_columns=run_string_ops_on_columns,
-                                                  expand_defs=expand_defs, remove_definitions=remove_definitions,
-                                                  error_handler=error_handler, **kwargs)
+                                                      expand_defs=expand_defs, remove_definitions=remove_definitions,
+                                                      error_handler=error_handler, **kwargs)
 
         start_at_one = 1
         if self._has_column_names:
             start_at_one += 1
-        for row_number, text_file_row in self._dataframe.iterrows():
-            # Skip any blank lines.
-            if all(text_file_row.isnull()):
-                continue
+
+        # Iter tuples is ~ 25% faster compared to iterrows in our use case
+        for row_number, text_file_row in enumerate(self._dataframe.itertuples(index=False)):
+            # todo: Double check if we need a check here for blank lines to avoid crashing.
 
             row_dict = mapper.expand_row_tags(text_file_row)
             column_to_hed_tags = row_dict[model_constants.COLUMN_TO_HED_TAGS]
@@ -364,7 +366,7 @@ def set_cell(self, row_number, column_number, new_string_obj, include_column_pre
         column_number : int
             The column number of the spreadsheet to set
         new_string_obj : HedString
-            Text to enter in the given cell
+            Text to put in the given cell
         include_column_prefix_if_exist : bool
             If true and the column matches one from mapper _column_prefix_dictionary, remove the name_prefix
         tag_form: str
@@ -504,7 +506,7 @@ def _run_column_ops(self, column_to_hed_tags_dictionary, column_ops, expansion_c
                     new_column_issues += expansion_column_issues[column_number]
 
                 if column_hed_string is not None:
-                    new_column_issues += column_hed_string.apply_ops(column_ops)
+                    new_column_issues += column_hed_string.apply_funcs(column_ops)
                 error_handler.add_context_to_issues(new_column_issues)
                 if column_hed_string is not None:
                     error_handler.pop_error_context()
@@ -515,7 +517,7 @@ def _run_column_ops(self, column_to_hed_tags_dictionary, column_ops, expansion_c
 
     def _run_row_ops(self, row_hed_string, row_ops, error_handler):
         error_handler.push_error_context(ErrorContext.HED_STRING, row_hed_string, increment_depth_after=False)
-        row_issues = row_hed_string.apply_ops(row_ops)
+        row_issues = row_hed_string.apply_funcs(row_ops)
         error_handler.add_context_to_issues(row_issues)
         error_handler.pop_error_context()
         return row_issues
@@ -601,7 +603,7 @@ def _translate_ops(self, hed_ops, run_string_ops_on_columns, expand_defs, remove
             if not run_string_ops_on_columns:
                 self._add_def_onset_mapper(hed_ops)
                 tag_funcs, string_funcs = translate_ops(hed_ops, split_tag_and_string_ops=True, expand_defs=expand_defs,
-                                                    **kwargs)
+                                                        **kwargs)
             else:
                 tag_funcs = translate_ops(hed_ops, expand_defs=expand_defs, **kwargs)
 
@@ -612,4 +614,4 @@ def _add_def_onset_mapper(self, hed_ops):
             if self._def_mapper:
                 hed_ops.append(self._def_mapper)
                 hed_ops.append(OnsetMapper(self._def_mapper))
-        return hed_ops
\ No newline at end of file
+        return hed_ops
diff --git a/hed/models/column_mapper.py b/hed/models/column_mapper.py
index 8f8e5e23f..66d34496d 100644
--- a/hed/models/column_mapper.py
+++ b/hed/models/column_mapper.py
@@ -175,7 +175,7 @@ def _expand_column(self, column_number, input_text):
         attribute_name_or_error_message: False or str
             Depends on the value of first return value.
             If None, this is an error message.
-            If string, this is an attribute name, that should be stored separately..
+            If string, this is an attribute name, that should be stored separately.
         """
         # Default 1-1 mapping if we don't have specific behavior.
         if not self._final_column_map:
@@ -199,6 +199,7 @@ def expand_row_tags(self, row_text):
         ----------
         row_text : [str]
             The text for the given row, one entry per column number.
+
         Returns
         -------
         expanded_dict: {str: }
@@ -212,6 +213,8 @@ def expand_row_tags(self, row_text):
         column_to_hed_tags_dictionary = {}
         column_issues_dict = {}
         for column_number, cell_text in enumerate(row_text):
+            # if column_number == 0:
+            #     continue
             translated_column, attribute_name_or_error = self._expand_column(column_number, str(cell_text))
             if translated_column is None:
                 if attribute_name_or_error:
@@ -295,7 +298,7 @@ def _set_column_prefix(self, column_number, new_required_prefix):
 
     def _finalize_mapping(self):
         """
-        Internal function that gathers up all the various sources of column rules and puts them
+        Internal function that gathers all the various sources of column rules and puts them
         in a list mapping from column number to definition.
 
         This needs to be called after all definitions and columns are added.
@@ -343,7 +346,6 @@ def _finalize_mapping(self):
         for column_number, prefix in self._column_prefix_dictionary.items():
             self._set_column_prefix(column_number, prefix)
 
-        # Finally check if any numbered columns don't have an entry in final columns and issue a warning.
         return self._finalize_mapping_issues
 
     def get_def_dicts(self):
diff --git a/hed/models/column_metadata.py b/hed/models/column_metadata.py
index f8ebfd504..ce725eb99 100644
--- a/hed/models/column_metadata.py
+++ b/hed/models/column_metadata.py
@@ -5,6 +5,7 @@
 from hed.errors import error_reporter
 from hed.errors.error_reporter import ErrorHandler
 from hed.models.hed_ops import translate_ops
+import copy
 
 
 class ColumnType(Enum):
@@ -126,7 +127,7 @@ def hed_string_iter(self, hed_ops=None, error_handler=None, **kwargs):
                 error_handler.push_error_context(ErrorContext.HED_STRING, hed_string_obj,
                                                  increment_depth_after=False)
                 if tag_funcs:
-                    new_col_issues += hed_string_obj.apply_ops(tag_funcs)
+                    new_col_issues += hed_string_obj.apply_funcs(tag_funcs)
 
                 error_handler.add_context_to_issues(new_col_issues)
                 yield hed_string_obj, key_name, new_col_issues
@@ -142,7 +143,7 @@ def _hed_iter(self, also_return_bad_types=False):
         also_return_bad_types: bool
             If true, this will also yield types other than HedString
 
-        Returns
+        Yields
         -------
         hed_string: HedString
         position: str
@@ -459,9 +460,12 @@ def _validate_pound_sign_count(self, hed_string):
         else:
             return []
 
-        # This needs to only account for the ones without definitions.
-        if hed_string.without_defs().count("#") != expected_pound_sign_count:
-            return ErrorHandler.format_error(error_type, pound_sign_count=str(hed_string).count("#"))
+        # Make a copy without definitions to check placeholder count.
+        hed_string_copy = copy.copy(hed_string)
+        hed_string_copy.remove_definitions()
+
+        if hed_string_copy.lower().count("#") != expected_pound_sign_count:
+            return ErrorHandler.format_error(error_type, pound_sign_count=str(hed_string_copy).count("#"))
 
         return []
 
diff --git a/hed/models/def_dict.py b/hed/models/def_dict.py
index 6abec97c4..1a24be46b 100644
--- a/hed/models/def_dict.py
+++ b/hed/models/def_dict.py
@@ -1,6 +1,6 @@
 from hed.models.hed_string import HedString
 from hed.models.hed_group import HedGroup
-from hed.errors.error_types import DefinitionErrors, ValidationErrors
+from hed.errors.error_types import DefinitionErrors
 from hed.errors.error_reporter import ErrorHandler
 import copy
 from functools import partial
@@ -32,6 +32,9 @@ def __init__(self, name, contents, takes_value, source_context):
             contents.cascade_mutable(False)
         self.takes_value = takes_value
         self.source_context = source_context
+        self.tag_dict = {}
+        if contents:
+            add_group_to_dict(contents, self.tag_dict)
 
     def get_definition(self, replace_tag, placeholder_value=None):
         """
@@ -57,6 +60,7 @@ def get_definition(self, replace_tag, placeholder_value=None):
         if self.contents:
             output_group = self.contents
             if placeholder_value:
+                # todo: This part could be optimized more
                 placeholder_tag = output_group.find_placeholder_tag()
                 if not placeholder_tag:
                     raise ValueError("Internal error related to placeholders in definition mapping")
@@ -105,6 +109,16 @@ def get_definition_issues(self):
         """
         return self._extract_def_issues
 
+    @property
+    def defs(self):
+        """
+            Provides direct access to internal dictionary.  Alter at your own risk.
+        Returns
+        -------
+        def_dict: {str: DefEntry}
+        """
+        return self._defs
+
     def __iter__(self):
         return iter(self._defs.items())
 
@@ -129,44 +143,35 @@ def check_for_definitions(self, hed_string_obj, error_handler=None):
         ----------
         """
         new_def_issues = []
-        for tag_group, is_top_level in hed_string_obj.get_all_groups(also_return_depth=True):
-            def_tags, group_tags, other_tags = self._extract_tags_from_group(tag_group)
-
-            # Now validate to see if we have a definition.  We want 1 definition, and the other parts are optional.
-            if not def_tags:
-                # If we don't have at least one valid definition tag, just move on.
-                continue
+        for definition_tag, group in hed_string_obj.find_top_level_tags(anchors={DefTagNames.DEFINITION_KEY}):
+            def_tag_name = definition_tag.extension_or_value_portion
 
-            if not is_top_level:
-                new_def_issues +=\
-                    ErrorHandler.format_error_with_context(error_handler,
-                                                           ValidationErrors.HED_TAG_GROUP_TAG, tag=def_tags[0])
-                continue
-
-            if len(def_tags) > 1:
-                new_def_issues += \
-                    ErrorHandler.format_error_with_context(error_handler,
-                                                           DefinitionErrors.WRONG_NUMBER_DEFINITION_TAGS,
-                                                           def_name=def_tags[0].extension_or_value_portion,
-                                                           tag_list=[tag for tag in def_tags[1:]])
-                continue
-            def_tag = def_tags[0]
-            def_tag_name = def_tag.extension_or_value_portion
-            if len(group_tags) > 1:
+            # initial validation
+            groups = group.groups()
+            if len(groups) > 1:
                 new_def_issues += \
                     ErrorHandler.format_error_with_context(error_handler,
                                                            DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
-                                                           def_name=def_tag_name, tag_list=group_tags + other_tags)
+                                                           def_name=def_tag_name, tag_list=groups)
                 continue
-            if len(other_tags) > 0:
+            if len(group.tags()) != 1:
                 new_def_issues += ErrorHandler.format_error_with_context(error_handler,
                                                                          DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
                                                                          def_name=def_tag_name,
-                                                                         tag_list=other_tags + group_tags)
+                                                                         tag_list=[tag for tag in group.tags() if tag is not definition_tag])
                 continue
 
-            group_tag = group_tags[0] if group_tags else None
+            group_tag = groups[0] if groups else None
 
+            # final validation
+            # Verify no other def or def expand tags found in group
+            if group_tag:
+                for def_tag in group.find_def_tags(recursive=True, include_groups=0):
+                    new_def_issues += ErrorHandler.format_error_with_context(error_handler,
+                                                                             DefinitionErrors.DEF_TAG_IN_DEFINITION,
+                                                                             tag=def_tag,
+                                                                             def_name=def_tag_name)
+                    continue
             def_takes_value = def_tag_name.lower().endswith("/#")
             if def_takes_value:
                 def_tag_name = def_tag_name[:-len("/#")]
@@ -178,7 +183,7 @@ def check_for_definitions(self, hed_string_obj, error_handler=None):
                                                                          def_name=def_tag_name)
                 continue
 
-            # Verify placeholders here.
+            # # Verify placeholders here.
             placeholder_tags = []
             if group_tag:
                 for tag in group_tag.get_all_tags():
@@ -209,16 +214,26 @@ def check_for_definitions(self, hed_string_obj, error_handler=None):
         self._extract_def_issues += new_def_issues
         return new_def_issues
 
-    @staticmethod
-    def _extract_tags_from_group(tag_group):
-        def_tags = []
-        group_tags = []
-        other_tags = []
-        for tag_or_group in tag_group.get_direct_children():
-            if isinstance(tag_or_group, HedGroup):
-                group_tags.append(tag_or_group)
-            elif tag_or_group.short_base_tag.lower() == DefTagNames.DEFINITION_KEY:
-                def_tags.append(tag_or_group)
-            else:
-                other_tags.append(tag_or_group)
-        return def_tags, group_tags, other_tags
+
+# This may be moved later
+def add_group_to_dict(group, tag_dict=None):
+    """
+        Note: Expects tags to have forms calculated already.
+    Parameters:
+        group: HedGroup
+            contents to add to the tag dict
+        tag_dict: {}
+            Output dictionary
+    Returns: dict
+        Dictionary of tags with a list of values
+    """
+    if tag_dict is None:
+        tag_dict = {}
+    for tag in group.get_all_tags():
+        short_base_tag = tag.short_base_tag
+        value = tag.extension_or_value_portion
+        value_dict = tag_dict.get(short_base_tag, {})
+        if value:
+            value_dict[value] = ''
+        tag_dict[short_base_tag] = value_dict
+    return tag_dict
diff --git a/hed/models/def_mapper.py b/hed/models/def_mapper.py
index 031dbf44b..4e34cc8dc 100644
--- a/hed/models/def_mapper.py
+++ b/hed/models/def_mapper.py
@@ -10,7 +10,7 @@
 class DefinitionMapper(HedOps):
     """Class for handling definitions in hed strings.
 
-       The class is responsible for gather and removing defintions.
+       The class is responsible for gather and removing definitions.
        The class also replaces labels in hed strings with the gathered definitions."""
 
     def __init__(self, def_dicts=None):
@@ -128,36 +128,19 @@ def expand_def_tags(self, hed_string_obj, expand_defs=True, shrink_defs=False):
 
         def_issues = []
         # We need to check for labels to expand in ALL groups
-        for tag_group in hed_string_obj.get_all_groups():
-            def_tags, def_expand_tags = self._find_def_tags(tag_group)
-
-            for def_tag in def_tags:
-                def_contents = self._get_definition_contents(def_tag, None, def_issues)
+        for def_tag, def_expand_group, def_group in hed_string_obj.find_def_tags(recursive=True):
+            def_contents = self._get_definition_contents(def_tag, def_expand_group, def_issues)
+            if def_expand_group is def_tag:
                 if def_contents is not None and expand_defs:
                     def_tag.short_base_tag = DefTagNames.DEF_EXPAND_ORG_KEY
-                    tag_group.replace_tag(def_tag, def_contents)
-            for def_expand_tag, def_expand_group in def_expand_tags:
-                def_contents = self._get_definition_contents(def_expand_tag, def_expand_group, def_issues)
+                    def_group.replace_tag(def_tag, def_contents)
+            else:
                 if def_contents is not None and shrink_defs:
-                    def_expand_tag.short_base_tag = DefTagNames.DEF_ORG_KEY
-                    tag_group.replace_tag(def_expand_group, def_expand_tag)
+                    def_tag.short_base_tag = DefTagNames.DEF_ORG_KEY
+                    def_group.replace_tag(def_expand_group, def_tag)
 
         return def_issues
 
-    def _find_def_tags(self, group):
-        def_tags = []
-        def_expand_tags = []
-        for child in group.get_direct_children():
-            if isinstance(child, HedTag):
-                if child.short_base_tag.lower() == DefTagNames.DEF_KEY:
-                    def_tags.append(child)
-            else:
-                for tag in child.tags():
-                    if tag.short_base_tag.lower() == DefTagNames.DEF_EXPAND_KEY:
-                        def_expand_tags.append((tag, child))
-
-        return def_tags, def_expand_tags
-
     def expand_and_remove_definitions(self, hed_string_obj, check_for_definitions=False, expand_defs=True,
                                       shrink_defs=False, remove_definitions=True):
         """
@@ -192,16 +175,16 @@ def expand_and_remove_definitions(self, hed_string_obj, check_for_definitions=Fa
 
         return def_issues
 
-    def _get_definition_contents(self, original_tag, def_expand_group, def_issues):
+    def _get_definition_contents(self, def_tag, def_expand_group, def_issues):
         """
             Checks for issues with expanding a tag from def to def-expand, and returns the expanded tag.
 
         Parameters
         ----------
-        original_tag : HedTag
-            Source hed tag that may be a Def-expand tag.
-        def_expand_group: HedGroup or None
-            Source group for this def-expand tag.  None if a def tag.
+        def_tag : HedTag
+            Source hed tag that may be a Def or Def-expand tag.
+        def_expand_group: HedGroup or HedTag
+            Source group for this def-expand tag.  Same as def_tag if this is not a def-expand tag.
         def_issues : [{}]
             List of issues to append any new issues to
 
@@ -211,9 +194,9 @@ def _get_definition_contents(self, original_tag, def_expand_group, def_issues):
             The contents to replace the previous def-tag with.
         """
         # todo: This check could be removed for optimizing
-        if original_tag.short_base_tag.lower() == DefTagNames.DEF_EXPAND_KEY or \
-                original_tag.short_base_tag.lower() == DefTagNames.DEF_KEY:
-            is_label_tag = original_tag.extension_or_value_portion
+        if def_tag.short_base_tag.lower() == DefTagNames.DEF_EXPAND_KEY or \
+                def_tag.short_base_tag.lower() == DefTagNames.DEF_KEY:
+            is_label_tag = def_tag.extension_or_value_portion
             placeholder = None
             found_slash = is_label_tag.find("/")
             if found_slash != -1:
@@ -223,20 +206,20 @@ def _get_definition_contents(self, original_tag, def_expand_group, def_issues):
             label_tag_lower = is_label_tag.lower()
             def_entry = self._gathered_defs.get(label_tag_lower)
             if def_entry is None:
-                def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_UNMATCHED, tag=original_tag)
+                def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_UNMATCHED, tag=def_tag)
             else:
-                def_tag_name, def_contents = def_entry.get_definition(original_tag, placeholder_value=placeholder)
+                def_tag_name, def_contents = def_entry.get_definition(def_tag, placeholder_value=placeholder)
                 if def_tag_name:
-                    if def_expand_group is not None and def_expand_group != def_contents:
-                        def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_EXPAND_BAD,
-                                                                tag=original_tag, actual_def=def_contents,
+                    if def_expand_group is not def_tag and def_expand_group != def_contents:
+                        def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_EXPAND_INVALID,
+                                                                tag=def_tag, actual_def=def_contents,
                                                                 found_def=def_expand_group)
                         return None
                     return def_contents
                 elif def_entry.takes_value:
-                    def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_VALUE_MISSING, tag=original_tag)
+                    def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_VALUE_MISSING, tag=def_tag)
                 else:
-                    def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_VALUE_EXTRA, tag=original_tag)
+                    def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_VALUE_EXTRA, tag=def_tag)
             pass
         else:
             raise ValueError("Internal error in DefinitionMapper")
diff --git a/hed/models/hed_group.py b/hed/models/hed_group.py
index 927acfc33..61f0baaa9 100644
--- a/hed/models/hed_group.py
+++ b/hed/models/hed_group.py
@@ -1,12 +1,12 @@
 from hed.models.hed_tag import HedTag
-from hed.models.model_constants import DefTagNames
 import copy
+from hed.models.hed_group_base import HedGroupBase
 
 
-class HedGroup:
+class HedGroup(HedGroupBase):
     """ Represents a single parenthesized hed string. """
 
-    def __init__(self, hed_string="", startpos=None, endpos=None, include_paren=True,
+    def __init__(self, hed_string="", startpos=None, endpos=None,
                  contents=None):
         """
         Returns an empty HedGroup object
@@ -19,21 +19,15 @@ def __init__(self, hed_string="", startpos=None, endpos=None, include_paren=True
             Starting index of group(including parentheses) in hed_string
         endpos : int
             Ending index of group(including parentheses) in hed_string
-        include_paren : bool
-            If False, this group does not have parentheses(only relevant for top level groups)
         contents : [HedTag or HedGroup]
             A list of tags and groups that will be set as the contents of this group.
         """
+        super().__init__(hed_string=hed_string, startpos=startpos, endpos=endpos)
         if contents:
             self._children = contents
         else:
             self._children = []
         self._original_children = self._children
-        self._startpos = startpos
-        self._endpos = endpos
-        self._include_paren = include_paren
-        self._hed_string = hed_string
-        self.is_definition = False
         self.mutable = True  # If False, this group is potentially referenced in other places and should not be altered
 
     def __eq__(self, other):
@@ -45,7 +39,7 @@ def __eq__(self, other):
 
         if self._children != other._children:
             return False
-        if self._include_paren != other._include_paren:
+        if self.is_group != other.is_group:
             return False
         return True
 
@@ -63,40 +57,9 @@ def append(self, new_tag_or_group):
 
         self._children.append(new_tag_or_group)
 
-    def get_direct_children(self):
-        """
-        Returns an iterator over all HedTags and HedGroups that are in this group.  Not recursive.
-
-        Returns
-        -------
-        iterator over the direct children
-        """
-        return self._children
-
-    def get_all_tags(self):
-        """
-        Returns all the tags, including descendants.
-
-        Returns
-        -------
-        tag_list: [HedTag]
-            The list of all tags in this group, including descendants.
-        """
-        node_list = [self]
-        final_list = []
-
-        # Using an iterator is worse performance wise here.
-        while node_list:
-            current_group_or_tag = node_list.pop(0)
-            if isinstance(current_group_or_tag, HedGroup):
-                node_list = current_group_or_tag._children + node_list
-            else:
-                final_list.append(current_group_or_tag)
-        return final_list
-
     def check_if_in_original_tags_and_groups(self, tag_or_group):
         """
-            Returns all original tags and groups.  Applies if a tag was deleted or replaced
+            Checks if the tag or group was in the original string, even if it has since been removed.
 
         Returns
         -------
@@ -112,168 +75,7 @@ def check_if_in_original_tags_and_groups(self, tag_or_group):
                 node_list = current_group_or_tag._original_children + node_list
             final_list.append(current_group_or_tag)
 
-        for val in final_list:
-            if tag_or_group is val:
-                return True
-
-        return False
-        return final_list
-
-    def is_group(self):
-        """
-            This is a hed string contained by parenthesis.
-
-        Returns
-        -------
-
-        """
-        return self._include_paren
-
-    def get_all_groups(self, also_return_depth=False):
-        """
-        Returns all the HedGroups, including descendants and self.
-
-        Parameters
-        ----------
-        also_return_depth : bool
-            If True, this yields tuples(group, depth) rather than just groups
-
-        Returns
-        -------
-        group_list: [HedGroup]
-            The list of all HedGroups in this group, including descendants and self.
-        """
-        node_list = [self]
-        final_list = []
-
-        # Using an iterator is worse performance wise here.
-        while node_list:
-            current_group_or_tag = node_list.pop(0)
-            if isinstance(current_group_or_tag, HedGroup):
-                node_list = current_group_or_tag._children + node_list
-                final_list.append(current_group_or_tag)
-
-        if also_return_depth:
-            top_groups = self.groups()
-
-            final_list = [(group, self._check_in_group(group, top_groups)) for group in final_list]
-        return final_list
-
-    @staticmethod
-    def _check_in_group(group, group_list):
-        for val in group_list:
-            if val is group:
-                return True
-        return False
-
-    def tags(self):
-        """
-        Returns the direct child tags of this group, filtering out HedGroup children
-
-        Returns
-        -------
-        tag_list: [HedTag]
-            The list of all tags directly in this group.
-        """
-        return [tag for tag in self._children if isinstance(tag, HedTag)]
-
-    def groups(self):
-        """
-        Returns the direct child groups of this group, filtering out HedTag children
-
-        Returns
-        -------
-        group_list: [HedGroup]
-            The list of all tags directly in this group.
-        """
-        return [group for group in self._children if isinstance(group, HedGroup)]
-
-    def get_original_hed_string(self):
-        """
-            Get the original hed string with zero modification
-
-        Returns
-        -------
-        hed_string: str
-            The original string
-        """
-        return self._hed_string[self._startpos:self._endpos]
-
-    @property
-    def span(self):
-        """
-        Return the source span of this group from the source hed_string
-        Returns
-        -------
-        span: (int, int)
-            The start and end index of the group(including parentheses) from the source string
-        """
-        return self._startpos, self._endpos
-
-    def __str__(self):
-        """
-        Convert this HedGroup to a string
-
-        Returns
-        -------
-        str
-            Returns the group as a string, including any modified HedTags
-        """
-        if self._include_paren:
-            return "(" + ",".join([str(child) for child in self._children]) + ")"
-        return ",".join([str(child) for child in self._children])
-
-    def get_as_short(self):
-        """
-        Convert this HedGroup to a short tag string
-
-        Returns
-        -------
-        str
-            Returns the group as a string, returning all tags as short tags.
-        """
-        return self.get_as_form("short_tag")
-
-    def get_as_long(self):
-        """
-        Convert this HedGroup to a long tag string
-
-        Returns
-        -------
-        str
-            Returns the group as a string, returning all tags as long tags.
-        """
-        return self.get_as_form("long_tag")
-
-    def get_as_form(self, tag_attribute, tag_transformer=None):
-        """
-
-        Parameters
-        ----------
-        tag_attribute : str
-            The hed_tag property to use to construct the string.  Most commonly short_tag or long_tag.
-        tag_transformer: func or None
-            A function that is applied to each tag string before returning.
-            signature: str def(HedTag, str)
-        Returns
-        -------
-        group_as_string: str
-            The constructed string
-        """
-        if tag_transformer:
-            result = ",".join([tag_transformer(child, child.__getattribute__(tag_attribute))
-                               if isinstance(child, HedTag) else child.get_as_form(tag_attribute, tag_transformer)
-                               for child in self._children])
-        else:
-            result = ",".join([child.__getattribute__(tag_attribute) if isinstance(child, HedTag) else
-                               child.get_as_form(tag_attribute) for child in self._children])
-        if self._include_paren:
-            return f"({result})"
-        return result
-
-    def lower(self):
-        """Convenience function, equivalent to str(self).lower()"""
-        return str(self).lower()
+        return self._check_in_group(tag_or_group, final_list)
 
     def replace_tag(self, tag_or_group, new_contents):
         """ Replaces an existing tag in the group with a new tag, list, or group
@@ -298,74 +100,6 @@ def replace_tag(self, tag_or_group, new_contents):
                 break
         self._children[replace_index] = new_contents
 
-    def find_placeholder_tag(self):
-        """
-            If a placeholder tag is present, this will return it.
-
-            Assumes a valid HedString with no erroneous "#" characters.
-
-        Returns
-        -------
-        found_tag: HedTag or None
-            The placeholder tag if found.
-        """
-        for tag in self.get_all_tags():
-            if "#" in tag.org_tag:
-                return tag
-
-        return None
-
-    def remove_groups(self, remove_groups):
-        """
-        Takes a list of HedGroups, and removes any that exist from this HedString.
-
-        Parameters
-        ----------
-        remove_groups : [HedGroup or HedTag]
-            A list of groups or tags to remove
-        """
-        if not self.mutable:
-            raise ValueError("Trying to alter immutable group")
-
-        if self._original_children is self._children:
-            self._original_children = self._children.copy()
-
-        for val in remove_groups:
-            if val is self:
-                self._children = []
-                self._hed_string = ""
-                return
-        # if self in remove_groups:
-        #     self._children = []
-        #     self._hed_string = ""
-        #     return
-
-        for group in self.get_all_groups():
-            new_children = []
-            for child in group._children:
-                skip_child = False
-                for remove_child in remove_groups:
-                    if child is remove_child:
-                        skip_child = True
-                        break
-                if skip_child:
-                    continue
-                new_children.append(child)
-            group._children = new_children
-            #group._children = [child for child in group._children if child not in remove_groups]
-
-    def without_defs(self):
-        """
-            Return this as a string, with definitions having been removed.
-
-        Returns
-        -------
-
-        """
-        if self._include_paren:
-            return "(" + ",".join([str(child) for child in self._children if not child.is_definition]) + ")"
-        return ",".join([str(child) for child in self._children if not child.is_definition])
-
     def __copy__(self):
         """
             Return a shallow copy with a new _children list, rather than the same list
@@ -383,9 +117,6 @@ def __copy__(self):
         result.mutable = True
         return result
 
-    def __bool__(self):
-        return bool(self._children)
-
     def cascade_mutable(self, value):
         """
             Sets the mutable property for all child tags and groups.
@@ -398,16 +129,48 @@ def cascade_mutable(self, value):
         Returns
         -------
         """
+        self.mutable = value
         for child in self._children:
-            if isinstance(child, HedTag):
-                child.mutable = value
-            else:
+            if isinstance(child, HedGroup):
                 child.cascade_mutable(value)
+            else:
+                child.mutable = value
+
+    def remove_groups(self, remove_groups):
+        """
+            Remove any tags/groups in remove_groups found in this hed string.
+
+            Any groups that become empty will also be pruned.
+
+        Parameters
+        ----------
+        remove_groups : [HedGroup or HedTag]
+            A list of groups or tags to remove.
+        """
+        empty_groups = []
+        all_groups = self.get_all_groups()
+        for remove_child in remove_groups:
+            for group in all_groups:
+                if remove_child in group._children:
+                    if not group.mutable:
+                        raise ValueError("Trying to alter immutable group")
+                    if group._original_children is group._children:
+                        group._original_children = group._children.copy()
+
+                    group._children = [child for child in group._children if child is not remove_child]
+                    if not group._children and group is not self:
+                        empty_groups.append(group)
+                    break
+
+        if empty_groups:
+            return self.remove_groups(empty_groups)
 
     def make_tag_mutable(self, tag):
         """
             Replace the given tag in the group with a tag you can alter.
 
+            This is more or less exclusively for def expansion.
+
         Parameters
         ----------
         tag: HedTag
@@ -428,16 +191,112 @@ def make_tag_mutable(self, tag):
             return tag_copy
         return tag
 
-    @staticmethod
-    def extract_tags_from_group(tag_group):
-        def_tags = []
-        group_tags = []
-        other_tags = []
-        for tag_or_group in tag_group.get_direct_children():
-            if isinstance(tag_or_group, HedGroup):
-                group_tags.append(tag_or_group)
-            elif tag_or_group.short_base_tag.lower() == DefTagNames.DEFINITION_KEY:
-                def_tags.append(tag_or_group)
+    def get_frozen(self):
+        """
+            Returns a frozen copy of this HedGroup.
+
+        Returns: HedGroupFrozen
+            A frozen copy of this HedGroup.  Note: tags still point to the same place.  Do not alter them.
+
+        """
+        return HedGroupFrozen(self)
+
+
+class HedGroupFrozen(HedGroupBase):
+    """
+        This is the same as a HedGroup, except it is frozen as sets and cannot be altered further.
+
+        Searching and getting all tags/groups will be faster.  Additionally, HedGroupFrozen is order-agnostic.
+        (a, b) = (b, a)
+    """
+    def __init__(self, contents, hed_string=None):
+        """
+
+        Args:
+            contents: HedGroup or [HedGroup or HedTag]
+                If this is a group, get the source hed_string from the group.
+                If a list, it gets the source hed_string from the parameter.
+            hed_string: str or None
+                If using a list, this is the raw string the contents came from
+        """
+        if isinstance(contents, HedGroupBase):
+            span = contents.span
+            hed_string = contents._hed_string
+            contents = contents._children
+        else:
+            if hed_string:
+                span = 0, len(hed_string)
             else:
-                other_tags.append(tag_or_group)
-        return def_tags, group_tags, other_tags
\ No newline at end of file
+                span = 0, 0
+
+        super().__init__(hed_string=hed_string, startpos=span[0], endpos=span[1])
+        if contents is not None:
+            self._children = frozenset(child if not isinstance(child, HedGroupBase)
+                                       else HedGroupFrozen(child) for child in contents)
+        else:
+            self._children = None
+
+    def __hash__(self):
+        return hash((self._children, self.is_group))
+
+    def __eq__(self, other):
+        if self is other:
+            return True
+
+        if not isinstance(other, HedGroupFrozen):
+            return False
+
+        if self._children != other._children:
+            return False
+
+        if self.is_group != other.is_group:
+            return False
+        return True
+
+    # Cache these values as we know this is frozen
+    def get_all_tags(self):
+        """
+        Returns all the tags, including descendants.
+
+        Returns
+        -------
+        tag_list: [HedTag]
+            The list of all tags in this group, including descendants.
+        """
+        if not hasattr(self, "_tags"):
+            self._tags = super().get_all_tags()
+
+        return self._tags
+
+    def get_all_groups(self, also_return_depth=False):
+        """
+        Returns all the HedGroups, including descendants and self.
+
+        Parameters
+        ----------
+        also_return_depth : bool
+            If True, this yields tuples(group, depth) rather than just groups
+
+        Returns
+        -------
+        group_list: [HedGroup]
+            The list of all HedGroups in this group, including descendants and self.
+        """
+        if also_return_depth:
+            if not hasattr(self, "_groups1"):
+                self._groups1 = super().get_all_groups(also_return_depth=also_return_depth)
+            return self._groups1
+        else:
+            if not hasattr(self, "_groups2"):
+                self._groups2 = super().get_all_groups(also_return_depth=also_return_depth)
+            return self._groups2
+
+    def get_frozen(self):
+        """
+            Returns this frozen group.  This is just used for the hed validator.
+
+        Returns: HedGroupFrozen
+            This same group.
+
+        """
+        return self
diff --git a/hed/models/hed_group_base.py b/hed/models/hed_group_base.py
new file mode 100644
index 000000000..41c0c10e8
--- /dev/null
+++ b/hed/models/hed_group_base.py
@@ -0,0 +1,349 @@
+from hed.models.hed_tag import HedTag
+
+
+class HedGroupBase:
+    """ This is an abstract baseclass used for HedGroup API, so it's shared with HedGroupFrozen """
+
+    def __init__(self, hed_string="", startpos=None, endpos=None):
+        """
+        Returns an empty HedGroup object
+
+        Parameters
+        ----------
+        hed_string : str
+            Source hed string for this group
+        startpos : int
+            Starting index of group(including parentheses) in hed_string
+        endpos : int
+            Ending index of group(including parentheses) in hed_string
+        """
+        self._startpos = startpos
+        self._endpos = endpos
+        self._hed_string = hed_string
+        self.mutable = True  # If False, this group is potentially referenced in other places and should not be altered
+
+        # placeholder just to make IDE not complain
+        self._children = None
+
+    @property
+    def is_group(self):
+        """
+            Returns True if this is a group with parenthesis.
+        """
+        return True
+
+    def get_direct_children(self):
+        """
+        Returns an iterator over all HedTags and HedGroups that are in this group.  Not recursive.
+
+        Returns
+        -------
+        iterator over the direct children
+        """
+        return self._children
+
+    def get_all_tags(self):
+        """
+        Returns all the tags, including descendants.
+
+        Returns
+        -------
+        tag_list: [HedTag]
+            The list of all tags in this group, including descendants.
+        """
+        node_list = [self]
+        final_list = []
+
+        # Using an iterator is worse performance wise here.
+        while node_list:
+            current_group_or_tag = node_list.pop(0)
+            if isinstance(current_group_or_tag, HedGroupBase):
+                node_list = list(current_group_or_tag._children) + node_list
+            else:
+                final_list.append(current_group_or_tag)
+        return final_list
+
+    def get_all_groups(self, also_return_depth=False):
+        """
+        Returns all the HedGroups, including descendants and self.
+
+        Parameters
+        ----------
+        also_return_depth : bool
+            If True, this yields tuples(group, depth) rather than just groups
+
+        Returns
+        -------
+        group_list: [HedGroup]
+            The list of all HedGroups in this group, including descendants and self.
+        """
+        node_list = [self]
+        final_list = []
+
+        # Using an iterator is worse performance wise here.
+        while node_list:
+            current_group_or_tag = node_list.pop(0)
+            if isinstance(current_group_or_tag, HedGroupBase):
+                node_list = list(current_group_or_tag._children) + node_list
+                final_list.append(current_group_or_tag)
+
+        if also_return_depth:
+            top_groups = self.groups()
+
+            final_list = [(group, self._check_in_group(group, top_groups)) for group in final_list]
+        return final_list
+
+    @staticmethod
+    def _check_in_group(group, group_list):
+        for val in group_list:
+            if val is group:
+                return True
+        return False
+
+    def tags(self):
+        """
+        Returns the direct child tags of this group, filtering out HedGroup children
+
+        Returns
+        -------
+        tag_list: [HedTag]
+            The list of all tags directly in this group.
+        """
+        return [tag for tag in self._children if isinstance(tag, HedTag)]
+
+    def groups(self):
+        """
+        Returns the direct child groups of this group, filtering out HedTag children
+
+        Returns
+        -------
+        group_list: [HedGroup]
+            The list of all tags directly in this group.
+        """
+        return [group for group in self._children if isinstance(group, HedGroupBase)]
+
+    def get_original_hed_string(self):
+        """
+            Get the original hed string with zero modification
+
+        Returns
+        -------
+        hed_string: str
+            The original string
+        """
+        return self._hed_string[self._startpos:self._endpos]
+
+    @property
+    def span(self):
+        """
+        Return the source span of this group from the source hed_string
+        Returns
+        -------
+        span: (int, int)
+            The start and end index of the group(including parentheses) from the source string
+        """
+        return self._startpos, self._endpos
+
+    def __str__(self):
+        """
+        Convert this HedGroup to a string
+
+        Returns
+        -------
+        str
+            Returns the group as a string, including any modified HedTags
+        """
+        if self.is_group:
+            return "(" + ",".join([str(child) for child in self._children]) + ")"
+        return ",".join([str(child) for child in self._children])
+
+    def get_as_short(self):
+        """
+        Convert this HedGroup to a short tag string
+
+        Returns
+        -------
+        str
+            Returns the group as a string, returning all tags as short tags.
+        """
+        return self.get_as_form("short_tag")
+
+    def get_as_long(self):
+        """
+        Convert this HedGroup to a long tag string
+
+        Returns
+        -------
+        str
+            Returns the group as a string, returning all tags as long tags.
+        """
+        return self.get_as_form("long_tag")
+
+    def get_as_form(self, tag_attribute, tag_transformer=None):
+        """
+
+        Parameters
+        ----------
+        tag_attribute : str
+            The hed_tag property to use to construct the string.  Most commonly short_tag or long_tag.
+        tag_transformer: func or None
+            A function that is applied to each tag string before returning.
+            signature: str def(HedTag, str)
+        Returns
+        -------
+        group_as_string: str
+            The constructed string
+        """
+        if tag_transformer:
+            result = ",".join([tag_transformer(child, child.__getattribute__(tag_attribute))
+                               if isinstance(child, HedTag) else child.get_as_form(tag_attribute, tag_transformer)
+                               for child in self._children])
+        else:
+            result = ",".join([child.__getattribute__(tag_attribute) if isinstance(child, HedTag) else
+                               child.get_as_form(tag_attribute) for child in self._children])
+        if self.is_group:
+            return f"({result})"
+        return result
+
+    def lower(self):
+        """Convenience function, equivalent to str(self).lower()"""
+        return str(self).lower()
+
+    def find_placeholder_tag(self):
+        """
+            If a placeholder tag is present, this will return it.
+
+            Assumes a valid HedString with no erroneous "#" characters.
+
+        Returns
+        -------
+        found_tag: HedTag or None
+            The placeholder tag if found.
+        """
+        for tag in self.get_all_tags():
+            if "#" in tag.org_tag:
+                return tag
+
+        return None
+
+    def __bool__(self):
+        return bool(self._children)
+
+    def find_tags(self, anchors, recursive=False, include_groups=2):
+        """
+            Find tags given anchors.
+
+            Note: This can only find identified tags.  By default, definition, def, def-expand, onset, and offset
+                are identified, even without a schema.
+
+        Args:
+            anchors: container
+                A container of short_base_tags to locate
+            recursive: bool
+                If true, also check subgroups.
+            include_groups: 0, 1 or 2
+                If 0: Return only tags
+                If 1: return only groups
+                If 2 or any other value: return both
+                
+        returns:
+        list:
+        tag: HedTag
+            The located tag
+        group: HedGroup
+            The group the located tag is in
+        """
+        found_tags = []
+        if recursive:
+            groups = self.get_all_groups()
+        else:
+            groups = (self, )
+
+        for sub_group in groups:
+            for tag in sub_group.tags():
+                if tag.short_base_tag.lower() in anchors:
+                    found_tags.append((tag, sub_group))
+                    
+        if include_groups == 0 or include_groups == 1:
+            return [tag[include_groups] for tag in found_tags]
+        return found_tags
+
+    def find_exact_tags(self, tags_or_groups, recursive=False):
+        """
+            Find the given tags_or_groups.  Note if you pass in groups it will only find EXACT matches.
+
+            If this is a HedGroup, order matters.  (b, a) != (a, b)
+
+            If this is a HedGroupFrozen:
+            if "(a, b)" in tags_or_groups, then it will match 1 and 2, but not 3.
+            1. (a, b)
+            2. (b, a)
+            3. (a, b, c)
+
+            Note: This can only find identified tags.  By default, definition, def, def-expand, onset, and offset
+                are identified, even without a schema.
+
+        Args:
+            tags_or_groups: [HedTag or HedGroupBase]
+                A container of tags to locate
+            recursive: bool
+                If true, also check subgroups.
+
+        returns:
+        group_list: [HedGroupBase]
+            A list of all groups the given tags/groups were found in.
+        """
+        found_tags = []
+        if recursive:
+            groups = self.get_all_groups()
+        else:
+            groups = (self,)
+
+        for sub_group in groups:
+            for search_tag in tags_or_groups:
+                if search_tag in sub_group._children:
+                    found_tags.append(sub_group)
+
+        return found_tags
+
+    def find_def_tags(self, recursive=False, include_groups=3):
+        """
+            Find any def and def-expand tags in the group.
+
+        Args:
+            recursive: bool
+                If true, also check subgroups.
+            include_groups: int, 0, 1, 2, 3
+                If 0: Return only def and def expand tags
+                If 1: Return only def tags and def-expand groups
+                If 2: Return only groups containing defs, or def-expand groups
+                If 3 or any other value: Return all 3 as a tuple.
+        Returns:
+        list:
+        def_tag: HedTag
+            The located def tag
+        def_expand_group: HedGroup or None
+            If this is a def-expand rather than def tag, this will be the entire def-expand group.
+        group: HedGroup
+            The group the def tag or def expand group is in.
+        """
+        from hed.models.def_dict import DefTagNames
+        if recursive:
+            groups = self.get_all_groups()
+        else:
+            groups = (self, )
+
+        def_tags = []
+        for group in groups:
+            for child in group.get_direct_children():
+                if isinstance(child, HedTag):
+                    if child.short_base_tag.lower() == DefTagNames.DEF_KEY:
+                        def_tags.append((child, child, group))
+                else:
+                    for tag in child.tags():
+                        if tag.short_base_tag.lower() == DefTagNames.DEF_EXPAND_KEY:
+                            def_tags.append((tag, child, group))
+
+        if include_groups == 0 or include_groups == 1 or include_groups == 2:
+            return [tag[include_groups] for tag in def_tags]
+        return def_tags
diff --git a/hed/models/hed_ops.py b/hed/models/hed_ops.py
index 0f2717774..7f05ba32c 100644
--- a/hed/models/hed_ops.py
+++ b/hed/models/hed_ops.py
@@ -3,7 +3,6 @@
 from hed.schema import HedSchema, HedSchemaGroup
 
 
-
 # These are the defaults if you pass in nothing.  Most built in routes will have other default values.
 default_arguments = {
     'allow_placeholders': False,
@@ -120,4 +119,4 @@ def _func_in_list(find_func, func_list):
             return True
         if isinstance(func, partial) and getattr(func, 'func') == find_func:
             return True
-    return False
\ No newline at end of file
+    return False
diff --git a/hed/models/hed_string.py b/hed/models/hed_string.py
index 7e30e2531..f4f45a566 100644
--- a/hed/models/hed_string.py
+++ b/hed/models/hed_string.py
@@ -7,6 +7,8 @@
 from hed.errors.error_types import ErrorContext
 from hed.models.hed_ops import translate_ops
 from hed.models.model_constants import DefTagNames
+from hed.models.hed_group import HedGroupFrozen
+from hed.models.hed_group_base import HedGroupBase
 
 
 class HedString(HedGroup):
@@ -15,11 +17,9 @@ class HedString(HedGroup):
     OPENING_GROUP_CHARACTER = '('
     CLOSING_GROUP_CHARACTER = ')'
 
-    def __init__(self, hed_string):
+    def __init__(self, hed_string, hed_schema=None):
         """Constructor for the HedString class.
 
-            raises ValueError if the string cannot be parsed due to bad groups.
-
         Parameters
         ----------
         hed_string: str
@@ -27,17 +27,22 @@ def __init__(self, hed_string):
         Returns
         -------
         """
-        # This is a tree like structure containing the entire hed string
-        hed_string = self._clean_hed_string(hed_string)
-
         # The sub HedStrings that make up this object.  Empty if this is the lowest level one.
         self._component_strings = []
+        # This is a tree like structure containing the entire hed string
         try:
-            contents = self.split_hed_string_into_groups(hed_string)
+            contents = self.split_hed_string_into_groups(hed_string, hed_schema)
         except ValueError:
             contents = []
 
-        super().__init__(hed_string, include_paren=False, contents=contents, startpos=0, endpos=len(hed_string))
+        super().__init__(hed_string, contents=contents, startpos=0, endpos=len(hed_string))
+
+    @property
+    def is_group(self):
+        """
+            Returns True if this is a group with parenthesis.
+        """
+        return False
 
     @staticmethod
     def create_from_other(hed_string_obj_list):
@@ -66,7 +71,6 @@ def create_from_other(hed_string_obj_list):
         new_hed_string_obj._hed_string = hed_string
         new_hed_string_obj._startpos = 0
         new_hed_string_obj._endpos = len(hed_string)
-        new_hed_string_obj._include_paren = False
 
         return new_hed_string_obj
 
@@ -90,26 +94,8 @@ def convert_to_canonical_forms(self, hed_schema):
         for tag in self.get_all_tags():
             validation_issues += tag.convert_to_canonical_forms(hed_schema)
 
-        self._identify_definitions()
         return validation_issues
 
-    def _identify_definitions(self):
-        """
-            Mark any definitions found in this string.  Including groups or tags that are part of a definition.
-
-            This should possibly be removed later.
-
-        Returns
-        -------
-
-        """
-        for tag_group in self.groups():
-            for tag in tag_group.tags():
-                if tag.short_base_tag.lower() == DefTagNames.DEFINITION_KEY:
-                    for def_tag in tag_group.get_all_tags():
-                        def_tag.is_definition = True
-                    tag_group.is_definition = True
-
     def remove_definitions(self):
         """
             Removes any definition tags and groups from this string.
@@ -119,11 +105,7 @@ def remove_definitions(self):
         issues: [{}]
             There are no possible issues, this list is always blank.
         """
-        definition_groups = []
-        for tag_group in self.get_all_groups():
-            if tag_group.is_definition:
-                definition_groups.append(tag_group)
-
+        definition_groups = self.find_top_level_tags({DefTagNames.DEFINITION_KEY}, include_groups=1)
         if definition_groups:
             self.remove_groups(definition_groups)
 
@@ -179,16 +161,22 @@ def convert_to_original(self):
         return self.get_as_form("org_tag")
 
     @staticmethod
-    def split_hed_string_into_groups(hed_string):
+    def split_hed_string_into_groups(hed_string, hed_schema=None):
         """Splits the hed_string into a tree of tag groups, tags, and delimiters.
 
         Parameters
         ----------
         hed_string
             A hed string consisting of tags and tag groups.
+        hed_schema
+            Schema to use to identify tags.
         Returns
         -------
+        group: HedGroup
+            The group containing this hed_string.
 
+        raises: ValueError
+            Raises ValueError if the string is significantly malformed, such as mis-matched parenthesis.
         """
         # Stack variable while processing
         current_tag_group = [[]]
@@ -196,7 +184,7 @@ def split_hed_string_into_groups(hed_string):
         input_tags = HedString.split_hed_string(hed_string)
         for is_hed_tag, (startpos, endpos) in input_tags:
             if is_hed_tag:
-                new_tag = HedTag(hed_string, (startpos, endpos))
+                new_tag = HedTag(hed_string, (startpos, endpos), hed_schema)
                 current_tag_group[-1].append(new_tag)
             else:
                 string_portion = hed_string[startpos:endpos]
@@ -229,10 +217,6 @@ def split_hed_string_into_groups(hed_string):
 
         return current_tag_group[0]
 
-    @staticmethod
-    def _clean_hed_string(hed_string):
-        return hed_string.replace("\n", " ")
-
     def _get_org_span(self, tag_or_group):
         """
             If this tag or group was in the original hed string, find it's original span.
@@ -340,25 +324,7 @@ def split_hed_string(hed_string):
 
         return result_positions
 
-    @staticmethod
-    def split_hed_string_return_tags(hed_string):
-        """
-        An easier to use variant of split_hed_string if you're only interested in tags
-
-        Parameters
-        ----------
-        hed_string : str
-            A hed string to split into tags
-        Returns
-        -------
-        hed_tags: [HedTag]
-            The string split apart into hed tags with all delimiters removed
-        """
-        result_positions = HedString.split_hed_string(hed_string)
-        return [HedTag(hed_string, span) for (is_hed_tag, span) in
-                result_positions if is_hed_tag]
-
-    def apply_ops(self, string_funcs):
+    def apply_funcs(self, string_funcs):
         """
             Run the list of functions on this string and gather issues found.
 
@@ -405,8 +371,86 @@ def validate(self, hed_ops=None, error_handler=None, **kwargs):
         tag_funcs = translate_ops(hed_ops, **kwargs)
 
         error_handler.push_error_context(ErrorContext.HED_STRING, self, increment_depth_after=False)
-        issues = self.apply_ops(tag_funcs)
+        issues = self.apply_funcs(tag_funcs)
         error_handler.add_context_to_issues(issues)
         error_handler.pop_error_context()
 
         return issues
+
+    def get_frozen(self):
+        """
+            Returns a frozen copy of this HedString.
+
+        Returns: HedStringFrozen
+            A frozen copy of this HedString.  Note: tags still point to the same place.  Do not alter them.
+
+        """
+        return HedStringFrozen(self)
+
+    def find_top_level_tags(self, anchors, include_groups=2):
+        """
+            Find top level groups containing the given anchor tags.
+
+            Max of 1 tag located her top level group.
+        Args:
+            anchors: container
+                A list/set/etc of short_base_tags to find groups by.
+            include_groups: 0, 1 or 2
+                If 0: Return only tags
+                If 1: return only groups
+                If 2 or any other value: return both
+        Returns:
+        list:
+        tag: HedTag
+            The located tag
+        group: HedGroup
+            The group the located tag is in
+        """
+        top_level_tags = []
+        for group in self.groups():
+            for tag in group.tags():
+                if tag.short_base_tag.lower() in anchors:
+                    top_level_tags.append((tag, group))
+                    # Only capture a max of 1 per group.  These are implicitly unique.
+                    break
+
+        if include_groups == 0 or include_groups == 1:
+            return [tag[include_groups] for tag in top_level_tags]
+        return top_level_tags
+
+
+class HedStringFrozen(HedGroupFrozen):
+    """
+        Represents an immutable hed string.
+    """
+    def __init__(self, hed_string, hed_schema=None):
+        """
+
+        Args:
+            hed_string: HedGroupBase or str
+                Source string
+            hed_schema: HedSchema or HedSchemaGroup or None
+                HedSchema to use to identify tags if hed_string is a str.
+        """
+        if isinstance(hed_string, HedGroupBase):
+            contents = hed_string
+            hed_string = hed_string._hed_string
+        else:
+            try:
+                contents = HedString.split_hed_string_into_groups(hed_string, hed_schema)
+            except ValueError:
+                contents = []
+
+        super().__init__(contents, hed_string)
+
+    @property
+    def is_group(self):
+        """
+            Returns True if this is a group with parenthesis.
+        """
+        return False
+
+    # Other functions from HedString we want.
+    apply_funcs = HedString.apply_funcs
+    validate = HedString.validate
+    find_top_level_tags = HedString.find_top_level_tags
diff --git a/hed/models/hed_tag.py b/hed/models/hed_tag.py
index f80b23118..168ee166f 100644
--- a/hed/models/hed_tag.py
+++ b/hed/models/hed_tag.py
@@ -1,4 +1,5 @@
 from hed.schema.hed_schema_constants import HedKey
+from hed.schema.hed_schema_entry import HedTagEntry
 
 
 class HedTag:
@@ -31,40 +32,19 @@ def __init__(self, hed_string, span=None, hed_schema=None):
         # if the tag has a name_prefix added, or is an expanded def.
         self._tag = None
 
-        # The long form of the tag, if generated.
-        self._long_tag = None
-
-        # offset into _long_tag where the short tag starts at.
-        self._short_tag_index = None
-
-        # offset into _long_tag where the base_tag ends at.  The "base tag" is the long form with no extension/value.
-        self._extension_index = None
-
         self._library_prefix = self._get_library_prefix(self.org_tag)
 
         # This is the schema this tag was converted to.
         self._schema = None
         self._schema_entry = None
 
-        if hed_schema:
-            self.convert_to_canonical_forms(hed_schema)
-
-        self.is_definition = False
-        self.mutable = True  # If False, this tag is potentially referenced in other places and should not be altered
-
-    def __eq__(self, other):
-        if self is other:
-            return True
-
-        if not isinstance(other, HedTag):
-            return False
+        self._extension_value = ""
 
-        if self.short_tag.lower() == other.short_tag.lower():
-            return True
+        self.mutable = True  # Note this only keeps track of tags also used in definitions.  You should also not
+                             # alter any tags used in a HedGroupFrozen
 
-        if self.org_tag.lower() == other.org_tag.lower():
-            return True
-        return False
+        if hed_schema:
+            self.convert_to_canonical_forms(hed_schema)
 
     @property
     def library_prefix(self):
@@ -89,10 +69,10 @@ def short_tag(self):
         short_tag: str
             The short version of the tag, including value or extension
         """
-        if self._short_tag_index is None:
-            return str(self)
+        if self._schema_entry:
+            return f"{self._library_prefix}{self._schema_entry.short_tag_name}{self._extension_value}"
 
-        return self._library_prefix + self._long_tag[self._short_tag_index:]
+        return str(self)
 
     @property
     def base_tag(self):
@@ -105,10 +85,9 @@ def base_tag(self):
         base_tag: str
             The long version of the tag, without value or extension
         """
-        if self._extension_index is None:
-            return str(self)
-
-        return self._long_tag[:self._extension_index]
+        if self._schema_entry:
+            return self._schema_entry.long_tag_name
+        return str(self)
 
     @property
     def short_base_tag(self):
@@ -122,10 +101,9 @@ def short_base_tag(self):
         base_tag: str
             The short non-extension port of a tag.
         """
-        if self._extension_index is None:
-            return str(self)
-
-        return self._long_tag[self._short_tag_index:self._extension_index]
+        if self._schema_entry:
+            return self._schema_entry.short_tag_name
+        return str(self)
 
     @short_base_tag.setter
     def short_base_tag(self, new_tag_val):
@@ -144,12 +122,15 @@ def short_base_tag(self, new_tag_val):
         """
         if not self.mutable:
             raise ValueError("Trying to alter immutable tag")
-        long_part = self._long_tag[:self._short_tag_index]
-        self._long_tag = f"{long_part}{new_tag_val}/{self.extension_or_value_portion}"
-        self._extension_index = self._short_tag_index + len(new_tag_val)
+        if self._schema_entry:
+            if self._schema:
+                tag_entry = self._schema.get_tag_entry(new_tag_val, library_prefix=self.library_prefix)
+            else:
+                tag_entry, remainder = HedTagEntry.get_fake_tag_entry(new_tag_val, [new_tag_val.lower()])
 
-        if self._schema:
-            self._update_schema_entry()
+            self._schema_entry = tag_entry
+        else:
+            raise ValueError("cannot set unidentified tags")
 
     @property
     def org_base_tag(self):
@@ -165,19 +146,17 @@ def org_base_tag(self):
         base_tag: str
             The original version of the tag, without value or extension
         """
-        if self._extension_index is None:
-            return str(self)
-
-        # This mess could be optimized better
-        extension_len = len(self.extension_or_value_portion)
-        if not extension_len:
-            return self.tag
+        if self._schema_entry:
+            extension_len = len(self._extension_value)
+            if not extension_len:
+                return self.tag
 
-        org_len = len(self.tag)
-        if org_len == extension_len:
-            return ""
+            org_len = len(self.tag)
+            if org_len == extension_len:
+                return ""
 
-        return self.tag[:org_len - (extension_len + 1)]
+            return self.tag[:org_len - extension_len]
+        return str(self)
 
     def tag_modified(self):
         """
@@ -208,8 +187,6 @@ def tag(self):
 
         return self.org_tag
 
-    # Honestly this should probably be removed
-    # this should be replaced with a "set long tag" which updates the short and long versions automatically.
     @tag.setter
     def tag(self, new_tag_val):
         """
@@ -217,7 +194,7 @@ def tag(self, new_tag_val):
 
             Primarily used to expand def tags.
 
-            Note: only valid after calling convert_to_canonical_forms
+            Note: only valid before calling convert_to_canonical_forms
 
         Parameters
         ----------
@@ -227,7 +204,7 @@ def tag(self, new_tag_val):
         if not self.mutable:
             raise ValueError("Trying to alter immutable tag")
 
-        if self._long_tag:
+        if self._schema_entry:
             raise ValueError("Can only edit tags before calculating canonical forms. " +
                              "This could be updated to instead remove computed forms.")
         self._tag = new_tag_val
@@ -243,10 +220,10 @@ def extension_or_value_portion(self):
         str
             The tag name.
         """
-        if self._extension_index is None:
-            return str(self)
+        if self._extension_value:
+            return self._extension_value[1:]
 
-        return self._long_tag[self._extension_index + 1:]
+        return ""
 
     @property
     def long_tag(self):
@@ -258,9 +235,9 @@ def long_tag(self):
         long_tag: str
             The long form of this tag.
         """
-        if self._long_tag is None:
-            return str(self)
-        return self._long_tag
+        if self._schema_entry:
+            return f"{self._library_prefix}{self._schema_entry.long_tag_name}{self._extension_value}"
+        return str(self)
 
     @property
     def org_tag(self):
@@ -283,8 +260,8 @@ def __str__(self):
         str
             Return the original tag if we haven't set a new tag.(e.g. short to long)
         """
-        if self._long_tag:
-            return self._long_tag
+        if self._schema_entry:
+            return self.long_tag
 
         if self._tag:
             return self._tag
@@ -316,8 +293,8 @@ def add_prefix_if_not_present(self, required_prefix):
         required_prefix : str
             The full name_prefix to add if not present
         """
-        if not self.mutable:
-            raise ValueError("Trying to alter immutable tag")
+        # if not self.mutable:
+        #     raise ValueError("Trying to alter immutable tag")
 
         checking_prefix = required_prefix
         while checking_prefix:
@@ -327,32 +304,13 @@ def add_prefix_if_not_present(self, required_prefix):
             if slash_index == 0:
                 break
             checking_prefix = checking_prefix[slash_index:]
-        self._tag = required_prefix + self.org_tag
+        self.tag = required_prefix + self.org_tag
+        # self._tag = required_prefix + self.org_tag
 
     def lower(self):
         """Convenience function, equivalent to str(self).lower()"""
         return str(self).lower()
 
-    def replace_placeholder(self, placeholder_value):
-        """
-            If this tag a placeholder character(#), replace them with the placeholder value.
-
-        Parameters
-        ----------
-        placeholder_value : str
-            Value to replace placeholder with.
-        """
-        if not self.mutable:
-            raise ValueError("Trying to alter immutable tag")
-
-        if "#" in self.org_tag:
-            if self._long_tag:
-                # This could possibly be more efficient
-                self._tag = self.org_tag.replace("#", placeholder_value)
-                self._long_tag = self._long_tag.replace("#", placeholder_value)
-            else:
-                self._tag = self.org_tag.replace("#", placeholder_value)
-
     def convert_to_canonical_forms(self, hed_schema):
         """
             This updates the internal tag states from the schema, and sets the schema entry.
@@ -373,15 +331,9 @@ def convert_to_canonical_forms(self, hed_schema):
         tag_entry, remainder, tag_issues = hed_schema.find_tag_entry(self, self.library_prefix)
         self._schema_entry = tag_entry
         self._schema = hed_schema
-        # todo: now that we always have a schema entry, have these get values from those instead.
         if self._schema_entry:
-            tag_name = tag_entry.long_tag_name
-            self._long_tag = self.library_prefix + tag_name + remainder
-            self._short_tag_index = len(self.library_prefix) \
-                                    + len(tag_entry.long_tag_name) - len(tag_entry.short_tag_name)
-            self._extension_index = len(self.library_prefix) + len(tag_name)
-        else:
-            self._long_tag = remainder
+            if remainder:
+                self._extension_value = remainder
 
         return tag_issues
 
@@ -620,66 +572,24 @@ def any_parent_has_attribute(self, attribute):
         if self._schema_entry:
             return self._schema_entry.any_parent_has_attribute(attribute=attribute)
 
-    def _update_schema_entry(self):
-        """
-            Sets the _schema_entry pointer based off the current schema.
-
-        Returns
-        -------
-
-        """
-        new_entry = None
-        if self.extension_or_value_portion:
-            new_entry = self._schema.get_tag_entry(self.base_tag.lower() + "/#")
-        if new_entry is None:
-            new_entry = self._schema.get_tag_entry(self.base_tag.lower())
-        self._schema_entry = new_entry
-
     def _convert_key_tags_to_canonical_form(self):
         """
             Finds the canonical form for basic known tags such as definition and def.
 
-            todo: This should have default HedTagEntries to point to eventually
         Returns
         -------
         issues_list: [{}]
             Always empty.
         """
         tags_to_identify = ["onset", "definition", "offset", "def-expand", "def"]
-        for key_tag in tags_to_identify:
-            is_key_tag = self._check_tag_starts_with(str(self), key_tag)
-            if is_key_tag is not None:
-                self._long_tag = str(self)
-                self._extension_index = len(str(self)) - len(is_key_tag)
-                self._short_tag_index = self._extension_index - len(key_tag)
-                break
+        tag_entry, remainder = HedTagEntry.get_fake_tag_entry(str(self), tags_to_identify)
+        if tag_entry:
+            self._schema_entry = tag_entry
+            self._schema = None
+            self._extension_value = remainder
 
         return []
 
-    @staticmethod
-    def _check_tag_starts_with(hed_tag, target_tag_short_name):
-        """ Check if a given tag starts with a given string, and returns the tag with name_prefix removed if it does.
-
-        Parameters
-        ----------
-        hed_tag : str
-            A single input tag
-        target_tag_short_name : str
-            The string to match e.g. find target_tag_short_name in hed_tag
-        Returns
-        -------
-            str: the tag without the removed name_prefix, or None
-        """
-        hed_tag_lower = hed_tag.lower()
-        found_index = hed_tag_lower.find(target_tag_short_name)
-
-        if found_index == -1:
-            return None
-
-        if found_index == 0 or hed_tag_lower[found_index - 1] == "/":
-            return hed_tag[found_index + len(target_tag_short_name):]
-        return None
-
     def _get_library_prefix(self, org_tag):
         first_slash = org_tag.find("/")
         first_colon = org_tag.find(":")
@@ -732,3 +642,45 @@ def _find_modifier_unit_entry(units, all_valid_unit_permutations):
                 possible_match = None
 
         return possible_match
+
+    def replace_placeholder(self, placeholder_value):
+        """
+            If this tag a placeholder character(#), replace them with the placeholder value.
+
+        Parameters
+        ----------
+        placeholder_value : str
+            Value to replace placeholder with.
+        """
+        # if not self.mutable:
+        #     raise ValueError("Trying to alter immutable tag")
+
+        if "#" in self.org_tag:
+            if self._schema_entry:
+                self._extension_value = self._extension_value.replace("#", placeholder_value)
+            else:
+                self._tag = self.tag.replace("#", placeholder_value)
+
+    def __hash__(self):
+        if self._schema_entry:
+            return hash(
+                self._library_prefix + self._schema_entry.short_tag_name.lower() + self._extension_value.lower())
+        else:
+            return hash(self.lower())
+
+    def __eq__(self, other):
+        if self is other:
+            return True
+
+        if isinstance(other, str):
+            return self.lower() == other
+
+        if not isinstance(other, HedTag):
+            return False
+
+        if self.short_tag.lower() == other.short_tag.lower():
+            return True
+
+        if self.org_tag.lower() == other.org_tag.lower():
+            return True
+        return False
diff --git a/hed/models/onset_mapper.py b/hed/models/onset_mapper.py
index fe49823d2..8363062f1 100644
--- a/hed/models/onset_mapper.py
+++ b/hed/models/onset_mapper.py
@@ -31,19 +31,21 @@ def check_for_onset_offset(self, hed_string_obj):
             if not found_onset:
                 return []
 
-            def_tags, def_groups = self._find_def_tags(found_group)
+            def_tags = found_group.find_def_tags()
             if not def_tags:
                 onset_issues += ErrorHandler.format_error(OnsetErrors.ONSET_NO_DEF_TAG_FOUND, found_onset)
                 continue
 
             if len(def_tags) > 1:
                 onset_issues += ErrorHandler.format_error(OnsetErrors.ONSET_TOO_MANY_DEFS,
-                                                          tag=def_tags[0],
-                                                          tag_list=[tag for tag in def_tags[1:]])
+                                                          tag=def_tags[0][0],
+                                                          tag_list=[tag[0] for tag in def_tags[1:]])
                 continue
 
-            def_tag = def_tags[0]
-            def_group = def_groups[0]
+            # Get all children but def group and onset/offset, then validate #/type of children.
+            def_tag, def_group, _ = def_tags[0]
+            if def_group is None:
+                def_group = def_tag
             children = [child for child in found_group.get_direct_children() if
                         def_group is not child and found_onset is not child]
             max_children = 1
@@ -69,30 +71,7 @@ def check_for_onset_offset(self, hed_string_obj):
         return onset_issues
 
     def _find_onset_tags(self, hed_string_obj):
-        for group in hed_string_obj.groups():
-            for tag in group.tags():
-                if tag.short_base_tag.lower() == DefTagNames.ONSET_KEY \
-                        or tag.short_base_tag.lower() == DefTagNames.OFFSET_KEY \
-                        and not tag.extension_or_value_portion:
-                    yield tag, group
-                    # only return one onset tag per group
-                    break
-
-    def _find_def_tags(self, onset_group):
-        def_tags = []
-        def_groups = []
-        for child in onset_group.get_direct_children():
-            if isinstance(child, HedTag):
-                if child.short_base_tag.lower() == DefTagNames.DEF_KEY:
-                    def_tags.append(child)
-                    def_groups.append(child)
-            else:
-                for tag in child.tags():
-                    if tag.short_base_tag.lower() == DefTagNames.DEF_EXPAND_KEY:
-                        def_tags.append(tag)
-                        def_groups.append(child)
-
-        return def_tags, def_groups
+        return hed_string_obj.find_top_level_tags(anchors={DefTagNames.ONSET_KEY, DefTagNames.OFFSET_KEY})
 
     def _handle_onset_or_offset(self, def_tag, onset_offset_tag):
         is_onset = onset_offset_tag.short_base_tag.lower() == DefTagNames.ONSET_KEY
diff --git a/hed/schema/hed_cache.py b/hed/schema/hed_cache.py
index cb4aff71f..af928db90 100644
--- a/hed/schema/hed_cache.py
+++ b/hed/schema/hed_cache.py
@@ -30,9 +30,9 @@
 
 DEFAULT_SKIP_FOLDERS = ('deprecated', )
 
-HED_CACHE_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../validator/hed_cache/')
+HED_CACHE_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../validator/hed_cache/')
 TIMESTAMP_FILENAME = "last_update.txt"
-CACHE_TIME_THRESHOLD = 300
+CACHE_TIME_THRESHOLD = 300 * 6
 
 version_pattern = re.compile(HED_VERSION_FINAL)
 
diff --git a/hed/schema/hed_schema.py b/hed/schema/hed_schema.py
index d13b05f5a..7c68ca3de 100644
--- a/hed/schema/hed_schema.py
+++ b/hed/schema/hed_schema.py
@@ -465,16 +465,16 @@ def find_tag_entry(self, tag, library_prefix=""):
                     error = ErrorHandler.format_error(ValidationErrors.INVALID_PARENT_NODE,
                                                       tag,
                                                       index_in_tag=len(parent_name) + 1 + prefix_tag_adj,
-                                                      index_in_tag_end=len(parent_name) + 1 + len(
-                                                          child_name) + prefix_tag_adj,
+                                                      index_in_tag_end=len(parent_name) + 1 +
+                                                                       len(child_name) + prefix_tag_adj,
                                                       expected_parent_tag=self.all_tags[child_name].name)
                     return None, None, error
                 if not parent_name:
                     error = ErrorHandler.format_error(ValidationErrors.NO_VALID_TAG_FOUND,
                                                       tag,
                                                       index_in_tag=len(parent_name) + prefix_tag_adj,
-                                                      index_in_tag_end=len(parent_name) + len(
-                                                          child_name) + prefix_tag_adj)
+                                                      index_in_tag_end=len(parent_name)
+                                                                       + len(child_name) + prefix_tag_adj)
                     return None, None, error
                 working_tag = parent_name
                 remainder = clean_tag[len(working_tag):][:len(child_name) + 1] + remainder
diff --git a/hed/schema/hed_schema_entry.py b/hed/schema/hed_schema_entry.py
index f6ad5a34a..032938708 100644
--- a/hed/schema/hed_schema_entry.py
+++ b/hed/schema/hed_schema_entry.py
@@ -10,7 +10,7 @@
 class HedSchemaEntry:
     """ Represents a single node in a HedSchema.
 
-        The structure contains all of the node information including attributes
+        The structure contains all the node information including attributes
         and properties.
     """
     def __init__(self, name, section):
@@ -116,6 +116,9 @@ def __eq__(self, other):
             return False
         return True
 
+    def __hash__(self):
+        return hash((self.name, self._section._section_key))
+
 
 class UnitClassEntry(HedSchemaEntry):
     """
@@ -191,9 +194,39 @@ def __init__(self, *args, **kwargs):
         # These always have any /# stripped off the end, so they can easily be used with normal code.
         self.long_tag_name = None
         self.short_tag_name = None
-        self.takes_value_child_entry = None # this is a child takes value tag, if one exists
+        self.takes_value_child_entry = None  # this is a child takes value tag, if one exists
         self._parent_tag = None
 
+    @staticmethod
+    def get_fake_tag_entry(tag, tags_to_identify):
+        """
+            Given a tag and a list of possible short tags, create a tag entry if we find a match.
+
+        Args:
+            tag: str
+                The short/mid/long form tag to identify
+            tags_to_identify: [str]
+                A list of lowercase short tags to identify.  Goes through tag left to right.
+
+        Returns:
+        tag_entry: HedTagEntry or None
+            The fake entry, showing the short tag name as the found tag
+        remainder: str
+            The remaining text after the located short tag.  May be empty.
+        """
+        split_names = tag.split("/")
+        index = 0
+        for name in split_names:
+            if name.lower() in tags_to_identify:
+                fake_entry = HedTagEntry(name=tag[:index + len(name)], section=None)
+                fake_entry.long_tag_name = fake_entry.name
+                fake_entry.short_tag_name = name
+                return fake_entry, tag[index + len(name):]
+
+            index += len(name) + 1
+
+        return None, ""
+
     def any_parent_has_attribute(self, attribute):
         """Checks to see if the tag (or any of its parents) have the given attribute.
 
diff --git a/hed/schema/hed_schema_group.py b/hed/schema/hed_schema_group.py
index 240a7fbbf..849c3f070 100644
--- a/hed/schema/hed_schema_group.py
+++ b/hed/schema/hed_schema_group.py
@@ -152,7 +152,7 @@ def get_tag_entry(self, name, key_class=HedSectionKey.AllTags, library_prefix=""
         """
         specific_schema = self.schema_for_prefix(library_prefix)
         if not specific_schema:
-            validation_issues = ErrorHandler.format_error(ValidationErrors.HED_UNKNOWN_PREFIX, name,
+            validation_issues = ErrorHandler.format_error(ValidationErrors.HED_LIBRARY_UNMATCHED, name,
                                                           library_prefix, self.valid_prefixes)
             return None, None, validation_issues
 
@@ -182,7 +182,7 @@ def find_tag_entry(self, tag, library_prefix=""):
         """
         specific_schema = self.schema_for_prefix(library_prefix)
         if not specific_schema:
-            validation_issues = ErrorHandler.format_error(ValidationErrors.HED_UNKNOWN_PREFIX, tag,
+            validation_issues = ErrorHandler.format_error(ValidationErrors.HED_LIBRARY_UNMATCHED, tag,
                                                           library_prefix, self.valid_prefixes)
             return None, None, validation_issues
 
diff --git a/hed/schema/io/wiki2schema.py b/hed/schema/io/wiki2schema.py
index 78a87bd86..63eb2e07f 100644
--- a/hed/schema/io/wiki2schema.py
+++ b/hed/schema/io/wiki2schema.py
@@ -71,7 +71,7 @@ class HedWikiSection:
 class HedSchemaWikiParser:
     def __init__(self, wiki_file_path, schema_as_string):
         self.filename = wiki_file_path
-        self.warnings = []
+        self.fatal_errors = []
         self._schema = HedSchema()
         self._schema.filename = wiki_file_path
 
@@ -90,11 +90,11 @@ def __init__(self, wiki_file_path, schema_as_string):
         except FileNotFoundError as e:
             raise HedFileError(HedExceptions.FILE_NOT_FOUND, e.strerror, wiki_file_path)
 
-        if self.warnings:
-            raise HedFileError(HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-                               f"{len(self.warnings)} issues found when parsing schema.  See the .issues "
+        if self.fatal_errors:
+            raise HedFileError(HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+                               f"{len(self.fatal_errors)} issues found when parsing schema.  See the .issues "
                                f"parameter on this exception for more details.", self.filename,
-                               issues=self.warnings)
+                               issues=self.fatal_errors)
         self._schema.finalize_dictionaries()
 
     @staticmethod
@@ -223,7 +223,7 @@ def _read_header_section(self, lines):
         for line in lines:
             if line.strip():
                 msg = f"Extra content [{line}] HED line and other sections"
-                raise HedFileError(HedExceptions.SCHEMA_HEADER_INVALID, msg,  filename=self.filename, issues=[msg])
+                raise HedFileError(HedExceptions.HED_SCHEMA_HEADER_INVALID, msg,  filename=self.filename, issues=[msg])
 
     def _read_text_block(self, lines):
         text = ""
@@ -273,12 +273,12 @@ def _read_schema(self, lines):
                 if level < len(parent_tags):
                     parent_tags = parent_tags[:level]
                 elif level > len(parent_tags):
-                    self._add_warning(line, "Line has too many *'s at the front.  You cannot skip a level.")
+                    self._add_fatal_error(line, "Line has too many *'s at the front.  You cannot skip a level.")
                     continue
             new_tag_name = self._add_tag_line(parent_tags, line)
             if not new_tag_name:
                 if new_tag_name != "":
-                    self._add_warning(line)
+                    self._add_fatal_error(line)
                 continue
 
             parent_tags.append(new_tag_name)
@@ -295,7 +295,7 @@ def _read_unit_classes(self, lines):
         for line in lines:
             unit_class_unit, _ = self._get_tag_name(line)
             if unit_class_unit is None:
-                self._add_warning(line)
+                self._add_fatal_error(line)
                 continue
             level = self._get_tag_level(line)
             # This is a unit class
@@ -379,7 +379,7 @@ def _get_header_attributes_old(self, version_line):
             divider_index = pair.find(':')
             if divider_index == -1:
                 msg = f"Found poorly matched key:value pair in header: {pair}"
-                raise HedFileError(HedExceptions.SCHEMA_HEADER_INVALID, msg, filename=self.filename, issues=[msg])
+                raise HedFileError(HedExceptions.HED_SCHEMA_HEADER_INVALID, msg, filename=self.filename, issues=[msg])
             key, value = pair[:divider_index], pair[divider_index + 1:]
             key = key.strip()
             value = value.strip()
@@ -424,9 +424,9 @@ def _remove_nowiki_tag_from_line(self, tag_line):
         index1 = tag_line.find(no_wiki_start_tag)
         index2 = tag_line.find(no_wiki_end_tag)
         if (index1 == -1 and index2 != -1) or (index2 == -1 and index1 != -1):
-            self._add_warning(tag_line, "Invalid or non matching <nowiki> tags found")
+            self._add_fatal_error(tag_line, "Invalid or non matching <nowiki> tags found")
         elif index1 != -1 and index2 != -1 and index2 <= index1:
-            self._add_warning(tag_line, "</nowiki> appears before <nowiki> on a line")
+            self._add_fatal_error(tag_line, "</nowiki> appears before <nowiki> on a line")
         tag_line = re.sub(no_wiki_tag, '', tag_line)
         return tag_line
 
@@ -561,19 +561,19 @@ def _add_tag_line(self, parent_tags, tag_line):
     def _add_single_line(self, tag_line, key_class, element_name=None):
         node_name, index = self._get_tag_name(tag_line)
         if node_name is None:
-            self._add_warning(tag_line)
+            self._add_fatal_error(tag_line)
             return
         if element_name:
             node_name = element_name
 
         node_attributes, index = self._get_tag_attributes(tag_line, index)
         if node_attributes is None:
-            self._add_warning(tag_line, "Attributes has mismatched delimiters")
+            self._add_fatal_error(tag_line, "Attributes has mismatched delimiters")
             return
 
         node_desc, _ = self._get_line_section(tag_line, index)
         if node_desc is None:
-            self._add_warning(tag_line, "Description has mismatched delimiters")
+            self._add_fatal_error(tag_line, "Description has mismatched delimiters")
             return
 
         tag_entry = self._schema._add_tag_to_dict(node_name, key_class)
@@ -585,6 +585,6 @@ def _add_single_line(self, tag_line, key_class, element_name=None):
 
         return tag_entry
 
-    def _add_warning(self, line, warning_message="Schema term is empty or the line is malformed"):
-        self.warnings.append((line, warning_message))
+    def _add_fatal_error(self, line, warning_message="Schema term is empty or the line is malformed"):
+        self.fatal_errors.append((line, warning_message))
         return
diff --git a/hed/schema/schema_validation_util.py b/hed/schema/schema_validation_util.py
index 5463be02e..d96a24f05 100644
--- a/hed/schema/schema_validation_util.py
+++ b/hed/schema/schema_validation_util.py
@@ -34,7 +34,7 @@ def is_hed3_version_number(version_string):
 
 
 attribute_validators = {
-        "version": (_validate_version_string, HedExceptions.BAD_HED_SEMANTIC_VERSION),
+        "version": (_validate_version_string, HedExceptions.HED_SCHEMA_VERSION_INVALID),
         "library": (_validate_library_name, HedExceptions.BAD_HED_LIBRARY_NAME)
     }
 
@@ -48,5 +48,5 @@ def validate_attributes(attrib_dict, filename):
                 raise HedFileError(error_code, result, filename)
 
     if constants.VERSION_ATTRIBUTE not in attrib_dict:
-        raise HedFileError(HedExceptions.BAD_HED_SEMANTIC_VERSION, "No version attribute found in header",
+        raise HedFileError(HedExceptions.HED_SCHEMA_VERSION_INVALID, "No version attribute found in header",
                            filename=filename)
diff --git a/hed/tools/__init__.py b/hed/tools/__init__.py
index 4183f8200..8bb3373bf 100644
--- a/hed/tools/__init__.py
+++ b/hed/tools/__init__.py
@@ -1,21 +1,26 @@
 """ HED tools for analysis and summarization. """
 
-from hed.tools.annotation.annotation_util import \
+
+from .annotation.file_dictionary import FileDictionary
+from .annotation.key_map import KeyMap
+from .annotation.key_template import KeyTemplate
+from .annotation.map_summary import get_columns_info, get_key_counts, update_dict_counts
+from .annotation.summary_entry import SummaryEntry
+from .annotation.tag_summary import TagSummary
+from .annotation.annotation_util import \
     check_df_columns, extract_tag, generate_sidecar_entry, hed_to_df, df_to_hed, merge_hed_dict
+
 from .bids.bids_dataset import BidsDataset
+from .bids.bids_dataset_summary import BidsDatasetSummary
 from .bids.bids_event_file import BidsEventFile
 from .bids.bids_event_files import BidsEventFiles
 from .bids.bids_file import BidsFile
+from .bids.bids_file_dictionary import BidsFileDictionary
 from .bids.bids_json_file import BidsJsonFile
 from .bids.bids_sidecar_file import BidsSidecarFile
+from .bids.bids_tsv_dictionary import BidsTsvDictionary
 from .bids.bids_tsv_file import BidsTsvFile
+from .bids.bids_tsv_summary import BidsTsvSummary
 
-from .annotation.column_summary import ColumnSummary
-from .annotation.dataset_summary import DatasetSummary
-from .annotation.key_map import KeyMap
-from .annotation.key_template import KeyTemplate
-from .annotation.map_summary import get_columns_info
-from .annotation.summary_entry import SummaryEntry
-from .annotation.tag_summary import TagSummary
 
 from .hed_logger import HedLogger
diff --git a/hed/tools/annotation/file_dictionary.py b/hed/tools/annotation/file_dictionary.py
new file mode 100644
index 000000000..0d8785065
--- /dev/null
+++ b/hed/tools/annotation/file_dictionary.py
@@ -0,0 +1,78 @@
+import os
+from hed.errors.exceptions import HedFileError
+from hed.util.data_util import get_new_dataframe
+from hed.tools.annotation.annotation_util import generate_sidecar_entry
+
+
+class FileDictionary:
+    """Holds a dictionary of path names keyed by specified entity pairs. """
+
+    def __init__(self, file_list, name_indices=(0, 2), separator='_'):
+        """ Create a dictionary with keys that are simplified file names and values that are full paths
+
+        This function is used for cross listing BIDS style files for different studies.
+
+        Args:
+            file_list (list):      List containing full paths of files of interest
+            name_indices (tuple):  List of indices into base file names of pieces to assemble for the key
+            separator (str):       Character used to separate pieces of key name
+        """
+        if name_indices:
+            self.file_dict = self.make_file_dict(file_list, name_indices=name_indices, separator=separator)
+        else:
+            self.file_dict = {}
+
+    @property
+    def key_list(self):
+        return list(self.file_dict.keys())
+
+    @property
+    def file_list(self):
+        return list(self.file_dict.values())
+
+    def get_file(self, key):
+        return self.file_dict.get(key, None)
+
+    def iter_files(self):
+        for key, file in self.file_dict.items():
+            yield key, file
+
+    def key_diffs(self, other_dict):
+        """Returns a list containing the symmetric difference of the keys in the two dictionaries
+
+        Args:
+            other_dict (FileDictionary)  A file dictionary object
+
+        Returns: list
+            A list of the symmetric difference of the keys in this dictionary and the other one.
+
+        """
+        diffs = set(self.file_dict.keys()).symmetric_difference(set(other_dict.file_dict.keys()))
+        return list(diffs)
+
+    def print_files(self, title=None):
+        if title:
+            print(f"{title} ({len(self.key_list)} files)")
+        for key, value in self.file_dict.items():
+            print(f"{key}: {os.path.basename(value)}")
+
+    @staticmethod
+    def make_file_dict(file_list, name_indices=(0, 2), separator='_'):
+        file_dict = {}
+        for the_file in file_list:
+            the_file = os.path.realpath(the_file)
+            base = os.path.basename(the_file)
+            key = FileDictionary.make_key(base, indices=name_indices, separator=separator)
+            if key in file_dict:
+                raise HedFileError("NonUniqueFileKeys",
+                                   f"dictionary key {key} is associated with {the_file} and {file_dict[key]}", "")
+            file_dict[key] = the_file
+        return file_dict
+
+    @staticmethod
+    def make_key(key_string, indices=(0, 2), separator='_'):
+        key_value = ''
+        pieces = key_string.split(separator)
+        for index in list(indices):
+            key_value += pieces[index] + separator
+        return key_value[:-1]
diff --git a/hed/tools/annotation/map_summary.py b/hed/tools/annotation/map_summary.py
index 0afad91e9..826689e7b 100644
--- a/hed/tools/annotation/map_summary.py
+++ b/hed/tools/annotation/map_summary.py
@@ -1,5 +1,4 @@
 """ Utilities for extracting counts of items in columns. """
-from hed.tools.annotation.column_summary import ColumnSummary
 from hed.util.io_util import get_file_list
 from hed.util.data_util import get_new_dataframe
 
@@ -35,28 +34,6 @@ def get_key_counts(root_dir, skip_cols=None):
     return count_dicts
 
 
-def make_combined_dicts(file_dict, skip_cols=None):
-    """ Return a combined dictionary of column information as we
-
-    Args:
-        file_dict (dict):  Dictionary of file name keys and full path
-        skip_cols (list):  Name of the column
-
-    Returns:
-        dict:  A combined dictionary
-    """
-
-    dicts_all = ColumnSummary(skip_cols=skip_cols)
-    dicts = {}
-    for key, file in file_dict.items():
-        orig_dict = ColumnSummary(skip_cols=skip_cols)
-        df = get_new_dataframe(file)
-        orig_dict.update(df)
-        dicts[key] = orig_dict
-        dicts_all.update_dict(orig_dict)
-    return dicts_all, dicts
-
-
 # def print_columns_info(columns_info, skip_cols=None):
 #
 #     for col_name, col_counts in columns_info.items():
diff --git a/hed/tools/annotation/summary_entry.py b/hed/tools/annotation/summary_entry.py
index f1811e738..061b061cf 100644
--- a/hed/tools/annotation/summary_entry.py
+++ b/hed/tools/annotation/summary_entry.py
@@ -96,34 +96,50 @@ def separate_anchored_groups(hed_string_obj, entry_dict, anchor_name=DefTagNames
 
 
 if __name__ == '__main__':
+    from hed.models import DefDict
+    from hed.models.def_dict import add_group_to_dict
     the_path = '../../../tests/data/bids/eeg_ds003654s_hed/task-FacePerception_events.json'
     sidecar = Sidecar(the_path)
     schema = load_schema_version(xml_version='8.0.0')
 
-    tag_dict = {}
-    no_defs_list = []
-    def_dict = {}
-    for column in sidecar:
-        for hed, key in column._hed_iter():
-            hed.convert_to_canonical_forms(schema)
-            no_defs_list += SummaryEntry.separate_anchored_groups(hed, def_dict, DefTagNames.DEFINITION_KEY)
-    add_tag_list_to_dict(no_defs_list, tag_dict)
-
-    hed_list = []
-    for column in sidecar:
-        for hed, key in column._hed_iter():
-            hed.convert_to_canonical_forms(schema)
-            hed_list.append(hed)
-
-    only_dict = {}
-    add_tag_list_to_dict(hed_list, only_dict )
-    print(only_dict.keys())
+    tag_dict_no_defs = {}
+    tag_dict_with_defs = {}
+    def_dict = DefDict()
+    for hed_string_obj, _, _ in sidecar.hed_string_iter([schema, def_dict]):
+        add_group_to_dict(hed_string_obj, tag_dict_with_defs)
+        hed_string_obj.remove_definitions()
+        add_group_to_dict(hed_string_obj, tag_dict_no_defs)
+
+    # tag_dict = {}
+    # no_defs_list = []
+    # def_dict = {}
+    # for column in sidecar:
+    #     for hed, key in column._hed_iter():
+    #         hed.convert_to_canonical_forms(schema)
+    #         no_defs_list += SummaryEntry.separate_anchored_groups(hed, def_dict, DefTagNames.DEFINITION_KEY)
+    # add_tag_list_to_dict(no_defs_list, tag_dict)
+    #
+    # hed_list = []
+    # for column in sidecar:
+    #     for hed, key in column._hed_iter():
+    #         hed.convert_to_canonical_forms(schema)
+    #         hed_list.append(hed)
+    #
+    # only_dict = {}
+    # add_tag_list_to_dict(hed_list, only_dict )
+    # print(only_dict.keys())
+    #
+    # # Todo: remove this block
+    # if tag_dict_with_defs != only_dict:
+    #     raise ValueError("def dict!")
+    # if tag_dict_no_defs != tag_dict:
+    #     raise ValueError("tag dict!")
 
     json_path = "../../../tests/data/summaries/tag_summary_template.json5"
     with open(json_path) as fp:
         rules = json.load(fp)
     breakout_list = rules["Tag-categories"]
-    breakout_dict = breakout_tags(schema, only_dict.keys(), breakout_list)
+    breakout_dict = breakout_tags(schema, tag_dict_with_defs.keys(), breakout_list)
     with open('d:/temp1.json', "w") as fp:
         json.dump(breakout_dict, fp, indent=4)
 
diff --git a/hed/tools/annotation/tag_summary.py b/hed/tools/annotation/tag_summary.py
index d7092c444..c4bed8103 100644
--- a/hed/tools/annotation/tag_summary.py
+++ b/hed/tools/annotation/tag_summary.py
@@ -3,8 +3,11 @@
 import json
 from hed.models.model_constants import DefTagNames
 from hed.tools.bids.bids_dataset import BidsDataset
+
 from hed.tools.annotation.summary_entry import SummaryEntry
 from hed.tools.annotation.summary_util import add_tag_list_to_dict, breakout_tags
+from hed.models.def_dict import add_group_to_dict
+from hed.models import DefDict
 
 DEFAULT_BREAKOUT_LIST = [
     "Sensory-event", "Agent-action", "Event", "Action", "Task-event-role", "Task-action-type",
@@ -18,7 +21,6 @@ class TagSummary:
     def __init__(self, dataset, breakout_list=None):
         self.dataset = dataset
         self.all_tags_dict = {}
-        self.all_defs_dict = {}
         self._set_all_tags()
         self.breakout_list = breakout_list
         if not breakout_list:
@@ -28,21 +30,15 @@ def __init__(self, dataset, breakout_list=None):
         self.cond_dict = SummaryEntry.extract_summary_info(self.all_defs_dict, 'Condition-variable')
 
     def _set_all_tags(self):
-        self.all_tags_dict = {}
-        self.all_defs_dict = {}
         schema = self.dataset.schemas
+        def_dict = DefDict()
+        self.all_defs_dict = def_dict.defs
         for bids_sidecar in self.dataset.event_files.sidecar_dict.values():
             sidecar = bids_sidecar.contents
-            tag_list = []
-            for column in sidecar:
-                for hed, key in column._hed_iter():
-                    hed.convert_to_canonical_forms(schema)
-                    tag_list.append(hed)
-                    SummaryEntry.separate_anchored_groups(hed, self.all_defs_dict, DefTagNames.DEFINITION_KEY)
-            add_tag_list_to_dict(tag_list, self.all_tags_dict, hed_schema=schema)
+            for hed_string_obj, _, _ in sidecar.hed_string_iter([schema, def_dict]):
+                add_group_to_dict(hed_string_obj, self.all_tags_dict)
 
     def get_design_matrices(self):
-
         cond_dict = self.cond_dict
         design_dict = {}
         other_list = []
@@ -65,7 +61,7 @@ def get_design_matrices(self):
 
 
 if __name__ == '__main__':
-    root_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+    root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                              '../../../tests/data/bids/eeg_ds003654s_hed')
 
     json_path = "../../../tests/data/summaries/tag_summary_template.json5"
diff --git a/hed/tools/bids/bids_dataset.py b/hed/tools/bids/bids_dataset.py
index 54a699229..c83e5f060 100644
--- a/hed/tools/bids/bids_dataset.py
+++ b/hed/tools/bids/bids_dataset.py
@@ -15,7 +15,7 @@ class BidsDataset:
     """Represents the metadata for a BIDS dataset."""
 
     def __init__(self, root_path):
-        self.root_path = os.path.abspath(root_path)
+        self.root_path = os.path.realpath(root_path)
         with open(os.path.join(self.root_path, "dataset_description.json"), "r") as fp:
             self.dataset_description = json.load(fp)
         self.participants = get_new_dataframe(os.path.join(self.root_path, "participants.tsv"))
@@ -63,12 +63,12 @@ def get_schema_versions(self):
 
 
 if __name__ == '__main__':
-    path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+    path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                         '../../../tests/data/bids/eeg_ds003654s_hed_library')
     bids = BidsDataset(path)
     issue_list = bids.validate()
     if issue_list:
-        issue_str = get_printable_issue_string(issue_list, "HED_library")
+        issue_str = get_printable_issue_string(issue_list, "HED validation errors:")
     else:
         issue_str = "No issues"
     print(issue_str)
diff --git a/hed/tools/annotation/dataset_summary.py b/hed/tools/bids/bids_dataset_summary.py
similarity index 89%
rename from hed/tools/annotation/dataset_summary.py
rename to hed/tools/bids/bids_dataset_summary.py
index 080de0cbe..2f5c07bec 100644
--- a/hed/tools/annotation/dataset_summary.py
+++ b/hed/tools/bids/bids_dataset_summary.py
@@ -1,9 +1,7 @@
-
-
 from hed.tools.bids.bids_dataset import BidsDataset
 
 
-class DatasetSummary:
+class BidsDatasetSummary:
     """ Summarizes a BIDS dataset. """
 
     def __init__(self, dataset):
diff --git a/hed/tools/bids/bids_event_file.py b/hed/tools/bids/bids_event_file.py
index 9e88b4390..7227c8002 100644
--- a/hed/tools/bids/bids_event_file.py
+++ b/hed/tools/bids/bids_event_file.py
@@ -7,7 +7,7 @@ class BidsEventFile(BidsTsvFile):
     """Represents a BIDS event file including its associated sidecars."""
 
     def __init__(self, file_path):
-        super().__init__(os.path.abspath(file_path), set_contents=False)
+        super().__init__(file_path, set_contents=False)
         self.contents = None
         self.sidecars = None
 
@@ -16,7 +16,7 @@ def clear_contents(self):
 
     def set_contents(self):
         self.contents = EventsInput(file=self.file_path, sidecars=self.sidecars,
-                                    name=os.path.abspath(self.file_path))
+                                    name=os.path.realpath(self.file_path))
 
     def set_sidecars(self, sidecars):
         if sidecars is None:
diff --git a/hed/tools/bids/bids_event_files.py b/hed/tools/bids/bids_event_files.py
index a08591e57..56a05b9e6 100644
--- a/hed/tools/bids/bids_event_files.py
+++ b/hed/tools/bids/bids_event_files.py
@@ -1,19 +1,17 @@
 import os
-from hed.errors.error_reporter import get_printable_issue_string
 from hed.schema.hed_schema_io import load_schema_version
 from hed.tools.bids.bids_event_file import BidsEventFile
 from hed.tools.bids.bids_sidecar_file import BidsSidecarFile
-from hed.tools.annotation.column_summary import ColumnSummary
+from hed.tools.bids.bids_tsv_summary import BidsTsvSummary
 from hed.models.events_input import EventsInput
 from hed.util.io_util import get_dir_dictionary, get_file_list, get_path_components
-from hed.validator.hed_validator import HedValidator
 
 
 class BidsEventFiles:
     """ Container for the event files and sidecars in a BIDS dataset."""
 
     def __init__(self, root_path):
-        self.root_path = os.path.abspath(root_path)
+        self.root_path = os.path.realpath(root_path)
         self.sidecar_dict = {}  # Dict uses os.path.abspath(file) as key
         self.events_dict = {}
         self.sidecar_dir_dict = {}
@@ -29,7 +27,7 @@ def _set_event_file_dict(self):
         files = get_file_list(self.root_path, name_suffix='_events', extensions=['.tsv'])
         file_dict = {}
         for file in files:
-            file_dict[os.path.abspath(file)] = BidsEventFile(file)
+            file_dict[os.path.realpath(file)] = BidsEventFile(file)
         self.events_dict = file_dict
 
     def _set_sidecar_dict(self):
@@ -37,7 +35,7 @@ def _set_sidecar_dict(self):
         files = get_file_list(self.root_path, name_suffix='_events', extensions=['.json'])
         file_dict = {}
         for file in files:
-            file_dict[os.path.abspath(file)] = BidsSidecarFile(os.path.abspath(file), set_contents=True)
+            file_dict[os.path.realpath(file)] = BidsSidecarFile(os.path.realpath(file), set_contents=True)
         self.sidecar_dict = file_dict
 
     def _set_sidecar_dir_dict(self):
@@ -47,15 +45,15 @@ def _set_sidecar_dir_dict(self):
         for this_dir, dir_list in dir_dict.items():
             new_dir_list = []
             for s_file in dir_list:
-                new_dir_list.append(self.sidecar_dict[os.path.abspath(s_file)])
-            sidecar_dir_dict[os.path.abspath(this_dir)] = new_dir_list
+                new_dir_list.append(self.sidecar_dict[os.path.realpath(s_file)])
+            sidecar_dir_dict[os.path.realpath(this_dir)] = new_dir_list
         self.sidecar_dir_dict = sidecar_dir_dict
 
     def get_sidecars_from_path(self, obj):
         sidecar_list = []
         current_path = ''
         for comp in get_path_components(obj.file_path, self.root_path):
-            current_path = os.path.abspath(os.path.join(current_path, comp))
+            current_path = os.path.realpath(os.path.join(current_path, comp))
             sidecars = self.sidecar_dir_dict.get(current_path, None)
             sidecar = BidsSidecarFile.get_sidecar(obj, sidecars)
             if sidecar:
@@ -63,7 +61,7 @@ def get_sidecars_from_path(self, obj):
         return sidecar_list
 
     def summarize(self, value_cols=None, skip_cols=None):
-        col_info = ColumnSummary(value_cols=None, skip_cols=None)
+        col_info = BidsTsvSummary(value_cols=None, skip_cols=None)
         for event_obj in self.events_dict.values():
             col_info.update(event_obj.file_path)
         return col_info
@@ -85,7 +83,7 @@ def validate(self, validators, check_for_warnings=True, keep_events=False):
 
 
 if __name__ == '__main__':
-    path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+    path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                         '../../../tests/data/bids/eeg_ds003654s_hed')
     bids = BidsEventFiles(path)
 
diff --git a/hed/tools/bids/bids_file.py b/hed/tools/bids/bids_file.py
index e2191fdfd..cd2c9a528 100644
--- a/hed/tools/bids/bids_file.py
+++ b/hed/tools/bids/bids_file.py
@@ -6,13 +6,13 @@ class BidsFile:
     """Represents the entity and file names for a BIDs file."""
 
     def __init__(self, file_path):
-        self.file_path = os.path.abspath(file_path)
+        self.file_path = os.path.realpath(file_path)
         suffix, ext, entity_dict, unmatched = parse_bids_filename(self.file_path)
         self.suffix = suffix
         self.ext = ext
-        self.entities = entity_dict
+        self.entity_dict = entity_dict
 
     def __str__(self):
         return self.file_path + ":\n\tname_suffix=" + self.suffix + " ext=" + self.ext + \
-               " entities=" + str(self.entities)
+               " entity_dict=" + str(self.entity_dict)
 
diff --git a/hed/tools/bids/bids_file_dictionary.py b/hed/tools/bids/bids_file_dictionary.py
new file mode 100644
index 000000000..68e557f4f
--- /dev/null
+++ b/hed/tools/bids/bids_file_dictionary.py
@@ -0,0 +1,130 @@
+import os
+from hed.errors.exceptions import HedFileError
+from hed.tools import BidsFile
+from hed.tools.annotation.file_dictionary import FileDictionary
+
+
+class BidsFileDictionary(FileDictionary):
+    """ Holds a dictionary of path names keyed by specified entity pairs. """
+
+    def __init__(self, file_list=None, entities=('sub', 'ses', 'task', 'run')):
+        """ Create a dictionary with keys that are simplified file names and values that are full paths
+
+        This function is used for cross listing BIDS style files for different studies.
+
+        Args:
+            file_list (list):      List containing full paths of files of interest
+            entities (tuple:  List of indices into base file names of pieces to assemble for the key
+        """
+        if file_list:
+            self.file_dict = self.make_bids_file_dict(file_list, entities)
+        else:
+            self.file_dict = {}
+        self.entities = entities
+
+    @property
+    def key_list(self):
+        return list(self.file_dict.keys())
+
+    @property
+    def file_list(self):
+        return list(self.file_dict.values())
+
+    def iter_files(self):
+        for key, file in self.file_dict.items():
+            yield key, file
+
+    def key_diffs(self, other_dict):
+        """Returns a list containing the symmetric difference of the keys in the two dictionaries
+
+        Args:
+            other_dict (FileDictionary)  A file dictionary object
+
+        Returns: list
+            A list of the symmetric difference of the keys in this dictionary and the other one.
+
+        """
+        diffs = set(self.file_dict.keys()).symmetric_difference(set(other_dict.file_dict.keys()))
+        return list(diffs)
+
+    def make_query(self, query_dict={'sub': '*', 'run': ['*']}):
+        response_dict = {}
+        for key, file in self.file_dict.items():
+            if self.match_query(query_dict, file.entity_dict):
+                response_dict[key] = file
+        return response_dict
+
+    def print_files(self, title=None):
+        if title:
+            print(f"{title} ({len(self.key_list)} files)")
+        for key, value in self.file_dict.items():
+            print(f"{key}: {os.path.basename(value.file_path)}")
+
+    def _create_dict_obj(self, file_dict):
+        dict_obj = BidsFileDictionary(file_list=None, entities=self.entities)
+        dict_obj.file_dict = file_dict
+        return dict_obj
+
+    def create_split_dict(self, entity):
+        split_dict, leftovers = self.split_dict_by_entity(self.file_dict, entity)
+        for entity_value, entity_dict in split_dict.items():
+            split_dict[entity_value] = self._create_dict_obj(split_dict[entity_value])
+        if leftovers:
+            leftover_dict = self._create_dict_obj(leftovers)
+        else:
+            leftover_dict = None
+        return split_dict, leftover_dict
+
+    @staticmethod
+    def split_dict_by_entity(file_dict, entity):
+        """ Split a dict of BidsFile into multiple dicts based on an entity
+
+        """
+        split_dict = {}
+        leftovers = {}
+        for key, file in file_dict.items():
+            if entity not in file.entity_dict:
+                leftovers[key] = file
+                continue
+            entity_value = file.entity_dict[entity]
+            entity_dict = split_dict.get(entity_value, {})
+            entity_dict[key] = file
+            split_dict[entity_value] = entity_dict
+        return split_dict, leftovers
+
+    @staticmethod
+    def make_bids_file_dict(file_list, entities):
+        file_dict = {}
+        for the_file in file_list:
+            the_file = BidsFile(the_file)
+            entity_dict = the_file.entity_dict
+            key_list = []
+            for entity in entities:
+                if entity in entity_dict:
+                    key_list.append(f"{entity}_{entity_dict[entity]}")
+            key = '_'.join(key_list)
+            if key in file_dict:
+                raise HedFileError("NonUniqueFileKeys",
+                                   f"dictionary key {key} is associated with {the_file} and {file_dict[key]}", "")
+            file_dict[key] = the_file
+        return file_dict
+
+    @staticmethod
+    def make_key(key_string, indices=(0, 2), separator='_'):
+        key_value = ''
+        pieces = key_string.split(separator)
+        for index in list(indices):
+            key_value += pieces[index] + separator
+        return key_value[:-1]
+
+    @staticmethod
+    def match_query(query_dict, entity_dict):
+
+        for query, query_value in query_dict.items():
+            if query not in entity_dict:
+                return False
+            elif isinstance(query_value, str) and query_value != '*':
+                return False
+            elif isinstance(query_value, list) and (entity_dict[query] not in query_value):
+                return False
+        return True
diff --git a/hed/tools/bids/bids_json_file.py b/hed/tools/bids/bids_json_file.py
index 6a2038d8f..fba91e19e 100644
--- a/hed/tools/bids/bids_json_file.py
+++ b/hed/tools/bids/bids_json_file.py
@@ -7,7 +7,7 @@ class BidsJsonFile(BidsFile):
     """Represents a BIDS JSON file."""
 
     def __init__(self, file_path, set_contents=False):
-        super().__init__(os.path.abspath(file_path))
+        super().__init__(file_path)
         self.contents = None
         if set_contents:
             self.set_contents()
diff --git a/hed/tools/bids/bids_sidecar_file.py b/hed/tools/bids/bids_sidecar_file.py
index a75543a84..99aafcc7b 100644
--- a/hed/tools/bids/bids_sidecar_file.py
+++ b/hed/tools/bids/bids_sidecar_file.py
@@ -8,7 +8,7 @@ class BidsSidecarFile(BidsJsonFile):
     """ Represents a BIDS JSON sidecar file."""
 
     def __init__(self, file_path, set_contents=False):
-        super().__init__(os.path.abspath(file_path), set_contents=False)
+        super().__init__(file_path, set_contents=False)
         if set_contents:
             self.set_contents()
 
@@ -28,13 +28,13 @@ def is_sidecar_for(self, obj):
         common_path = os.path.commonpath([obj.file_path, self.file_path])
         if common_path != os.path.dirname(self.file_path):
             return False
-        for key, item in self.entities.items():
-            if key not in obj.entities or obj.entities[key] != item:
+        for key, item in self.entity_dict.items():
+            if key not in obj.entity_dict or obj.entity_dict[key] != item:
                 return False
         return True
 
     def set_contents(self):
-        self.contents = Sidecar(self.file_path, name=os.path.abspath(self.file_path))
+        self.contents = Sidecar(self.file_path, name=os.path.realpath(self.file_path))
 
     @staticmethod
     def get_sidecar(obj, sidecars):
diff --git a/hed/tools/bids/bids_tsv_dictionary.py b/hed/tools/bids/bids_tsv_dictionary.py
new file mode 100644
index 000000000..1b4e3ea88
--- /dev/null
+++ b/hed/tools/bids/bids_tsv_dictionary.py
@@ -0,0 +1,53 @@
+from hed.util.data_util import get_new_dataframe
+from hed.tools.bids.bids_file_dictionary import BidsFileDictionary
+
+
+class BidsTsvDictionary(BidsFileDictionary):
+    """ Holds a key-file dictionary, but also reads each tsv file and keeps track of number of rows and column names."""
+
+    def __init__(self, file_list=None, entities = ('sub', 'ses', 'task', 'run')):
+        """ Create a dictionary with keys that are simplified file names and values that are full paths
+
+        This function is used for cross listing BIDS style files for different studies.
+
+        Args:
+            file_list (list):      List containing full paths of files of interest
+            entities (tuple):  List of indices into base file names of pieces to assemble for the key
+        """
+
+        super().__init__(file_list=file_list, entities=entities)
+        self.column_dict = {}
+        self.rowcount_dict = {}
+        self._set_tsv_info()
+
+    def _set_tsv_info(self):
+        for key, file in self.file_dict.items():
+            df = get_new_dataframe(file.file_path)
+            self.rowcount_dict[key] = len(df.index)
+            self.column_dict[key] = list(df.columns.values)
+
+    def iter_tsv_info(self):
+        for key, file in self.file_dict.items():
+            yield key, file, self.rowcount_dict[key], self.column_dict[key]
+
+    def count_diffs(self, other_dict):
+        """Returns a list containing the keys in which the number of events differ
+
+        Args:
+            other_dict (FileDictionary)  A file dictionary object
+
+        Returns: list of tuple
+            A list (key, count1, count2) tuples
+
+        """
+        diff_list = []
+        for key in self.file_dict.keys():
+            if self.rowcount_dict[key] != other_dict.rowcount_dict[key]:
+                diff_list.append((key, self.rowcount_dict[key], other_dict.rowcount_dict[key]))
+        return diff_list
+
+    def _create_dict_obj(self, file_dict):
+        dict_obj = BidsTsvDictionary(file_list=None, entities=self.entities)
+        dict_obj.file_dict = file_dict
+        dict_obj._set_tsv_info()
+        return dict_obj
diff --git a/hed/tools/bids/bids_tsv_file.py b/hed/tools/bids/bids_tsv_file.py
index 6c203578e..d4f608018 100644
--- a/hed/tools/bids/bids_tsv_file.py
+++ b/hed/tools/bids/bids_tsv_file.py
@@ -18,7 +18,7 @@ def __init__(self, file_path, set_contents=False):
                 If true then the file contents are read into a dataframe.
 
         """
-        super().__init__(os.path.abspath(file_path))
+        super().__init__(file_path)
         self.contents = None
         if set_contents:
             self.set_contents()
diff --git a/hed/tools/annotation/column_summary.py b/hed/tools/bids/bids_tsv_summary.py
similarity index 82%
rename from hed/tools/annotation/column_summary.py
rename to hed/tools/bids/bids_tsv_summary.py
index 77708773e..3658038b9 100644
--- a/hed/tools/annotation/column_summary.py
+++ b/hed/tools/bids/bids_tsv_summary.py
@@ -3,10 +3,10 @@
 from hed.tools.annotation.annotation_util import generate_sidecar_entry
 
 
-class ColumnSummary:
-    """Summarizes the contents of a spreadsheet. """
+class BidsTsvSummary:
+    """Summarizes the contents of tsv files. """
 
-    def __init__(self, value_cols=None, skip_cols=None, name='', header_char='*'):
+    def __init__(self, value_cols=None, skip_cols=None, name=''):
         """ .
 
         Args:
@@ -17,7 +17,6 @@ def __init__(self, value_cols=None, skip_cols=None, name='', header_char='*'):
         """
 
         self.name = name
-        self.header_char = header_char
         self.categorical_info = {}
         self.value_info = {}
         if value_cols and skip_cols and set(value_cols).intersection(skip_cols):
@@ -48,7 +47,6 @@ def get_number_unique_values(self, column_names=None):
     def extract_sidecar_template(self):
         """ Extract a dictionary compatible with a BIDS JSON event sidecar."""
         side_dict = {}
-        print("Extracting template.....")
         for column_name, columns in self.categorical_info.items():
             column_values = list(columns.keys())
             column_values.sort()
@@ -110,19 +108,19 @@ def update(self, data):
         else:
             self._update_dataframe(data)
 
-    def update_dict(self, col_dict):
-        """ Adds the values of another ColDict object to this object.
+    def update_summary(self, col_sum):
+        """ Adds the values of another ColumnSummary object to this object.
 
         The value_cols and skip_cols are updated as long as they are not contradictory. A new skip column
         cannot be in the
 
           Args:
-              col_dict (ColDict):    A column dictionary to be combined with
+              col_sum (BidsTsvSummary):    A ColumnSummary to be combined with
 
           """
-        self._update_dict_skip(col_dict)
-        self._update_dict_value(col_dict)
-        self._update_dict_categorical(col_dict)
+        self._update_dict_skip(col_sum)
+        self._update_dict_value(col_sum)
+        self._update_dict_categorical(col_sum)
 
     def _update_categorical(self, col_name, values):
         if col_name not in self.categorical_info:
@@ -188,3 +186,24 @@ def _update_dict_value(self, col_dict):
             else:
                 self.value_info[col] = self.value_info[col] + col_dict.value_info[col]
 
+    @staticmethod
+    def make_combined_dicts(file_dictionary, skip_cols=None):
+        """ Return a combined dictionary of column information as well as individual summaries
+
+        Args:
+            file_dictionary (FileDictionary):  Dictionary of file name keys and full path
+            skip_cols (list):  Name of the column
+
+        Returns:
+            (BidsTsvSummary, dict)  A BidsTsvSummary of the file_dict, plus dict of individual BidsTsvSummary objects
+        """
+
+        summary_all = BidsTsvSummary(skip_cols=skip_cols)
+        summary_dict = {}
+        for key, file in file_dictionary.file_dict.items():
+            orig_dict = BidsTsvSummary(skip_cols=skip_cols)
+            df = get_new_dataframe(file.file_path)
+            orig_dict.update(df)
+            summary_dict[key] = orig_dict
+            summary_all.update_summary(orig_dict)
+        return summary_all, summary_dict
diff --git a/hed/util/__init__.py b/hed/util/__init__.py
index 0dea901e8..00f5fc56d 100644
--- a/hed/util/__init__.py
+++ b/hed/util/__init__.py
@@ -2,4 +2,4 @@
 
 from .data_util import get_new_dataframe
 from .file_util import get_file_extension
-from .io_util import generate_filename,get_file_list, make_file_dict
+from .io_util import generate_filename, get_file_list
diff --git a/hed/util/io_util.py b/hed/util/io_util.py
index 542320ce8..cfd2795e0 100644
--- a/hed/util/io_util.py
+++ b/hed/util/io_util.py
@@ -4,7 +4,6 @@
 from werkzeug.utils import secure_filename
 
 
-
 def check_filename(test_file, name_prefix=None, name_suffix=None, extensions=None):
     """ Determines whether test_file has correct extension, name_suffix, and name_prefix.
 
@@ -67,7 +66,7 @@ def generate_filename(base_name, name_prefix=None, name_suffix=None, extension=N
 
 
 def get_dir_dictionary(dir_path, name_prefix=None, name_suffix=None, extensions=None, skip_empty=True):
-    """ Traverses a directory tree and creates dictionary with keys that are directories.
+    """ Traverses a directory tree and creates dictionary with keys that are directory paths a.
 
     Args:
         dir_path (str):               Full path of the directory tree to be traversed (no ending slash)
@@ -80,34 +79,64 @@ def get_dir_dictionary(dir_path, name_prefix=None, name_suffix=None, extensions=
         dict:             Dictionary with directories and keys and file lists values
     """
     dir_dict = {}
-    for r, d, f in os.walk(dir_path):
+    for root, dirs, files in os.walk(dir_path, topdown=True):
         file_list = []
-        for r_file in f:
+        for r_file in files:
             if check_filename(r_file, name_prefix, name_suffix, extensions):
-                file_list.append(os.path.abspath(os.path.join(r, r_file)))
+                file_list.append(os.path.join(os.path.realpath(root), r_file))
         if skip_empty and not file_list:
             continue
-        dir_dict[os.path.abspath(r)] = file_list
+        dir_dict[os.path.realpath(root)] = file_list
     return dir_dict
 
 
-def get_file_list(dir_path, name_prefix=None, name_suffix=None, extensions=None):
+def get_filtered_list(root_path, file_list, name_prefix=None, name_suffix=None, extensions=None):
+    """ Returns a new list with the filenames in file_list
+
+    Everything is converted to lower case prior to testing so this test should be case insensitive.
+
+     Args:
+         root_path (str) :           Path of filename to test
+         file_list (list):           List of files to test
+         name_prefix (str):          An optional name_prefix for the base filename
+         name_suffix (str):          An optional name_suffix for the base file name
+         extensions (list):     An optional list of file extensions
+
+     Returns:
+         tuple (list, list):            Returns two lists one with
+     """
+    filtered_files = []
+    for r_file in file_list:
+        file_split = os.path.splitext(r_file.lower())
+        if extensions and file_split[1] not in [x.lower() for x in extensions]:
+            continue
+        elif name_suffix and not file_split[0].endswith(name_suffix.lower()):
+            continue
+        elif name_prefix and not file_split[0].startswith(name_prefix.lower()):
+            continue
+        filtered_files.append(r_file)
+    return filtered_files
+
+
+def get_file_list(root_path, name_prefix=None, name_suffix=None, extensions=None, exclude_dirs=[]):
     """ Traverses a directory tree and returns a list of paths to files ending with a particular name_suffix.
-    TODO: Add exclude_dirs parameter
+
     Args:
-        dir_path (str):              Full path of the directory tree to be traversed (no ending slash)
+        root_path (str):              Full path of the directory tree to be traversed (no ending slash)
         name_prefix (str, None):     An optional name_prefix for the base filename
         name_suffix (str, None):     The name_suffix of the paths to be extracted
         extensions (list, None):     A list of extensions to be selected
+        exclude_dirs (list, None):    A list of paths to be excluded
 
     Returns:
         list:             A list of full paths
     """
     file_list = []
-    for r, d, f in os.walk(dir_path):
-        for r_file in f:
+    for root, dirs, files in os.walk(root_path, topdown=True):
+        dirs[:] = [d for d in dirs if d not in exclude_dirs]
+        for r_file in files:
             if check_filename(r_file, name_prefix, name_suffix, extensions):
-                file_list.append(os.path.join(r, r_file))
+                file_list.append(os.path.join(os.path.realpath(root), r_file))
     return file_list
 
 
@@ -125,8 +154,8 @@ def get_path_components(this_path, root_path):
                               remaining elements directory components to the file.
     """
 
-    base_path = os.path.abspath(root_path)
-    cur_path = os.path.abspath(this_path)
+    base_path = os.path.realpath(root_path)
+    cur_path = os.path.realpath(this_path)
     common_path = os.path.commonpath([base_path, cur_path])
     if common_path != base_path:
         return None
@@ -138,44 +167,51 @@ def get_path_components(this_path, root_path):
         return [base_path]
 
 
-def make_file_dict(file_list, indices=(0, -2), separator='_'):
-    """ Return a dictionary with keys that are simplified file names and values that are full paths
-
-    This function is used for cross listing BIDS style files for different studies.
-
-    Args:
-        file_list (list):      List containing full paths of files of interest
-        indices (tuple):       List of indices into base file names of pieces to assemble for the key
-        separator (str):       Character used to separate pieces of key name
-    Returns:
-        dict:  A dictionary of simplified, path-independent key names and full paths values.
-    """
-    file_dict = {}
-    for the_file in file_list:
-        the_file = os.path.abspath(the_file)
-        base = os.path.basename(the_file)
-        key = make_key(base, indices=indices, separator=separator)
-        file_dict[key] = the_file
-    return file_dict
-
-
-def make_key(key_string, indices=(0, -2), separator='_'):
-    key_value = ''
-    pieces = key_string.split(separator)
-    for index in list(indices):
-        key_value += pieces[index] + separator
-    return key_value[:-1]
-
-
-if __name__ == '__main__':
-    path = 'D:\\Research\\HED\\hed-examples\\datasets\\eeg_ds003654s'
-    files = get_file_list(path, name_prefix=None, name_suffix="events", extensions=None)
-    for file in files:
-        get_path_components(file, path)
+# def compare_dict_keys(dict1, dict2):
+#     """ Return a dictionary with keys that are simplified file names and values that are full paths
+#     This function is used for cross listing BIDS style files for different studies.
+#     Args:
+#         dict1 (dict):  List containing full paths of files of interest
+#         dict2 (dict):  List of indices into base file names of pieces to assemble for the key
+#     Returns: tuple
+#         (list1, list2):  Lists of keys missing from respective lists
+#     """
+#     keys1 = set(dict1.keys())
+#     keys2 = set(dict2.keys())
+#     list1 = list(keys1.difference(keys2))
+#     list2 = list(keys2.difference(keys1))
+#     return list1, list2
+#
+#
+# def make_file_dict(file_list, name_indices=(0, 2), separator='_'):
+#     """ Return a dictionary with keys that are simplified file names and values that are full paths
+#     This function is used for cross listing BIDS style files for different studies.
+#     Args:
+#         file_list (list):      List containing full paths of files of interest
+#         name_indices (tuple):  List of indices into base file names of pieces to assemble for the key
+#         separator (str):       Character used to separate pieces of key name
+#     Returns:
+#         dict:  A dictionary of simplified, path-independent key names and full paths values.
+#     """
+#     file_dict = {}
+#     for the_file in file_list:
+#         the_file = os.path.abspath(the_file)
+#         base = os.path.basename(the_file)
+#         key = make_key(base, indices=name_indices, separator=separator)
+#         file_dict[key] = the_file
+#     return file_dict
+#
+#
+# def make_key(key_string, indices=(0, 2), separator='_'):
+#     key_value = ''
+#     pieces = key_string.split(separator)
+#     for index in list(indices):
+#         key_value += pieces[index] + separator
+#     return key_value[:-1]
 
 
 def parse_bids_filename(file_path):
-    """ Split a filename into its BIDS suffix, extension, and entities
+    """ Split a filename into its BIDS suffix, extension, and entity_dict
 
         Args:
             file_path (str)     Path to be parsed
@@ -183,7 +219,7 @@ def parse_bids_filename(file_path):
         Returns:
             suffix (str)        BIDS suffix name
             ext (str)           File extension (including the .)
-            entities (dict)     Dictionary with key-value pair being (entity type, entity value)
+            entity_dict (dict)     Dictionary with key-value pair being (entity type, entity value)
             unmatched (list)    List of unmatched pieces of the filename
 
     """
@@ -206,4 +242,4 @@ def parse_bids_filename(file_path):
             unmatched.append(entity[1])
         else:
             entities[pieces[0]] = pieces[1]
-    return suffix, ext, entities, unmatched
\ No newline at end of file
+    return suffix, ext, entities, unmatched
diff --git a/hed/validator/hed_validator.py b/hed/validator/hed_validator.py
index 6470e1fe2..b88b6e489 100644
--- a/hed/validator/hed_validator.py
+++ b/hed/validator/hed_validator.py
@@ -9,6 +9,7 @@
 from hed.errors.error_reporter import ErrorHandler
 
 from hed.models.hed_string import HedString
+from hed.models import HedTag
 from hed.validator.tag_validator import TagValidator
 from functools import partial
 from hed.models.hed_ops import HedOps
@@ -22,7 +23,7 @@ def __init__(self, hed_schema=None, run_semantic_validation=True):
         Parameters
         ----------
         hed_schema: HedSchema
-            HedSchema object to use to use for validation
+            HedSchema object to use for validation
         run_semantic_validation: bool
             True if the validator should check the HED data against a schema. False for syntax-only validation.
         Returns
@@ -73,15 +74,38 @@ def _validate_groups_in_hed_string(self, hed_string_obj):
          """
         validation_issues = []
         for original_tag_group, is_top_level in hed_string_obj.get_all_groups(also_return_depth=True):
-            is_group = original_tag_group.is_group()
+            is_group = original_tag_group.is_group
             if not original_tag_group and is_group:
                 validation_issues += ErrorHandler.format_error(ValidationErrors.HED_GROUP_EMPTY,
                                                                tag=original_tag_group)
+
+            validation_issues += self._check_for_duplicate_groups(original_tag_group)
             validation_issues += self._tag_validator.run_tag_level_validators(original_tag_group.tags(), is_top_level,
                                                                               is_group)
 
         return validation_issues
 
+    def _check_for_duplicate_groups(self, original_group):
+        # Todo: This could be optimized in various ways.  It repeatedly converts subgroups to HedGroupFrozen
+        frozen_group_or_string = original_group.get_frozen()
+        if len(frozen_group_or_string._children) != len(original_group._children):
+            validation_issues = []
+            valid_content = set(frozen_group_or_string._children)
+            for child in original_group._children:
+                if isinstance(child, HedTag):
+                    error_code = ValidationErrors.HED_TAG_REPEATED
+                    frozen_child = child
+                else:
+                    error_code = ValidationErrors.HED_TAG_REPEATED_GROUP
+                    frozen_child = child.get_frozen()
+                if frozen_child in valid_content:
+                    valid_content.remove(frozen_child)
+                else:
+                    validation_issues += ErrorHandler.format_error(error_code, child)
+
+            return validation_issues
+        return ()
+
     def _validate_tags_in_hed_string(self, hed_string_obj, check_for_warnings=False):
         """Validates the multi-tag properties in a hed string, eg required tags.
 
@@ -114,11 +138,16 @@ def _validate_individual_tags_in_hed_string(self, hed_string_obj, allow_placehol
              The issues associated with the individual tags in the HED string.
 
          """
+        from hed.models.def_dict import DefTagNames
         validation_issues = []
-        tags = hed_string_obj.get_all_tags()
-        for hed_tag in tags:
-            validation_issues += \
-                self._tag_validator.run_individual_tag_validators(hed_tag, allow_placeholders=allow_placeholders,
-                                                                  check_for_warnings=check_for_warnings)
+        def_groups = hed_string_obj.find_top_level_tags(anchors={DefTagNames.DEFINITION_KEY}, include_groups=1)
+        all_def_groups = [group for sub_group in def_groups for group in sub_group.get_all_groups()]
+        for group in hed_string_obj.get_all_groups():
+            is_definition = group in all_def_groups
+            for hed_tag in group.tags():
+                validation_issues += \
+                    self._tag_validator.run_individual_tag_validators(hed_tag, allow_placeholders=allow_placeholders,
+                                                                      check_for_warnings=check_for_warnings,
+                                                                      is_definition=is_definition)
 
         return validation_issues
diff --git a/hed/validator/tag_validator.py b/hed/validator/tag_validator.py
index 3301f27f7..67f3778a7 100644
--- a/hed/validator/tag_validator.py
+++ b/hed/validator/tag_validator.py
@@ -69,7 +69,8 @@ def run_hed_string_validators(self, hed_string_obj):
             validation_issues += self.check_tag_formatting(tag)
         return validation_issues
 
-    def run_individual_tag_validators(self, original_tag, check_for_warnings, allow_placeholders=False):
+    def run_individual_tag_validators(self, original_tag, check_for_warnings, allow_placeholders=False,
+                                      is_definition=False):
         """Runs the hed_ops on the individual tags in a HED string.
 
          Parameters
@@ -80,6 +81,8 @@ def run_individual_tag_validators(self, original_tag, check_for_warnings, allow_
              If True, also check for warnings.
          allow_placeholders: bool
              Allow value class or extensions to be placeholders rather than a specific value.
+         is_definition: bool
+            This tag is part of a Definition, not a normal line.
          Returns
          -------
          []
@@ -97,7 +100,7 @@ def run_individual_tag_validators(self, original_tag, check_for_warnings, allow_
                 validation_issues += self.check_for_invalid_extension_chars(original_tag)
 
             if not allow_placeholders:
-                validation_issues += self.check_for_placeholder(original_tag)
+                validation_issues += self.check_for_placeholder(original_tag, is_definition)
             validation_issues += self.check_tag_requires_child(original_tag)
         if check_for_warnings:
             validation_issues += self.check_capitalization(original_tag)
@@ -121,7 +124,6 @@ def run_tag_level_validators(self, original_tag_list, is_top_level, is_group):
             The validation issues associated with each level in a HED string.
         """
         validation_issues = []
-        validation_issues += self.check_duplicate_tags_exist(original_tag_list)
         validation_issues += self.check_tag_level_issue(original_tag_list, is_top_level, is_group)
         return validation_issues
 
@@ -444,30 +446,34 @@ def check_capitalization(self, original_tag):
                 break
         return validation_issues
 
-    def check_duplicate_tags_exist(self, original_tag_list):
-        """Reports a validation error if two or more tags are the same.
-
-        This only tracks exact matches, it will not catch two identical  value tags with different values.
-        Parameters
-        ----------
-        original_tag_list: [HedTag]
-            A list containing tags that are used to report the error.
-        Returns
-        -------
-        []
-            A validation issues list. If no issues are found then an empty list is returned.
-
-        """
-        validation_issues = []
-        tag_set = set()
-        for tag in original_tag_list:
-            formatted_tag = tag.lower()
-            if formatted_tag in tag_set:
-                validation_issues += ErrorHandler.format_error(ValidationErrors.HED_TAG_REPEATED, tag=tag)
-                continue
-            tag_set.add(formatted_tag)
-
-        return validation_issues
+    # # Possible new style check(but causes many existing errors, so holding off)
+    # def check_capitalization(self, original_tag):
+    #     """Reports a validation warning if the tag isn't correctly capitalized.
+    #
+    #     Parameters
+    #     ----------
+    #     original_tag: HedTag
+    #         The original tag that is used to report the warning.
+    #     Returns
+    #     -------
+    #     []
+    #         A validation issues list. If no issues are found then an empty list is returned.
+    #     """
+    #     validation_issues = []
+    #     org_name = original_tag.org_base_tag
+    #     if original_tag.library_prefix:
+    #         org_name = org_name[len(original_tag.library_prefix):]
+    #     tag_names = org_name.split("/")
+    #     for tag_name in tag_names:
+    #         correct_tag_name = tag_name.capitalize()
+    #         if tag_name != correct_tag_name:
+    #             # We assume any with spaces are correctly capitalized(descriptions, values, etc)
+    #             if " " in tag_name:
+    #                 continue
+    #             validation_issues += ErrorHandler.format_error(ValidationErrors.HED_STYLE_WARNING,
+    #                                                            tag=original_tag)
+    #             break
+    #     return validation_issues
 
     def check_tag_level_issue(self, original_tag_list, is_top_level, is_group):
         """
@@ -622,20 +628,22 @@ def _character_is_delimiter(character):
         """
         return character == TagValidator.COMMA
 
-    def check_for_placeholder(self, original_tag):
+    def check_for_placeholder(self, original_tag, is_definition=False):
         """
             Checks for a placeholder character in the extension/value portion of a tag, unless they are allowed.
 
         Parameters
         ----------
         original_tag : HedTag
+        is_definition: bool
+            If True, placeholders are allowed.
 
         Returns
         -------
         error_list: [{}]
         """
         validation_issues = []
-        if not original_tag.is_definition:
+        if not is_definition:
             starting_index = len(original_tag.org_base_tag) + 1
             for i, character in enumerate(original_tag.extension_or_value_portion):
                 if character == "#":


Test patch:
diff --git a/tests/models/test_base_file_input.py b/tests/models/test_base_file_input.py
index f889c9da5..63a874125 100644
--- a/tests/models/test_base_file_input.py
+++ b/tests/models/test_base_file_input.py
@@ -12,7 +12,7 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.base_output_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/tests_output/")
+        cls.base_output_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/tests_output/")
         os.makedirs(cls.base_output_folder, exist_ok=True)
 
     @classmethod
diff --git a/tests/models/test_column_mapper.py b/tests/models/test_column_mapper.py
index 8ce41117c..054129477 100644
--- a/tests/models/test_column_mapper.py
+++ b/tests/models/test_column_mapper.py
@@ -21,7 +21,7 @@ def setUpClass(cls):
         cls.category_tags = HedString('Participant response, Stimulus')
         cls.row_with_hed_tags = ['event1', 'tag1', 'tag2']
 
-        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
         cls.basic_events_json = os.path.join(cls.base_data_dir, "sidecar_tests/both_types_events.json")
         cls.basic_event_name = "trial_type"
         cls.basic_event_type = ColumnType.Categorical
@@ -176,7 +176,7 @@ def test__prepend_prefix_to_required_tag_column_if_needed(self):
 
     # Verify reading/writing a short tag to a file column with a name_prefix works
     def test_add_prefix_verify_short_tag_conversion(self):
-        schema_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.schema_file)
+        schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), self.schema_file)
         hed_schema = load_schema(schema_file)
         prepended_hed_string = ColumnMetadata._prepend_prefix_to_required_tag_column_if_needed(
             HedString(self.short_tag_with_missing_prefix), self.short_tag_key)
diff --git a/tests/models/test_def_dict.py b/tests/models/test_def_dict.py
index 62c4f2455..f1c66fe9d 100644
--- a/tests/models/test_def_dict.py
+++ b/tests/models/test_def_dict.py
@@ -3,6 +3,7 @@
 from hed.models.def_dict import DefDict
 from hed.errors import ErrorHandler, DefinitionErrors
 from hed.models.hed_string import HedString
+from hed import HedTag
 
 
 class TestDefBase(unittest.TestCase):
@@ -13,6 +14,8 @@ def check_def_base(self, test_strings, expected_issues):
             hed_string_obj.convert_to_canonical_forms(None)
             test_issues = def_dict.check_for_definitions(hed_string_obj)
             expected_issue = expected_issues[test_key]
+            # print(test_issues)
+            # print(expected_issue)
             self.assertCountEqual(test_issues, expected_issue, HedString(test_strings[test_key]))
 
 
@@ -61,6 +64,8 @@ def test_definitions(self):
             'defTooManyPlaceholders': self.placeholder_invalid_def_string,
             'invalidPlaceholder': "(Definition/InvalidDef1/InvalidPlaceholder)",
             'invalidPlaceholderExtension': "(Definition/InvalidDef1/this-part-is-not-allowed/#)",
+            'defInGroup': "(Definition/ValidDefName, (Def/ImproperlyPlacedDef))",
+            'defExpandInGroup': "(Definition/ValidDefName, (Def-expand/ImproperlyPlacedDef, (ImproperContents)))"
         }
         expected_results = {
             'noGroupTag': [],
@@ -68,7 +73,7 @@ def test_definitions(self):
                                                                "InvalidDef1", expected_count=1, tag_list=[]),
             'placeholderWrongSpot': ErrorHandler.format_error(DefinitionErrors.INVALID_DEFINITION_EXTENSION,
                                                               "InvalidDef1#"),
-            'twoDefTags': ErrorHandler.format_error(DefinitionErrors.WRONG_NUMBER_DEFINITION_TAGS,
+            'twoDefTags': ErrorHandler.format_error(DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
                                                     "ValidDef1", ["Definition/InvalidDef2"]),
             'twoGroupTags': ErrorHandler.format_error(DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
                                                       "InvalidDef1",
@@ -86,6 +91,10 @@ def test_definitions(self):
                                                                      "InvalidDef1/this-part-is-not-allowed"),
             'invalidPlaceholder': ErrorHandler.format_error(DefinitionErrors.INVALID_DEFINITION_EXTENSION,
                                                             "InvalidDef1/InvalidPlaceholder"),
+            'defInGroup': ErrorHandler.format_error(DefinitionErrors.DEF_TAG_IN_DEFINITION,
+                                                    tag=HedTag("Def/ImproperlyPlacedDef"), def_name="ValidDefName"),
+            'defExpandInGroup': ErrorHandler.format_error(DefinitionErrors.DEF_TAG_IN_DEFINITION,
+                                                    tag=HedTag("Def-expand/ImproperlyPlacedDef"), def_name="ValidDefName")
         }
 
         self.check_def_base(test_strings, expected_results)
diff --git a/tests/models/test_def_mapper.py b/tests/models/test_def_mapper.py
index 762569bec..274aced29 100644
--- a/tests/models/test_def_mapper.py
+++ b/tests/models/test_def_mapper.py
@@ -11,7 +11,7 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
         hed_xml_file = os.path.join(cls.base_data_dir, "hed_pairs/HED8.0.0t.xml")
         cls.hed_schema = schema.load_schema(hed_xml_file)
         cls.def_contents_string = "(Item/TestDef1,Item/TestDef2)"
@@ -68,11 +68,11 @@ def base_def_validator(self, test_strings, result_strings, valid_strings, expand
         hed_ops.append(def_mapper)
 
         for key in test_strings:
-            string, result, invalid = test_strings[key], result_strings[key], valid_strings[key]
+            string, expected_result, invalid = test_strings[key], result_strings[key], valid_strings[key]
             test_string = HedString(string)
             def_issues = test_string.validate(hed_ops, expand_defs=expand_defs, shrink_defs=shrink_defs, remove_definitions=remove_definitions)
             self.assertEqual(invalid, bool(def_issues))
-            self.assertEqual(test_string.get_as_short(), result)
+            self.assertEqual(test_string.get_as_short(), expected_result)
 
     def test_expand_def_tags(self):
         basic_def_strings = {
@@ -239,6 +239,52 @@ def test_bad_def_expand(self):
         def_issues = invalid_placeholder.validate(def_mapper)
         self.assertTrue(bool(def_issues))
 
+    def test_def_no_content(self):
+        def_dict = DefDict()
+        def_string = HedString("(Definition/EmptyDef)")
+        def_string.convert_to_canonical_forms(None)
+        def_dict.check_for_definitions(def_string)
+        def_mapper = DefinitionMapper(def_dict)
+
+        valid_empty = HedString("Def/EmptyDef")
+        def_issues = valid_empty.validate(def_mapper, expand_defs=True)
+        self.assertEqual(str(valid_empty), "(Def-expand/EmptyDef)")
+        self.assertFalse(def_issues)
+
+        valid_empty = HedString("Def/EmptyDef")
+        def_issues = valid_empty.validate(def_mapper, expand_defs=False)
+        self.assertFalse(def_issues)
+
+    def test_mutable(self):
+        import copy
+        basic_definition_string = "(Definition/TestDef, (Keypad-key/TestDef1,Keyboard-key/TestDef2))"
+        def_dict = DefDict()
+        def_string = HedString(basic_definition_string)
+        def_string.convert_to_canonical_forms(self.hed_schema)
+        def_dict.check_for_definitions(def_string)
+
+        def_mapper = DefinitionMapper(def_dict)
+        hed_ops = []
+        hed_ops.append(self.hed_schema)
+        hed_ops.append(def_mapper)
+
+        def_contents = def_dict.defs['testdef'].contents
+        def_contents.cascade_mutable(True)
+        # todo: clean this up some.  I think it's mostly good.
+        label_def_string = "Def/TestDef"
+        test_string = HedString(label_def_string)
+        test_string2 = HedString(label_def_string)
+        def_issues = test_string.validate(hed_ops, expand_defs=True)
+        def_issues = test_string2.validate(hed_ops, expand_defs=True)
+        tags_to_remove = test_string.find_tags(anchors=["keypad-key"], recursive=True)
+
+        for tag, group in tags_to_remove:
+            test_string.remove_groups([tag])
+
+
+        self.assertEqual(test_string, test_string2)
+
+        breakHEre = 3
 
 
 if __name__ == '__main__':
diff --git a/tests/models/test_events_file_input.py b/tests/models/test_events_file_input.py
index d626c91e0..7217e52f6 100644
--- a/tests/models/test_events_file_input.py
+++ b/tests/models/test_events_file_input.py
@@ -12,7 +12,7 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.base_output_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/tests_output/")
+        cls.base_output_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/tests_output/")
         os.makedirs(cls.base_output_folder, exist_ok=True)
 
     @classmethod
@@ -20,13 +20,13 @@ def tearDownClass(cls):
         shutil.rmtree(cls.base_output_folder)
 
     def test_missing_column_name_issue(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_schema.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events_bad_column_name.tsv')
 
         hed_schema = schema.load_schema(schema_path)
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
         validator = HedValidator(hed_schema=hed_schema)
         sidecar = Sidecar(json_path)
         issues = sidecar.validate_entries(validator)
@@ -39,19 +39,20 @@ def test_missing_column_name_issue(self):
         self.assertEqual(len(validation_issues), 1)
 
     def test_expand_column_issues(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_schema.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events_bad_category_key.tsv')
 
         hed_schema = schema.load_schema(schema_path)
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
         validator = HedValidator(hed_schema=hed_schema)
         sidecar = Sidecar(json_path)
         issues = sidecar.validate_entries(validator)
         self.assertEqual(len(issues), 0)
         input_file = EventsInput(events_path, sidecars=sidecar)
 
+        #Fix whatever is wrong with onset tag here.  It's thinking Description/Onset continues is an invalid tag???'
         validation_issues = input_file.validate_file_sidecars(validator)
         self.assertEqual(len(validation_issues), 0)
         validation_issues = input_file.validate_file(validator, check_for_warnings=True)
diff --git a/tests/models/test_hed_file_input.py b/tests/models/test_hed_file_input.py
index bfa8129a9..47a92d9b9 100644
--- a/tests/models/test_hed_file_input.py
+++ b/tests/models/test_hed_file_input.py
@@ -12,7 +12,7 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.default_test_file_name = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.default_test_file_name = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                                   "../data/validator_tests/ExcelMultipleSheets.xlsx")
         cls.generic_file_input = HedInput(cls.default_test_file_name)
         cls.integer_key_dictionary = {1: 'one', 2: 'two', 3: 'three'}
@@ -26,7 +26,7 @@ def setUpClass(cls):
         cls.category_tags = 'Participant response, Stimulus'
         cls.row_with_hed_tags = ['event1', 'tag1', 'tag2']
 
-        cls.base_output_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/tests_output/")
+        cls.base_output_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/tests_output/")
         os.makedirs(cls.base_output_folder, exist_ok=True)
 
     @classmethod
@@ -58,10 +58,10 @@ def test_get_row_hed_tags(self):
         self.assertTrue(column_to_hed_tags_dictionary)
 
     def test_file_as_string(self):
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
 
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
         sidecar = Sidecar(json_path)
         self.assertEqual(len(sidecar.validate_entries(expand_defs=True)), 0)
         input_file = EventsInput(events_path, sidecars=sidecar)
@@ -120,9 +120,9 @@ def test_loading_and_reloading(self):
         self.assertTrue(test_input_file._dataframe.equals(reloaded_input._dataframe))
 
     def test_loading_and_reset_mapper(self):
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
         sidecar = Sidecar(json_path)
         self.assertEqual(len(sidecar.validate_entries()), 0)
         input_file_1 = EventsInput(events_path, sidecars=sidecar)
diff --git a/tests/models/test_hed_tag.py b/tests/models/test_hed_tag.py
index 628167981..76c976374 100644
--- a/tests/models/test_hed_tag.py
+++ b/tests/models/test_hed_tag.py
@@ -7,6 +7,8 @@ class TestValidatorUtilityFunctions(TestHedBase):
 
     def test_if_tag_exists(self):
         valid_tag1 = HedTag('Left-handed', hed_schema=self.hed_schema)
+        hash1 = hash(valid_tag1)
+        hash2 = hash(valid_tag1)
         valid_tag2 = HedTag('Geometric-object', hed_schema=self.hed_schema)
         valid_tag3 = HedTag('duration/#', hed_schema=self.hed_schema)
         invalid_tag1 = HedTag('something', hed_schema=self.hed_schema)
@@ -152,13 +154,28 @@ def test_determine_allows_extensions(self):
         self.assertEqual(no_extension_tag2_result, False)
         self.assertEqual(no_extension_tag3_result, False)
 
+    def test_finding_tags_no_schema(self):
+        # Verify basic tag identification works.
+        tag = HedTag("Onset")
+        tag.convert_to_canonical_forms(hed_schema=None)
+        self.assertTrue(tag._schema_entry)
 
-    def test__check_tag_starts_with(self):
-        target_tag_name = "definition/"
+        tag2 = HedTag("OtherFolders/Onset")
+        tag2.convert_to_canonical_forms(hed_schema=None)
+        self.assertTrue(tag2._schema_entry)
 
-        test_tags = ["Definition/TempTestDef", "Informational/Definition/TempTestDef",
-                     "Attribute/Informational/Definition/TempTestDef"]
+        tag4 = HedTag("OtherFolders/Onset/Extension")
+        tag4.convert_to_canonical_forms(hed_schema=None)
+        self.assertTrue(tag4._schema_entry)
 
-        for tag in test_tags:
-            result = HedTag._check_tag_starts_with(tag, target_tag_name)
-            self.assertTrue(result)
+        tag3 = HedTag("OtherFolders/Onset-NotOnset")
+        tag3.convert_to_canonical_forms(hed_schema=None)
+        self.assertFalse(tag3._schema_entry)
+
+        tag = HedTag("Onset")
+        tag.convert_to_canonical_forms(hed_schema=self.hed_schema)
+        self.assertTrue(tag._schema_entry)
+
+        tag2 = HedTag("Property/Data-property/Data-marker/Temporal-marker/Onset")
+        tag2.convert_to_canonical_forms(hed_schema=self.hed_schema)
+        self.assertTrue(tag._schema_entry)
\ No newline at end of file
diff --git a/tests/models/test_onset_mapper.py b/tests/models/test_onset_mapper.py
index e4f36c932..7c5ec6b97 100644
--- a/tests/models/test_onset_mapper.py
+++ b/tests/models/test_onset_mapper.py
@@ -9,10 +9,11 @@
 from tests.validator.test_tag_validator_base import TestHedBase
 
 
+# Todo: Add test case for Onset/Value and Offset/Value.  Make sure they bomb.
 class Test(TestHedBase):
     @classmethod
     def setUpClass(cls):
-        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
         hed_xml_file = os.path.join(cls.base_data_dir, "hed_pairs/HED8.0.0.mediawiki")
         cls.hed_schema = schema.load_schema(hed_xml_file)
         cls.placeholder_label_def_string = "def/TestDefPlaceholder/2471"
diff --git a/tests/models/test_sidecar.py b/tests/models/test_sidecar.py
index 8e8a49a3a..f5acc2cde 100644
--- a/tests/models/test_sidecar.py
+++ b/tests/models/test_sidecar.py
@@ -12,7 +12,7 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
         hed_xml_file = os.path.join(cls.base_data_dir, "hed_pairs/HED8.0.0t.xml")
         cls.hed_schema = schema.load_schema(hed_xml_file)
         cls.json_filename = os.path.join(cls.base_data_dir, "sidecar_tests/both_types_events.json")
diff --git a/tests/schema/test_base_tag.py b/tests/schema/test_base_tag.py
index 8a077f164..c51b777c9 100644
--- a/tests/schema/test_base_tag.py
+++ b/tests/schema/test_base_tag.py
@@ -11,7 +11,7 @@ class TestBaseTagBase(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
+        hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.schema_file)
         cls.hed_schema = load_schema(hed_xml)
         cls.error_handler = ErrorHandler()
 
diff --git a/tests/schema/test_hed_cache.py b/tests/schema/test_hed_cache.py
index 3e874fc4f..3e32390d1 100644
--- a/tests/schema/test_hed_cache.py
+++ b/tests/schema/test_hed_cache.py
@@ -1,6 +1,8 @@
 import unittest
 import os
 import itertools
+import urllib.error
+
 from hed.schema import hed_cache
 from hed import schema
 import shutil
@@ -9,7 +11,8 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.hed_cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../schema_cache_test/')
+        cls.hed_cache_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../schema_cache_test/')
+        cls.saved_cache_folder = hed_cache.HED_CACHE_DIRECTORY
         schema.set_cache_directory(cls.hed_cache_dir)
 
         cls.default_xml_base_filename = "HED8.0.0t.xml"
@@ -21,13 +24,18 @@ def setUpClass(cls):
         cls.semantic_version_three = '1.2.5'
         cls.semantic_version_list = ['1.2.3', '1.2.4', '1.2.5']
 
-        cls.specific_hed_url = \
-            """https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED8.0.0.xml"""
-        hed_cache.cache_all_hed_xml_versions(cache_folder=cls.hed_cache_dir)
+        try:
+            cls.specific_hed_url = \
+                """https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED8.0.0.xml"""
+            hed_cache.cache_all_hed_xml_versions(cache_folder=cls.hed_cache_dir)
+        except urllib.error.HTTPError as e:
+            schema.set_cache_directory(cls.saved_cache_folder)
+            raise e
 
     @classmethod
     def tearDownClass(cls):
         shutil.rmtree(cls.hed_cache_dir)
+        schema.set_cache_directory(cls.saved_cache_folder)
 
     def test_get_hed_version_path(self):
         latest_hed_version_path = hed_cache.get_hed_version_path()
diff --git a/tests/schema/test_hed_schema.py b/tests/schema/test_hed_schema.py
index 8e9054bae..a4b3942be 100644
--- a/tests/schema/test_hed_schema.py
+++ b/tests/schema/test_hed_schema.py
@@ -12,8 +12,8 @@ class TestHedSchema(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.hed_xml_3g = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file_3g_xml)
-        cls.hed_wiki_3g = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file_3g)
+        cls.hed_xml_3g = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.schema_file_3g_xml)
+        cls.hed_wiki_3g = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.schema_file_3g)
         cls.hed_schema_3g_wiki = load_schema(cls.hed_wiki_3g)
         cls.hed_schema_3g = load_schema(cls.hed_xml_3g)
 
diff --git a/tests/schema/test_schema_compliance.py b/tests/schema/test_schema_compliance.py
index 5e9364d17..0d2174672 100644
--- a/tests/schema/test_schema_compliance.py
+++ b/tests/schema/test_schema_compliance.py
@@ -12,7 +12,7 @@ class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.error_handler = ErrorHandler()
-        cls.schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
+        cls.schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.schema_file)
 
     def validate_term_base(self, input_text, expected_issues):
         for text, issues in zip(input_text, expected_issues):
diff --git a/tests/schema/test_schema_converters.py b/tests/schema/test_schema_converters.py
index 20906d21e..aba70878b 100644
--- a/tests/schema/test_schema_converters.py
+++ b/tests/schema/test_schema_converters.py
@@ -13,9 +13,9 @@ class TestConverterBase(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.xml_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.xml_file)
+        cls.xml_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.xml_file)
         cls.hed_schema_xml = schema.load_schema(cls.xml_file)
-        cls.wiki_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.wiki_file)
+        cls.wiki_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.wiki_file)
         cls.hed_schema_wiki = schema.load_schema(cls.wiki_file)
 
     def test_schema2xml(self):
@@ -84,9 +84,9 @@ class TestComplianceBase(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.xml_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.xml_file)
+        cls.xml_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.xml_file)
         cls.hed_schema_xml = schema.load_schema(cls.xml_file)
-        cls.wiki_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.wiki_file)
+        cls.wiki_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.wiki_file)
         cls.hed_schema_wiki = schema.load_schema(cls.wiki_file)
 
     def test_compliance(self):
@@ -172,7 +172,7 @@ class TestConverterSavingPrefix(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.xml_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.xml_file)
+        cls.xml_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.xml_file)
         cls.hed_schema_xml = schema.load_schema(cls.xml_file)
         cls.hed_schema_xml_prefix = schema.load_schema(cls.xml_file, library_prefix="tl:")
 
diff --git a/tests/schema/test_schema_wiki_fatal_errors.py b/tests/schema/test_schema_wiki_fatal_errors.py
index ca1a803ee..6d1eb7204 100644
--- a/tests/schema/test_schema_wiki_fatal_errors.py
+++ b/tests/schema/test_schema_wiki_fatal_errors.py
@@ -10,24 +10,24 @@ class TestHedSchema(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.full_base_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.base_schema_dir)
+        cls.full_base_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.base_schema_dir)
         cls.files_and_errors = {
             "HED_schema_no_start.mediawiki": HedExceptions.SCHEMA_START_MISSING,
             "HED_schema_no_end.mediawiki": HedExceptions.SCHEMA_END_INVALID,
             "HED_hed_no_end.mediawiki": HedExceptions.HED_END_INVALID,
             "HED_separator_invalid.mediawiki": HedExceptions.INVALID_SECTION_SEPARATOR,
             "HED_header_missing.mediawiki": HedExceptions.SCHEMA_HEADER_MISSING,
-            "HED_header_invalid.mediawiki": HedExceptions.SCHEMA_HEADER_INVALID,
-            "HED_header_invalid_version.mediawiki": HedExceptions.BAD_HED_SEMANTIC_VERSION,
-            "HED_header_missing_version.mediawiki": HedExceptions.BAD_HED_SEMANTIC_VERSION,
+            "HED_header_invalid.mediawiki": HedExceptions.HED_SCHEMA_HEADER_INVALID,
+            "HED_header_invalid_version.mediawiki": HedExceptions.HED_SCHEMA_VERSION_INVALID,
+            "HED_header_missing_version.mediawiki": HedExceptions.HED_SCHEMA_VERSION_INVALID,
             "HED_header_bad_library.mediawiki": HedExceptions.BAD_HED_LIBRARY_NAME,
             "HED_schema_out_of_order.mediawiki": HedExceptions.SCHEMA_START_MISSING,
-            "empty_node.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-            "malformed_line.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-            "malformed_line2.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-            "malformed_line3.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-            "malformed_line4.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
-            "malformed_line5.mediawiki": HedExceptions.HED_SCHEMA_WIKI_WARNINGS,
+            "empty_node.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+            "malformed_line.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+            "malformed_line2.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+            "malformed_line3.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+            "malformed_line4.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
+            "malformed_line5.mediawiki": HedExceptions.HED_WIKI_DELIMITERS_INVALID,
             "empty_node.xml": HedExceptions.HED_SCHEMA_NODE_NAME_INVALID
         }
 
diff --git a/tests/tools/annotation/test_annotation_util.py b/tests/tools/annotation/test_annotation_util.py
index 48a648576..b7bde804d 100644
--- a/tests/tools/annotation/test_annotation_util.py
+++ b/tests/tools/annotation/test_annotation_util.py
@@ -2,8 +2,10 @@
 import json
 import unittest
 from pandas import DataFrame
-from hed.tools import check_df_columns, df_to_hed, extract_tag, generate_sidecar_entry, hed_to_df, merge_hed_dict
+from hed.tools import check_df_columns, df_to_hed, extract_tag, generate_sidecar_entry, hed_to_df, merge_hed_dict, \
+    BidsTsvSummary
 from hed.tools.annotation.annotation_util import _flatten_cat_col, _flatten_val_col,  _get_value_entry, _update_cat_dict
+from hed.util import get_file_list
 
 
 # noinspection PyBroadException
@@ -11,8 +13,9 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        json_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                                 '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json'))
+        cls.bids_root_path = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                          '../../data/bids/eeg_ds003654s_hed'))
+        json_path = os.path.realpath(os.path.join(cls.bids_root_path, 'task-FacePerception_events.json'))
         cls.sidecar1a = {"a": {"c": {"c1": "blech3", "c2": "blech3a"}, "d": "blech4", "e": "blech5"},
                          "b": {"HED": "Purple"}}
         cls.sidecar1b = {"a": {"c": {"c1": "blech3", "c2": "blech3a"}, "d": "blech4", "e": "blech5"},
@@ -137,6 +140,23 @@ def test_merge_hed_dict_value_col(self):
         merge_hed_dict(self.sidecar1b, hed1b)
         self.assertIn('Description', self.sidecar1b['b'], "merge_hed_dict should not have description when n/a")
 
+    def test_merge_hed_dict_full(self):
+        exclude_dirs = ['stimuli']
+        name_indices = (0, 2)
+        skip_columns = ["onset", "duration", "sample", "trial", "response_time"]
+        value_columns = ["rep_lag", "stim_file", "value"]
+        event_files = get_file_list(self.bids_root_path, extensions=[".tsv"], name_suffix="_events",
+                                    exclude_dirs=exclude_dirs)
+        value_sum = BidsTsvSummary(value_cols=value_columns, skip_cols=skip_columns)
+        value_sum.update(event_files)
+        sidecar_template = value_sum.extract_sidecar_template()
+        example_spreadsheet = hed_to_df(sidecar_template)
+        spreadsheet_sidecar = df_to_hed(example_spreadsheet, description_tag=False)
+        example_sidecar = {}
+        self.assertEqual(0, len(example_sidecar), 'merge_hed_dict input is empty for this test')
+        merge_hed_dict(example_sidecar, spreadsheet_sidecar)
+        self.assertEqual(6, len(example_sidecar), 'merge_hed_dict merges with the correct length')
+
     def test_flatten_cat_col(self):
         col1 = self.sidecar2c["a"]
         col2 = self.sidecar2c["b"]
diff --git a/tests/tools/annotation/test_column_summary.py b/tests/tools/annotation/test_column_summary.py
deleted file mode 100644
index f0d7c91dd..000000000
--- a/tests/tools/annotation/test_column_summary.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import unittest
-import os
-from unittest import mock
-import pandas as pd
-from hed.errors.exceptions import HedFileError
-from hed.tools.annotation.column_summary import ColumnSummary
-from hed.util.data_util import get_new_dataframe
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/attention_shift')
-        bids_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/bids')
-        cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
-        cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
-        cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
-        cls.stern_test3_path = os.path.join(stern_base_dir, "sternberg_no_quotes_events.tsv")
-        cls.attention_shift_path = os.path.join(att_base_dir, "auditory_visual_shift_events.tsv")
-        cls.wh_events_path = \
-            os.path.abspath(os.path.join(bids_base_dir,
-                            'eeg_ds003654s_hed/sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv'))
-
-    def test_constructor(self):
-        dict1 = ColumnSummary()
-        self.assertIsInstance(dict1, ColumnSummary, "ColumnSummary constructor is allowed to have no arguments")
-        self.assertFalse(dict1.value_info)
-
-        dict2 = ColumnSummary(value_cols=['a', 'b', 'c'], name='baloney')
-        self.assertIsInstance(dict2, ColumnSummary, "ColumnSummary: multiple values are okay in constructor")
-        self.assertEqual(len(dict2.value_info.keys()), 3, "ColumnSummary should have keys for each value column")
-
-    def test_get_number_unique_values(self):
-        dict1 = ColumnSummary()
-        wh_df = pd.read_csv(self.wh_events_path, delimiter='\t', header=0)
-        dict1.update(wh_df)
-        self.assertEqual(len(dict1.value_info.keys()), 0, "ColumnSummary value_info should be empty if no value columns")
-        self.assertEqual(len(dict1.categorical_info.keys()), len(wh_df.columns),
-                         "ColumnSummary categorical_info should have all the columns if no restrictions")
-        count_dict = dict1.get_number_unique_values()
-        self.assertEqual(len(count_dict), 10, "get_number_unique_values should have the correct number of entries")
-        self.assertEqual(count_dict['onset'], 551, "get_number_unique_values should have the right number of unique")
-
-    # def test_get_flattened(self):
-    #     t_map = ColumnSummary()
-    #     t_map.update(self.stern_map_path)
-    #     df1 = t_map.get_flattened()
-    #     self.assertEqual(len(df1), 137, "Flattening should produce same number of items whether or not value used")
-    #     self.assertEqual(len(df1.columns), 2, "A flattened map should have 2 columns")
-    #     t_map2 = ColumnSummary(value_cols=['letter'], skip_cols=['type'])
-    #     t_map2.update(self.stern_map_path)
-    #     df2 = t_map2.get_flattened()
-    #     self.assertGreater(len(df1), len(df2), "Flattening of few columns produces smaller flattened array")
-    #     self.assertEqual(len(df2.columns), 2, "Flattening should produce 2 columns when value and skip columns")
-
-    def test_print(self):
-        from io import StringIO
-        t_map = ColumnSummary()
-        t_map.update(self.stern_map_path)
-        df = get_new_dataframe(self.stern_map_path)
-        t_map.update(self.stern_map_path)
-        self.assertEqual(len(t_map.categorical_info.keys()), len(df.columns),
-                         "ColumnSummary should have all columns as categorical if no value or skip are given")
-        with mock.patch('sys.stdout', new=StringIO()):
-            t_map.print()
-            print("This should be eaten by the StringIO")
-
-    def test_update(self):
-        dict1 = ColumnSummary()
-        stern_df = pd.read_csv(self.stern_map_path, delimiter='\t', header=0)
-        dict1.update(stern_df)
-        self.assertEqual(len(dict1.value_info.keys()), 0, "ColumnSummary value_info should be empty if no value columns")
-        self.assertEqual(len(dict1.categorical_info.keys()), len(stern_df.columns),
-                         "ColumnSummary categorical_info should have all the columns if no restrictions")
-
-        dict2 = ColumnSummary(value_cols=['letter'], skip_cols=['event_type'])
-        dict2.update(stern_df)
-        self.assertEqual(len(dict2.value_info.keys()), 1, "ColumnSummary value_info should have letter value column")
-        self.assertEqual(dict2.value_info['letter'], len(stern_df), "ColumnSummary value counts should length of column")
-        self.assertEqual(len(dict2.skip_cols), 1, "ColumnSummary should have one skip column")
-        self.assertEqual(len(dict2.categorical_info.keys()), len(stern_df.columns) - 2,
-                         "ColumnSummary categorical_info should have all the columns except value and skip columns")
-        dict2.update(stern_df)
-        self.assertEqual(len(dict2.value_info.keys()), 1, "ColumnSummary value_info should have letter value column")
-        self.assertEqual(dict2.value_info['letter'], 2*len(stern_df),
-                         "ColumnSummary value counts should update by column length each time update is called")
-
-    def test_update_dict(self):
-        dict1 = ColumnSummary()
-        dict2 = ColumnSummary()
-        stern_df_map = pd.read_csv(self.stern_map_path, delimiter='\t', header=0)
-        dict1.update(stern_df_map)
-        dict2.update_dict(dict1)
-        dict2.update_dict(dict1)
-        dict1.update(stern_df_map)
-        cat_keys = dict1.categorical_info.keys()
-        for col in cat_keys:
-            self.assertTrue(col in dict2.categorical_info.keys())
-            key_dict1 = dict1.categorical_info[col]
-            key_dict2 = dict2.categorical_info[col]
-            for key, value in key_dict1.items():
-                self.assertEqual(value, key_dict2[key], "update_dict should give same values as update")
-
-    def test_update_dict_with_value_cols(self):
-        stern_df_test1 = pd.read_csv(self.stern_test1_path, delimiter='\t', header=0)
-        stern_df_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
-        dict1 = ColumnSummary(value_cols=['latency'])
-        dict1.update(stern_df_test3)
-        dict2 = ColumnSummary(value_cols=['latency'])
-        dict2.update(stern_df_test1)
-        dict2.update_dict(dict1)
-        dict1.update(stern_df_test1)
-        value_keys = dict1.value_info.keys()
-        for col in value_keys:
-            self.assertEqual(dict1.value_info[col], dict2.value_info[col], "update_dict should update values correctly")
-
-    def test_update_dict_with_bad_value_cols(self):
-        stern_df_test1 = pd.read_csv(self.stern_test1_path, delimiter='\t', header=0)
-        stern_df_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
-        dict1 = ColumnSummary(value_cols=['latency'])
-        dict1.update(stern_df_test3)
-        dict3 = ColumnSummary()
-        dict3.update(stern_df_test1)
-        try:
-            dict1.update_dict(dict3)
-        except HedFileError:
-            pass
-        except Exception as ex:
-            self.fail(f'update_dict threw the wrong exception {ex} when value columns should be categorical')
-        else:
-            self.fail('update_dict should have thrown a HedFileError when value columns should be categorical')
-
-    def test_update_dict_bad_skip_col(self):
-        stern_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
-        dict1 = ColumnSummary(skip_cols=['latency'])
-        dict1.update(stern_test3)
-        dict2 = ColumnSummary(value_cols=['latency'])
-        dict2.update(stern_test3)
-        try:
-            dict2.update_dict(dict1)
-        except HedFileError:
-            pass
-        except Exception as ex:
-            self.fail(f'update_dict threw the wrong exception {ex} when value columns should be categorical')
-        else:
-            self.fail('update_dict should have thrown a HedFileError when value columns should be categorical')
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/tools/annotation/test_definition_summary.py b/tests/tools/annotation/test_definition_summary.py
index abd0625bb..a0f94ea01 100644
--- a/tests/tools/annotation/test_definition_summary.py
+++ b/tests/tools/annotation/test_definition_summary.py
@@ -8,7 +8,7 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.test_json = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.test_json = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                      '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
 
     def test_constructor(self):
@@ -20,13 +20,13 @@ def test_constructor(self):
         sidecar1 = BidsSidecarFile(Test.test_json, set_contents=False)
         self.assertEqual(sidecar1.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar1.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar1.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar1.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertFalse(sidecar1.contents)
 
         sidecar2 = BidsSidecarFile(Test.test_json, set_contents=True)
         self.assertEqual(sidecar2.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar2.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar2.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar2.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertIsInstance(sidecar2.contents, Sidecar, "BidsSidecarFile should contain a Sidecar")
 
 
diff --git a/tests/tools/annotation/test_file_dictionary.py b/tests/tools/annotation/test_file_dictionary.py
new file mode 100644
index 000000000..c854e3173
--- /dev/null
+++ b/tests/tools/annotation/test_file_dictionary.py
@@ -0,0 +1,35 @@
+import unittest
+import os
+
+from hed.errors.exceptions import HedFileError
+from hed.tools import FileDictionary
+from hed.util import get_file_list
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.bids_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                         '../../data/bids/eeg_ds003654s_hed')
+
+    def test_constructor_valid(self):
+        file_list = get_file_list(self.bids_base_dir, name_suffix="_events",
+                                  extensions=['.tsv'], exclude_dirs=['stimuli'])
+        dict1 = FileDictionary(file_list)
+        self.assertEqual(6, len(dict1.key_list), "FileDictionary should have correct number of entries when key okay")
+
+    def test_constructor_invalid(self):
+        file_list = get_file_list(self.bids_base_dir, name_suffix="_events",
+                                  extensions=['.tsv'], exclude_dirs=['stimuli'])
+        try:
+            dict1 = FileDictionary(file_list, name_indices=(0, 1))
+        except HedFileError:
+            pass
+        except Exception:
+            self.fail("FileDictionary threw the wrong exception when duplicate key")
+        else:
+            self.fail("FileDictionary should have thrown a HedFileError when duplicate key")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/tools/annotation/test_key_dict.py b/tests/tools/annotation/test_key_dict.py
index e63b549e6..e9b1441e6 100644
--- a/tests/tools/annotation/test_key_dict.py
+++ b/tests/tools/annotation/test_key_dict.py
@@ -10,8 +10,8 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/attention_shift')
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/attention_shift')
         cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
         cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
diff --git a/tests/tools/annotation/test_key_map.py b/tests/tools/annotation/test_key_map.py
index 8c7cd2465..831ebe473 100644
--- a/tests/tools/annotation/test_key_map.py
+++ b/tests/tools/annotation/test_key_map.py
@@ -10,9 +10,9 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/')
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/attention_shift')
+        cls.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/')
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/attention_shift')
         cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
         cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
diff --git a/tests/tools/annotation/test_map_summary.py b/tests/tools/annotation/test_map_summary.py
index e790511fc..30d49d596 100644
--- a/tests/tools/annotation/test_map_summary.py
+++ b/tests/tools/annotation/test_map_summary.py
@@ -1,8 +1,6 @@
 import os
 import unittest
-from hed.tools.annotation.column_summary import ColumnSummary
-from hed.util.io_util import get_file_list, make_file_dict
-from hed.tools.annotation.map_summary import get_columns_info, get_key_counts, make_combined_dicts, update_dict_counts
+from hed.tools import get_columns_info, get_key_counts, update_dict_counts
 from hed.util.data_util import get_new_dataframe
 
 
@@ -10,9 +8,9 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.bids_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/bids/eeg_ds003654s_hed')
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/attention_shift')
+        cls.bids_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/bids/eeg_ds003654s_hed')
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/attention_shift')
         cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
         cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
@@ -51,25 +49,9 @@ def test_get_key_counts(self):
         self.assertEqual(len(key_counts2['event_type']), 8, "get_key_counts has right number of entries for event_type")
         self.assertTrue('onset' not in key_counts2, "get_key_counts dictionary does not have onset if skipped")
 
-    def test_make_combined_dicts(self):
-        files_bids = get_file_list(self.bids_dir, extensions=[".tsv"], name_suffix="_events")
-        file_dict = make_file_dict(files_bids)
-        dicts_all1, dicts1 = make_combined_dicts(file_dict)
-        self.assertTrue(isinstance(dicts_all1, ColumnSummary), "make_combined_dicts should return a ColumnSummary")
-        self.assertTrue(isinstance(dicts1, dict), "make_combined_dicts should also return a dictionary of file names")
-        self.assertEqual(len(dicts1), 6, "make_combined_dicts should return correct number of file names")
-        self.assertEqual(len(dicts_all1.categorical_info), 10,
-                         "make_combined_dicts should return right number of entries")
-        dicts_all2, dicts2 = make_combined_dicts(file_dict, skip_cols=["onset", "duration", "sample"])
-        self.assertTrue(isinstance(dicts_all2, ColumnSummary), "make_combined_dicts should return a ColumnSummary")
-        self.assertTrue(isinstance(dicts2, dict), "make_combined_dicts should also return a dictionary of file names")
-        self.assertEqual(len(dicts2), 6, "make_combined_dicts should return correct number of file names")
-        self.assertEqual(len(dicts_all2.categorical_info), 7,
-                         "make_combined_dicts should return right number of entries")
-
     def test_update_dict_counts(self):
         file_name = os.path.join(self.bids_dir, 'sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv')
-        file_name = os.path.abspath(file_name)
+        file_name = os.path.realpath(file_name)
         dataframe = get_new_dataframe(file_name)
         count_dicts = {}
         update_dict_counts(count_dicts, "onset", dataframe["onset"])
diff --git a/tests/tools/annotation/test_sidecar_summary.py b/tests/tools/annotation/test_sidecar_summary.py
index 1c96655d9..c40d25644 100644
--- a/tests/tools/annotation/test_sidecar_summary.py
+++ b/tests/tools/annotation/test_sidecar_summary.py
@@ -1,8 +1,6 @@
 import os
 import unittest
 from hed.models.sidecar import Sidecar
-from hed.tools.bids.bids_event_file import BidsEventFile
-from hed.tools.bids.bids_file import BidsFile
 from hed.tools.bids.bids_sidecar_file import BidsSidecarFile
 
 
@@ -10,20 +8,20 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.sidecar_path1 = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.sidecar_path1 = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                          '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
 
     def test_constructor(self):
         sidecar1 = BidsSidecarFile(Test.sidecar_path1, set_contents=False)
         self.assertEqual(sidecar1.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar1.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar1.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar1.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertFalse(sidecar1.contents)
 
         sidecar2 = BidsSidecarFile(Test.sidecar_path1, set_contents=True)
         self.assertEqual(sidecar2.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar2.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar2.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar2.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertIsInstance(sidecar2.contents, Sidecar, "BidsSidecarFile should contain a Sidecar")
 
 
diff --git a/tests/tools/annotation/test_summary_util.py b/tests/tools/annotation/test_summary_util.py
index fd307baac..cf69904e0 100644
--- a/tests/tools/annotation/test_summary_util.py
+++ b/tests/tools/annotation/test_summary_util.py
@@ -10,7 +10,7 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.sidecar_path1 = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.sidecar_path1 = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                          '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
         cls.schema = load_schema_version(xml_version="8.0.0")
         cls.str1 = f"(Task,Experiment-participant,(See,Face),(Discriminate,(Face, Symmetrical))," \
@@ -21,7 +21,7 @@ def setUpClass(cls):
         cls.str_with_def = f"(Definition/First-show-cond, ((Condition-variable/Repetition-type," \
                            f" (Item-count/1, Face), Item-interval/0), " \
                            f"Description/Factor level indicating the first display of this face.))"
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                  "../../data/summaries/tag_summary_template.json5")
         with open(json_path) as fp:
             cls.rules = json.load(fp)
diff --git a/tests/tools/bids/test_bids_dataset.py b/tests/tools/bids/test_bids_dataset.py
index 24cc056af..c4e7d09b2 100644
--- a/tests/tools/bids/test_bids_dataset.py
+++ b/tests/tools/bids/test_bids_dataset.py
@@ -11,8 +11,8 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.root_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/bids/eeg_ds003654s_hed')
-        cls.library_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/bids/eeg_ds003654s_hed')
+        cls.library_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                         '../../data/bids/eeg_ds003654s_hed_library')
 
     def test_bids_constructor(self):
diff --git a/tests/tools/bids/test_bids_dataset_summary.py b/tests/tools/bids/test_bids_dataset_summary.py
new file mode 100644
index 000000000..2bc63cc28
--- /dev/null
+++ b/tests/tools/bids/test_bids_dataset_summary.py
@@ -0,0 +1,25 @@
+import os
+import json
+import unittest
+import pandas as pd
+from hed.schema.hed_schema_group import HedSchemaGroup
+from hed.tools import BidsDataset, BidsDatasetSummary
+from hed.tools.bids.bids_event_files import BidsEventFiles
+
+
+class Test(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/bids/eeg_ds003654s_hed')
+        cls.library_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                        '../../data/bids/eeg_ds003654s_hed_library')
+
+    def test_bids_constructor(self):
+        bids1 = BidsDatasetSummary(Test.root_path)
+        self.assertIsInstance(bids1, BidsDatasetSummary,
+                              "BidsDatasetSummary should create a valid object from valid dataset")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/tools/bids/test_bids_event_file.py b/tests/tools/bids/test_bids_event_file.py
index 7f0fda4e5..bc5e4f82f 100644
--- a/tests/tools/bids/test_bids_event_file.py
+++ b/tests/tools/bids/test_bids_event_file.py
@@ -10,16 +10,16 @@ class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.event_path = \
-            os.path.join(os.path.dirname(os.path.abspath(__file__)),
+            os.path.join(os.path.dirname(os.path.realpath(__file__)),
                          '../../data/bids/eeg_ds003654s_hed/sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv')
-        cls.sidecar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.sidecar_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                         '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
 
     def test_constructor(self):
         events = BidsEventFile(Test.event_path)
         self.assertEqual(events.suffix, 'events', "BidsEventFile should have correct events suffix")
         self.assertEqual(events.ext, '.tsv', "BidsEventFile should have a .tsv extension")
-        self.assertEqual(len(events.entities), 3, "BidsEventFile should have right number of entities")
+        self.assertEqual(len(events.entity_dict), 3, "BidsEventFile should have right number of entity_dict")
         events_str = str(events)
         self.assertTrue(events_str, "BidsEventFile should have a string representation")
 
diff --git a/tests/tools/bids/test_bids_event_files.py b/tests/tools/bids/test_bids_event_files.py
index 05e78eedc..ca4401acc 100644
--- a/tests/tools/bids/test_bids_event_files.py
+++ b/tests/tools/bids/test_bids_event_files.py
@@ -9,13 +9,13 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.root_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../data/bids/eeg_ds003654s_hed')
+        cls.root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/bids/eeg_ds003654s_hed')
         cls.event_path = \
-            os.path.join(os.path.dirname(os.path.abspath(__file__)),
+            os.path.join(os.path.dirname(os.path.realpath(__file__)),
                          '../../data/bids/eeg_ds003654s_hed/sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv')
         cls.sidecar_path = \
-            os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                         '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.tsv')
+            os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                         '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.tsv')
 
     def test_constructor(self):
         events = BidsEventFiles(Test.root_path)
diff --git a/tests/tools/bids/test_bids_file.py b/tests/tools/bids/test_bids_file.py
index f7daad355..f15d53419 100644
--- a/tests/tools/bids/test_bids_file.py
+++ b/tests/tools/bids/test_bids_file.py
@@ -7,21 +7,21 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.event_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.event_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                       '../../data/bids_old/sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv')
-        cls.sidecar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.sidecar_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                         '../../data/bids_old/task-FacePerception_events.json')
 
     def test_bids_file_constructor(self):
         bids1 = BidsFile(Test.event_path)
         self.assertEqual(bids1.suffix, 'events', "BidsFile should have correct events suffix")
         self.assertEqual(bids1.ext, '.tsv', "BidsFile should have a .tsv extension")
-        self.assertEqual(len(bids1.entities), 3, "BidsFile should have right number of entities")
+        self.assertEqual(len(bids1.entity_dict), 3, "BidsFile should have right number of entity_dict")
 
         bids2 = BidsFile(Test.sidecar_path)
         self.assertEqual(bids2.suffix, 'events', "BidsFile should have correct events suffix")
         self.assertEqual(bids2.ext, '.json', "BidsFile should have a .json extension")
-        self.assertEqual(len(bids2.entities), 1, "BidsFile should have right number of entities")
+        self.assertEqual(len(bids2.entity_dict), 1, "BidsFile should have right number of entity_dict")
 
     def test_bids_file_str(self):
         bids = BidsFile(Test.sidecar_path)
diff --git a/tests/tools/bids/test_bids_file_dictionary.py b/tests/tools/bids/test_bids_file_dictionary.py
new file mode 100644
index 000000000..ff08feb48
--- /dev/null
+++ b/tests/tools/bids/test_bids_file_dictionary.py
@@ -0,0 +1,65 @@
+import unittest
+import os
+
+from hed.errors.exceptions import HedFileError
+from hed.tools import BidsFileDictionary
+from hed.util import get_file_list
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.bids_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                         '../../data/bids/eeg_ds003654s_hed')
+        cls.file_list = get_file_list(cls.bids_base_dir, name_suffix="_events",
+                                  extensions=['.tsv'], exclude_dirs=['stimuli'])
+
+    def test_constructor_valid(self):
+        dict1 = BidsFileDictionary(self.file_list, entities=('sub', 'run'))
+        self.assertEqual(6, len(dict1.key_list),
+                         "BidsFileDictionary should have correct number of entries when key okay")
+
+    def test_constructor_invalid(self):
+        try:
+            dict1 = BidsFileDictionary(self.file_list, entities=('sub', 'task'))
+        except HedFileError:
+            pass
+        except Exception:
+            self.fail("BidsFileDictionary threw the wrong exception when duplicate key")
+        else:
+            self.fail("BidsFileDictionary should have thrown a HedFileError when duplicate key")
+
+    def test_match_query(self):
+        entity_dict = {'sub':'01', 'task': 'tempTask', 'run':'2'}
+        query_dict1 = {'sub':['01', '03']}
+        result1 = BidsFileDictionary.match_query(query_dict1, entity_dict)
+        self.assertTrue(result1, "match_query should return true when entity in the dictionary")
+        query_dict2 = {'sub':['02', '03']}
+        result2 = BidsFileDictionary.match_query(query_dict2, entity_dict)
+        self.assertFalse(result2, "match_query should return False when entity not in the dictionary")
+        query_dict3 = {'sub': ['01', '03'], 'run':['1', '2']}
+        result3 = BidsFileDictionary.match_query(query_dict3, entity_dict)
+        self.assertTrue(result3, "match_query should return True when entity in the dictionary")
+        query_dict4 = {'sub': ['01', '03'], 'run': ['3', '2']}
+        result4 = BidsFileDictionary.match_query(query_dict4, entity_dict)
+        self.assertTrue(result4, "match_query should return False when entity not in the dictionary")
+
+    def test_make_query(self):
+        dict1 = BidsFileDictionary(self.file_list, entities=('sub', 'run'))
+        results1 = dict1.make_query(query_dict={'sub': '*', 'run': '*'})
+        self.assertEqual(len(results1), len(dict1.file_dict), "make_query should return all of the entries when *")
+        results2 = dict1.make_query(query_dict={'sub': '*', 'run': ['1']})
+        self.assertEqual(len(results2), 2, "make_query should return the right number of entries ")
+
+    def test_create_split_dict(self):
+        dict1 = BidsFileDictionary(self.file_list, entities=('sub', 'run'))
+        dist1_split, leftovers = dict1.create_split_dict('run')
+        self.assertIsInstance(dist1_split, dict, "create_split_dict returns a dictionary")
+        self.assertEqual(3, len(dist1_split), 'create_split_dict should return the correct number of items')
+        for value in dist1_split.values():
+            self.assertIsInstance(value, BidsFileDictionary,
+                                  'create_split_dict dictionary values should be BidsFileDictionary objects')
+        self.assertFalse(leftovers, "create_split_dict leftovers should be empty")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/tools/bids/test_bids_json_file.py b/tests/tools/bids/test_bids_json_file.py
index 4bc0fdb30..9d9db45ee 100644
--- a/tests/tools/bids/test_bids_json_file.py
+++ b/tests/tools/bids/test_bids_json_file.py
@@ -7,22 +7,22 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.description_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.description_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                             '../../data/bids/eeg_ds003654s_hed/dataset_description.json')
-        cls.json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                        '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
+        cls.json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                     '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
 
     def test_constructor(self):
         json1 = BidsJsonFile(Test.description_path, set_contents=False)
         self.assertEqual(json1.suffix, 'dataset_description', "BidsJsonFile should have correct name_suffix")
         self.assertEqual(json1.ext, '.json', "BidsJsonFile should have correct ext")
-        self.assertEqual(len(json1.entities), 0, "BidssonFile should have right number of entities")
+        self.assertEqual(len(json1.entity_dict), 0, "BidssonFile should have right number of entity_dict")
         self.assertFalse(json1.contents)
 
         sidecar2 = BidsJsonFile(Test.json_path, set_contents=True)
         self.assertEqual(sidecar2.suffix, 'events', "BidsJsonFile should have correct name_suffix")
         self.assertEqual(sidecar2.ext, '.json', "BidsJsonFile should have correct ext")
-        self.assertEqual(len(sidecar2.entities), 1, "BidsJsonFile should have right number of entities")
+        self.assertEqual(len(sidecar2.entity_dict), 1, "BidsJsonFile should have right number of entity_dict")
         self.assertIsInstance(sidecar2.contents, dict, "BidsJsonFile should contain a dictionary")
 
     def test_bids_json_file_str(self):
diff --git a/tests/tools/bids/test_bids_sidecar_file.py b/tests/tools/bids/test_bids_sidecar_file.py
index ba5c6c90c..5cb40bca5 100644
--- a/tests/tools/bids/test_bids_sidecar_file.py
+++ b/tests/tools/bids/test_bids_sidecar_file.py
@@ -10,23 +10,23 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.event_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.event_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                       '../../data/bids/eeg_ds003654s_hed/sub-002/'
                                       'eeg/sub-002_task-FacePerception_run-1_events.tsv')
-        cls.sidecar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        cls.sidecar_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                         '../../data/bids/eeg_ds003654s_hed/task-FacePerception_events.json')
 
     def test_constructor(self):
         sidecar1 = BidsSidecarFile(Test.sidecar_path, set_contents=False)
         self.assertEqual(sidecar1.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar1.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar1.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar1.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertFalse(sidecar1.contents)
 
         sidecar2 = BidsSidecarFile(Test.sidecar_path, set_contents=True)
         self.assertEqual(sidecar2.suffix, 'events', "BidsSidecarFile should have correct name_suffix")
         self.assertEqual(sidecar2.ext, '.json', "BidsSidecarFile should have correct ext")
-        self.assertEqual(len(sidecar2.entities), 1, "BidsSidecarFile should have right number of entities")
+        self.assertEqual(len(sidecar2.entity_dict), 1, "BidsSidecarFile should have right number of entity_dict")
         self.assertIsInstance(sidecar2.contents, Sidecar, "BidsSidecarFile should contain a Sidecar")
 
     def test_is_sidecar_for(self):
@@ -41,13 +41,13 @@ def test_is_sidecar_for(self):
         other1 = BidsSidecarFile('/d/base1/task-overt_run-2_bold.json')
         self.assertFalse(other1.is_sidecar_for(bids), "is_a_parent returns false if directories don't match")
         other2 = BidsSidecarFile('/d/base/task-overt_run-3_bold.json')
-        self.assertFalse(other2.is_sidecar_for(bids), "is_a_parent returns false if entities don't match")
+        self.assertFalse(other2.is_sidecar_for(bids), "is_a_parent returns false if entity_dict don't match")
         other3 = BidsSidecarFile('/d/base/sub-01/sub-01_task-overt_bold.json')
-        self.assertTrue(other3.is_sidecar_for(bids), "is_a_parent returns true if entities  match")
+        self.assertTrue(other3.is_sidecar_for(bids), "is_a_parent returns true if entity_dict  match")
         other4 = BidsSidecarFile('/d/base/sub-01/sub-01_task-overt_events.json')
         self.assertFalse(other4.is_sidecar_for(bids), "is_a_parent returns false if suffixes don't match")
         other5 = BidsSidecarFile('/d/base/sub-01/ses-test/func/temp/sub-01_ses-test_task-overt_run-2_bold.json')
-        self.assertFalse(other5.is_sidecar_for(bids), "is_a_parent returns false for child even if entities match")
+        self.assertFalse(other5.is_sidecar_for(bids), "is_a_parent returns false for child even if entity_dict match")
 
 
 if __name__ == '__main__':
diff --git a/tests/tools/bids/test_bids_tsv_file.py b/tests/tools/bids/test_bids_tsv_file.py
index 6802556d0..88f298361 100644
--- a/tests/tools/bids/test_bids_tsv_file.py
+++ b/tests/tools/bids/test_bids_tsv_file.py
@@ -9,14 +9,14 @@ class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.tsv_path = \
-            os.path.join(os.path.dirname(os.path.abspath(__file__)),
+            os.path.join(os.path.dirname(os.path.realpath(__file__)),
                          '../../data/bids/eeg_ds003654s_hed/sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv')
 
     def test_bids_tsv_file_constructor(self):
         tsv_file = BidsTsvFile(Test.tsv_path)
         self.assertEqual(tsv_file.suffix, 'events', "BidsTsvFile should have correct events suffix")
         self.assertEqual(tsv_file.ext, '.tsv', "BidsTsvFile should have a .tsv extension")
-        self.assertEqual(len(tsv_file.entities), 3, "BidsTsvFile should have right number of entities")
+        self.assertEqual(len(tsv_file.entity_dict), 3, "BidsTsvFile should have right number of entity_dict")
 
     def test_bids_tsv_file_str(self):
         tsv_file1 = BidsTsvFile(Test.tsv_path)
diff --git a/tests/tools/bids/test_bids_tsv_file_dictionary.py b/tests/tools/bids/test_bids_tsv_file_dictionary.py
new file mode 100644
index 000000000..0b159f077
--- /dev/null
+++ b/tests/tools/bids/test_bids_tsv_file_dictionary.py
@@ -0,0 +1,43 @@
+import unittest
+import os
+
+from hed.errors.exceptions import HedFileError
+from hed.tools import BidsTsvDictionary
+from hed.util import get_file_list
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.bids_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                         '../../data/bids/eeg_ds003654s_hed')
+        cls.file_list = get_file_list(cls.bids_base_dir, name_suffix="_events",
+                                      extensions=['.tsv'], exclude_dirs=['stimuli'])
+
+    def test_bids_tsv_dictionary_constructor_valid(self):
+        dict1 = BidsTsvDictionary(self.file_list, entities=('sub', 'run'))
+        self.assertEqual(6, len(dict1.key_list),
+                         "BidsTsvDictionary should have correct number of entries when key okay")
+
+    def test_bids_tsv_dictionary_constructor_invalid(self):
+        try:
+            dict1 = BidsTsvDictionary(self.file_list, entities=('sub'))
+        except HedFileError:
+            pass
+        except Exception:
+            self.fail("BidsTsvDictionary threw the wrong exception when duplicate key")
+        else:
+            self.fail("FileDictionary should have thrown a HedFileError when duplicate key")
+
+    def test_create_split_dict(self):
+        dict1 = BidsTsvDictionary(self.file_list, entities=('sub', 'run'))
+        dist1_split, leftovers = dict1.create_split_dict('run')
+        self.assertIsInstance(dist1_split, dict, "create_split_dict returns a dictionary")
+        self.assertEqual(3, len(dist1_split), 'create_split_dict should return the correct number of items')
+        for value in dist1_split.values():
+            self.assertIsInstance(value, BidsTsvDictionary, "create_split_dict dict has BidsTsvDictionary objects")
+        self.assertFalse(leftovers, "create_split_dict leftovers should be empty")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/tools/bids/test_bids_tsv_summary.py b/tests/tools/bids/test_bids_tsv_summary.py
new file mode 100644
index 000000000..f0594cbda
--- /dev/null
+++ b/tests/tools/bids/test_bids_tsv_summary.py
@@ -0,0 +1,164 @@
+import unittest
+import os
+from unittest import mock
+import pandas as pd
+from hed.errors.exceptions import HedFileError
+from hed.tools import BidsTsvSummary, BidsFileDictionary
+from hed.util import get_file_list, get_new_dataframe
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/attention_shift')
+        cls.bids_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                         '../../data/bids/eeg_ds003654s_hed')
+        cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
+        cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
+        cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
+        cls.stern_test3_path = os.path.join(stern_base_dir, "sternberg_no_quotes_events.tsv")
+        cls.attention_shift_path = os.path.join(att_base_dir, "auditory_visual_shift_events.tsv")
+        cls.wh_events_path = \
+            os.path.realpath(os.path.join(cls.bids_base_dir,
+                                          'sub-002/eeg/sub-002_task-FacePerception_run-1_events.tsv'))
+
+    def test_constructor(self):
+        dict1 = BidsTsvSummary()
+        self.assertIsInstance(dict1, BidsTsvSummary, "BidsTsvSummary constructor is allowed to have no arguments")
+        self.assertFalse(dict1.value_info)
+
+        dict2 = BidsTsvSummary(value_cols=['a', 'b', 'c'], name='baloney')
+        self.assertIsInstance(dict2, BidsTsvSummary, "BidsTsvSummary: multiple values are okay in constructor")
+        self.assertEqual(len(dict2.value_info.keys()), 3, "BidsTsvSummary should have keys for each value column")
+
+    def test_get_number_unique_values(self):
+        dict1 = BidsTsvSummary()
+        wh_df = pd.read_csv(self.wh_events_path, delimiter='\t', header=0)
+        dict1.update(wh_df)
+        self.assertEqual(len(dict1.value_info.keys()), 0,
+                         "BidsTsvSummary value_info should be empty if no value columns")
+        self.assertEqual(len(dict1.categorical_info.keys()), len(wh_df.columns),
+                         "BidsTsvSummary categorical_info should have all the columns if no restrictions")
+        count_dict = dict1.get_number_unique_values()
+        self.assertEqual(len(count_dict), 10, "get_number_unique_values should have the correct number of entries")
+        self.assertEqual(count_dict['onset'], 551, "get_number_unique_values should have the right number of unique")
+
+    def test_print(self):
+        from io import StringIO
+        t_map = BidsTsvSummary()
+        t_map.update(self.stern_map_path)
+        df = get_new_dataframe(self.stern_map_path)
+        t_map.update(self.stern_map_path)
+        self.assertEqual(len(t_map.categorical_info.keys()), len(df.columns),
+                         "BidsTsvSummary should have all columns as categorical if no value or skip are given")
+        with mock.patch('sys.stdout', new=StringIO()):
+            t_map.print()
+            print("This should be eaten by the StringIO")
+
+    def test_update(self):
+        dict1 = BidsTsvSummary()
+        stern_df = pd.read_csv(self.stern_map_path, delimiter='\t', header=0)
+        dict1.update(stern_df)
+        self.assertEqual(len(dict1.value_info.keys()), 0,
+                         "BidsTsvSummary value_info should be empty if no value columns")
+        self.assertEqual(len(dict1.categorical_info.keys()), len(stern_df.columns),
+                         "BidsTsvSummary categorical_info should have all the columns if no restrictions")
+
+        dict2 = BidsTsvSummary(value_cols=['letter'], skip_cols=['event_type'])
+        dict2.update(stern_df)
+        self.assertEqual(len(dict2.value_info.keys()), 1,
+                         "BidsTsvSummary value_info should have letter value column")
+        self.assertEqual(dict2.value_info['letter'], len(stern_df),
+                         "BidsTsvSummary value counts should length of column")
+        self.assertEqual(len(dict2.skip_cols), 1, "BidsTsvSummary should have one skip column")
+        self.assertEqual(len(dict2.categorical_info.keys()), len(stern_df.columns) - 2,
+                         "BidsTsvSummary categorical_info should have all the columns except value and skip columns")
+        dict2.update(stern_df)
+        self.assertEqual(len(dict2.value_info.keys()), 1,
+                         "BidsTsvSummary value_info should have letter value column")
+        self.assertEqual(dict2.value_info['letter'], 2*len(stern_df),
+                         "BidsTsvSummary value counts should update by column length each time update is called")
+
+    def test_update_dict(self):
+        dict1 = BidsTsvSummary()
+        dict2 = BidsTsvSummary()
+        stern_df_map = pd.read_csv(self.stern_map_path, delimiter='\t', header=0)
+        dict1.update(stern_df_map)
+        dict2.update_summary(dict1)
+        dict2.update_summary(dict1)
+        dict1.update(stern_df_map)
+        cat_keys = dict1.categorical_info.keys()
+        for col in cat_keys:
+            self.assertTrue(col in dict2.categorical_info.keys())
+            key_dict1 = dict1.categorical_info[col]
+            key_dict2 = dict2.categorical_info[col]
+            for key, value in key_dict1.items():
+                self.assertEqual(value, key_dict2[key], "update_summary should give same values as update")
+
+    def test_update_dict_with_value_cols(self):
+        stern_df_test1 = pd.read_csv(self.stern_test1_path, delimiter='\t', header=0)
+        stern_df_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
+        dict1 = BidsTsvSummary(value_cols=['latency'])
+        dict1.update(stern_df_test3)
+        dict2 = BidsTsvSummary(value_cols=['latency'])
+        dict2.update(stern_df_test1)
+        dict2.update_summary(dict1)
+        dict1.update(stern_df_test1)
+        value_keys = dict1.value_info.keys()
+        for col in value_keys:
+            self.assertEqual(dict1.value_info[col], dict2.value_info[col],
+                             "update_summary should update values correctly")
+
+    def test_update_dict_with_bad_value_cols(self):
+        stern_df_test1 = pd.read_csv(self.stern_test1_path, delimiter='\t', header=0)
+        stern_df_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
+        dict1 = BidsTsvSummary(value_cols=['latency'])
+        dict1.update(stern_df_test3)
+        dict3 = BidsTsvSummary()
+        dict3.update(stern_df_test1)
+        try:
+            dict1.update_summary(dict3)
+        except HedFileError:
+            pass
+        except Exception as ex:
+            self.fail(f'update_summary threw the wrong exception {ex} when value columns should be categorical')
+        else:
+            self.fail('update_summary should have thrown a HedFileError when value columns should be categorical')
+
+    def test_update_dict_bad_skip_col(self):
+        stern_test3 = pd.read_csv(self.stern_test3_path, delimiter='\t', header=0)
+        dict1 = BidsTsvSummary(skip_cols=['latency'])
+        dict1.update(stern_test3)
+        dict2 = BidsTsvSummary(value_cols=['latency'])
+        dict2.update(stern_test3)
+        try:
+            dict2.update_summary(dict1)
+        except HedFileError:
+            pass
+        except Exception as ex:
+            self.fail(f'update_summary threw the wrong exception {ex} when value columns should be categorical')
+        else:
+            self.fail('update_summary should have thrown a HedFileError when value columns should be categorical')
+
+    def test_make_combined_dicts(self):
+        files_bids = get_file_list(self.bids_base_dir, extensions=[".tsv"], name_suffix="_events")
+        file_dict = BidsFileDictionary(files_bids)
+        dicts_all1, dicts1 = BidsTsvSummary.make_combined_dicts(file_dict)
+        self.assertTrue(isinstance(dicts_all1, BidsTsvSummary),
+                        "make_combined_dicts should return a BidsTsvSummary")
+        self.assertTrue(isinstance(dicts1, dict), "make_combined_dicts should also return a dictionary of file names")
+        self.assertEqual(6, len(dicts1), "make_combined_dicts should return correct number of file names")
+        self.assertEqual(10, len(dicts_all1.categorical_info),
+                         "make_combined_dicts should return right number of entries")
+        dicts_all2, dicts2 = BidsTsvSummary.make_combined_dicts(file_dict, skip_cols=["onset", "duration", "sample"])
+        self.assertTrue(isinstance(dicts_all2, BidsTsvSummary),
+                        "make_combined_dicts should return a BidsTsvSummary")
+        self.assertTrue(isinstance(dicts2, dict), "make_combined_dicts should also return a dictionary of file names")
+        self.assertEqual(6, len(dicts2), "make_combined_dicts should return correct number of file names")
+        self.assertEqual(len(dicts_all2.categorical_info), 7,
+                         "make_combined_dicts should return right number of entries")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/util/test_data_util.py b/tests/util/test_data_util.py
index 721d2aa23..f1a527a9e 100644
--- a/tests/util/test_data_util.py
+++ b/tests/util/test_data_util.py
@@ -12,8 +12,8 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/attention_shift')
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/attention_shift')
         cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
         cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
diff --git a/tests/util/test_file_util.py b/tests/util/test_file_util.py
index 2c7a19e06..c7242f7b8 100644
--- a/tests/util/test_file_util.py
+++ b/tests/util/test_file_util.py
@@ -8,7 +8,7 @@ class Test(unittest.TestCase):
     def setUpClass(cls):
         cls.default_test_url = \
             """https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED8.0.0.xml"""
-        cls.hed_xml_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../hed_pairs/HED8.0.0t.xml')
+        cls.hed_xml_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../hed_pairs/HED8.0.0t.xml')
 
     def test_url_to_file(self):
         downloaded_file = file_util.url_to_file(self.default_test_url)
diff --git a/tests/util/test_io_util.py b/tests/util/test_io_util.py
index 1b14e65f5..a984bf0fd 100644
--- a/tests/util/test_io_util.py
+++ b/tests/util/test_io_util.py
@@ -9,9 +9,9 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.bids_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/bids/eeg_ds003654s_hed')
-        stern_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/sternberg')
-        att_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/attention_shift')
+        cls.bids_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/bids/eeg_ds003654s_hed')
+        stern_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/sternberg')
+        att_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/attention_shift')
         cls.stern_map_path = os.path.join(stern_base_dir, "sternberg_map.tsv")
         cls.stern_test1_path = os.path.join(stern_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(stern_base_dir, "sternberg_with_quotes_events.tsv")
@@ -59,7 +59,8 @@ def test_get_dir_dictionary(self):
         self.assertEqual(len(dir_dict), 3, "get_dir_dictionary returns a dictionary of the correct length")
 
     def test_get_file_list_files(self):
-        dir_pairs = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/hed_pairs/prologue_tests')
+        dir_pairs = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/hed_pairs/prologue_tests')
+        dir_pairs = os.path.realpath(dir_pairs)
         test_files = [name for name in os.listdir(dir_pairs) if os.path.isfile(os.path.join(dir_pairs, name))]
         file_list1 = get_file_list(dir_pairs)
         for file in file_list1:
@@ -73,7 +74,7 @@ def test_get_file_list_files(self):
             raise HedFileError("FileShouldNotBeFound", f"get_event_files should have not have found file {file}", "")
 
     def test_get_get_file_list_suffix(self):
-        dir_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data')
+        dir_data = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data')
         file_list = get_file_list(dir_data, extensions=[".json", ".tsv"])
         for item in file_list:
             if item.endswith(".json") or item.endswith(".tsv"):
@@ -81,32 +82,41 @@ def test_get_get_file_list_suffix(self):
             raise HedFileError("BadFileType", "get_event_files expected only .html or .js files", "")
 
     def test_get_file_list_prefix(self):
-        dir_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/sternberg')
+        dir_data = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/sternberg')
         file_list = get_file_list(dir_data, name_prefix='sternberg', extensions=[".tsv"])
         for item in file_list:
             filename = os.path.basename(item)
             self.assertTrue(filename.startswith('sternberg'))
 
     def test_get_file_list_case(self):
-        dir_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/sternberg')
+        dir_data = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/sternberg')
         file_list = get_file_list(dir_data, name_prefix='STERNBerg', extensions=[".Tsv"])
         for item in file_list:
             filename = os.path.basename(item)
             self.assertTrue(filename.startswith('sternberg'))
 
+    def test_get_file_list_exclude_dir(self):
+        dir_data = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/bids/eeg_ds003654s_hed')
+        file_list1 = get_file_list(dir_data, extensions=[".bmp"])
+        self.assertEqual(345, len(file_list1), 'get_file_list has the right number of files when no exclude')
+        file_list2 = get_file_list(dir_data, extensions=[".bmp"], exclude_dirs=[])
+        self.assertEqual(len(file_list1), len(file_list2), 'get_file_list should not change when exclude_dir is empty')
+        file_list3 = get_file_list(dir_data, extensions=[".bmp"], exclude_dirs=['stimuli'])
+        self.assertFalse(file_list3, 'get_file_list should return an empty list when all are excluded')
+
     def test_get_path_components(self):
         base_path = 'D:/Research/HED/hed-examples/datasets/eeg_ds003654s'
         file_path1 = 'D:/Research/HED/hed-examples/datasets/eeg_ds003654s/sub-01/ses-1/eeg/temp_events.tsv'
         comps1 = get_path_components(file_path1, base_path)
-        self.assertEqual(comps1[0], os.path.abspath(base_path),
+        self.assertEqual(comps1[0], os.path.realpath(base_path),
                          "get_path_components base_path is is the first component")
         self.assertEqual(len(comps1), 4, "get_path_components has correct number of components")
         comps2 = get_path_components(base_path, base_path)
-        self.assertEqual(comps2[0], os.path.abspath(base_path), "get_path_components base_path is its own base_path")
+        self.assertEqual(comps2[0], os.path.realpath(base_path), "get_path_components base_path is its own base_path")
         self.assertEqual(len(comps2), 1, "get_path_components base_path has no additional components")
         file_path3 = 'D:/Research/HED/hed-examples/datasets/eeg_ds003654s/temp_events.tsv'
         comps3 = get_path_components(file_path3, base_path)
-        self.assertEqual(comps3[0], os.path.abspath(base_path), "get_path_components base_path is its own base_path")
+        self.assertEqual(comps3[0], os.path.realpath(base_path), "get_path_components base_path is its own base_path")
         self.assertEqual(len(comps2), 1, "get_path_components file in base_path has no additional components")
 
     def test_parse_bids_filename_full(self):
@@ -114,12 +124,12 @@ def test_parse_bids_filename_full(self):
         suffix1, ext1, entity_dict1, unmatched1 = parse_bids_filename(the_path1)
         self.assertEqual(suffix1, 'bold', "parse_bids_filename should correctly parse name_suffix for full path")
         self.assertEqual(ext1, '.json', "parse_bids_filename should correctly parse ext for full path")
-        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entities as a dictionary")
+        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entity_dict as a dictionary")
         self.assertEqual(entity_dict1['sub'], '01', "parse_bids_filename should have a sub entity")
         self.assertEqual(entity_dict1['ses'], 'test', "parse_bids_filename should have a ses entity")
         self.assertEqual(entity_dict1['task'], 'overt', "parse_bids_filename should have a task entity")
         self.assertEqual(entity_dict1['run'], '2', "parse_bids_filename should have a run entity")
-        self.assertEqual(len(entity_dict1), 4, "parse_bids_filename should 4 entities in the dictionary")
+        self.assertEqual(len(entity_dict1), 4, "parse_bids_filename should 4 entity_dict in the dictionary")
         self.assertEqual(len(unmatched1), 0, "parse_bids_filename should not have unmatched items")
 
     def test_parse_bids_filename_partial(self):
@@ -127,21 +137,21 @@ def test_parse_bids_filename_partial(self):
         suffix1, ext1, entity_dict1, unmatched1 = parse_bids_filename(path1)
         self.assertEqual(suffix1, 'bold', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext1, '.json', "parse_bids_filename should correctly parse ext for name")
-        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entities as a dictionary")
+        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entity_dict as a dictionary")
         self.assertEqual(entity_dict1['task'], 'overt', "parse_bids_filename should have a task entity")
-        self.assertEqual(len(entity_dict1), 1, "parse_bids_filename should 1 entities in the dictionary")
+        self.assertEqual(len(entity_dict1), 1, "parse_bids_filename should 1 entity_dict in the dictionary")
         path2 = 'task-overt_bold'
         suffix2, ext2, entity_dict2, unmatched2 = parse_bids_filename(path2)
         self.assertEqual(suffix2, 'bold', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext2, '', "parse_bids_filename should return empty extension when only name")
-        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entities as a dictionary")
+        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entity_dict as a dictionary")
         self.assertEqual(entity_dict2['task'], 'overt', "parse_bids_filename should have a task entity")
         path3 = 'bold'
         suffix3, ext3, entity_dict3, unmatched3 = parse_bids_filename(path3)
         self.assertEqual(suffix3, 'bold', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext3, '', "parse_bids_filename should return empty extension when only name")
-        self.assertIsInstance(entity_dict3, dict, "parse_bids_filename should return entities as a dictionary")
-        self.assertEqual(len(entity_dict3), 0, "parse_bids_filename should return empty dictionary when no entities")
+        self.assertIsInstance(entity_dict3, dict, "parse_bids_filename should return entity_dict as a dictionary")
+        self.assertEqual(len(entity_dict3), 0, "parse_bids_filename should return empty dictionary when no entity_dict")
 
     def test_parse_bids_filename_unmatched(self):
         path1 = 'dataset_description.json'
@@ -149,17 +159,17 @@ def test_parse_bids_filename_unmatched(self):
         self.assertEqual(suffix1, 'dataset_description',
                          "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext1, '.json', "parse_bids_filename should return empty extension when only name")
-        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entities as a dictionary")
-        self.assertEqual(len(entity_dict1), 0, "parse_bids_filename should handle names with no entities")
+        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entity_dict as a dictionary")
+        self.assertEqual(len(entity_dict1), 0, "parse_bids_filename should handle names with no entity_dict")
         self.assertEqual(unmatched1, [],
-                         "parse_bids_filename a name with no entities should be unmatched")
+                         "parse_bids_filename a name with no entity_dict should be unmatched")
 
         path2 = 'participants.json'
         suffix2, ext2, entity_dict2, unmatched2 = parse_bids_filename(path2)
         self.assertEqual(suffix2, 'participants', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext2, '.json', "parse_bids_filename should return correct extension when only suffix")
-        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entities as a dictionary")
-        self.assertEqual(len(entity_dict2), 0, "parse_bids_filename should handle names with no entities")
+        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entity_dict as a dictionary")
+        self.assertEqual(len(entity_dict2), 0, "parse_bids_filename should handle names with no entity_dict")
         self.assertFalse(unmatched2, "parse_bids_filename a name with only suffix should have no unmatched")
 
     def test_parse_bids_filename_invalid(self):
@@ -167,8 +177,8 @@ def test_parse_bids_filename_invalid(self):
         suffix1, ext1, entity_dict1, unmatched1 = parse_bids_filename(path1)
         self.assertEqual(suffix1, 'description', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext1, '.json', "parse_bids_filename correctly return extension when unmatched entity")
-        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entities as a dictionary")
-        self.assertEqual(len(entity_dict1), 1, "parse_bids_filename should handle names with no entities")
+        self.assertIsInstance(entity_dict1, dict, "parse_bids_filename should return entity_dict as a dictionary")
+        self.assertEqual(len(entity_dict1), 1, "parse_bids_filename should handle names with no entity_dict")
         self.assertEqual(len(unmatched1), 1,
                          "parse_bids_filename unmatched list should have correct length when unmatched entity")
         self.assertEqual(unmatched1[0], 'task',
@@ -177,7 +187,7 @@ def test_parse_bids_filename_invalid(self):
         suffix2, ext2, entity_dict2, unmatched2 = parse_bids_filename(path2)
         self.assertEqual(suffix2, '', "parse_bids_filename should correctly parse name_suffix for name")
         self.assertEqual(ext2, '.json', "parse_bids_filename correctly return extension when unmatched entity")
-        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entities as a dictionary")
+        self.assertIsInstance(entity_dict2, dict, "parse_bids_filename should return entity_dict as a dictionary")
         self.assertEqual(len(entity_dict2), 0, "parse_bids_filename should handle names with no suffix")
         self.assertEqual(len(unmatched2), 1,
                          "parse_bids_filename unmatched list should have correct length when no suffix")
diff --git a/tests/validator/test_hed_validator.py b/tests/validator/test_hed_validator.py
index 7aa70e78a..19794c864 100644
--- a/tests/validator/test_hed_validator.py
+++ b/tests/validator/test_hed_validator.py
@@ -12,12 +12,13 @@ class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.base_hed_input = 'Event'
-        cls.hed_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/hed_pairs/')
+        cls.hed_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/hed_pairs/')
         schema_filename = os.path.join(cls.hed_base_dir, "HED8.0.0t.xml")
         hed_schema = schema.load_schema(schema_filename)
-        cls.generic_hed_input_reader = HedValidator(hed_schema=hed_schema)
+        cls.hed_schema = hed_schema
+        cls.hed_validator = HedValidator(hed_schema=hed_schema)
         cls.validation_issues = []
-        cls.hed_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/validator_tests/')
+        cls.hed_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/validator_tests/')
         cls.hed_filepath_with_errors = os.path.join(cls.hed_base_dir, "ExcelMultipleSheets.xlsx")
         cls.hed_file_with_errors = HedInput(cls.hed_filepath_with_errors)
 
@@ -30,36 +31,42 @@ def setUpClass(cls):
 
     def test__validate_input(self):
         test_string_obj = HedString(self.base_hed_input)
-        validation_issues = test_string_obj.validate(self.generic_hed_input_reader)
+        validation_issues = test_string_obj.validate(self.hed_validator)
         self.assertIsInstance(validation_issues, list)
 
         name = "DummyDisplayFilename.txt"
-        validation_issues = self.hed_file_with_errors.validate_file(self.generic_hed_input_reader, name=name)
+        validation_issues = self.hed_file_with_errors.validate_file(self.hed_validator, name=name)
         self.assertIsInstance(validation_issues, list)
         self.assertTrue(name in validation_issues[0][ErrorContext.FILE_NAME])
 
     def test__validate_input_major_errors(self):
         name = "DummyDisplayFilename.txt"
-        validation_issues = self.hed_file_with_major_errors.validate_file(self.generic_hed_input_reader, name=name)
+        validation_issues = self.hed_file_with_major_errors.validate_file(self.hed_validator, name=name)
 
         self.assertIsInstance(validation_issues, list)
         self.assertTrue(name in validation_issues[0][ErrorContext.FILE_NAME])
 
     def test__validate_input_major_errors_columns(self):
         name = "DummyDisplayFilename.txt"
-        validation_issues = self.hed_file_with_major_errors.validate_file(self.generic_hed_input_reader,
+        validation_issues = self.hed_file_with_major_errors.validate_file(self.hed_validator,
                                                                           check_for_warnings=True, name=name)
         self.assertIsInstance(validation_issues, list)
         self.assertTrue(name in validation_issues[0][ErrorContext.FILE_NAME])
 
+    def test__validate_input_major_errors_multi_column(self):
+        validation_issues = self.hed_file_with_major_errors_multi_column.validate_file(self.hed_validator,
+                                                                                       check_for_warnings=True)
+        self.assertIsInstance(validation_issues, list)
+        self.assertEqual(len(validation_issues), 2)
+
     def test_complex_file_validation(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_schema.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
 
         hed_schema = schema.load_schema(schema_path)
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
         validator = HedValidator(hed_schema=hed_schema)
         sidecar = Sidecar(json_path)
         issues = sidecar.validate_entries(validator)
@@ -72,13 +79,13 @@ def test_complex_file_validation(self):
         self.assertEqual(len(validation_issues), 0)
 
     def test_complex_file_validation_invalid(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_schema.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
 
         hed_schema = schema.load_schema(schema_path)
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                  "../data/validator_tests/bids_events_bad_defs.json")
         validator = HedValidator(hed_schema=hed_schema)
         sidecar = Sidecar(json_path)
@@ -95,13 +102,13 @@ def test_complex_file_validation_invalid(self):
     def test_complex_file_validation_invalid_definitions_removed(self):
         # This verifies definitions are being removed from sidecar strings before being added, or it will produce
         # extra errors.
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_schema.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
 
         hed_schema = schema.load_schema(schema_path)
-        json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                  "../data/validator_tests/bids_events_bad_defs2.json")
         sidecar = Sidecar(json_path)
         input_file = EventsInput(events_path, sidecars=sidecar)
@@ -114,9 +121,9 @@ def test_complex_file_validation_invalid_definitions_removed(self):
         self.assertEqual(len(validation_issues), 42)
 
     def test_file_bad_defs_in_spreadsheet(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/hed_pairs/HED8.0.0t.xml')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/hed3_tags_single_sheet_bad_defs.xlsx')
         hed_schema = schema.load_schema(schema_path)
 
@@ -128,12 +135,12 @@ def test_file_bad_defs_in_spreadsheet(self):
 
         validator = HedValidator(hed_schema=hed_schema)
         validation_issues = loaded_file.validate_file(validator, check_for_warnings=True)
-        self.assertEqual(len(validation_issues), 3)
+        self.assertEqual(len(validation_issues), 4)
 
     def test_error_spans_from_file_and_missing_required_column(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/hed_pairs/HED8.0.0.mediawiki')
-        events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/tag_error_span_test.tsv')
 
         hed_schema = schema.load_schema(schema_path)
@@ -145,8 +152,20 @@ def test_error_spans_from_file_and_missing_required_column(self):
         self.assertEqual(validation_issues[2]['char_index'], 6)
         self.assertEqual(len(validation_issues), 3)
 
+    # todo: move this test somewhere more appropriate
+    def test_org_tag_missing(self):
+        test_string_obj = HedString("Event, Item/NotItem")
+        removed_tag = test_string_obj.tags()[0]
+        test_string_obj.remove_groups([removed_tag])
+        from hed import HedTag
+        source_span = test_string_obj._get_org_span(removed_tag)
+        self.assertEqual(source_span, (0, 5))
+
+        source_span = test_string_obj._get_org_span(HedTag("Event"))
+        self.assertEqual(source_span, (None, None))
+
     def test_def_mapping_single_line(self):
-        schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/hed_pairs/HED8.0.0.mediawiki')
         hed_schema = schema.load_schema(schema_path)
         validator = HedValidator(hed_schema=hed_schema)
diff --git a/tests/validator/test_tag_validator.py b/tests/validator/test_tag_validator.py
index 97a5e8722..6ded2df32 100644
--- a/tests/validator/test_tag_validator.py
+++ b/tests/validator/test_tag_validator.py
@@ -92,6 +92,60 @@ def test_proper_capitalization(self):
         }
         self.validator_syntactic(test_strings, expected_results, expected_issues, True)
 
+    # def test_proper_capitalization(self):
+    #     test_strings = {
+    #         'proper': 'Event/Sensory-event',
+    #         'camelCase': 'EvEnt/Something',
+    #         'takesValue': 'Sampling-rate/20 Hz',
+    #         'numeric': 'Statistical-uncertainty/20',
+    #         'lowercase': 'Event/something',
+    #         'multipleUpper': 'Event/SomeThing'
+    #     }
+    #     expected_results = {
+    #         'proper': True,
+    #         'camelCase': False,
+    #         'takesValue': True,
+    #         'numeric': True,
+    #         'lowercase': False,
+    #         'multipleUpper': False
+    #     }
+    #     expected_issues = {
+    #         'proper': [],
+    #         'camelCase': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0),
+    #         'takesValue': [],
+    #         'numeric': [],
+    #         'lowercase': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0),
+    #         'multipleUpper': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0)
+    #     }
+    #     self.validator_syntactic(test_strings, expected_results, expected_issues, True)
+    #
+    # def test_proper_capitalization_semantic(self):
+    #     test_strings = {
+    #         'proper': 'Event/Sensory-event',
+    #         'camelCase': 'EvEnt/Sensory-event',
+    #         'takesValue': 'Sampling-rate/20 Hz',
+    #         'numeric': 'Statistical-uncertainty/20',
+    #         'lowercase': 'Event/sensory-event',
+    #         'multipleUpper': 'Event/Sensory-Event'
+    #     }
+    #     expected_results = {
+    #         'proper': True,
+    #         'camelCase': False,
+    #         'takesValue': True,
+    #         'numeric': True,
+    #         'lowercase': False,
+    #         'multipleUpper': False
+    #     }
+    #     expected_issues = {
+    #         'proper': [],
+    #         'camelCase': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0),
+    #         'takesValue': [],
+    #         'numeric': [],
+    #         'lowercase': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0),
+    #         'multipleUpper': self.format_error(ValidationErrors.HED_STYLE_WARNING, tag=0)
+    #     }
+    #     self.validator_semantic(test_strings, expected_results, expected_issues, True)
+
     def test_child_required(self):
         test_strings = {
             'hasChild': 'Experimental-stimulus',
@@ -316,18 +370,22 @@ def test_no_duplicates(self):
                            'Purple-color/Purple',
             'legalDuplicate': 'Item/Object/Man-made-object/VehicleTrain,(Item/Object/Man-made-object/VehicleTrain,'
                               'Event/Sensory-event)',
+            'duplicateGroup': 'Sensory-event, (Sensory-event, Man-made-object/VehicleTrain), (Man-made-object/VehicleTrain, Sensory-event)'
         }
         expected_results = {
             'topLevelDuplicate': False,
             'groupDuplicate': False,
             'legalDuplicate': True,
-            'noDuplicate': True
+            'noDuplicate': True,
+            'duplicateGroup': False
         }
+        from hed import HedString
         expected_issues = {
             'topLevelDuplicate': self.format_error(ValidationErrors.HED_TAG_REPEATED, tag=1),
             'groupDuplicate': self.format_error(ValidationErrors.HED_TAG_REPEATED, tag=3),
             'legalDuplicate': [],
-            'noDuplicate': []
+            'noDuplicate': [],
+            'duplicateGroup': self.format_error(ValidationErrors.HED_TAG_REPEATED_GROUP, group=HedString("(Man-made-object/VehicleTrain, Sensory-event)")),
         }
         self.validator_syntactic(test_strings, expected_results, expected_issues, False)
 
diff --git a/tests/validator/test_tag_validator_base.py b/tests/validator/test_tag_validator_base.py
index f2d61a140..08e63cfc7 100644
--- a/tests/validator/test_tag_validator_base.py
+++ b/tests/validator/test_tag_validator_base.py
@@ -13,7 +13,7 @@ class TestHedBase(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         if cls.schema_file:
-            hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
+            hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), cls.schema_file)
             cls.hed_schema = schema.load_schema(hed_xml)
         elif not cls.hed_schema:
             raise ValueError("No schema set for test case")
diff --git a/tests/validator/test_tag_validator_library.py b/tests/validator/test_tag_validator_library.py
index 95a663413..fb9c9464d 100644
--- a/tests/validator/test_tag_validator_library.py
+++ b/tests/validator/test_tag_validator_library.py
@@ -16,7 +16,7 @@ class TestHed3(TestValidatorBase):
     @classmethod
     def setUpClass(cls):
         schema_file = '../data/hed_pairs/HED8.0.0t.xml'
-        hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), schema_file)
+        hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), schema_file)
         hed_schema1 = schema.load_schema(hed_xml)
         hed_schema2 = schema.load_schema(hed_xml, library_prefix="tl:")
         cls.hed_schema = HedSchemaGroup([hed_schema1, hed_schema2])
@@ -26,7 +26,7 @@ def setUpClass(cls):
 
     def test_invalid_load(self):
         schema_file = '../data/hed_pairs/HED8.0.0t.xml'
-        hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), schema_file)
+        hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), schema_file)
         hed_schema1 = schema.load_schema(hed_xml, library_prefix="tl:")
         hed_schema2 = schema.load_schema(hed_xml, library_prefix="tl:")
 
@@ -34,7 +34,7 @@ def test_invalid_load(self):
 
     def test_invalid_load_prefix(self):
         schema_file = '../data/hed_pairs/HED8.0.0t.xml'
-        hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), schema_file)
+        hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), schema_file)
         hed_schema1 = schema.load_schema(hed_xml)
         hed_schema2 = schema.load_schema(hed_xml)
 
@@ -101,7 +101,7 @@ def test_exist_in_schema(self):
                 ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=14,
                 index_in_tag_end=18, expected_parent_tag="Item"),
             'unknownPrefix': self.format_error(
-                ValidationErrors.HED_UNKNOWN_PREFIX, tag=0, unknown_prefix="ul:", known_prefixes=["", "tl:"]),
+                ValidationErrors.HED_LIBRARY_UNMATCHED, tag=0, unknown_prefix="ul:", known_prefixes=["", "tl:"]),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)