Repository: BASIN-3D/basin3d (language: python)

Issue:
Enable mapping multiple datasource variables to same BASIN-3D variable
**Is your feature request related to a problem? Please describe.**
A data source may have multiple variables that should be mapped to the same BASIN-3D variable.

**Describe the solution you'd like**
Enable multiple data source variable mapping to the same BASIN-3D variable (within a single data source)

**Additional context**
Solution not yet determined. Found this in BASIN-3D meeting notes:
Look into Postgres 11 “multicorn” for mapping multiple variables

Need to have Charu give known examples.


latest version of mypy generates error with existing code
**Is your feature request related to a problem? Please describe.**
Yes. Existing code fails with version of mypy.

----From Val----
**New errors in newer version of mypy**
I started with a fresh installation of my python environment and discovered that mypy has some new warnings!  See the errors below.  There are only two unique errors and they both relate to new confiruation options

The following are new mypy options that we need to consider
```
# The new default is True. We may just want to set this to False
no_implicit_optional=False

# Either option raise errors
check_untyped_defs=(True|False)
```

### Full mypy error output
```
=========================================================================================================================== FAILURES ===========================================================================================================================
_________________________________________________________________________________________________________________________ test session _________________________________________________________________________________________________________________________
mypy exited with status 1.
_____________________________________________________________________________________________________________________ basin3d/synthesis.py _____________________________________________________________________________________________________________________
51: error: Incompatible default for argument "plugins" (default has type "None", argument has type "List[str]")  [assignment]
51: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
51: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
160: error: Incompatible default for argument "query" (default has type "None", argument has type "Union[QueryById, QueryMonitoringFeature]")  [assignment]
160: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
160: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
245: error: Incompatible default for argument "query" (default has type "None", argument has type "QueryMeasurementTimeseriesTVP")  [assignment]
245: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
245: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
___________________________________________________________________________________________________________________ basin3d/core/catalog.py ____________________________________________________________________________________________________________________
331: error: Incompatible default for argument "variable_names" (default has type "None", argument has type "List[str]")  [assignment]
331: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
331: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
____________________________________________________________________________________________________________________ basin3d/core/models.py ____________________________________________________________________________________________________________________
232: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
233: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
234: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
235: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
236: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
293: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
294: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
350: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
351: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
425: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
426: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
427: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
503: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
504: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
505: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
506: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
507: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
508: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
587: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
662: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
663: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
664: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
665: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
754: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
829: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
830: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
831: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
832: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
833: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
909: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
937: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
938: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1021: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1022: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1023: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1072: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1073: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1074: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1159: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1160: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1161: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1162: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1163: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1164: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1165: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1166: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1294: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1295: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1342: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1343: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1372: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1373: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1403: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1404: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1433: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1434: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1465: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
1466: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
___________________________________________________________________________________________________________________ basin3d/core/monitor.py ____________________________________________________________________________________________________________________
93: error: Incompatible default for argument "name" (default has type "None", argument has type "str")  [assignment]
93: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
93: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
179: error: Incompatible default for argument "log_config_path" (default has type "None", argument has type "str")  [assignment]
179: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
179: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
============================================================================================================================= mypy =============================================================================================================================
Found 6 errors in 3 files (checked 46 source files)

```


Discussion / hints:

Fix patch:
diff --git a/basin3d/core/catalog.py b/basin3d/core/catalog.py
index 5f5cc9e..15a7244 100644
--- a/basin3d/core/catalog.py
+++ b/basin3d/core/catalog.py
@@ -18,11 +18,12 @@
 from basin3d.core import monitor
 from importlib import resources
 from inspect import getmodule
+import re
 from string import whitespace
-from typing import Dict, Iterator, List, Optional
+from typing import Dict, Iterator, List, Optional, Union
 
-from basin3d.core.models import DataSource, ObservedProperty, ObservedPropertyVariable
-from basin3d.core.types import SamplingMedium
+from basin3d.core.schema.enum import MAPPING_DELIMITER, NO_MAPPING_TEXT, MappedAttributeEnum, set_mapped_attribute_enum_type
+from basin3d.core.models import DataSource, AttributeMapping, ObservedProperty
 
 logger = monitor.get_logger(__name__)
 
@@ -33,7 +34,7 @@ class CatalogException(Exception):
 
 class CatalogBase:
 
-    def __init__(self, variable_filename: str = 'basin3d_variables_hydrology.csv'):
+    def __init__(self, variable_filename: str = 'basin3d_observed_property_vocabulary.csv'):
         self.variable_dir = 'basin3d.data'
         self.variable_filename = variable_filename
 
@@ -67,7 +68,7 @@ def initialize(self, plugin_list: list):
                         f'Could not retrieve plugin_id. Check that plugin is configured / registered properly.')
                     raise CatalogException
 
-                mapping_filename = f'mapping_{plugin_id.lower()}.csv'
+                mapping_filename = f'{plugin_id.lower()}_mapping.csv'
                 plugin_module = getmodule(plugin)
                 if not plugin_module:
                     logger.error(f'Plugin {plugin_id} cannot be found')
@@ -78,14 +79,18 @@ def initialize(self, plugin_list: list):
                 has_mapping_file = resources.is_resource(plugin_package, mapping_filename)
 
                 if not has_mapping_file:
-                    logger.error(f'Plugin {plugin_id} does not have mapping file {mapping_filename}')
-                    raise CatalogException(f'Plugin {plugin_id} does not have mapping file {mapping_filename}')
+                    logger.error(f'Plugin {plugin_id} does not have variable mapping file {mapping_filename}')
+                    raise CatalogException(f'Plugin {plugin_id} does not have variable mapping file {mapping_filename}')
 
                 datasource = plugin.get_datasource()
-                self._process_plugin_variable_mapping(plugin, mapping_filename, datasource)
+                self._process_plugin_attr_mapping(plugin, mapping_filename, datasource)
+                self._insert(datasource)
 
             logger.info(f"Initialized {self.__class__.__name__} metadata catalog ")
 
+    # ---------------------------------
+    # Methods to be over-witten by children classes
+
     def _init_catalog(self):
         """
 
@@ -100,22 +105,32 @@ def _insert(self, record):
         """
         raise NotImplementedError
 
-    def _get_observed_property_variable(self, basin3d_id) -> Optional[ObservedPropertyVariable]:
+    def _get_datasource(self, datasource_id) -> Optional[DataSource]:
+        """
+        Access a single datasource
+
+        :param datasource_id: the datasource_identifier
+        :return:
+        """
+        raise NotImplementedError
+
+    def _get_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
         """
         Access a single observed property variable
 
-        :param basin3d_id: the observed property variable identifier
+        :param basin3d_vocab: the observed property variable identifier
         :return:
         """
         raise NotImplementedError
 
-    def _get_observed_property(self, datasource_id, basin3d_id, datasource_variable_id) -> Optional[ObservedProperty]:
+    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[AttributeMapping]:
         """
-        Access a single observed property
+        Access a single attribute mapping
 
-        :param datasource_id:  datasource identifier
-        :param basin3d_id:  BASIN-3D variable identifier
-        :param datasource_variable_id: datasource variable identifier
+        :param datasource_id:
+        :param attr_type:
+        :param basin3d_vocab:
+        :param datasource_vocab:
         :return:
         """
         raise NotImplementedError
@@ -124,68 +139,61 @@ def is_initialized(self) -> bool:
         """Has the catalog been initialized?"""
         raise NotImplementedError
 
-    def find_observed_property(self, datasource_id, variable_name) -> Optional[ObservedProperty]:
+    def find_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
         """
-        Get the measurement to the specified variable_name
+        Return the :class:`basin3d.models.ObservedProperty` object for the BASIN-3D vocabulary specified.
 
-        :param datasource_id: the datasource identifier
-        :param variable_name: the variable name to get the :class:`~basin3d.models.ObservedProperty` for
-        :return: :class:`~basin3d.models.ObservedProperty`
+        :param basin3d_vocab: BASIN-3D vocabulary
+        :return: a :class:`basin3d.models.ObservedProperty` object
         """
         raise NotImplementedError
 
-    def find_observed_properties(self, datasource_id=None, variable_names=None) -> Iterator[ObservedProperty]:
+    def find_observed_properties(self, basin3d_vocab=None) -> Iterator[Optional[ObservedProperty]]:
         """
-        Get the measurement to the specified variable_name
+        Report the observed_properties available based on the BASIN-3D vocabularies specified. If no BASIN-3D vocabularies are specified, then return all observed properties available.
 
-        :param datasource_id:  the datasource identifier
-        :param variable_names: the variable names to get the :class:`~basin3d.models.ObservedProperty` for
-        :type variable_names: list
-        :return: :class:`~basin3d.models.ObservedProperty`
+        :param basin3d_vocab: list of the BASIN-3D observed properties
+        :return: generator that yields :class:`basin3d.models.ObservedProperty` objects
         """
         raise NotImplementedError
 
-    def find_observed_property_variable(self, datasource_id, variable_name, from_basin3d=False) -> Optional[
-            ObservedPropertyVariable]:
+    def find_datasource_attribute_mapping(self, datasource_id, attr_type, attr_vocab) -> Optional[AttributeMapping]:
         """
-        Convert the given name to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
+        Find the datasource attribute vocabulary to BASIN-3D mapping given a specific datasource_id, attr_type, and datasource attr_vocab.
 
-        :param datasource_id: the datasource identifier
-        :param variable_name:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             name to convert
-        :param: from_basin3d: boolean that says whether the variable name is a
-            BASIN-3D variable. If not, then this a datasource variable name.
-        :type from_basin3d: boolean
-        :return: A variable name
-        :rtype: str
+        :param: datasource_id: the datasource identifier
+        :param: attr_type: attribute type
+        :param: datasource_vocab: the datasource attribute vocabulary
+        :return: a :class:`basin3d.models.AttributeMapping` object
         """
         raise NotImplementedError
 
-    def find_observed_property_variables(self, datasource_id=None, variable_names=None, from_basin3d=False) -> Iterator[
-            ObservedPropertyVariable]:
+    def find_attribute_mappings(self, datasource, attr_type, attr_vocab, from_basin3d) -> Iterator[AttributeMapping]:
         """
-        Convert the given list of names to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
+        Find the list of attribute mappings given the specified fields.
+        Exact matches are returned (see attr_vocab formats below for BASIN-3D vocab nuances).
+        If no fields are specified, all registered attribute mappings will be returned.
 
         :param datasource_id: the datasource identifier
-        :param variable_names:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             names to convert
-        :type variable_names: iterable
-        :param: from_basin3d: boolean that says whether the variable name is a
-            BASIN-3D variable. If not, then this a datasource variable names.
-        :type from_basin3d: boolean
-        :return: list of variable names
-        :rtype: iterable
-        """
+        :param attr_type: the attribute type
+        :param attr_vocab: the attribute vocabulary, the formats are one of the following:
+                           1) datasource vocab that is a complete vocab
+                           2) BASIN-3D vocab that is a complete vocab for a given attr_type regardless if it is compound or not.
+                           3) BASIN-3D vocab that is compound and fully specified for each attr_type either with the complete vocab or with wildcards.
+        :param from_basin3d: boolean that says whether the attr_vocab is a BASIN-3D vocabulary. If not, then this a datasource vocabulary.
+        :return: generator that yields :class:`basin3d.models.AttributeMapping` objects
+        """
+        # class extensions should use _validate_fine_attribute_mappings_arguments
         raise NotImplementedError
 
+    # --------------------------------------
+
     def _gen_variable_store(self):
         """
         Generate a variable store. Loads this from the provided vocabulary CSV
         :return:
         """
-        fields = ['basin3d_id', 'description', 'categories', 'units']
+        fields = ['basin3d_vocab', 'description', 'categories', 'units']
         # location of mapping file might change.
         with resources.open_text(self.variable_dir, self.variable_filename) as variable_file:
 
@@ -197,10 +205,10 @@ def _gen_variable_store(self):
                 raise CatalogException
 
             for row in reader:
-                basin3d_id = row[fields[0]]
+                basin3d_vocab = row[fields[0]]
 
-                if self._get_observed_property_variable(basin3d_id) is not None:
-                    logger.warning(f'Duplicate BASIN-3D variable {basin3d_id} found. Skipping duplicate.')
+                if self._get_observed_property(basin3d_vocab) is not None:
+                    logger.warning(f'Duplicate BASIN-3D variable {basin3d_vocab} found. Skipping duplicate.')
                     continue
 
                 categories_list = []
@@ -209,24 +217,37 @@ def _gen_variable_store(self):
                     categories_list = categories_str.split(',')
                     categories_list = [category.strip(whitespace) for category in categories_list]
 
-                observed_property_variable = ObservedPropertyVariable(
-                    basin3d_id=basin3d_id,
+                observed_property_variable = ObservedProperty(
+                    basin3d_vocab=basin3d_vocab,
                     full_name=row[fields[1]],
                     categories=categories_list,
                     units=row[fields[3]])
 
                 self._insert(observed_property_variable)
 
-    def _process_plugin_variable_mapping(self, plugin, map_filename: str, datasource: DataSource):
+    def _get_attribute_enum(self, str_value, enum_type):
+        """
+        Get the enum for a given string value
+        :param str_value: string value to convert to enum, e.g., Max
+        :param enum_type: the enum type, e.g., StatisticEnum
+        :return:
+        """
+        enum_value = None
+        try:
+            enum_value = getattr(enum_type, str_value)
+        except AttributeError:
+            pass
+        return enum_value
+
+    def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSource):
         """
 
         :param plugin:
-        :param map_filename:
+        :param filename:
         :param datasource:
-        :param variable_store:
         :return:
         """
-        fields = ['basin3d_id', 'datasource_variable', 'sampling_medium', 'description']
+        fields = ['attr_type', 'basin3d_vocab', 'datasource_vocab', 'datasource_desc']
 
         plugin_id = plugin.get_meta().id
         plugin_module = getmodule(plugin)
@@ -236,211 +257,361 @@ def _process_plugin_variable_mapping(self, plugin, map_filename: str, datasource
 
         plugin_package = ".".join(plugin_module.__name__.split(".")[0:-1])
 
-        with resources.open_text(plugin_package, map_filename) as map_file:
-            logger.debug(f"Mapping file {map_filename} for plugin package {plugin_package}")
-            reader = csv.DictReader(map_file)
+        with resources.open_text(plugin_package, filename) as attr_file:
+            logger.debug(f"Mapping file {filename} for plugin package {plugin_package}")
+            reader = csv.DictReader(attr_file)
 
             # For now, force a specific file format; could change later to just require specific field names
             if reader.fieldnames != fields:
                 logger.critical(
-                    f'Plugin {plugin_id}: {map_filename} is not in correct format. Cannot create catalog.')
+                    f'Plugin {plugin_id}: {filename} is not in correct format. Cannot create catalog.')
                 raise CatalogException(
-                    f'Plugin {plugin_id}: {map_filename} is not in correct format. Cannot create catalog.')
+                    f'Plugin {plugin_id}: {filename} is not in correct format. Cannot create catalog.')
 
+            # loop thru the file
             for row in reader:
-                basin3d_id = row[fields[0]]
-                datasource_var = row[fields[1]]
-                sampling_medium = row[fields[2]]
-                description = row[fields[3]]
-                observed_property_variable = self._get_observed_property_variable(basin3d_id)
-                if observed_property_variable is None:
-                    logger.warning(
-                        f'Plugin {plugin_id}: Datasource variable {datasource_var} not in BASIN-3D variables.')
-                    continue
-                if self._get_observed_property(datasource.id, basin3d_id, datasource_var) is not None:
-                    logger.warning(
-                        f'Plugin {plugin_id}: Duplicate BASIN-3D variable detected. Cannot handle duplicate mappings yet. '
-                        f'Skipping datasource_variable {datasource_var}.')
-                    continue
-                if not sampling_medium in SamplingMedium.SAMPLING_MEDIUMS:
+                attr_type = row[fields[0]]
+                basin3d_vocab = row[fields[1]]
+                datasource_vocab = row[fields[2]]
+                datasource_desc = row[fields[3]]
+
+                # ToDo: add some validation if Cat does not have it in her reader
+                if self._get_attribute_mapping(datasource.id, attr_type, basin3d_vocab, datasource_vocab) is not None:
                     logger.warning(
-                        f'Plugin {plugin_id}: Datasource sampling medium {sampling_medium} is not BASIN-3D sampling medium type.')
+                        f'Plugin {plugin_id}: Duplicate BASIN-3D attr detected. Cannot handle duplicate mappings yet. '
+                        f'Skipping datasource attribute {datasource_vocab}.')
+
+                # build the basin3d_description from objects (e.g., for OBSERVED_PROPERTY) or enums
+                basin3d_desc = []
+                for a_type, b3d_vocab in zip(attr_type.split(MAPPING_DELIMITER), basin3d_vocab.split(MAPPING_DELIMITER)):
+                    valid_type_vocab = True
+
+                    b3d_attr_type = None
+                    try:
+                        b3d_attr_type = getattr(MappedAttributeEnum, a_type)
+                        if b3d_attr_type == MappedAttributeEnum.OBSERVED_PROPERTY:
+                            b3d_desc = self._get_observed_property(b3d_vocab)
+                        else:
+                            b3d_enum_type = set_mapped_attribute_enum_type(b3d_attr_type)
+                            b3d_desc = self._get_attribute_enum(b3d_vocab, b3d_enum_type)
+                    except AttributeError:
+                        logger.warning(f'Datasource {datasource.id}: Attribute type {a_type} is not supported. Skipping mapping.')
+                        valid_type_vocab = False
+                        break
+
+                    if not b3d_desc:
+                        logger.warning(f'Datasource {datasource.id}: basin3d_vocab {b3d_vocab} for attr_type {a_type} is not a valid BASIN-3D vocabulary. Skipping attribute mapping.')
+                        valid_type_vocab = False
+                        break
+
+                    basin3d_desc.append(b3d_desc)
+
+                if not valid_type_vocab:
                     continue
 
-                observed_property = ObservedProperty(
-                    datasource_variable=datasource_var,
-                    observed_property_variable=observed_property_variable,
-                    sampling_medium=sampling_medium, datasource=datasource,
-                    datasource_description=description)
+                attr_mapping = AttributeMapping(
+                    attr_type=attr_type,
+                    basin3d_vocab=basin3d_vocab,
+                    basin3d_desc=basin3d_desc,
+                    datasource_vocab=datasource_vocab,
+                    datasource_desc=datasource_desc,
+                    datasource=datasource)
+
+                self._insert(attr_mapping)
+                logger.debug(f"{datasource.id}: Mapped {attr_type} {datasource_vocab} to {basin3d_vocab}")
 
-                self._insert(observed_property)
-                logger.debug(f"Mapped {datasource_var} to {observed_property_variable}")
+    def _validate_find_attribute_mappings_arguments(self, datasource_id: str = None, attr_type: str = None):
+        """
+        Validate arguments for method find_attribute_mappings
+
+        :param datasource_id: datasource ID
+        :param attr_type: Attribute Type
+        """
+        if datasource_id and self._get_datasource(datasource_id) is None:
+            msg = f'Specified datasource_id: "{datasource_id}" has not been registered.'
+            logger.critical(msg)
+            raise CatalogException(msg)
+
+        if attr_type and attr_type not in MappedAttributeEnum.values():
+            msg = f'"{attr_type}" is not an attribute type supported by BASIN-3D.'
+            logger.critical(msg)
+            raise CatalogException(msg)
 
 
 class CatalogTinyDb(CatalogBase):
 
-    def __init__(self, variable_filename: str = 'basin3d_variables_hydrology.csv'):
+    def __init__(self, variable_filename: str = 'basin3d_observed_property_vocabulary.csv'):
         super().__init__(variable_filename)
 
         self.in_memory_db = None
+        self.in_memory_db_attr = None
         self._observed_properties: Dict[str, ObservedProperty] = {}
-        self._observed_property_variables: Dict[str, ObservedPropertyVariable] = {}
+        self._attribute_mappings: Dict[str, AttributeMapping] = {}
+        self._datasources: Dict[str, DataSource] = {}
 
     def is_initialized(self) -> bool:
         """Has the catalog been initialized?"""
         return self.in_memory_db is not None
 
-    def _get_observed_property_variable(self, basin3d_id) -> Optional[ObservedPropertyVariable]:
+    def _get_datasource(self, datasource_id) -> Optional[DataSource]:
         """
-        Access a single observed property variable
-
-        :param basin3d_id: the observed property variable identifier
-        :return:
+        Access a datasource via the datasource_id
+        :param datasource_id: str, the datasource id
+        :return: a :class:`basin3d.models.DataSource` object
         """
-        return self._observed_property_variables.get(basin3d_id, None)
+        return self._datasources.get(datasource_id, None)
 
-    def _get_observed_property(self, datasource_id, basin3d_id, datasource_variable_id) -> Optional[ObservedProperty]:
+    def _get_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
         """
         Access a single observed property
 
-        :param datasource_id:  datasource identifier
-        :param basin3d_id:  BASIN-3D variable identifier
-        :param datasource_variable_id: datasource variable identifier
-        :return:
+        :param basin3d_vocab: str, the observed property identifier
+        :return: an :class:`basin3d.models.ObservedProperty` object
         """
-        return self._observed_properties.get(f"{datasource_id}-{basin3d_id}-{datasource_variable_id}", None)
+        return self._observed_properties.get(basin3d_vocab, None)
 
-    def find_observed_property(self, datasource_id, variable_name) -> Optional[ObservedProperty]:
+    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[AttributeMapping]:
         """
-        Get the measurement to the specified variable_name
+        Access a single attribute mapping
 
-        :param variable_name: the variable name to get the :class:`~basin3d.models.ObservedProperty` for
-        :return: :class:`~basin3d.models.ObservedProperty`
+        :param datasource_id: str, datasource identifier
+        :param attr_type: str, attribute type
+        :param basin3d_vocab: str, BASIN-3D vocabulary
+        :param datasource_vocab: str, datasource vocabulary
+        :return: a :class:`basin3d.models.AttributeMapping` object
         """
-        if self.in_memory_db is None:
-            raise CatalogException("Variable Store has not been initialized")
-
-        from tinydb import Query
-        query = Query()
-        results = self.in_memory_db.search((query.basin3d_id == variable_name) & (query.datasource_id == datasource_id))
+        return self._attribute_mappings.get(f'{datasource_id}-{attr_type}-{basin3d_vocab}-{datasource_vocab}', None)
 
-        if len(results) > 0:
-            return self._get_observed_property(**results[0])
-        return None
+    def find_observed_property(self, basin3d_vocab: str) -> Optional[ObservedProperty]:
+        """
+        Return the :class:`basin3d.models.ObservedProperty` object for the BASIN-3D vocabulary specified.
 
-    def find_observed_properties(self, datasource_id=None, variable_names: List[str] = None) -> Iterator[
-            ObservedProperty]:
+        :param basin3d_vocab: BASIN-3D vocabulary
+        :return: a :class:`basin3d.models.ObservedProperty` object
         """
-        Get the observed properties to the specified variable_names and datasource
 
-        :param variable_names: the variable names to get the :class:`~basin3d.models.ObservedProperty` for
-        :type variable_names: list
-        :param datasource_id: The datasource to filter by
+        if not self._observed_properties:
+            msg = "Variable Store has not been initialized."
+            logger.critical(msg)
+            raise CatalogException(msg)
+
+        return self._get_observed_property(basin3d_vocab)
 
-        :return: :class:`~basin3d.models.ObservedProperty`
+    def find_observed_properties(self, basin3d_vocab: Optional[List[str]] = None) -> Iterator[Optional[ObservedProperty]]:
         """
-        if self.in_memory_db is None:
-            raise CatalogException("Variable Store has not been initialized")
+        Report the observed_properties available based on the BASIN-3D vocabularies specified. If no BASIN-3D vocabularies are specified, then return all observed properties available.
 
-        from tinydb import Query
-        query = Query()
-        is_in = lambda x: x in variable_names
-        if not datasource_id:
-            if variable_names:
-                results = self.in_memory_db.search(
-                    (query.basin3d_id.any(variable_names)))
-            else:
-                results = self.in_memory_db.all()
-        else:
-            if variable_names:
-                results = self.in_memory_db.search(
-                    (query.basin3d_id.test(is_in)) & (query.datasource_id == datasource_id))
-            else:
-                results = self.in_memory_db.search(query.datasource_id == datasource_id)
+        :param basin3d_vocab: list of the BASIN-3D observed properties
+        :return: generator that yields :class:`basin3d.models.ObservedProperty` objects
+        """
 
-        for r in results:
-            yield self._get_observed_property(**r)
+        if not self._observed_properties:
+            msg = "Variable Store has not been initialized."
+            logger.critical(msg)
+            raise CatalogException(msg)
+
+        opv: Optional[ObservedProperty]
+        if basin3d_vocab is None:
+            for opv in self._observed_properties.values():
+                yield opv
+        else:
+            for b3d_vocab in basin3d_vocab:
+                opv = self._get_observed_property(b3d_vocab)
+                if opv is not None:
+                    yield opv
+                else:
+                    logger.warning(f'BASIN-3D does not support variable {b3d_vocab}')
 
-    def find_observed_property_variable(self, datasource_id, variable_name, from_basin3d=False) -> Optional[
-            ObservedPropertyVariable]:
+    def find_datasource_attribute_mapping(self, datasource_id: str, attr_type: str, datasource_vocab: str) -> Optional[AttributeMapping]:
         """
-        Convert the given name to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
+        Find the datasource attribute vocabulary to BASIN-3D mapping given a specific datasource_id, attr_type, and datasource attr_vocab.
 
-        :param variable_name:  The :class:`~basin3d.models.ObservedPropertyVariable`
-            name to convert
-        :param: from_basin3d: boolean that says whether the variable name is a
-           BASIN-3D variable. If not, then this a datasource variable name.
-        :type from_basin3d: boolean
-        :return: A variable name
-        :rtype: str
+        :param: datasource_id: the datasource identifier
+        :param: attr_type: attribute type
+        :param: datasource_vocab: the datasource attribute vocabulary
+        :return: a :class:`basin3d.models.AttributeMapping` object
         """
-        if self.in_memory_db is None:
-            raise CatalogException("Variable Store has not been initialized")
+        if self.in_memory_db_attr is None:
+            msg = 'Attribute Store has not been initialized.'
+            logger.critical(msg)
+            raise CatalogException(msg)
 
         from tinydb import Query
         query = Query()
-        if from_basin3d:
-            results = self.in_memory_db.search(
-                (query.basin3d_id == variable_name) & (query.datasource_id == datasource_id))
-        else:
-            # Convert from DataSource variable name to BASIN-3D
-            results = self.in_memory_db.search(
-                (query.datasource_variable_id == variable_name) & (query.datasource_id == datasource_id))
-        basin3d_variable_id = results[0]['basin3d_id']
-        return self._get_observed_property_variable(basin3d_variable_id)
 
-    def find_observed_property_variables(self, datasource_id=None, variable_names=None, from_basin3d=False) -> Iterator[
-            ObservedPropertyVariable]:
+        # attr_type is search to accommodate compound mappings
+        results = self.in_memory_db_attr.search(
+            (query.datasource_vocab == datasource_vocab) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
+
+        # Only one result should be returned. If so return it and all is good.
+        if len(results) == 1:
+            return self._get_attribute_mapping(**results[0])
+        # If not, deal with results:
+        # Case 1: more than one mapping for the datasource vocabulary. This should not happen. Raise an exception.
+        elif len(results) > 1:
+            error_msg = (f'More than one attribute mapping found for datasource vocab: "{datasource_vocab}" '
+                         f'in datasource: "{datasource_id}". This should never happen.')
+            logger.critical(error_msg)
+            raise CatalogException(error_msg)
+
+        # Case 2: no mapping was found. Return an empty AttributeMapping object
+        msg = f'No mapping was found for datasource vocab: "{datasource_vocab}" in datasource: "{datasource_id}".'
+        datasource = self._get_datasource(datasource_id)
+
+        if datasource is None:
+            datasource = DataSource()
+            msg = f'No datasource was found for id "{datasource_id}".'
+            logger.warning(msg)
+
+        return AttributeMapping(attr_type=attr_type, basin3d_vocab=NO_MAPPING_TEXT, basin3d_desc=[],
+                                datasource_vocab=datasource_vocab, datasource_desc=msg, datasource=datasource)
+
+    def find_attribute_mappings(self, datasource_id: str = None, attr_type: str = None, attr_vocab: Union[str, List] = None,
+                                from_basin3d: bool = False) -> Iterator[AttributeMapping]:
         """
-        Convert the given list of names to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
+        Find the list of attribute mappings given the specified fields.
+        Exact matches are returned (see attr_vocab formats below for BASIN-3D vocab nuances).
+        If no fields are specified, all registered attribute mappings will be returned.
 
-        :param variable_names:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             names to convert
-        :type variable_names: iterable
-        :param: from_basin3d: boolean that says whether the variable name is a
-            BASIN-3D variable. If not, then this a datasource variable names.
-        :type from_basin3d: boolean
-        :return: list of variable names
-        :rtype: iterable
+        :param datasource_id: the datasource identifier
+        :param attr_type: the attribute type
+        :param attr_vocab: the attribute vocabulary, the formats are one of the following:
+                           1) datasource vocab that is a complete vocab
+                           2) BASIN-3D vocab that is a complete vocab for a given attr_type regardless if it is compound or not.
+                           3) BASIN-3D vocab that is compound and fully specified for each attr_type either with the complete vocab or with wildcards.
+        :param from_basin3d: boolean that says whether the attr_vocab is a BASIN-3D vocabulary. If not, then this a datasource vocabulary.
+        :return: generator that yields :class:`basin3d.models.AttributeMapping` objects
         """
+        catalog_messages: List[str] = []
+
+        self._validate_find_attribute_mappings_arguments(datasource_id, attr_type)
 
-        if self.in_memory_db is None:
-            raise CatalogException("Variable Store has not been initialized")
+        if self.in_memory_db_attr is None:
+            msg = 'Attribute Store has not been initialized.'
+            logger.critical(msg)
+            raise CatalogException(msg)
 
         from tinydb import Query
         query = Query()
 
-        is_in = lambda x: x in variable_names
-        if not datasource_id and not variable_names:
-            # return all observe_property_variables possible
-            for opv in self._observed_property_variables.values():
-                yield opv
+        # Set the attribute vocab list to track mappings not found
+        specified_attr_vocab_not_found = []
+        if attr_vocab:
+            if isinstance(attr_vocab, str):
+                attr_vocab = [attr_vocab]
+            elif not isinstance(attr_vocab, List):
+                raise CatalogException("attr_vocab must be a str or list")
+            specified_attr_vocab_not_found = attr_vocab.copy()
+
+        # Function for TinyDB search
+        def is_in(x, attr_vocabs=attr_vocab, is_from_basin3d=from_basin3d):
+            for a_vocab in attr_vocabs:
+                # if attr_vocabs is datasource vocab(s)
+                # OR attr_vocabs is BASIN-3D vocab(s) AND they have the compound mapping delimiter (i.e., it has : and possibly wildcards)
+                # AND it is a fullmatch
+                if (not is_from_basin3d or (is_from_basin3d and MAPPING_DELIMITER in a_vocab)) and re.fullmatch(a_vocab, x):
+                    return True
+                # if attr_vocabs is BASIN-3D vocab(s) and they do not have the compound mapping delimiter,
+                # then match exactly for the individual attr_type components.
+                # e.g. attr_vocabs = ['MAX', 'MIN'] and the mapping is compound e.g. x = 'Al:WATER:MAX'
+                # re.search does not work here b/c of a case like OBSERVED_PROPERPTY variables TEMP:AIR and SONIC_TEMP:AIR,
+                # in which a attr_vocab of ['TEMP'] would return both instead on just TEMP:AIR.
+                # A general search would need to be handled differently.
+                elif is_from_basin3d and MAPPING_DELIMITER not in a_vocab:
+                    for x_vocab in x.split(MAPPING_DELIMITER):
+                        if x_vocab == a_vocab:
+                            return True
+            return False
+
+        # If no query parameters --> get all mapped attributes back for all registered plugins
+        if not datasource_id and not attr_vocab and not attr_type:
+            # return all mapped attributes possible possible
+            for attr_mapping in self._attribute_mappings.values():
+                yield attr_mapping
+        # Otherwise search depends on the set of parameters provided
         else:
             if not datasource_id:
-                # It wouldn't make sense to return observed property variables without a data source filter
-                raise CatalogException(
-                    "find_observed_property_variables: 'datasource' should be specified with 'variable_names'")
-
-            if not variable_names:
-                # This returns all possible observed property variables for a data source
-                results = self.in_memory_db.search(query.datasource_id == datasource_id)
-
+                if attr_type and attr_vocab and from_basin3d:
+                    results = self.in_memory_db_attr.search((query.basin3d_vocab.test(is_in)) & (query.attr_type.search(attr_type)))
+                elif attr_type and attr_vocab:
+                    results = self.in_memory_db_attr.search((query.datasource_vocab.test(is_in)) & (query.attr_type.search(attr_type)))
+                elif not attr_type and from_basin3d:
+                    results = self.in_memory_db_attr.search(query.basin3d_vocab.test(is_in))
+                elif not attr_type:
+                    results = self.in_memory_db_attr.search(query.datasource_vocab.test(is_in))
+                else:
+                    results = self.in_memory_db_attr.search(query.attr_type.search(attr_type))
+
+            # Returns all possible attributes for a data source
+            elif not attr_type and not attr_vocab:
+                results = self.in_memory_db_attr.search(query.datasource_id == datasource_id)
+
+            # Returns all attributes for a data source and attribute type
+            elif not attr_vocab:
+                results = self.in_memory_db_attr.search((query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
+
+            # Return mappings if only the datasource and attr_vocab are specified.
+            elif not attr_type:
+                if from_basin3d:
+                    results = self.in_memory_db_attr.search((query.basin3d_vocab.test(is_in)) & (query.datasource_id == datasource_id))
+                else:
+                    results = self.in_memory_db_attr.search((query.datasource_vocab.test(is_in)) & (query.datasource_id == datasource_id))
+
+            # Finally, all parameters specified:
+            # Convert from BASIN-3D to DataSource variable name
             elif from_basin3d:
+                results = self.in_memory_db_attr.search(
+                    (query.basin3d_vocab.test(is_in)) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
 
-                # Convert from BASIN-3D to DataSource variable name
-                results = self.in_memory_db.search(
-                    (query.basin3d_id.test(is_in)) & (query.datasource_id == datasource_id))
-
+            # Convert from DataSource variable name to BASIN-3D
             else:
-                # Convert from DataSource variable name to BASIN-3D
-                results = self.in_memory_db.search(
-                    (query.datasource_variable_id.test(is_in)) & (query.datasource_id == datasource_id))
+                results = self.in_memory_db_attr.search(
+                    (query.datasource_vocab.test(is_in)) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
 
             # Yield the results
+            attr_map: Optional[AttributeMapping]
             for r in results:
-                yield self._get_observed_property_variable(r['basin3d_id'])
+                attr_map = self._get_attribute_mapping(**r)
+
+                # if AttributeMapping is not found: THIS SHOULD NEVER HAPPEN.
+                if attr_map is None:
+                    logger.error(f'AttributeMapping not found given database search results. THIS SHOULD NEVER HAPPPEN.')
+                    continue
+
+                # If attr_vocab was specified, track what was found for the datasource
+                if attr_vocab:
+                    vocabs = [attr_map.datasource_vocab]
+                    if from_basin3d:
+                        vocabs = attr_map.basin3d_vocab.split(MAPPING_DELIMITER)
+
+                    # look for it in the specified vocabs, remove it from the not found list
+                    for vocab in vocabs:
+                        try:
+                            idx = specified_attr_vocab_not_found.index(vocab)
+                            specified_attr_vocab_not_found.pop(idx)
+                        except ValueError:
+                            pass
+
+                # yield the AttributeMapping
+                yield attr_map
+
+            # If any specified attr_vocabs were not found, add info messages
+            if specified_attr_vocab_not_found:
+                attr_vocab_text = ', '.join(specified_attr_vocab_not_found)
+                attr_vocab_source = 'datasource'
+                if from_basin3d:
+                    attr_vocab_source = 'BASIN-3D'
+                if not results:
+                    # if no results,
+                    msg = (f'No attribute mappings found for specified parameters: datasource id = "{datasource_id}", '
+                           f'attribute type = "{attr_type}", {attr_vocab_source} vocabularies: {attr_vocab_text}.')
+                else:
+                    msg = (f'No attribute mappings found for the following {attr_vocab_source} vocabularies: {attr_vocab_text}. '
+                           f'Note: specified datasource id = {datasource_id} and attribute type = {attr_type}')
+                logger.info(msg)
+                catalog_messages.append(msg)
+
+            return StopIteration(catalog_messages)
 
     def _init_catalog(self):
         """
@@ -448,23 +619,33 @@ def _init_catalog(self):
 
         :return:
         """
+        # Note: op are held in dictionary in this TinyDB
         from tinydb import TinyDB
         from tinydb.storages import MemoryStorage
         self.in_memory_db = TinyDB(storage=MemoryStorage)
-        self.in_memory_db.truncate()
+        self.in_memory_db_attr = self.in_memory_db.table('attr')
+        self.in_memory_db_attr.truncate()
 
     def _insert(self, record):
         """
         :param record:
         """
         if self.in_memory_db is not None:
-            if isinstance(record, ObservedPropertyVariable):
-                self._observed_property_variables[record.basin3d_id] = record
-            elif isinstance(record, ObservedProperty):
-                self._observed_properties[
-                    f"{record.datasource.id}-{record.observed_property_variable.basin3d_id}-{record.datasource_variable}"] = record
-                self.in_memory_db.insert({'datasource_id': record.datasource.id,
-                                          'datasource_variable_id': record.datasource_variable,
-                                          'basin3d_id': record.observed_property_variable.basin3d_id, })
+            if isinstance(record, ObservedProperty):
+                self._observed_properties[record.basin3d_vocab] = record
+            elif isinstance(record, AttributeMapping):
+                key = f'{record.datasource.id}-{record.attr_type}-{record.basin3d_vocab}-{record.datasource_vocab}'
+                logger.debug(key)
+                self._attribute_mappings[key] = record
+                self.in_memory_db_attr.insert({'datasource_id': record.datasource.id,
+                                               'attr_type': record.attr_type,
+                                               'basin3d_vocab': record.basin3d_vocab,
+                                               'basin3d_desc': record.basin3d_desc,
+                                               'datasource_vocab': record.datasource_vocab,
+                                               'datasource_desc': record.datasource_desc, })
+            elif isinstance(record, DataSource):
+                self._datasources[record.id] = record
         else:
-            raise CatalogException(f'Could not insert record.  Catalog not initialize')
+            msg = 'Could not insert record. Catalog not initialized.'
+            logger.critical(msg)
+            raise CatalogException(msg)
diff --git a/basin3d/core/models.py b/basin3d/core/models.py
index 8c7f384..3533baf 100644
--- a/basin3d/core/models.py
+++ b/basin3d/core/models.py
@@ -18,17 +18,24 @@
     :backlinks: top
 
 """
+
 import datetime
 import enum
 import json
+
 from collections import namedtuple
 from dataclasses import dataclass, field
+from itertools import repeat
 from numbers import Number
-from typing import List
+from typing import List, Optional, Union
 
-from basin3d.core.schema.enum import FEATURE_SHAPE_TYPES, FeatureTypeEnum, ResultQualityEnum, TimeFrequencyEnum
+from basin3d.core import monitor
+from basin3d.core.schema.enum import FeatureTypeEnum, FEATURE_SHAPE_TYPES, MappedAttributeEnum, MAPPING_DELIMITER, NO_MAPPING_TEXT
+from basin3d.core.translate import get_datasource_mapped_attribute, translate_attributes
 from basin3d.core.types import SpatialSamplingShapes
 
+logger = monitor.get_logger(__name__)
+
 
 class JSONSerializable:
     """
@@ -63,11 +70,11 @@ class DataSource(JSONSerializable):
     """
     Data Source definition
 
-    Attributes:
+    Fields:
         - *id:* string (inherited)
         - *name:* string
         - *id_prefix:* string, prefix that is added to all data source ids
-        - *location*
+        - *location:*
         - *credentials:*
 
     """
@@ -85,21 +92,19 @@ def __unicode__(self):
 
 
 @dataclass
-class ObservedPropertyVariable(JSONSerializable):
+class ObservedProperty(JSONSerializable):
     """
     Defining the properties being observed (measured). See http://vocabulary.odm2.org/variablename/ for controlled vocabulary
 
-    Attributes:
-        - *basin3d_id:* string,
+    Fields:
+        - *basin3d_vocab:* string,
         - *full_name:* string,
         - *categories:* List of strings (in order of priority).
         - *units:* string
 
     See http://vocabulary.odm2.org/variabletype/ for options, although I think we should have our own list (theirs is a bit funky).
-
-
     """
-    basin3d_id: str = ''
+    basin3d_vocab: str = ''
     full_name: str = ''
     categories: list = field(default_factory=list)
     units: str = field(default='')
@@ -108,32 +113,89 @@ def __str__(self):
         return self.__unicode__()
 
     def __unicode__(self):
-        return self.full_name
+        return self.basin3d_vocab
 
 
 @dataclass
-class ObservedProperty(JSONSerializable):
+class AttributeMapping(JSONSerializable):
     """
-    Defining the attributes for a single/multiple Observed Properties
-
-    Attributes:
-        - *datasource_variable:* id, e.g., Cs 137 air dose rate car survey campaigns
-        - *observed_property_variable_id:* string, e.g., Cs137MVID
-        - *sampling_medium:* enum (WATER, GAS, SOLID PHASE, OTHER, NOT APPLICABLE)
-        - *datasource:*
-        - *datasource_description:*
+    A data class for attribute mappings between datasource vocabularies and BASIN-3D vocabularies.
+    These are the associations defined in the datasource (i.e., plugin) mapping file.
+
+    Fields:
+         - *attr_type:* Attribute Type; e.g., STATISTIC, RESULT_QUALITY, OBSERVED_PROPERTY; separate compound mappings with ':'
+         - *basin3d_vocab:* The BASIN-3D vocabulary; separate compound mappings with ':'
+         - *basin3d_desc:* The BASIN-3D vocabulary descriptions; objects or enum
+         - *datasource_vocab:* The datasource vocabulary
+         - *datasource_desc:* The datasource vocabulary description
+         - *datasource:* The datasource of the mapping
     """
-    datasource_variable: str = ''
-    observed_property_variable: ObservedPropertyVariable = field(default_factory=lambda: ObservedPropertyVariable())
-    sampling_medium: str = ''
+    attr_type: str
+    basin3d_vocab: str
+    basin3d_desc: list
+    datasource_vocab: str
+    datasource_desc: str
     datasource: DataSource = DataSource()
-    datasource_description: str = ''
 
     def __str__(self):
         return self.__unicode__()
 
     def __unicode__(self):
-        return self.datasource_variable
+        return self.datasource_vocab
+
+
+@dataclass
+class MappedAttribute(JSONSerializable):
+    """
+    A data class for an attribute that is translated (i.e., mapped) from a datasource vocabulary to BASIN-3D vocabulary.
+    Note that this model holds an AttributeMapping that maybe compound in nature; however this class specifies only one attribute types.
+    For example, if the AttributeMapping is for a compound mapping of attribute types OBSERVED_PROPERTY:SAMPLING_MEDIUM,
+    then the attr_type field would be either OBSERVED_PROPERTY or SAMPLING_MEDIUM but not both.
+
+    Fields:
+         - *attr_type:* Attribute Type; e.g., STATISTIC, RESULT_QUALITY, OBSERVED_PROPERTY, etc; single type only
+         - *attr_mapping:* AttributeMapping as described in the datasource's (i.e., plugin's mapping file).
+    """
+    attr_type: MappedAttributeEnum
+    attr_mapping: AttributeMapping
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.get_basin3d_vocab()
+
+    def get_basin3d_vocab(self) -> Optional[str]:
+        if self.attr_mapping.basin3d_vocab == NO_MAPPING_TEXT:
+            return NO_MAPPING_TEXT
+        attrs = self.attr_mapping.attr_type.split(MAPPING_DELIMITER)
+        b3d_vocabs = self.attr_mapping.basin3d_vocab.split(MAPPING_DELIMITER)
+        try:
+            for attr, vocab in zip(attrs, b3d_vocabs):
+                if attr == self.attr_type.upper():
+                    return vocab
+        except Exception as e:
+            logger.error(f'Issue returning basin3d_vocab for MappedAttribute. THIS SHOULD NEVER HAPPEN. {e}')
+        return None
+
+    def get_basin3d_desc(self):
+        b3d_descs = self.attr_mapping.basin3d_desc
+        if not b3d_descs:
+            return None
+        attrs = self.attr_mapping.attr_type.split(MAPPING_DELIMITER)
+        try:
+            for attr, desc in zip(attrs, b3d_descs):
+                if attr == self.attr_type.upper():
+                    return desc
+        except Exception as e:
+            logger.error(f'Issue returning basin3d_desc for MappedAttribute. THIS SHOULD NEVER HAPPEN. {e}')
+        return None
+
+    def get_datasource_vocab(self) -> str:
+        return self.attr_mapping.datasource_vocab
+
+    def get_datasource_desc(self) -> str:
+        return self.attr_mapping.datasource_desc
 
 
 class Base(JSONSerializable):
@@ -196,6 +258,30 @@ def __delattr__(self, *ignore_args):
         self.__setattr__ = __setattr__
         self.__delattr__ = __delattr__
 
+    def _create_mapped_attributes(self, attr_type, attr_mappings):
+        """
+
+        :param attr_type:
+        :param attr_mappings:
+        :return:
+        """
+
+        def create_mapped_attribute(a_type, a_mapping):
+            return MappedAttribute(a_type, a_mapping)
+
+        if isinstance(attr_mappings, list) and attr_mappings:
+            return list(map(create_mapped_attribute, repeat(attr_type), attr_mappings))
+
+        elif attr_mappings:
+            return create_mapped_attribute(attr_type, attr_mappings)
+
+    def _translate_mapped_attributes(self, plugin_access, mapped_attrs, **kwargs):
+        translated_attrs = translate_attributes(plugin_access, mapped_attrs, **kwargs)
+        for attr in mapped_attrs:
+            if attr in translated_attrs.keys() and translated_attrs[attr]:
+                translated_attrs[attr] = self._create_mapped_attributes(attr.upper(), translated_attrs[attr])
+        return translated_attrs
+
     @property
     def datasource_ids(self):
         return self._datasource_ids
@@ -830,21 +916,18 @@ def __init__(self, plugin_access, **kwargs):
         self._name: str = None
         self._description: str = None
         self._feature_type: str = None
-        self._observed_property_variables: List[str] = None
+        self._observed_properties: Union[List[MappedAttribute], List[str]] = None
 
         # Initialize after the attributes have been set
         super().__init__(plugin_access, **kwargs)
 
-        if self.observed_property_variables and isinstance(self.observed_property_variables, (tuple, list, enumerate)):
-            # synthesize measurement variables
-            synth_params = []
-            for synth_param in plugin_access.get_observed_property_variables(self.observed_property_variables):
-                synth_params.append(synth_param.basin3d_id)
-
-            self.observed_property_variables = synth_params
-
-        elif self.observed_property_variables:
-            self.observed_property_variables = [self.observed_property_variables]
+        if self.observed_properties:
+            if not isinstance(self.observed_properties, list):
+                logger.warning("observed_properties parameter not in expected list format")
+            self.observed_properties = self._create_mapped_attributes('OBSERVED_PROPERTY', get_datasource_mapped_attribute(
+                plugin_access, attr_type='OBSERVED_PROPERTY', datasource_vocab=self.observed_properties))
+        else:
+            self.observed_properties = None
 
     def __validate__(self):
         """
@@ -854,6 +937,12 @@ def __validate__(self):
         if self.feature_type is not None and self.feature_type not in FeatureTypeEnum.values():
             raise AttributeError("Feature attr feature_type must be FeatureTypeEnum.")
 
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self._id
+
     @property
     def id(self) -> str:
         """Unique identifier for the feature"""
@@ -891,13 +980,13 @@ def feature_type(self, value: str):
         self._feature_type = value
 
     @property
-    def observed_property_variables(self) -> List[str]:
-        """List of observed property variables"""
-        return self._observed_property_variables
+    def observed_properties(self) -> Union[List[MappedAttribute], List[str]]:
+        """List of observed properties"""
+        return self._observed_properties
 
-    @observed_property_variables.setter
-    def observed_property_variables(self, value: List[str]):
-        self._observed_property_variables = value
+    @observed_properties.setter
+    def observed_properties(self, value: Union[List[MappedAttribute], List[str]]):
+        self._observed_properties = value
 
 
 class SamplingFeature(Feature):
@@ -1160,10 +1249,12 @@ def __init__(self, plugin_access, **kwargs):
         self._type: str = None
         self._utc_offset: int = None
         self._phenomenon_time: str = None
-        self._observed_property: ObservedProperty = None
+        self._observed_property: MappedAttribute = None
         self._feature_of_interest: MonitoringFeature = None
         self._feature_of_interest_type: FeatureTypeEnum = None
-        self._result_quality: List[ResultQualityEnum] = []
+        self._result_quality: List[MappedAttribute] = []
+
+        kwargs = self._translate_attributes(plugin_access, **kwargs)
 
         # Initialize after the attributes have been set
         super().__init__(plugin_access, **kwargs)
@@ -1181,6 +1272,11 @@ def __validate__(self):
         if self.feature_of_interest_type and self.feature_of_interest_type not in FeatureTypeEnum.values():
             raise AttributeError("feature_of_interest_type must be FeatureType")
 
+    def _translate_attributes(self, plugin_access, **kwargs):
+        # ToDo: see note with TimeseriesTVPObservation
+        mapped_attrs = ('observed_property', 'result_quality')
+        return self._translate_mapped_attributes(plugin_access, mapped_attrs, **kwargs)
+
     @property
     def id(self) -> str:
         """Unique observation identifier"""
@@ -1219,12 +1315,12 @@ def phenomenon_time(self, value: str):
         self._phenomenon_time = value
 
     @property
-    def observed_property(self) -> 'ObservedProperty':
+    def observed_property(self) -> 'MappedAttribute':
         """The property that was observed"""
         return self._observed_property
 
     @observed_property.setter
-    def observed_property(self, value: 'ObservedProperty'):
+    def observed_property(self, value: 'MappedAttribute'):
         self._observed_property = value
 
     @property
@@ -1246,12 +1342,12 @@ def feature_of_interest_type(self, value: 'FeatureTypeEnum'):
         self._feature_of_interest_type = value
 
     @property
-    def result_quality(self) -> List['ResultQualityEnum']:
+    def result_quality(self) -> List['MappedAttribute']:
         """The result quality assessment. See :class:`ResultQuality`"""
         return self._result_quality
 
     @result_quality.setter
-    def result_quality(self, value: List['ResultQualityEnum']):
+    def result_quality(self, value: List['MappedAttribute']):
         self._result_quality = value
 
 
@@ -1259,28 +1355,6 @@ class TimeMetadataMixin(object):
     """
     Metadata attributes for Observations with a time
     """
-
-    #: Observations aggregated by year
-    AGGREGATION_DURATION_YEAR = TimeFrequencyEnum.YEAR
-
-    #: Observations aggregated by month
-    AGGREGATION_DURATION_MONTH = TimeFrequencyEnum.MONTH
-
-    #: Observations aggregated by day
-    AGGREGATION_DURATION_DAY = TimeFrequencyEnum.DAY
-
-    #: Observations aggregated by hour
-    AGGREGATION_DURATION_HOUR = TimeFrequencyEnum.HOUR
-
-    #: Observations aggregated by minute
-    AGGREGATION_DURATION_MINUTE = TimeFrequencyEnum.MINUTE
-
-    #: Observations aggregated by second
-    AGGREGATION_DURATION_SECOND = TimeFrequencyEnum.SECOND
-
-    #: Observations aggregated by no standard frequency, used for instantaneous values
-    AGGREGATION_DURATION_NONE = TimeFrequencyEnum.NONE
-
     #: Observation taken at the start
     TIME_REFERENCE_START = "START"
 
@@ -1291,20 +1365,20 @@ class TimeMetadataMixin(object):
     TIME_REFERENCE_END = "END"
 
     def __init__(self, *args, **kwargs):
-        self._aggregation_duration: str = None
+        self._aggregation_duration: MappedAttribute = None
         self._time_reference_position: str = None
 
         # Instantiate the serializer superclass
         super(TimeMetadataMixin, self).__init__(*args, **kwargs)
 
     @property
-    def aggregation_duration(self) -> str:
+    def aggregation_duration(self) -> 'MappedAttribute':
         """Time period represented by the observation. Follows OGC TM_PeriodDuration.
            Use constants prefixed with `AGGREGATION_DURATION` from :class:`TimeseriesMetadataMixin`"""
         return self._aggregation_duration
 
     @aggregation_duration.setter
-    def aggregation_duration(self, value: str):
+    def aggregation_duration(self, value: 'MappedAttribute'):
         self._aggregation_duration = value
 
     @property
@@ -1323,44 +1397,29 @@ class MeasurementMetadataMixin(object):
     Metadata attributes for Observations type Measurement
     """
 
-    #: Statistical Instantaneous
-    STATISTIC_INSTANTANEOUS = "INSTANT"
-
-    #: Statistical Mean
-    STATISTIC_MEAN = "MEAN"
-
-    #: Statistical Minimum
-    STATISTIC_MIN = "MIN"
-
-    #: Statistical Maximum
-    STATISTIC_MAX = "MAX"
-
-    #: Statistical Sum
-    STATISTIC_TOTAL = "TOTAL"
-
     def __init__(self, *args, **kwargs):
-        self._observed_property_variable: str = None
-        self._statistic: str = None
+        self._sampling_medium: MappedAttribute = None
+        self._statistic: MappedAttribute = None
 
         # Instantiate the serializer superclass
         super(MeasurementMetadataMixin, self).__init__(*args, **kwargs)
 
     @property
-    def observed_property_variable(self) -> str:
-        """The observed property that was measured"""
-        return self._observed_property_variable
+    def sampling_medium(self) -> 'MappedAttribute':
+        """Sampling medium in which the observed property was measured"""
+        return self._sampling_medium
 
-    @observed_property_variable.setter
-    def observed_property_variable(self, value: str):
-        self._observed_property_variable = value
+    @sampling_medium.setter
+    def sampling_medium(self, value: 'MappedAttribute'):
+        self._sampling_medium = value
 
     @property
-    def statistic(self) -> str:
+    def statistic(self) -> 'MappedAttribute':
         """The statistical property of the observation result. Use constants prefixed with `STATISTIC_` from :class:`MeasurementMetadataMixin`"""
         return self._statistic
 
     @statistic.setter
-    def statistic(self, value: str):
+    def statistic(self, value: 'MappedAttribute'):
         self._statistic = value
 
 
@@ -1368,12 +1427,20 @@ class ResultListTVP(Base):
     """
     Result Point Float
     """
-    def __init__(self, **kwargs):
+    def __init__(self, plugin_access, **kwargs):
         self._value: List['TimeValuePair'] = []
-        self._quality: List['ResultQualityEnum'] = []
+        self._result_quality: List['MappedAttribute'] = []
+
+        # translate quality
+        kwargs = self._translate_attributes(plugin_access, **kwargs)
 
         # Initialize after the attributes have been set
-        super().__init__(None, **kwargs)
+        super().__init__(plugin_access, **kwargs)
+
+    def _translate_attributes(self, plugin_access, **kwargs):
+        # ToDo: see note with TimeseriesTVPObservation
+        mapped_attrs = ('result_quality', )
+        return self._translate_mapped_attributes(plugin_access, mapped_attrs, **kwargs)
 
     @property
     def value(self) -> List['TimeValuePair']:
@@ -1385,13 +1452,13 @@ def value(self, value: List['TimeValuePair']):
         self._value = value
 
     @property
-    def quality(self) -> List['ResultQualityEnum']:
+    def result_quality(self) -> List['MappedAttribute']:
         """Result that was measured"""
-        return self._quality
+        return self._result_quality
 
-    @quality.setter
-    def quality(self, value: List['ResultQualityEnum']):
-        self._quality = value
+    @result_quality.setter
+    def result_quality(self, value: List['MappedAttribute']):
+        self._result_quality = value
 
 
 class MeasurementTimeseriesTVPResultMixin(object):
@@ -1429,13 +1496,21 @@ class ResultPointFloat(Base):
     """
     Result Point Float
     """
-    def __init__(self, **kwargs):
+    def __init__(self, plugin_access, **kwargs):
         self._value: float = None
-        self._quality: 'ResultQualityEnum' = None
+        self._result_quality: 'MappedAttribute' = None
+
+        # translate quality
+        kwargs = self._translate_attributes(plugin_access, **kwargs)
 
         # Initialize after the attributes have been set
         super().__init__(None, **kwargs)
 
+    def _translate_attributes(self, plugin_access, **kwargs):
+        # ToDo: see note with TimeseriesTVPObservation
+        mapped_attrs = ('result_quality', )
+        return self._translate_mapped_attributes(plugin_access, mapped_attrs, **kwargs)
+
     @property
     def value(self) -> float:
         """Result that was measured"""
@@ -1446,13 +1521,13 @@ def value(self, value: float):
         self._value = value
 
     @property
-    def quality(self) -> 'ResultQualityEnum':
+    def result_quality(self) -> 'MappedAttribute':
         """Result that was measured"""
-        return self._quality
+        return self._result_quality
 
-    @quality.setter
-    def quality(self, value: 'ResultQualityEnum'):
-        self._quality = value
+    @result_quality.setter
+    def result_quality(self, value: 'MappedAttribute'):
+        self._result_quality = value
 
 
 class MeasurementResultMixin(object):
@@ -1498,15 +1573,16 @@ class MeasurementTimeseriesTVPObservation(TimeMetadataMixin, MeasurementMetadata
     def __init__(self, plugin_access, **kwargs):
         kwargs["type"] = self.TYPE_MEASUREMENT_TVP_TIMESERIES
 
-        if "observed_property_variable" in kwargs:
-            datasource_variable_id = kwargs['observed_property_variable']
-            observed_property_variable = plugin_access.get_observed_property_variable(datasource_variable_id)
-            basin3d_variable_id = observed_property_variable.basin3d_id
-            kwargs['observed_property_variable'] = basin3d_variable_id
-            kwargs['observed_property'] = plugin_access.get_observed_property(basin3d_variable_id)
+        self._translate_attributes(plugin_access, **kwargs)
 
         # Initialize after the attributes have been set
         super(MeasurementTimeseriesTVPObservation, self).__init__(plugin_access, **kwargs)
 
     def __eq__(self, other):
         return self.id == other.id
+
+    def _translate_attributes(self, plugin_access, **kwargs):
+        # ToDo: Introspect which attributes are MappedAttributes. Cannot do this easily b/c using @property decorator.
+        # For a simple dataclass, typing.get_type_hints would work. As properties, the fget for each property can be introspected with typing.get_type_hints
+        mapped_attrs = ('observed_property', 'statistic', 'aggregation_duration', 'result_quality', 'sampling_medium')
+        return self._translate_mapped_attributes(plugin_access, mapped_attrs, **kwargs)
diff --git a/basin3d/core/plugin.py b/basin3d/core/plugin.py
index 66055dc..2eb955e 100644
--- a/basin3d/core/plugin.py
+++ b/basin3d/core/plugin.py
@@ -15,10 +15,10 @@
 
 
 """
-from basin3d.core import monitor
 from types import MethodType
 from typing import Dict
 
+from basin3d.core import monitor
 from basin3d.core.catalog import CatalogTinyDb
 from basin3d.core.models import DataSource
 from basin3d.core.schema.enum import FeatureTypeEnum
@@ -63,63 +63,26 @@ def __init__(self, datasource: DataSource, catalog: CatalogTinyDb):
     def datasource(self):
         return self._datasource
 
-    def get_observed_property(self, variable_name: str):
+    def get_datasource_attribute_mapping(self, attr_type, attr_vocab):
         """
-        Convert the given name to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
+        Get attribute mapping for the specified attribute type and datasource attribute vocab
 
-        :param variable_name:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             name to convert
-        :return: An observed property variable name
-        :rtype: str
+        :param attr_type: attribute type
+        :param attr_vocab: datasource attribute vocabulary
+        :return: a `basin3d.models.AttributeMapping` object
         """
+        return self._catalog.find_datasource_attribute_mapping(self.datasource.id, attr_type, attr_vocab)
 
-        return self._catalog.find_observed_property(self.datasource.id, variable_name)
-
-    def get_observed_properties(self, variable_names):
+    def get_attribute_mappings(self, attr_type=None, attr_vocab=None, from_basin3d=False):
         """
-        Convert the given name to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
-
-        :param variable_names:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             name to convert
-        :return: An observerd proeprty variable name
-        :rtype: str
-        """
-
-        return self._catalog.find_observed_properties(self.datasource.id, variable_names)
-
-    def get_observed_property_variable(self, variable_name, from_basin3d=False):
-        """
-        Convert the given name to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
-
-        :param variable_name:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             name to convert
-        :param: from_basin3d: boolean that says whether the variable name is a
-            BASIN-3D variable. If not, then this a datasource variable name.
-        :type from_basin3d: boolean
-        :return: A variable name
-        :rtype: str
-        """
-
-        return self._catalog.find_observed_property_variable(self.datasource.id, variable_name, from_basin3d)
-
-    def get_observed_property_variables(self, variable_names=None, from_basin3d=False):
-        """
-        Convert the given list of names to either BASIN-3D from :class:`~basin3d.models.DataSource`
-        variable name or the other way around.
-
-        :param variable_names:  The :class:`~basin3d.models.ObservedPropertyVariable`
-             names to convert
-        :type variable_names: iterable
-        :param: from_basin3d: boolean that says whether the variable name is a
-            BASIN-3D variable. If not, then this a datasource variable names.
-        :type from_basin3d: boolean
-        :return: list of variable names
-        :rtype: iterable
+        General purpose search for attribute mappings from datasource or BASIN-3D vocabularies.
+        Returns an iterator that yields `basin3d.models.MappedAttribute` objects that match the criteria
+        :param attr_type:
+        :param attr_vocab:
+        :param from_basin3d:
+        :return:
         """
-        return self._catalog.find_observed_property_variables(self.datasource.id, variable_names, from_basin3d)
+        return self._catalog.find_attribute_mappings(self.datasource.id, attr_type, attr_vocab, from_basin3d)
 
 
 def basin3d_plugin(cls):
diff --git a/basin3d/core/schema/enum.py b/basin3d/core/schema/enum.py
index 44b3cd5..7260995 100644
--- a/basin3d/core/schema/enum.py
+++ b/basin3d/core/schema/enum.py
@@ -19,6 +19,10 @@
 
 from basin3d.core.types import SpatialSamplingShapes
 
+MAPPING_DELIMITER = ':'
+NO_MAPPING_TEXT = 'NOT_SUPPORTED'
+
+
 class BaseEnum(Enum):
     """Base Enumeration Class that adds some helper methods"""
 
@@ -32,6 +36,17 @@ def names(cls):
         return cls._member_names_
 
 
+class MappedAttributeEnum(str, BaseEnum):
+    """
+    Enumeration for mapped attributes
+    """
+    OBSERVED_PROPERTY = 'OBSERVED_PROPERTY'
+    AGGREGATION_DURATION = 'AGGREGATION_DURATION'
+    RESULT_QUALITY = 'RESULT_QUALITY'
+    SAMPLING_MEDIUM = 'SAMPLING_MEDIUM'
+    STATISTIC = 'STATISTIC'
+
+
 class TimeFrequencyEnum(str, BaseEnum):
     """
     Enumeration for time frequencies
@@ -45,6 +60,33 @@ class TimeFrequencyEnum(str, BaseEnum):
     MINUTE = "MINUTE"
     SECOND = "SECOND"
     NONE = "NONE"
+    NOT_SUPPORTED = NO_MAPPING_TEXT
+
+
+class AggregationDurationEnum(str, BaseEnum):
+    """
+    Aggregation Duration enums
+    """
+    #: Observations aggregated by year
+    YEAR = TimeFrequencyEnum.YEAR.value
+
+    #: Observations aggregated by month
+    MONTH = TimeFrequencyEnum.MONTH.value
+
+    #: Observations aggregated by day
+    DAY = TimeFrequencyEnum.DAY.value
+
+    #: Observations aggregated by hour
+    HOUR = TimeFrequencyEnum.HOUR.value
+
+    #: Observations aggregated by minute
+    MINUTE = TimeFrequencyEnum.MINUTE.value
+
+    #: Observations aggregated by second
+    SECOND = TimeFrequencyEnum.SECOND.value
+
+    #: Observations aggregated by no standard frequency, used for instantaneous values
+    NONE = TimeFrequencyEnum.NONE.value
 
 
 class FeatureTypeEnum(str, BaseEnum):
@@ -92,7 +134,7 @@ class ResultQualityEnum(str, BaseEnum):
     ESTIMATED = "ESTIMATED"
 
     #: The quality type is not supported
-    NOT_SUPPORTED = "NOT_SUPPORTED"
+    NOT_SUPPORTED = NO_MAPPING_TEXT
 
 
 class StatisticEnum(str, BaseEnum):
@@ -102,6 +144,19 @@ class StatisticEnum(str, BaseEnum):
     MIN = "MIN"
     MAX = "MAX"
     TOTAL = "TOTAL"
+    NOT_SUPPORTED = NO_MAPPING_TEXT
+
+
+class SamplingMediumEnum(str, BaseEnum):
+    """
+    Types of sampling mediums for Observed Properties
+    """
+    SOLID_PHASE = "SOLID_PHASE"
+    WATER = "WATER"
+    GAS = "GAS"
+    OTHER = "OTHER"
+    NOT_APPLICABLE = "NOT_APPLICABLE"
+    NOT_SUPPORTED = NO_MAPPING_TEXT
 
 
 class MessageLevelEnum(str, BaseEnum):
@@ -110,3 +165,21 @@ class MessageLevelEnum(str, BaseEnum):
     WARN = "WARN"
     ERROR = "ERROR"
     CRITICAL = "CRITICAL"
+
+
+def set_mapped_attribute_enum_type(attr_type: str):
+    """
+    Return the enum type for the specified MappedAttributeEnum.
+    :param attr_type: MappedAttributeEnum
+    :return: the type's Enum
+    """
+    if attr_type == MappedAttributeEnum.AGGREGATION_DURATION:
+        return AggregationDurationEnum
+    elif attr_type == MappedAttributeEnum.RESULT_QUALITY:
+        return ResultQualityEnum
+    elif attr_type == MappedAttributeEnum.SAMPLING_MEDIUM:
+        return SamplingMediumEnum
+    elif attr_type == MappedAttributeEnum.STATISTIC:
+        return StatisticEnum
+    else:
+        return None
diff --git a/basin3d/core/schema/query.py b/basin3d/core/schema/query.py
index 2a8d0bf..b0665fe 100644
--- a/basin3d/core/schema/query.py
+++ b/basin3d/core/schema/query.py
@@ -16,11 +16,11 @@
 
 """
 from datetime import date
-from typing import List, Optional, Union
+from typing import ClassVar, List, Optional, Union
 
 from pydantic import BaseModel, Field
 
-from basin3d.core.schema.enum import FeatureTypeEnum, MessageLevelEnum, ResultQualityEnum, StatisticEnum, TimeFrequencyEnum
+from basin3d.core.schema.enum import FeatureTypeEnum, MessageLevelEnum, ResultQualityEnum, SamplingMediumEnum, StatisticEnum, AggregationDurationEnum
 
 
 def _to_camelcase(string) -> str:
@@ -38,6 +38,8 @@ class QueryBase(BaseModel):
 
     datasource: Optional[List[str]] = Field(title="Datasource Identifiers",
                                             description="List of datasource identifiers to query by.")
+    is_valid_translated_query: Union[None, bool] = Field(default=None, title="Valid translated query",
+                                                         description="Indicates whether the translated query is valid: None = is not translated")
 
     def __init__(self, **data):
         """
@@ -61,6 +63,12 @@ class Config:
         # Validate all fields when initialized
         validate_all = True
 
+    # Get the query fields that have mappings. Subclasses may overwrite this base function
+    mapped_fields: ClassVar[List[str]] = []
+
+    # Get the query fields that have prefixes. Subclasses may overwrite ths base function
+    prefixed_fields: ClassVar[List[str]] = []
+
 
 class QueryById(QueryBase):
     """Query for a single data object by identifier"""
@@ -72,10 +80,10 @@ class QueryMonitoringFeature(QueryBase):
     """Query :class:`basin3d.core.models.MonitoringFeature`"""
     feature_type: Optional[FeatureTypeEnum] = Field(title="Feature Type",
                                                     description="Filter results by the specified feature type.")
-    monitoring_features: Optional[List[str]] = Field(title="Monitoring Features",
-                                                     description="Filter by the list of monitoring feature identifiers")
-    parent_features: Optional[List[str]] = Field(title="Parent Monitoring Features",
-                                                 description="Filter by the list of parent monitoring feature identifiers")
+    monitoring_feature: Optional[List[str]] = Field(title="Monitoring Features",
+                                                    description="Filter by the list of monitoring feature identifiers")
+    parent_feature: Optional[List[str]] = Field(title="Parent Monitoring Features",
+                                                description="Filter by the list of parent monitoring feature identifiers")
 
     def __init__(self, **data):
         """
@@ -84,8 +92,8 @@ def __init__(self, **data):
         :param data: the data
         """
 
-        # convert strings to lists for some fields
-        for field in ["monitoring_features", "parent_features", "monitoringFeatures", "parentFeatures"]:
+        # convert strings to lists for some fields; the camel case is for Pydantic validation
+        for field in ["monitoring_feature", "monitoringFeature", "parent_feature", "parentFeature"]:
             if field in data and data[field] and isinstance(data[field], str):
                 data[field] = list([data[field]])
 
@@ -95,21 +103,28 @@ def __init__(self, **data):
                 data[field] = isinstance(data[field], str) and data[field].upper() or data[field]
         super().__init__(**data)
 
+    prefixed_fields: ClassVar[List[str]] = ['monitoring_feature', 'parent_feature']
+
 
 class QueryMeasurementTimeseriesTVP(QueryBase):
     """Query :class:`basin3d.core.models.MeasurementTimeseriesTVP`"""
-    aggregation_duration: Optional[TimeFrequencyEnum] = Field(default='DAY', title="Aggregation Duration",
-                                                              description="Filter by the specified time frequency")
-    monitoring_features: List[str] = Field(min_items=1, title="Monitoring Features",
-                                           description="Filter by the list of monitoring feature identifiers")
-    observed_property_variables: List[str] = Field(min_items=1, title="Observed Property Variables",
-                                                   description="Filter by the list of observed property variables")
+    # required
+    monitoring_feature: List[str] = Field(min_items=1, title="Monitoring Features",
+                                          description="Filter by the list of monitoring feature identifiers")
+    observed_property: List[str] = Field(min_items=1, title="Observed Property Variables",
+                                         description="Filter by the list of observed property variables")
     start_date: date = Field(title="Start Date", description="Filter by data taken on or after the start date")
+
+    # optional
+    aggregation_duration: AggregationDurationEnum = Field(default='DAY', title="Aggregation Duration",
+                                                          description="Filter by the specified aggregation duration or time frequency")
     end_date: Optional[date] = Field(title="End Date", description="Filter by data taken on or before the end date")
     statistic: Optional[List[StatisticEnum]] = Field(title="Statistic",
                                                      description="Return specified statistics, if they exist.")
     result_quality: Optional[List[ResultQualityEnum]] = Field(title="Result Quality",
                                                               description="Filter by specified result qualities")
+    sampling_medium: Optional[List[SamplingMediumEnum]] = Field(title="Sampling Medium",
+                                                                description="Filter results by specified sampling medium")
 
     def __init__(self, **data):
         """
@@ -118,13 +133,31 @@ def __init__(self, **data):
         :param data: the data
         """
 
-        # convert strings to lists for some fields
-        for field in ["monitoring_features", "observed_property_variables", "monitoringFeatures",
-                      "observedPropertyVariables"]:
+        # convert strings to lists for some fields; the camel case are for Pydantic validation (don't delete)
+        for field in ["monitoring_feature", "monitoringFeature", "observed_property", "observedProperty",
+                      "statistic", "result_quality", "sampling_medium"]:
             if field in data and data[field] and isinstance(data[field], str):
                 data[field] = list([data[field]])
+
+        data = self.__validate__(**data)
+
         super().__init__(**data)
 
+    @staticmethod
+    def __validate__(**data):
+        """
+        Valiate
+        :return:
+        """
+        if 'aggregation_duration' in data and data['aggregation_duration'] is None:
+            del data['aggregation_duration']
+        return data
+
+    # observed_property_variables is first b/c it is most likely to have compound mappings.
+    # ToDo: check how order may affect translation (see core/synthesis)
+    mapped_fields: ClassVar[List[str]] = ['observed_property', 'aggregation_duration', 'statistic', 'result_quality', 'sampling_medium']
+    prefixed_fields: ClassVar[List[str]] = ['monitoring_feature']
+
 
 class SynthesisMessage(BaseModel):
     """BASIN-3D Synthesis Message """
@@ -149,6 +182,7 @@ class Config:
         # Validate all fields when initialized
         validate_all = True
 
+
 class SynthesisResponse(BaseModel):
     """BASIN-3D Synthesis Response """
 
diff --git a/basin3d/core/synthesis.py b/basin3d/core/synthesis.py
index 1d5d125..9e3e14e 100644
--- a/basin3d/core/synthesis.py
+++ b/basin3d/core/synthesis.py
@@ -20,42 +20,14 @@
 from basin3d.core import monitor
 from basin3d.core.models import Base, MeasurementTimeseriesTVPObservation, MonitoringFeature
 from basin3d.core.plugin import DataSourcePluginAccess, DataSourcePluginPoint
-from basin3d.core.schema.enum import MessageLevelEnum, TimeFrequencyEnum
+from basin3d.core.schema.enum import MessageLevelEnum, AggregationDurationEnum
 from basin3d.core.schema.query import QueryBase, QueryById, QueryMeasurementTimeseriesTVP, \
     QueryMonitoringFeature, SynthesisMessage, SynthesisResponse
+from basin3d.core.translate import translate_query
 
 logger = monitor.get_logger(__name__)
 
 
-def _synthesize_query_identifiers(values, id_prefix) -> List[str]:
-    """
-    Extract the ids from the specified query params
-
-    :param values:  the ids to synthesize
-    :param id_prefix:  the datasource id prefix
-    :return: The list of synthesizes identifiers
-    """
-    # Synthesize the ids (remove datasource id_prefix)
-    if isinstance(values, str):
-        values = values.split(",")
-
-    def extract_id(identifer):
-        """
-        Extract the datasource identifier from the broker identifier
-        :param identifer:
-        :return:
-        """
-        if identifer:
-            site_list = identifer.split("-")
-            identifer = identifer.replace("{}-".format(site_list[0]),
-                                          "", 1)  # The datasource id prefix needs to be removed
-        return identifer
-
-    return [extract_id(x) for x in
-            values
-            if x.startswith("{}-".format(id_prefix))]
-
-
 class MonitorMixin(object):
     """
     Adds monitor log functionality to a class for logging synthesis messages
@@ -191,15 +163,19 @@ def __next__(self) -> Base:
                     if self._model_access.synthesis_model in plugin_views and \
                             hasattr(plugin_views[self._model_access.synthesis_model], "list"):
 
-                        # Now synthesize the query object
-                        synthesized_query_params: QueryBase = self._model_access.synthesize_query(
+                        # Now translate the query object
+                        translated_query_params: QueryBase = self._model_access.synthesize_query(
                             plugin_views[self._model_access.synthesis_model],
                             self._synthesis_response.query)
-                        synthesized_query_params.datasource = [plugin.get_datasource().id]
+                        translated_query_params.datasource = [plugin.get_datasource().id]
+
+                        # Get the model access iterator if synthesized query is valid
+                        if translated_query_params.is_valid_translated_query:
+                            self._model_access_iterator = plugin_views[self._model_access.synthesis_model].list(
+                                query=translated_query_params)
+                        else:
+                            self.warn(f'Translated query for datasource {plugin.get_datasource().id} is not valid.')
 
-                        # Get the model access iterator
-                        self._model_access_iterator = plugin_views[self._model_access.synthesis_model].list(
-                            query=synthesized_query_params)
                     else:
                         self.warn("Plugin view does not exist")
 
@@ -247,7 +223,6 @@ def synthesize_query(self, plugin_access: DataSourcePluginAccess,
         Synthesizes query parameters, if necessary
 
         :param query: The query information to be synthesized
-        :param request: the request to synthesize
         :param plugin_access: The plugin view to synthesize query params for
         :return: The synthesized query information
         """
@@ -317,7 +292,7 @@ class MonitoringFeatureAccess(DataSourceModelAccess):
     * *name:* string, Feature name
     * *description:* string, Description of the feature
     * *feature_type:* sting, FeatureType: REGION, SUBREGION, BASIN, SUBBASIN, WATERSHED, SUBWATERSHED, SITE, PLOT, HORIZONTAL PATH, VERTICAL PATH, POINT
-    * *observed_property_variables:* list of observed variables made at the feature. Observed property variables are configured via the plugins.
+    * *observed_properties:* list of observed properties (variables) made at the feature. Observed properties are configured via the plugins.
     * *related_sampling_feature_complex:* list of related_sampling features. PARENT features are currently supported.
     * *shape:* string, Shape of the feature: POINT, CURVE, SURFACE, SOLID
     * *coordinates:* location of feature in absolute and/or representative datum
@@ -344,20 +319,7 @@ def synthesize_query(self, plugin_access: DataSourcePluginAccess, query: QueryMo
         :param plugin_access: The plugin view to synthesize query params for
         :return: The synthesized query information
         """
-        synthesized_query = query.copy()
-
-        if query:
-            id_prefix = plugin_access.datasource.id_prefix
-            if query.monitoring_features:
-                synthesized_query.monitoring_features = _synthesize_query_identifiers(
-                    values=query.monitoring_features,
-                    id_prefix=id_prefix)
-            if query.parent_features:
-                synthesized_query.parent_features = _synthesize_query_identifiers(
-                    values=query.parent_features,
-                    id_prefix=id_prefix)
-
-        return synthesized_query
+        return translate_query(plugin_access, query)
 
 
 class MeasurementTimeseriesTVPObservationAccess(DataSourceModelAccess):
@@ -374,23 +336,24 @@ class MeasurementTimeseriesTVPObservationAccess(DataSourceModelAccess):
     * *utc_offset:* float, Coordinate Universal Time offset in hours (offset in hours), e.g., +9
     * *feature_of_interest:* MonitoringFeature obj, feature on which the observation is being made
     * *feature_of_interest_type:* enum (FeatureTypes), feature type of the feature of interest
-    * *result_points:* list of TimeValuePair obj, observed values of the observed property being assessed
+    * *result:* dictionary of 2 lists: "value" contains TimeValuePair obj and "quality" the corresponding quality assessment per value, observed values and their quality for the observed property being assessed
     * *time_reference_position:* enum, position of timestamp in aggregated_duration (START, MIDDLE, END)
     * *aggregation_duration:* enum, time period represented by observation (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, NONE)
     * *unit_of_measurement:* string, units in which the observation is reported
-    * *statistic:* enum, statistical property of the observation result (MEAN, MIN, MAX, TOTAL)
-    * *result_quality:* enum, quality assessment of the result (VALIDATED, UNVALIDATED, SUSPECTED, REJECTED, ESTIMATED)
+    * *statistic:* list, statistical properties of the observation result (MEAN, MIN, MAX, TOTAL)
+    * *result_quality:* list, quality assessment values contained in the result (VALIDATED, UNVALIDATED, SUSPECTED, REJECTED, ESTIMATED)
 
     **Filter** by the following attributes (?attribute=parameter&attribute=parameter&...):
 
-    * *monitoring_features (required):* List of monitoring_features ids
-    * *observed_property_variables (required):* List of observed property variable ids
+    * *monitoring_feature (required):* List of monitoring_features ids
+    * *observed_property (required):* List of observed property variable ids
     * *start_date (required):* date YYYY-MM-DD
     * *end_date (optional):* date YYYY-MM-DD
     * *aggregation_duration (default: DAY):* enum (YEAR|MONTH|DAY|HOUR|MINUTE|SECOND|NONE)
     * *statistic (optional):* List of statistic options, enum (INSTANT|MEAN|MIN|MAX|TOTAL)
     * *datasource (optional):* a single data source id prefix (e.g ?datasource=`datasource.id_prefix`)
     * *result_quality (optional):* enum (VALIDATED|UNVALIDATED|SUSPECTED|REJECTED|ESTIMATED)
+    * *sampling_medium (optional):* ADD here -- probably should be enum
 
     **Restrict fields** with query parameter ‘fields’. (e.g. ?fields=id,name)
 
@@ -403,8 +366,8 @@ def synthesize_query(self, plugin_access: DataSourcePluginAccess, query: QueryMe
         Synthesizes query parameters, if necessary
 
         Parameters Synthesized:
-          + monitoring_features
-          + observed_property_variables
+          + monitoring_feature
+          + observed_property
           + aggregation_duration (default: DAY)
           + statistic
           + quality_checked
@@ -414,24 +377,9 @@ def synthesize_query(self, plugin_access: DataSourcePluginAccess, query: QueryMe
         :return: The query parameters
         """
 
-        id_prefix = plugin_access.datasource.id_prefix
-        synthesized_query = query.copy()
-
-        if query:
-
-            if query.monitoring_features:
-                synthesized_query.monitoring_features = _synthesize_query_identifiers(values=query.monitoring_features,
-                                                                                      id_prefix=id_prefix)
-
-            # Synthesize ObservedPropertyVariable (from BASIN-3D to DataSource variable name)
-            if query.observed_property_variables:
-                synthesized_query.observed_property_variables = [o.datasource_variable for o in
-                                                                 plugin_access.get_observed_properties(
-                                                                     query.observed_property_variables)]
-        # Aggregation duration will be default to DAY in QueryMeasurementTimeseriesTVP.
-        # Query will accept aggregation duration NONE and DAY only
-        synthesized_query.aggregation_duration = query.aggregation_duration
-        if synthesized_query.aggregation_duration != TimeFrequencyEnum.NONE:
-            synthesized_query.aggregation_duration = TimeFrequencyEnum.DAY
+        # only allow instantaneous data (NONE) or daily data (DAY) data
+        # NOTE: query at this point is still in BASIN-3D vocab
+        if query.aggregation_duration != AggregationDurationEnum.NONE:
+            query.aggregation_duration = AggregationDurationEnum.DAY
 
-        return synthesized_query
+        return translate_query(plugin_access, query)
diff --git a/basin3d/core/translate.py b/basin3d/core/translate.py
new file mode 100644
index 0000000..63b8ef7
--- /dev/null
+++ b/basin3d/core/translate.py
@@ -0,0 +1,407 @@
+"""
+`basin3d.core.translate`
+************************
+
+.. currentmodule:: basin3d.core.translate
+
+:synopsis: The BASIN-3D Translate functionality
+:module author: Danielle Svehla Christianson <dschristianson@lbl.gov>
+
+.. contents:: Contents
+    :local:
+    :backlinks: top
+
+"""
+
+from itertools import product, repeat
+from typing import List, Optional, Union
+
+from basin3d.core import monitor
+from basin3d.core.schema.enum import MAPPING_DELIMITER, NO_MAPPING_TEXT
+from basin3d.core.schema.query import QueryBase, QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
+
+
+logger = monitor.get_logger(__name__)
+
+
+def _clean_query(translated_query: QueryBase) -> QueryBase:
+    """
+    Remove any NOT_SUPPORTED translations
+
+    :param translated_query: the translated query that may have NOT_SUPPORTED translation results
+    :return: cleaned query that has all NOT_SUPPORTED translations removed
+    """
+    for attr in translated_query.mapped_fields:
+        attr_value = getattr(translated_query, attr)
+        if attr_value and isinstance(attr_value, list):
+            clean_list = [val for val in attr_value if val != NO_MAPPING_TEXT]
+            unique_list = list(set(clean_list))
+            setattr(translated_query, attr, unique_list)
+        elif attr_value and attr_value == NO_MAPPING_TEXT:
+            setattr(translated_query, attr, None)
+    return translated_query
+
+
+def _get_attr_types_in_compound_mappings(plugin_access) -> list:
+    """
+    Get all the compound mappings for a datasource if any exist
+
+    :param plugin_access: plugin access
+    :return: list of attribute types that are part of a compound mapping.
+    """
+    compound_mapping_attrs = set()
+
+    attr_mapping_iterator = plugin_access.get_attribute_mappings()
+
+    for attr_mapping in attr_mapping_iterator:
+        if MAPPING_DELIMITER in attr_mapping.attr_type:
+            compound_mapping_attrs.add(attr_mapping.attr_type)
+
+    return list(compound_mapping_attrs)
+
+
+def _get_attr_type_if_compound_mapping(plugin_access, attr_type: str) -> Optional[str]:
+    """
+    Return the compound attr_type str if the specified attr_type is part of a compound_mapping
+
+    :param plugin_access:
+    :param attr_type:
+    :return: str if attr mapping is part of compound mapping
+    """
+    compound_mapping_str = None
+    attr_type = attr_type.upper()
+    attr_mapping_iterator = plugin_access.get_attribute_mappings(attr_type=attr_type)
+
+    # look at the first element returned if there is one
+    for attr_mapping in attr_mapping_iterator:
+        if MAPPING_DELIMITER in attr_mapping.attr_type:
+            compound_mapping_str = attr_mapping.attr_type
+        # only need to look at the first attribute mapping returned
+        break
+
+    return compound_mapping_str
+
+
+def _get_single_attr_types_in_compound_mappings(plugin_access, attr_type: str, include_specified_type: bool = False) -> list:
+    """
+    Return the attributes if attr_type is part of a compound mapping
+
+    :param attr_type: the attribute type
+    :param include_specified_type: bool, True = include in the return the specified attr_type. False: return the other attribute types that are part of the compound mapping.
+    :return: list of attributes in the compound mapping
+    """
+
+    compound_mapping_attrs: List[str] = []
+
+    compound_mapping_str = _get_attr_type_if_compound_mapping(plugin_access, attr_type)
+
+    if not compound_mapping_str:
+        return compound_mapping_attrs
+
+    for attr in compound_mapping_str.split(MAPPING_DELIMITER):
+        if attr == attr_type.upper() and not include_specified_type:
+            continue
+        compound_mapping_attrs.append(attr)
+
+    return compound_mapping_attrs
+
+
+def _is_translated_query_valid(datasource_id, query, translated_query) -> Optional[bool]:
+    """
+    Determine if the translated query is valid. A valid translated query has at least one datasoure vocabulary
+    for any query field that was supecified (i.e., with a BASIN-3D vocab)
+
+    :param datasource_id: the datasource id
+    :param query: the original query
+    :param translated_query: the translated query
+    :return: boolean (True = valid translated query, False = invalid translated query) or None (translated query could not be assessed)
+    """
+    # loop thru kwargs
+    for attr in query.mapped_fields:
+        translated_attr_value = getattr(translated_query, attr)
+        b3d_attr_value = getattr(query, attr)
+        if isinstance(b3d_attr_value, list):
+            b3d_attr_value = ', '.join(b3d_attr_value)
+        if translated_attr_value and isinstance(translated_attr_value, list):
+            # if list and all of list == NOT_SUPPORTED, False
+            if all([x == NO_MAPPING_TEXT for x in translated_attr_value]):
+                logger.warning(f'Translated query for datasource {datasource_id} is invalid. No vocabulary found for attribute {attr} with values: {b3d_attr_value}.')
+                return False
+        elif translated_attr_value and isinstance(translated_attr_value, str):
+            # if single NOT_SUPPORTED, False
+            if translated_attr_value == NO_MAPPING_TEXT:
+                logger.warning(f'Translated query for datasource {datasource_id} is invalid. No vocabulary found for attribute {attr} with value: {b3d_attr_value}.')
+                return False
+        elif translated_attr_value:
+            logger.warning(
+                f'Translated query for datasource {datasource_id} cannot be assessed. Translated value for {attr} is not expected type.')
+            return None
+    return True
+
+
+def _order_mapped_fields(plugin_access, query_mapped_fields):
+    """
+    Order the mapped fields according to the order of the attributes in any compound mappings. And then handle the rest of the mappings.
+
+    :param plugin_access:
+    :param query_mapped_fields:
+    :return:
+    """
+    query_mapped_fields_ordered = []
+
+    # get list of compound mappings if any
+    compound_mappings = _get_attr_types_in_compound_mappings(plugin_access)
+
+    # If there are compound mappings...
+    if compound_mappings:
+        cm_fields = []
+        # Split up the compound mappings, preserving the order of the attributes as specified in the plugin mapping file.
+        # The order only matters relative to the individual compound mapping.
+        for cm in compound_mappings:
+            cm_attrs = cm.split(MAPPING_DELIMITER)
+            cm_fields.extend([cm_attr.lower() for cm_attr in cm_attrs])
+        # first loop thru the compound mapping fields
+        for cm in cm_fields:
+            # if the attribute is one of the mapped fields in this particular query
+            if cm in query_mapped_fields:
+                # add it to the ordered list
+                query_mapped_fields_ordered.append(cm)
+                # then remove it from the mapped field list
+                query_mapped_fields.pop(query_mapped_fields.index(cm))
+        # then, add any remaining non-compound fields
+        query_mapped_fields_ordered.extend(query_mapped_fields)
+    else:
+        # if there are no compound mappings, then the order doesn't matter, just copy the mapped field list.
+        query_mapped_fields_ordered = query_mapped_fields
+
+    return query_mapped_fields_ordered
+
+
+def _translate_mapped_query_attrs(plugin_access, query: Union[QueryMeasurementTimeseriesTVP, QueryMonitoringFeature, QueryById]) -> QueryBase:
+    """
+    Translation functionality
+    """
+    query_mapped_fields = query.mapped_fields.copy()
+
+    # if there are no mapped fields, return the query as is.
+    if not query_mapped_fields:
+        return query
+
+    # order the query fields by any compound attributes
+    query_mapped_fields_ordered = _order_mapped_fields(plugin_access, query_mapped_fields)
+
+    for attr in query_mapped_fields_ordered:
+        # if the attribute is specified, proceed to translate it
+        # NOTE: looking in the translated_query which is mutable. As the translation occurs, translated query fields may change
+        #       and the if statement may have different values for a given field during the loop.
+        if getattr(query, attr):
+            b3d_vocab = getattr(query, attr)
+
+            if isinstance(b3d_vocab, str):
+                ds_vocab = _translate_to_datasource_vocab(plugin_access, attr.upper(), b3d_vocab, query)
+            else:
+                ds_vocab = []
+                for b3d_value in b3d_vocab:
+                    # handle multiple values returned
+                    ds_vocab.extend(_translate_to_datasource_vocab(plugin_access, attr.upper(), b3d_value, query))
+            setattr(query, attr, ds_vocab)
+
+            # look up whether the attr is part of a compound mapping
+            compound_attrs = _get_single_attr_types_in_compound_mappings(plugin_access, attr)
+            # if so: for any compound attrs, clear out the values in the synthesized query b/c search needs to be done on the coupled datasource_vocab
+            for compound_attr in compound_attrs:
+                setattr(query, compound_attr.lower(), None)
+
+    # NOTE: always returns list for each mapped attr b/c multiple datasource vocab can be mapped to a single BASIN-3D vocab.
+    return query
+
+
+def _translate_prefixed_query_attrs(plugin_access, query: Union[QueryMeasurementTimeseriesTVP, QueryMonitoringFeature, QueryById]) -> QueryBase:
+    """
+
+    :param plugin_access:
+    :param query:
+    :return:
+    """
+    def extract_id(identifer):
+        """
+        Extract the datasource identifier from the broker identifier
+        :param identifer:
+        :return:
+        """
+        if identifer:
+            site_list = identifer.split("-")
+            identifer = identifer.replace("{}-".format(site_list[0]), "", 1)  # The datasource id prefix needs to be removed
+        return identifer
+
+    id_prefix = plugin_access.datasource.id_prefix
+
+    for attr in query.prefixed_fields:
+        attr_value = getattr(query, attr)
+        if attr_value:
+            if isinstance(attr_value, str):
+                attr_value = attr_value.split(",")
+
+            translated_values = [extract_id(x) for x in attr_value if x.startswith("{}-".format(id_prefix))]
+
+            setattr(query, attr, translated_values)
+
+    return query
+
+
+def _translate_to_datasource_vocab(plugin_access, attr_type: str, basin3d_vocab: str, b3d_query) -> list:
+    """
+    Find the datasource vocabulary(ies) for the specified datasource, attribute type, BASIN-3D vocabulary, and full query that may specify other attributes.
+    Because multiple datasource vocabularies can be mapped to the same BASIN-3D vocabulary, the return is a list of the datasource vocabs.
+
+    :param plugin_access: plugin access
+    :param attr_type: the attribute type
+    :param basin3d_vocab: the BASIN-3D vocabulary
+    :param b3d_query: either a QueryBase class or subclass object, or a dictionary
+    :return: list of the datasource vocabularies
+    """
+    # convert attr_type to uppercase
+    attr_type = attr_type.upper()
+
+    # is the attr_type part of a compound mapping?
+    compound_mapping_attrs = _get_single_attr_types_in_compound_mappings(plugin_access, attr_type, include_specified_type=True)
+
+    b3d_vocab_combo_str = [basin3d_vocab]
+
+    # if compound_mapping, find all the relevant value combos given the query
+    if compound_mapping_attrs:
+        b3d_vocab_filter_lists = []  # list to hold lists of specified filters, one for each attr
+
+        # loop thru each of the compound mapping attributes, build a list of lists of query combos
+        for attr in compound_mapping_attrs:
+            attr_value = None
+
+            # by default: match any number of characters excepting a new line for the attribute
+            # replace this value below if a value for the attribute is specified in the query
+            filter_values: List[str] = ['.*']
+            # if the attr is the attr_type, set the filter to the specified vocab
+            if attr == attr_type:
+                filter_values = [basin3d_vocab]
+            elif issubclass(b3d_query.__class__, QueryBase) and hasattr(b3d_query, attr.lower()):
+                attr_value = getattr(b3d_query, attr.lower())
+            elif isinstance(b3d_query, dict) and attr.lower() in b3d_query.keys():
+                attr_value = b3d_query.get(attr.lower())
+
+            # if there is a value, replace the default value
+            if attr_value:
+                filter_values = attr_value
+                # if the values are a str, change it to a list
+                if isinstance(attr_value, str):
+                    filter_values = attr_value.split(',')
+
+            # append the filter list to the main list
+            b3d_vocab_filter_lists.append(filter_values)
+
+        # create a list of sets contain the combinations of filter options for each attr
+        b3d_vocab_combo_sets = list(product(*b3d_vocab_filter_lists))
+
+        # change each set into a str for search
+        b3d_vocab_combo_str = [MAPPING_DELIMITER.join(v) for v in b3d_vocab_combo_sets]
+
+    ds_vocab: List[str] = []
+    no_match_list: List[str] = []
+
+    # Loop thru the list of vocabulary string combos to search the attribute mapping database
+    for basin3d_vocab_str in b3d_vocab_combo_str:
+        results_iterator = plugin_access.get_attribute_mappings(attr_type=attr_type,
+                                                                attr_vocab=basin3d_vocab_str, from_basin3d=True)
+        query_results = []
+
+        # there may be more than one datasource variable mapped to the specified BASIN-3D vocab
+        # Loop thru the results and collect all of them.
+        for qr in results_iterator:
+            query_results.append(qr.datasource_vocab)
+
+        # If there were indeed mappings, add them to the main ds_vocab list and move on
+        if query_results:
+            ds_vocab.extend(query_results)
+            continue
+
+        # if not result for the combo, add it to the no_match_list
+        no_match_list.append(basin3d_vocab_str)
+
+    if not ds_vocab:
+        ds_vocab = [NO_MAPPING_TEXT]
+
+    if no_match_list:
+        if compound_mapping_attrs:
+            attr_type = ':'.join(compound_mapping_attrs)
+        logger.info(f'Datasource "{plugin_access.datasource.id}" did not have matches for attr_type '
+                    f'{attr_type} and BASIN-3D vocab {", ".join(no_match_list)}.')
+
+    return ds_vocab
+
+
+def get_datasource_mapped_attribute(plugin_access, attr_type, datasource_vocab):
+    """
+    Get the :class:`basin3d.core.models.MappedAttribute` object(s) for the specified attribute type and datasource attribute vocab(s)
+
+    :param plugin_access: plugin_access
+    :param attr_type: attribute type
+    :param datasource_vocab: datasource attribute vocabulary
+    :return: a single or list of `basin3d.core.models.MappedAttribute` objects
+    """
+
+    if isinstance(datasource_vocab, str):
+        return plugin_access.get_datasource_attribute_mapping(attr_type, datasource_vocab)
+
+    elif isinstance(datasource_vocab, list):
+        return list(map(plugin_access.get_datasource_attribute_mapping, repeat(attr_type), datasource_vocab))
+
+
+def translate_attributes(plugin_access, mapped_attrs, **kwargs):
+    """
+    Translate datasource vocabularies to BASIN-3D vocabularies via
+    :class:`basin3d.core.models.AttributeMapping` objects specified in the datasource plugin
+    for specified model attributes (i.e., those that have mappings).
+    This function is used by data model objects during object creation. See :class:`basin3d.core.models`.
+
+    :param plugin_access: plugin_access
+    :param mapped_attrs: a model's mapped attributes
+    :param kwargs: the full set of a model's attributes
+    :return: kwargs: the model's attributes, including the translated attributes
+    """
+
+    # copy the kwargs be able to loop thru the original while modifying the actual for compound mappings
+    kwargs_orig = kwargs.copy()
+
+    for attr in mapped_attrs:
+        if attr in kwargs_orig:
+            datasource_vocab = kwargs[attr]
+            attr_mapping = get_datasource_mapped_attribute(plugin_access, attr_type=attr.upper(), datasource_vocab=datasource_vocab)
+            kwargs[attr] = attr_mapping
+
+            # If the attr is part of a compound mapping and the compound attr is not part of the kwargs, set it.
+            cm_attrs = _get_single_attr_types_in_compound_mappings(plugin_access, attr)
+            if cm_attrs:
+                for cm_attr in cm_attrs:
+                    if cm_attr.lower() not in kwargs:
+                        cm_attr_mapping = get_datasource_mapped_attribute(plugin_access, attr_type=cm_attr.upper(), datasource_vocab=datasource_vocab)
+                        kwargs[cm_attr.lower()] = cm_attr_mapping
+
+    return kwargs
+
+
+def translate_query(plugin_access, query: Union[QueryMeasurementTimeseriesTVP, QueryMonitoringFeature, QueryById]) -> QueryBase:
+    """
+    Translate BASIN-3D vocabulary specified in a query to the datasource vocabularies defined by :class:`basin3d.core.models.AttributeMapping` objects specified in the datasource plugin.
+
+    :param plugin_access: plugin access
+    :param query: query to be translated
+    :return: translated query as a :class:`basin3d.core.schema.query.QueryBase` object
+    """
+    translated_query = query.copy()
+    _translate_mapped_query_attrs(plugin_access, translated_query)
+    _translate_prefixed_query_attrs(plugin_access, translated_query)
+    is_valid_translated_query = _is_translated_query_valid(plugin_access.datasource.id, query, translated_query)
+
+    if is_valid_translated_query:
+        translated_query.is_valid_translated_query = is_valid_translated_query
+        _clean_query(translated_query)
+
+    return translated_query
diff --git a/basin3d/core/types.py b/basin3d/core/types.py
index b542631..913167c 100644
--- a/basin3d/core/types.py
+++ b/basin3d/core/types.py
@@ -34,18 +34,3 @@ class SpatialSamplingShapes(object):
 
     #: The shape of a spatially extensive sampling feature which provides a complete sampling domain.
     SHAPE_POINT = "POINT"
-
-
-class SamplingMedium:
-    """
-    Types of sampling mediums for Observed Properties
-    """
-
-    SOLID_PHASE = "SOLID PHASE"
-    WATER = "WATER"
-    GAS = "GAS"
-    OTHER = "OTHER"
-    NOT_APPLICABLE = "N/A"
-    SAMPLING_MEDIUMS = [WATER, GAS, SOLID_PHASE, OTHER, NOT_APPLICABLE]
-
-
diff --git a/basin3d/data/basin3d_variables_hydrology.csv b/basin3d/data/basin3d_observed_property_vocabulary.csv
similarity index 99%
rename from basin3d/data/basin3d_variables_hydrology.csv
rename to basin3d/data/basin3d_observed_property_vocabulary.csv
index be689d6..830c03d 100644
--- a/basin3d/data/basin3d_variables_hydrology.csv
+++ b/basin3d/data/basin3d_observed_property_vocabulary.csv
@@ -1,4 +1,4 @@
-basin3d_id,description,categories,units
+basin3d_vocab,description,categories,units
 ACT,Acetate (CH3COO),"Biogeochemistry,Anions",mM
 Br,Bromide (Br),"Biogeochemistry,Anions",mM
 Cl,Chloride (Cl),"Biogeochemistry,Anions",mM
diff --git a/basin3d/plugins/mapping_usgs.csv b/basin3d/plugins/mapping_usgs.csv
deleted file mode 100644
index 7ef13e5..0000000
--- a/basin3d/plugins/mapping_usgs.csv
+++ /dev/null
@@ -1,44 +0,0 @@
-basin3d_id,datasource_variable,sampling_medium,description
-PH,00400,WATER,"pH, water, unfiltered, field, standard units"
-RDC,00060,WATER,"Discharge, cubic feet per second"
-WLE,63161,WATER,"Stream water level elevation above NAVD 1988, in meters"
-WT,00010,WATER,"Temperature, water, degrees Celsius"
-DO,00300,WATER,"Dissolved oxygen, water, unfiltered, lab, milligrams per liter"
-SC,00095,WATER," Specific conductance, water, unfiltered, microsiemens per centimeter at 25 degrees Celsius"
-TDS,70301,WATER,"Dissolved solids, water, filtered, sum of constituents, milligrams per liter"
-DIN,00631,WATER,"Nitrate plus nitrite, water, filtered, milligrams per liter as nitrogen"
-DTN,00602,WATER,"Total nitrogen [nitrate + nitrite + ammonia + organic-N], water, filtered, milligrams per liter"
-NH4,00608,WATER,"Ammonia (NH3 + NH4+), water, filtered, milligrams per liter as nitrogen"
-POC,00680,WATER,"Organic carbon, water, unfiltered, milligrams per liter"
-DOC,00681,WATER,"Organic carbon, water, filtered, milligrams per liter"
-P,00666,WATER,"Phosphorus, water, filtered, milligrams per liter as phosphorus"
-Na,00930,WATER,"Sodium, water, filtered, milligrams per liter"
-K,00935,WATER,"Potassium, water, filtered, milligrams per liter"
-Mg,00925,WATER,"Magnesium, water, filtered, milligrams per liter"
-Ca,00915,WATER,"Calcium, water, filtered, milligrams per liter"
-Si,01140,WATER,"Silicon, water, filtered, micrograms per liter"
-Cl,00940,WATER,"Chloride, water, filtered, milligrams per liter"
-SO4,00945,WATER,"Sulfate, water, filtered, milligrams per liter"
-NO3,00618,WATER,"Nitrate, water, filtered, milligrams per liter as nitrogen"
-NO2,00613,WATER,"Nitrite, water, filtered, milligrams per liter as nitrogen"
-S2,00746,WATER,"Sulfide, water, filtered, milligrams per liter"
-B,01020,WATER,"Boron, water, filtered, micrograms per liter"
-Al,01106,WATER,"Aluminum, water, filtered, micrograms per liter"
-Sb,01095,WATER,"Antimony, water, filtered, micrograms per liter"
-As,01000,WATER,"Arsenic, water, filtered, micrograms per liter"
-Ba,01005,WATER,"Barium, water, filtered, micrograms per liter"
-Be,01010,WATER,"Beryllium, water, filtered, micrograms per liter"
-Cd,01025,WATER,"Cadmium, water, filtered, micrograms per liter"
-Cr,01030,WATER,"Chromium, water, filtered, micrograms per liter"
-Cu,01040,WATER,"Copper, water, filtered, micrograms per liter"
-FeT,01046,WATER,"Iron, water, filtered, micrograms per liter"
-Pb,01049,WATER,"Lead, water, filtered, micrograms per liter"
-Mo,01060,WATER,"Molybdenum, water, filtered, micrograms per liter"
-Mn,01056,WATER,"Manganese, water, filtered, micrograms per liter"
-Hg,50287,WATER,"Mercury, water, filtered, nanograms per liter"
-Ni,01065,WATER,"Nickel, water, filtered, micrograms per liter"
-Se,01145,WATER,"Selenium, water, filtered, micrograms per liter"
-Ag,01075,WATER,"Silver, water, filtered, micrograms per liter"
-Th,01057,WATER,"Thallium, water, filtered, micrograms per liter"
-U,80020,WATER,"Uranium, water, filtered, extraction fluorometric method, micrograms per liter"
-Zn,01090,WATER,"Zinc, water, filtered, micrograms per liter"
\ No newline at end of file
diff --git a/basin3d/plugins/usgs.py b/basin3d/plugins/usgs.py
index 3a60c56..30c8df0 100644
--- a/basin3d/plugins/usgs.py
+++ b/basin3d/plugins/usgs.py
@@ -57,12 +57,12 @@
 import requests
 
 # Get an instance of a logger
-from basin3d.core.schema.query import FeatureTypeEnum, QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature, \
-    ResultQualityEnum
+from basin3d.core.schema.enum import FeatureTypeEnum, AggregationDurationEnum
+from basin3d.core.schema.query import QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
 from basin3d.core.access import get_url
 from basin3d.core.models import AbsoluteCoordinate, AltitudeCoordinate, Coordinate, GeographicCoordinate, \
-    MeasurementMetadataMixin, MeasurementTimeseriesTVPObservation, MonitoringFeature, RelatedSamplingFeature, \
-    TimeMetadataMixin, TimeValuePair, TimeFrequencyEnum, ResultListTVP
+    MeasurementTimeseriesTVPObservation, MonitoringFeature, RelatedSamplingFeature, \
+    TimeMetadataMixin, TimeValuePair, ResultListTVP
 from basin3d.core.plugin import DataSourcePluginAccess, DataSourcePluginPoint, basin3d_plugin
 from basin3d.core.types import SpatialSamplingShapes
 from basin3d.plugins import usgs_huc_codes
@@ -70,11 +70,6 @@
 logger = monitor.get_logger(__name__)
 
 URL_USGS_HUC = "https://water.usgs.gov/GIS/new_huc_rdb.txt"
-USGS_STATISTIC_MAP: Dict = {
-    MeasurementMetadataMixin.STATISTIC_MEAN: '00003',
-    MeasurementMetadataMixin.STATISTIC_MIN: '00002',
-    MeasurementMetadataMixin.STATISTIC_MAX: '00001'
-}
 
 
 def convert_discharge(data, parameter, units):
@@ -93,13 +88,6 @@ def convert_discharge(data, parameter, units):
     return data, units
 
 
-def map_statistic_code(stat_cd):
-    for k, v in USGS_STATISTIC_MAP.items():
-        if stat_cd == v:
-            return k
-    return 'NOT_SUPPORTED'  # consider making this part of the Mixin Statistic type
-
-
 def generator_usgs_measurement_timeseries_tvp_observation(view,
                                                           query: QueryMeasurementTimeseriesTVP,
                                                           synthesis_messages):
@@ -128,35 +116,28 @@ def generator_usgs_measurement_timeseries_tvp_observation(view,
     if query.end_date:
         search_params.append(("endDT", query.end_date))
 
-    search_params.append(("parameterCd", ",".join([str(o) for o in query.observed_property_variables])))
+    search_params.append(("parameterCd", ",".join([str(o) for o in query.observed_property])))
 
     if query.statistic:
-        statistics: List[str] = []
         # if aggregation duration is NONE (iv) and there is a query that has a stat param, clear the statistics list
         # add warning message to user, but there are no statistic values for IV call. need synthesis param to add message
-        if query.aggregation_duration == TimeFrequencyEnum.NONE:
+        if query.aggregation_duration[0] == AggregationDurationEnum.NONE:
             synthesis_messages.append(
-                f"USGS Instantaneous Values service does not support statistics and cannot be specified when aggregation_duration = {TimeFrequencyEnum.NONE}. Specified statistic arguments will be ignored.")
+                f"USGS Instantaneous Values service does not support statistics and cannot be specified when aggregation_duration = {AggregationDurationEnum.NONE}. Specified statistic arguments will be ignored.")
             logger.info(
-                f"USGS Instantaneous Values service does not support statistics and cannot be specified when aggregation_duration = {TimeFrequencyEnum.NONE}. Specified statistic arguments will be ignored.")
+                f"USGS Instantaneous Values service does not support statistics and cannot be specified when aggregation_duration = {AggregationDurationEnum.NONE}. Specified statistic arguments will be ignored.")
         else:
-            for stat in query.statistic:
-                sythesized_stat = USGS_STATISTIC_MAP.get(stat)
-                if not sythesized_stat:
-                    synthesis_messages.append(f"USGS Daily Values service does not support statistic {stat}")
-                    logger.info(f"USGS Daily Values service does not support statistic {stat}")
-                else:
-                    statistics.append(sythesized_stat)
-            search_params.append(("statCd", ",".join(statistics)))
+            search_params.append(("statCd", ",".join([str(o) for o in query.statistic])))
+
     else:
         search_params.append(("siteStatus", "all"))
 
-    if len(query.monitoring_features[0]) > 2:
+    if len(query.monitoring_feature[0]) > 2:
         # search for stations
-        search_params.append(("sites", ",".join(query.monitoring_features)))
+        search_params.append(("sites", ",".join(query.monitoring_feature)))
     else:
         # search for stations by specifying the huc
-        search_params.append(("huc", ",".join(query.monitoring_features)))
+        search_params.append(("huc", ",".join(query.monitoring_feature)))
 
     # look for station locations only
     search_params.append(("siteType", "ST"))
@@ -167,7 +148,7 @@ def generator_usgs_measurement_timeseries_tvp_observation(view,
     # Request the data points, calls IV or DV depending on aggregation duration passed in param
     # Default to DV service if aggregation duration is DAY or when nothing is specified
     # Calls IV service in aggregation duration is NONE
-    endpoint = query.aggregation_duration == TimeFrequencyEnum.NONE and "iv" or "dv"
+    endpoint = query.aggregation_duration[0] == AggregationDurationEnum.NONE and "iv" or "dv"
     response = get_url(f'{{}}{endpoint}'.format(view.datasource.location), params=search_params)
 
     if response.status_code == 200:
@@ -275,7 +256,7 @@ def _load_point_obj(datasource, json_obj, observed_property_variables, synthesis
             )],
             # geographical_group_id=huc_accounting_unit_id,
             # geographical_group_type=FeatureTypeEnum.REGION,
-            observed_property_variables=mf_opv,
+            observed_properties=mf_opv,
             coordinates=Coordinate(
                 absolute=AbsoluteCoordinate(
                     horizontal_position=GeographicCoordinate(
@@ -311,7 +292,8 @@ def _parse_sites_response(usgs_site_response):
         param, site, stat = v['parm_cd'], v['site_no'], v['stat_cd']
         observed_properties_variables.setdefault(site, [])
 
-        if param not in observed_properties_variables[site] and map_statistic_code(stat) != 'NOT_SUPPORTED':
+        # FIX: stat -- need to change to B3D vocab
+        if param not in observed_properties_variables[site] and stat != 'NOT_SUPPORTED':
             observed_properties_variables[site].append(param)
         if site not in unique_usgs_sites:
             unique_usgs_sites[site] = v
@@ -358,8 +340,8 @@ def list(self, query: QueryMonitoringFeature):
             usgs_regions = []
             usgs_subbasins = []
             parent_features = []
-            if query.parent_features:
-                for value in query.parent_features:
+            if query.parent_feature:
+                for value in query.parent_feature:
                     parent_features.append(value)
                     if len(value) < 4:
                         usgs_regions.append(value)
@@ -408,16 +390,16 @@ def list(self, query: QueryMonitoringFeature):
 
                     # Determine whether to yield the monitoring feature object
                     if monitoring_feature:
-                        if query.monitoring_features and json_obj['huc'] in query.monitoring_features:
+                        if query.monitoring_feature and json_obj['huc'] in query.monitoring_feature:
                             yield monitoring_feature
-                        elif not query.monitoring_features:
+                        elif not query.monitoring_feature:
                             yield monitoring_feature
 
             else:
                 base_url = '{}site/?{}={}&seriesCatalogOutput=true&outputDataTypeCd=iv,dv&siteStatus=all&format=rdb'
                 # Points by id: USGS calls these sites
-                if query.monitoring_features is not None:
-                    usgs_sites = ",".join(query.monitoring_features)
+                if query.monitoring_feature is not None:
+                    usgs_sites = ",".join(query.monitoring_feature)
                     url = base_url.format(self.datasource.location, 'sites', usgs_sites)
                 else:
                     # Point by subbasin: USGS calls subbasin as huc (instead of sites) to retrieve all subbasins
@@ -474,22 +456,22 @@ def get(self, query: QueryById):
         """
 
         if len(query.id) == 2:
-            mf_query = QueryMonitoringFeature(monitoring_features=[query.id], feature_type=FeatureTypeEnum.REGION)
+            mf_query = QueryMonitoringFeature(monitoring_feature=[query.id], feature_type=FeatureTypeEnum.REGION)
         elif len(query.id) == 4:
-            mf_query = QueryMonitoringFeature(monitoring_features=[query.id], feature_type=FeatureTypeEnum.SUBREGION)
+            mf_query = QueryMonitoringFeature(monitoring_feature=[query.id], feature_type=FeatureTypeEnum.SUBREGION)
         elif len(query.id) == 6:
-            mf_query = QueryMonitoringFeature(monitoring_features=[query.id], feature_type=FeatureTypeEnum.BASIN)
+            mf_query = QueryMonitoringFeature(monitoring_feature=[query.id], feature_type=FeatureTypeEnum.BASIN)
         elif len(query.id) == 8:
-            mf_query = QueryMonitoringFeature(monitoring_features=[query.id], feature_type=FeatureTypeEnum.SUBBASIN)
+            mf_query = QueryMonitoringFeature(monitoring_feature=[query.id], feature_type=FeatureTypeEnum.SUBBASIN)
         else:
-            mf_query = QueryMonitoringFeature(monitoring_features=[query.id], feature_type=FeatureTypeEnum.POINT)
+            mf_query = QueryMonitoringFeature(monitoring_feature=[query.id], feature_type=FeatureTypeEnum.POINT)
 
         for o in self.list(query=mf_query):
             return o
 
         # An 8 character code can also be a point, Try that
         if len(query.id) == 8:
-            for o in self.list(query=QueryMonitoringFeature(monitoring_features=[query.id],
+            for o in self.list(query=QueryMonitoringFeature(monitoring_feature=[query.id],
                                                             feature_type=FeatureTypeEnum.POINT)):
                 return o
         return None
@@ -532,7 +514,7 @@ def _load_huc_obj(self, json_obj, feature_type, description=None,
                 shape=SpatialSamplingShapes.SHAPE_SURFACE,
                 coordinates=None,
                 related_sampling_feature_complex=related_sampling_feature_complex,
-                observed_property_variables=None)
+                observed_properties=None)
         return result
 
 
@@ -552,47 +534,27 @@ class USGSMeasurementTimeseriesTVPObservationAccess(DataSourcePluginAccess):
     --------------------- --- ------------------------------------------------------------------------------------
     Instantaneous Values  >>  :class:`basin3d.synthesis.models.measurement.MeasurementTimeseriesTVPObservation`
     ===================== === ====================================================================================
+
+    Daily Value and Instantaneous Value Qualification Code (dv_rmk_cd)
+
+    =============  =========  ================================================================================
+    BASIN-3D Code  USGS Code  Description
+    =============  =========  ================================================================================
+    ESTIMATED      e          Value has been edited or estimated by USGS personnel and is write protected
+    NOT_SUPPORTED  &          Value was computed from affected unit values
+    ESTIMATED      E          Value was computed from estimated unit values.
+    VALIDATED      A          Approved for publication -- Processing and review completed.
+    UNVALIDATED    P          Provisional data subject to revision.
+    NOT_SUPPORTED  <          The value is known to be less than reported value and is write protected.
+    NOT_SUPPORTED  >          The value is known to be greater than reported value and is write protected.
+    NOT_SUPPORTED  1          Value is write protected without any remark code to be printed
+    NOT_SUPPORTED  2          Remark is write protected without any remark code to be printed
+    NOT_SUPPORTED  _          No remark (blank)
+    =============  =========  ================================================================================
     """
 
     synthesis_model_class = MeasurementTimeseriesTVPObservation
 
-    def map_result_quality(self, qualifiers):
-        """
-        Daily Value and Instantaneous Value Qualification Code (dv_rmk_cd)
-
-        =============  =========  ================================================================================
-        BASIN-3D Code  USGS Code  Description
-        =============  =========  ================================================================================
-        ESTIMATED      e          Value has been edited or estimated by USGS personnel and is write protected
-        NOT_SUPPORTED  &          Value was computed from affected unit values
-        ESTIMATED      E          Value was computed from estimated unit values.
-        VALIDATED      A          Approved for publication -- Processing and review completed.
-        UNVALIDATED    P          Provisional data subject to revision.
-        NOT_SUPPORTED  <          The value is known to be less than reported value and is write protected.
-        NOT_SUPPORTED  >          The value is known to be greater than reported value and is write protected.
-        NOT_SUPPORTED  1          Value is write protected without any remark code to be printed
-        NOT_SUPPORTED  2          Remark is write protected without any remark code to be printed
-        NOT_SUPPORTED  _          No remark (blank)
-        =============  =========  ================================================================================
-
-        :param qualifiers:
-        :return:
-        """
-        if "A" in qualifiers:
-            return ResultQualityEnum.VALIDATED
-        elif "P" in qualifiers:
-            return ResultQualityEnum.UNVALIDATED
-        elif "E" in qualifiers or "e" in qualifiers:
-            return ResultQualityEnum.ESTIMATED
-        else:
-            return ResultQualityEnum.NOT_SUPPORTED
-
-    def get_result_qualifiers(self, qualifiers):
-        timeseries_qualifiers = set()
-        for qualifier in qualifiers:
-            timeseries_qualifiers.add(self.map_result_quality(qualifier["qualifierCode"]))
-        return timeseries_qualifiers
-
     def list(self, query: QueryMeasurementTimeseriesTVP):
         """
         Get the Data Points for USGS Daily Values or Instantaneous Values
@@ -609,10 +571,10 @@ def list(self, query: QueryMeasurementTimeseriesTVP):
         """
         synthesis_messages = []
         feature_obj_dict = {}
-        if not query.monitoring_features:
+        if not query.monitoring_feature:
             return None
 
-        search_params = ",".join(query.monitoring_features)
+        search_params = ",".join(query.monitoring_feature)
 
         base_url = '{}site/?{}={}&seriesCatalogOutput=true&outputDataTypeCd=iv,dv&siteStatus=all&format=rdb'
         url = base_url.format(self.datasource.location, 'sites', search_params)
@@ -657,11 +619,6 @@ def list(self, query: QueryMeasurementTimeseriesTVP):
                 # ToDo: log message
                 monitoring_feature = None
 
-            # deal with statistic
-            basin3d_statistic = "NOT_SET"
-            if statistic:
-                basin3d_statistic = map_statistic_code(statistic)
-
             result_TVPs = []
             result_TVP_quality = []
             result_quality = set()
@@ -678,7 +635,12 @@ def list(self, query: QueryMeasurementTimeseriesTVP):
 
                 for value in values["value"]:
 
-                    result_point_quality = self.map_result_quality(value['qualifiers'])
+                    if len(value['qualifiers']) > 1:
+                        #ToDo: add some error messaging.
+                        pass
+
+                    # result_point_quality = self.map_result_quality(value['qualifiers'])
+                    result_point_quality = value['qualifiers'][0]
 
                     if not query.result_quality or result_point_quality in query.result_quality:
 
@@ -723,12 +685,12 @@ def list(self, query: QueryMeasurementTimeseriesTVP):
                 feature_of_interest_type=FeatureTypeEnum.POINT,
                 feature_of_interest=monitoring_feature,
                 utc_offset=int(timezone_offset.split(":")[0]),
-                result=ResultListTVP(value=result_TVPs, quality=result_TVP_quality),
-                observed_property_variable=parameter,
+                result=ResultListTVP(plugin_access=self, value=result_TVPs, result_quality=result_TVP_quality),
+                observed_property=parameter,
                 result_quality=list(result_quality),
-                aggregation_duration=query.aggregation_duration,
+                aggregation_duration=query.aggregation_duration[0],
                 time_reference_position=TimeMetadataMixin.TIME_REFERENCE_MIDDLE,
-                statistic=basin3d_statistic
+                statistic=statistic
             )
 
             yield measurement_timeseries_tvp_observation
diff --git a/basin3d/plugins/usgs_mapping.csv b/basin3d/plugins/usgs_mapping.csv
new file mode 100644
index 0000000..234129d
--- /dev/null
+++ b/basin3d/plugins/usgs_mapping.csv
@@ -0,0 +1,54 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,PH:WATER,00400,"pH, water, unfiltered, field, standard units"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,RDC:WATER,00060,"Discharge, cubic feet per second"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,WLE:WATER,63161,"Stream water level elevation above NAVD 1988, in meters"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,WT:WATER,00010,"Temperature, water, degrees Celsius"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,DO:WATER,00300,"Dissolved oxygen, water, unfiltered, lab, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,SC:WATER,00095,"Specific conductance, water, unfiltered, microsiemens per centimeter at 25 degrees Celsius"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,TDS:WATER,70301,"Dissolved solids, water, filtered, sum of constituents, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,DIN:WATER,00631,"Nitrate plus nitrite, water, filtered, milligrams per liter as nitrogen"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,DTN:WATER,00602,"Total nitrogen [nitrate + nitrite + ammonia + organic-N], water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,NH4:WATER,00608,"Ammonia (NH3 + NH4+), water, filtered, milligrams per liter as nitrogen"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,POC:WATER,00680,"Organic carbon, water, unfiltered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,DOC:WATER,00681,"Organic carbon, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,P:WATER,00666,"Phosphorus, water, filtered, milligrams per liter as phosphorus"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Na:WATER,00930,"Sodium, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,K:WATER,00935,"Potassium, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Mg:WATER,00925,"Magnesium, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ca:WATER,00915,"Calcium, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Si:WATER,01140,"Silicon, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Cl:WATER,00940,"Chloride, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,SO4:WATER,00945,"Sulfate, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,NO3:WATER,00618,"Nitrate, water, filtered, milligrams per liter as nitrogen"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,NO2:WATER,00613,"Nitrite, water, filtered, milligrams per liter as nitrogen"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,S2:WATER,00746,"Sulfide, water, filtered, milligrams per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,B:WATER,01020,"Boron, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Al:WATER,01106,"Aluminum, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Sb:WATER,01095,"Antimony, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,As:WATER,01000,"Arsenic, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ba:WATER,01005,"Barium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Be:WATER,01010,"Beryllium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Cd:WATER,01025,"Cadmium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Cr:WATER,01030,"Chromium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Cu:WATER,01040,"Copper, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,FeT:WATER,01046,"Iron, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Pb:WATER,01049,"Lead, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Mo:WATER,01060,"Molybdenum, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Mn:WATER,01056,"Manganese, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Hg:WATER,50287,"Mercury, water, filtered, nanograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ni:WATER,01065,"Nickel, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Se:WATER,01145,"Selenium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ag:WATER,01075,"Silver, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Th:WATER,01057,"Thallium, water, filtered, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,U:WATER,80020,"Uranium, water, filtered, extraction fluorometric method, micrograms per liter"
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Zn:WATER,01090,"Zinc, water, filtered, micrograms per liter"
+STATISTIC,MEAN,00003,
+STATISTIC,MIN,00002,
+STATISTIC,MAX,00001,
+STATISTIC,TOTAL,00006,
+RESULT_QUALITY,ESTIMATED,e,"Value has been edited or estimated by USGS personnel and is write protected"
+RESULT_QUALITY,ESTIMATED,E,"Value was computed from estimated unit values."
+RESULT_QUALITY,VALIDATED,A,"Approved for publication -- Processing and review completed."
+RESULT_QUALITY,UNVALIDATED,P,"Provisional data subject to revision."
+AGGREGATION_DURATION,DAY,DAY,
+AGGREGATION_DURATION,NONE,NONE,
\ No newline at end of file
diff --git a/basin3d/synthesis.py b/basin3d/synthesis.py
index 900c100..7ad8194 100644
--- a/basin3d/synthesis.py
+++ b/basin3d/synthesis.py
@@ -24,10 +24,10 @@
 synthesis.DataSynthesizer Functions
 -----------------------------------
 
+* :func:`DataSynthesizer.attribute_mappings`- Search for attribute_mappings which describe how the datasource vocabularies are mapped to BASIN-3D vocabularies, including observed properties, statistics, result_quality, etc.
 * :func:`DataSynthesizer.measurement_timeseries_tvp_observations`- Search for Measurement Timeseries TVP Observation (Instantaneous and Daily Values supported) from USGS Monitoring features and observed property variables
 * :func:`DataSynthesizer.monitoring_features`- Search for all USGS monitoring features, USGS points by parent monitoring features, or look for a single monitoring feature by id.
 * :func:`DataSynthesizer.observed_properties`- Search for observed properties
-* :func:`DataSynthesizer.observed_property_variables`- Common names for observed property variables. An observed property variable defines what is being measured. Data source observed property variables are mapped to these synthesized observed property variables.
 
 ----------------------------------
 """
@@ -118,44 +118,72 @@ def datasources(self) -> List[DataSource]:
         """
         return list(self._datasources.values())
 
-    def observed_properties(self, datasource_id=None, variable_names=None):
+    def observed_properties(self):
         """
-        Search for observed properties
 
-        :param datasource_id: Unique feature identifier of datasource
-        :param variable_names: Observed property variables
-        :return: a list of observed properties
+        >>> from basin3d.plugins import usgs
+        >>> from basin3d import synthesis
+        >>> synthesizer = synthesis.register()
+        >>> response = synthesizer.observed_properties()
+        >>> for opv in response:
+        ...     print(f'{opv.basin3d_vocab} -- {opv.full_name} -- {opv.units}')
+        ACT -- Acetate (CH3COO) -- mM
+        Br -- Bromide (Br) -- mM
+        Cl -- Chloride (Cl) -- mM
+        DIN -- Dissolved Inorganic Nitrogen (Nitrate + Nitrite) -- mg/L
+        DTN -- Dissolved Total Nitrogen (DTN) -- mM
+        F -- Fluoride (F) -- mM
+        ...
+
+        BASIN-3D observed properties. An observed property defines what is being measured.
+        Data source observed property vocabularies are mapped and thus synthesized to the BASIN-3D observed property vocabulary.
+
+        :return: an iterator of :class:`ObservedProperty` objects
 
         """
-        return self._catalog.find_observed_properties(datasource_id, variable_names)
+        return self._catalog.find_observed_properties()
 
-    def observed_property_variables(self, datasource_id=None):
+    def attribute_mappings(self, datasource_id=None, attr_type=None, attr_vocab=None, from_basin3d=False):
         """
 
         >>> from basin3d.plugins import usgs
         >>> from basin3d import synthesis
         >>> synthesizer = synthesis.register()
-        >>> response = synthesizer.observed_property_variables(datasource_id='USGS')
-        >>> for opv in response:
-        ...     print(opv)
-        pH
-        River Discharge
-        Water Level Elevation
-        Water Temperature
-        Dissolved Oxygen (DO)
-        Specific Conductance (SC)
-        Total Dissolved Solids (TDS)
+        >>> response = synthesizer.attribute_mappings()  # list all attribute mappings registered
+        >>> for attr_mapping in response:
+        ...     print(f'{attr_mapping.attr_type} | {attr_mapping.basin3d_vocab} -- {attr_mapping.datasource_vocab}')
+        OBSERVED_PROPERTY:SAMPLING_MEDIUM | PH:WATER -- 00400
+        OBSERVED_PROPERTY:SAMPLING_MEDIUM | RDC:WATER -- 00060
+        OBSERVED_PROPERTY:SAMPLING_MEDIUM | WLE:WATER -- 63161
+        OBSERVED_PROPERTY:SAMPLING_MEDIUM | WT:WATER -- 00010
+        OBSERVED_PROPERTY:SAMPLING_MEDIUM | DO:WATER -- 00300
         ...
 
-
-        Common names for observed property variables. An observed property variable defines what is being measured.
-        Data source observed property variables are mapped to these synthesized observed property variables.
-
-        :param datasource_id: filter observer properity variables by data source
-        :return: a list of observed property variables
-
+        >>> response = synthesizer.attribute_mappings(datasource_id='USGS', attr_type='STATISTIC')
+        >>> for attr_mapping in response:
+        ...     print(f'{attr_mapping.attr_type} | {attr_mapping.basin3d_vocab} -- {attr_mapping.datasource_vocab}')
+        STATISTIC | MEAN -- 00003
+        STATISTIC | MIN -- 00002
+        STATISTIC | MAX -- 00001
+        STATISTIC | TOTAL -- 00006
+
+        >>> response = synthesizer.attribute_mappings(datasource_id='USGS', attr_type='RESULT_QUALITY', attr_vocab=['VALIDATED', 'ESTIMATED'], from_basin3d=True)
+        >>> for attr_mapping in response:
+        ...     print(f'{attr_mapping.attr_type} | {attr_mapping.basin3d_vocab} -- {attr_mapping.datasource_vocab}, {attr_mapping.datasource_desc}')
+        RESULT_QUALITY | ESTIMATED -- e, Value has been edited or estimated by USGS personnel and is write protected
+        RESULT_QUALITY | ESTIMATED -- E, Value was computed from estimated unit values.
+        RESULT_QUALITY | VALIDATED -- A, Approved for publication -- Processing and review completed.
+
+        Return all the :class:`AttributMapping` registered or those that match the specified fields.
+
+        :param datasource_id: str, The datasource identifier
+        :param attr_type: str, The attribute type (e.g., OBSERVED_PROPERTY, STATISTIC, etc)
+        :param attr_vocab: str, The attribute vocabulary, either the BASIN-3D vocabulary or the datasource vocabulary
+        :param from_basin3d: bool, True = the specified attr_vocab is a BASIN-3D vocabulary, False: the specified attr_vocab is from the datasource
+
+        :return: iterator of :class:`AttributeMapping` objects
         """
-        return self._catalog.find_observed_property_variables(datasource_id=datasource_id)
+        return self._catalog.find_attribute_mappings(datasource_id=datasource_id, attr_type=attr_type, attr_vocab=attr_vocab, from_basin3d=from_basin3d)
 
     def monitoring_features(self, query: Union[QueryById, QueryMonitoringFeature] = None, **kwargs) -> Union[
             DataSourceModelIterator, SynthesisResponse]:
@@ -188,7 +216,7 @@ def monitoring_features(self, query: Union[QueryById, QueryMonitoringFeature] =
 
         **Search for USGS points by parent (subbasin) monitoring features:**
 
-        >>> for mf in synthesizer.monitoring_features(feature_type='point',parent_features=['USGS-17040101']): # doctest: +ELLIPSIS
+        >>> for mf in synthesizer.monitoring_features(feature_type='point',parent_feature=['USGS-17040101']): # doctest: +ELLIPSIS
         ...    print(f"{mf.id} {mf.coordinates and [(p.x, p.y) for p in mf.coordinates.absolute.horizontal_position]}")
         USGS-13010000 [(-110.6647222, 44.1336111)]
         USGS-13010065 [(-110.6675, 44.09888889)]
@@ -215,15 +243,9 @@ def monitoring_features(self, query: Union[QueryById, QueryMonitoringFeature] =
 
 
         :param query: (optional) The Monitoring Feature Query object
-        :param id: (optional) Unique feature identifier. This returns a single monitoring feature
-        :param feature_type: (optional) feature type
-        :param datasource: (optional) Datasource id prefix (e.g USGS)
-        :param monitoring_features: (optional) List of monitoring feature identifiers (eg. USGS-0010)
-        :param parent_features: (optional) List of parent monitoring features to search by
-
 
         :return: a single :class:`~basin3d.core.schema.query.SynthesisResponse` for a query by id
-            or a :class:`~basin3d.core.synthesis.DataSourceModelIterator` for multple.
+            or a :class:`~basin3d.core.synthesis.DataSourceModelIterator` for multple :class:`MonitoringFeature` objects.
         """
         if not query:
             if "id" in kwargs and isinstance(kwargs["id"], str):
@@ -254,9 +276,9 @@ def measurement_timeseries_tvp_observations(self, query: QueryMeasurementTimeser
             >>> from basin3d.plugins import usgs
             >>> from basin3d import synthesis
             >>> synthesizer = synthesis.register()
-            >>> timeseries = synthesizer.measurement_timeseries_tvp_observations(monitoring_features=['USGS-09110990'],observed_property_variables=['RDC','WT'],start_date='2019-10-01',end_date='2019-10-30',aggregation_duration='DAY')
+            >>> timeseries = synthesizer.measurement_timeseries_tvp_observations(monitoring_feature=['USGS-09110990'],observed_property=['RDC','WT'],start_date='2019-10-01',end_date='2019-10-30',aggregation_duration='DAY')
             >>> for timeseries in timeseries:
-            ...    print(f"{timeseries.feature_of_interest.id} - {timeseries.observed_property_variable}")
+            ...    print(f"{timeseries.feature_of_interest.id} - {timeseries.observed_property.get_basin3d_vocab()}")
             USGS-09110990 - RDC
 
             **Search with aggregation duration NONE (Instantaneous Values Service):**
@@ -264,15 +286,15 @@ def measurement_timeseries_tvp_observations(self, query: QueryMeasurementTimeser
             >>> from basin3d.plugins import usgs
             >>> from basin3d import synthesis
             >>> synthesizer = synthesis.register()
-            >>> timeseries = synthesizer.measurement_timeseries_tvp_observations(monitoring_features=["USGS-09110990", "USGS-09111250"],observed_property_variables=['RDC','WT'],start_date='2020-04-01',end_date='2020-04-30',aggregation_duration='NONE')
+            >>> timeseries = synthesizer.measurement_timeseries_tvp_observations(monitoring_feature=["USGS-09110990", "USGS-09111250"],observed_property=['RDC','WT'],start_date='2020-04-01',end_date='2020-04-30',aggregation_duration='NONE')
             >>> for timeseries in timeseries:
-            ...    print(f"{timeseries.feature_of_interest.id} - {timeseries.observed_property_variable}")
+            ...    print(f"{timeseries.feature_of_interest.id} - {timeseries.observed_property.get_basin3d_vocab()}")
             USGS-09110990 - RDC
             USGS-09111250 - RDC
 
 
         :param query: The query information for this function
-        :return: generator that yields MeasurementTimeseriesTVPObservations
+        :return: a :class:`~basin3d.core.synthesis.DataSourceModelIterator` that yields :class:`MeasurementTimeseriesTVPObservations` objects
 
         """
         if not query:
@@ -284,5 +306,3 @@ def measurement_timeseries_tvp_observations(self, query: QueryMeasurementTimeser
         return cast(DataSourceModelIterator,
                     self._measurement_timeseries_tvp_observation_access.list(
                         query))
-
-
diff --git a/docs/api_reference.rst b/docs/api_reference.rst
index c755a7d..6872f2d 100644
--- a/docs/api_reference.rst
+++ b/docs/api_reference.rst
@@ -48,6 +48,10 @@ API Reference
     :members:
     :undoc-members:
 
+.. automodule:: basin3d.core.translate
+    :members:
+    :undoc-members:
+
 .. automodule:: basin3d.core.types
     :members:
     :undoc-members:
diff --git a/docs/quick_guide.rst b/docs/quick_guide.rst
index 80b5ad3..e998f8e 100644
--- a/docs/quick_guide.rst
+++ b/docs/quick_guide.rst
@@ -23,14 +23,20 @@ Register a Synthesizer
 
 
 Get a List of Locations for a Data Source
-------------------------------------------------
+-----------------------------------------
 .. autofunction:: basin3d.synthesis.DataSynthesizer.monitoring_features
     :noindex:
 
 
-Get a List of Variables Supported by a BASIN-3D Data Source
-------------------------------------------------------------
-.. autofunction:: basin3d.synthesis.DataSynthesizer.observed_property_variables
+Get a List of Attribute Mappings for a Data Source
+--------------------------------------------------
+.. autofunction:: basin3d.synthesis.DataSynthesizer.attribute_mappings
+    :noindex:
+
+
+Get a List of Variables Supported by a BASIN-3D
+-----------------------------------------------
+.. autofunction:: basin3d.synthesis.DataSynthesizer.observed_properties
     :noindex:
 
 
diff --git a/mypy.ini b/mypy.ini
index 96fb86b..8d0daa3 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -7,4 +7,5 @@ warn_return_any = True
 #flag may be useful to debug misspelled section names.
 warn_unused_configs = True
 ignore_missing_imports = True
-
+no_implicit_optional=False
+check_untyped_defs=(True|False)
diff --git a/setup.py b/setup.py
index 3953294..c0e1d9b 100644
--- a/setup.py
+++ b/setup.py
@@ -51,8 +51,8 @@
       author_email='vchendrix@lbl.gov, dschristianson@lbl.gov, cvaradharajan@lbl.gov, catwong@lbl.gov',
       url='https://github.com/BASIN-3D/basin3d',
       packages=packages,
-      data_files=['basin3d/data/basin3d_variables_hydrology.csv',
-                  'basin3d/plugins/mapping_usgs.csv'],
+      data_files=['basin3d/data/basin3d_observed_property_vocabulary.csv',
+                  'basin3d/plugins/usgs_mapping.csv'],
       include_package_data=True,
       install_requires=required,
       extras_require={


Test patch:
diff --git a/tests/integration/test_iv_values_usgs.py b/tests/integration/test_iv_values_usgs.py
index 728714a..f872fa5 100644
--- a/tests/integration/test_iv_values_usgs.py
+++ b/tests/integration/test_iv_values_usgs.py
@@ -14,8 +14,8 @@ def test_measurement_timeseries_tvp_observations_usgs_iv():
     synthesizer = register(['basin3d.plugins.usgs.USGSDataSourcePlugin'])
 
     query0 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": [],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": [],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "aggregation_duration": TimeFrequencyEnum.NONE,
@@ -26,8 +26,8 @@ def test_measurement_timeseries_tvp_observations_usgs_iv():
         synthesizer.measurement_timeseries_tvp_observations(**query0)
 
     query1 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": ["RDC"],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "aggregation_duration": TimeFrequencyEnum.NONE,
@@ -45,8 +45,8 @@ def test_measurement_timeseries_tvp_observations_usgs_iv():
         pytest.fail("Returned object must be iterator")
 
     query2 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": ["RDC"],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "aggregation_duration": TimeFrequencyEnum.NONE,
@@ -63,7 +63,7 @@ def test_measurement_timeseries_tvp_observations_usgs_iv():
             count += 1
             # checking to make sure the query statistic is not the statistic in timeseries data,
             # and checking to make sure that when aggregation duration is NONE the statistic in timeseries data has the value 'NOT_SUPPORTED'
-            assert timeseries.statistic == 'NOT_SUPPORTED'
+            assert timeseries.statistic.get_basin3d_vocab() == 'NOT_SUPPORTED'
         expected_msgs = "USGS Instantaneous Values service does not support statistics and cannot be specified when aggregation_duration = NONE. Specified statistic arguments will be ignored."
         msgs = measurement_timeseries_tvp_observations.synthesis_response.messages[0].msg
         assert msgs == expected_msgs
@@ -73,7 +73,7 @@ def test_measurement_timeseries_tvp_observations_usgs_iv():
         pytest.fail("Returned object must be iterator")
 
     query3 = {
-        "observed_property_variables": ["RDC"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "aggregation_duration": TimeFrequencyEnum.NONE,
diff --git a/tests/integration/test_plugins_usgs.py b/tests/integration/test_plugins_usgs.py
index 8376624..2e085b6 100644
--- a/tests/integration/test_plugins_usgs.py
+++ b/tests/integration/test_plugins_usgs.py
@@ -16,24 +16,24 @@ def test_measurement_timeseries_tvp_observations_usgs():
     synthesizer = register(['basin3d.plugins.usgs.USGSDataSourcePlugin'])
 
     query0 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": [],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": [],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
-        "aggregation_duration": TimeFrequencyEnum.DAY,
-        "results_quality": ResultQualityEnum.VALIDATED
+        "aggregation_duration": "DAY",
+        "results_quality": "VALIDATED"
     }
 
     with pytest.raises(ValidationError):
         synthesizer.measurement_timeseries_tvp_observations(**query0)
 
     query1 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": ["RDC"],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
-        "aggregation_duration": TimeFrequencyEnum.DAY,
-        "results_quality": ResultQualityEnum.VALIDATED
+        "aggregation_duration": "DAY",
+        "results_quality": "VALIDATED"
     }
     measurement_timeseries_tvp_observations = synthesizer.measurement_timeseries_tvp_observations(**query1)
 
@@ -48,23 +48,23 @@ def test_measurement_timeseries_tvp_observations_usgs():
         pytest.fail("Returned object must be iterator")
 
     query2 = {
-        "observed_property_variables": ["RDC"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
-        "aggregation_duration": TimeFrequencyEnum.DAY,
-        "results_quality": ResultQualityEnum.VALIDATED
+        "aggregation_duration": "DAY",
+        "results_quality": "VALIDATED"
     }
 
     with pytest.raises(ValidationError):
         synthesizer.measurement_timeseries_tvp_observations(**query2)
 
     query3 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": ["RDC"],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "statistic": ["MEAN"],
-        "results_quality": ResultQualityEnum.VALIDATED
+        "results_quality": "VALIDATED"
     }
     measurement_timeseries_tvp_observations = synthesizer.measurement_timeseries_tvp_observations(**query3)
     if isinstance(measurement_timeseries_tvp_observations, Iterator):
@@ -78,8 +78,8 @@ def test_measurement_timeseries_tvp_observations_usgs():
         pytest.fail("Returned object must be iterator")
 
     query4 = {
-        "monitoring_features": ["USGS-09110990", "USGS-09111250"],
-        "observed_property_variables": ["RDC"],
+        "monitoring_feature": ["USGS-09110990", "USGS-09111250"],
+        "observed_property": ["RDC"],
         "start_date": "2020-04-01",
         "end_date": "2020-04-30",
         "aggregation_duration": None,
@@ -120,7 +120,7 @@ def test_usgs_monitoring_feature(query, feature_type):
 
 @pytest.mark.integration
 @pytest.mark.parametrize("query, expected_count", [({"datasource": "USGS"}, 2889),
-                                                   ({"monitoring_features": ['USGS-02']}, 1),
+                                                   ({"monitoring_feature": ['USGS-02']}, 1),
                                                    ({"feature_type": "region"}, 21),
                                                    ({"feature_type": "subregion"}, 222),
                                                    ({"feature_type": "basin"}, 379),
@@ -132,11 +132,11 @@ def test_usgs_monitoring_feature(query, feature_type):
                                                    ({"feature_type": "vertical path"}, 0),
                                                    ({"feature_type": "horizontal path"}, 0),
                                                    ({"feature_type": "point"}, 0),
-                                                   ({"monitoring_features": ["USGS-09129600"], "feature_type": "point"}, 1),
-                                                   ({"parent_features": ['USGS-02']}, 118),
-                                                   ({"parent_features": ['USGS-02020004'], "feature_type": "point"}, 52),
-                                                   ({"parent_features": ['USGS-0202'], "feature_type": "subbasin"}, 8),
-                                                   ({"parent_features": ['USGS-020200'], "feature_type": "point"}, 0)],
+                                                   ({"monitoring_feature": ["USGS-09129600"], "feature_type": "point"}, 1),
+                                                   ({"parent_feature": ['USGS-02']}, 118),
+                                                   ({"parent_feature": ['USGS-02020004'], "feature_type": "point"}, 54),
+                                                   ({"parent_feature": ['USGS-0202'], "feature_type": "subbasin"}, 8),
+                                                   ({"parent_feature": ['USGS-020200'], "feature_type": "point"}, 0)],
                          ids=["all", "region_by_id", "region", "subregion",
                               "basin", "subbasin",
                               "watershed", "subwatershed",
diff --git a/tests/resources/basin3d_variables_duplicate.csv b/tests/resources/basin3d_variables_duplicate.csv
index 67764ed..f2d233a 100644
--- a/tests/resources/basin3d_variables_duplicate.csv
+++ b/tests/resources/basin3d_variables_duplicate.csv
@@ -1,4 +1,4 @@
-basin3d_id,description,categories,units
+basin3d_vocab,description,categories,units
 ACT,Acetate (CH3COO),"Biogeochemistry,Anions",mM
 Br,Bromide (Br),"Biogeochemistry,Anions",mM
 Br,Bromide ions,"Biogeochemistry,Anions",mM
diff --git a/tests/test_catalog.py b/tests/test_catalog.py
deleted file mode 100644
index c71808c..0000000
--- a/tests/test_catalog.py
+++ /dev/null
@@ -1,250 +0,0 @@
-import logging
-from unittest.mock import Mock
-
-import pytest
-from tinydb import TinyDB
-
-from basin3d.core.models import DataSource, ObservedPropertyVariable
-from tests.testplugins import alpha
-from basin3d.plugins import usgs
-
-log = logging.Logger(__name__)
-
-
-# ---------------------------------------
-# Helper and mock functions
-
-def mock_plugin_get_meta():
-    class PluginMeta:
-        id = 'Alpha'
-        name = 'Alpha'
-        location = 'https://asource.foo/'
-        id_prefix = 'A'
-
-    return PluginMeta
-
-
-def mock_init_catalog(dummyself):
-    test_db_filepath = './tests/resources/test.json'
-    test_catalog = TinyDB(test_db_filepath)
-    test_catalog.truncate()
-    return test_catalog
-
-
-# ---------------------------------------
-# Fixtures
-
-
-@pytest.fixture
-def test_catalog():
-    return mock_init_catalog('dummyself')
-
-
-@pytest.fixture
-def catalog(monkeypatch):
-    from basin3d.core.catalog import CatalogTinyDb
-
-    catalog = CatalogTinyDb()
-    catalog.plugin_dir = 'tests.testplugins'
-
-    catalog.initialize([])
-
-    return catalog
-
-
-# ---------------------------------------
-# Tests
-# ********* ORDER of tests matters!! see comments
-# ********* The catalog is only first created in the test_create_catalog function below.
-
-
-@pytest.mark.parametrize("plugins", [(["foo"]), [Mock()]])
-def test_catalog_error(plugins):
-    """Test Catalog Error"""
-    from basin3d.core.catalog import CatalogTinyDb, CatalogException
-    catalog = CatalogTinyDb()
-    pytest.raises(CatalogException, catalog.initialize, plugins)
-
-
-# Test with different variable files.
-# Last test uses the default basin3d hydrology variables which is needed for rest of tests.
-def test_gen_basin3d_variable_store(catalog, caplog):
-    caplog.set_level(logging.INFO, logger=__name__)
-    catalog.variable_dir = 'tests.resources'
-
-    caplog.clear()
-    catalog.variable_filename = 'basin3d_variables_duplicate.csv'
-    catalog._gen_variable_store()
-    assert catalog._get_observed_property_variable('Br').to_dict() == ObservedPropertyVariable(
-        basin3d_id='Br', full_name='Bromide (Br)',
-        categories=['Biogeochemistry', 'Anions'], units='mM').to_dict()
-    log_msgs = [rec.message for rec in caplog.records]
-    assert 'Duplicate BASIN-3D variable Br found. Skipping duplicate.' in log_msgs
-
-    catalog.variable_filename = 'basin3d_variables_wrong_header.csv'
-    from basin3d.core.catalog import CatalogException
-    with pytest.raises(CatalogException):
-        catalog._gen_variable_store()
-
-    # KEEP this test last: it resets the variable file.
-    catalog.variable_filename = 'basin3d_variables_hydrology.csv'
-    catalog.variable_dir = 'basin3d.data'
-    catalog._gen_variable_store()
-    assert catalog._get_observed_property_variable('ACT') == ObservedPropertyVariable(
-        basin3d_id='ACT', full_name='Acetate (CH3COO)',
-        categories=['Biogeochemistry', 'Anions'], units='mM')
-    assert catalog._get_observed_property_variable('heya') is None
-
-
-def test_process_plugin_variable_mapping(catalog):
-    """Test a bad mapping file with the wrong header"""
-    from tests.testplugins import alpha
-    from basin3d.core.catalog import CatalogException
-    with pytest.raises(CatalogException):
-        catalog._process_plugin_variable_mapping(
-            alpha.AlphaSourcePlugin(catalog), map_filename='mapping_alpha_wrong_header.csv', datasource=DataSource())
-
-
-def test_get_observed_property_variables():
-    """Test that all of the observed property variable are returned"""
-    from tests.testplugins import alpha
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
-    plugins = [alpha.AlphaSourcePlugin(catalog)]
-    catalog.initialize(plugins)
-    assert ['ACT', 'Br', 'Cl', 'DIN', 'DTN', 'F', 'NO3', 'NO2', 'PO4', 'SO4', 'S2', 'S2O3', 'HCO3', 'DIC', 'DOC',
-            'NPOC', 'POC', 'TIC', 'TOC', 'd13C', 'CO2_d13C_soilgas', 'CH4_d13C_soilgas', 'd15N', 'N2O_d15N_soilgas',
-            'CO2_d18O_soilgas', 'N2O_d18O_soilgas', 'SO4_d34S', 'U235', 'H2O_d18O', 'H2O_dD', 'NH4', 'Ca', 'Mg', 'K',
-            'Si', 'Na', 'MICRO_COV', 'MGO1', 'MGO2', 'MGO3', 'GENE_TRAN', 'MICRO_PEP', 'MICRO_PLFA', 'NH3', 'CO2', 'H2',
-            'H2S', 'CH4', 'N2', 'N2O', 'O2', 'Al', 'Sb', 'As', 'Ba', 'Be', 'B', 'Cd', 'Cs', 'Co', 'Cu', 'Eu', 'Ge',
-            'Fe2', 'Hg', 'Pb', 'Li', 'Mn2', 'Mo', 'Ni', 'Rb', 'Se', 'Ag', 'Sr', 'Th', 'Sn', 'Ti', 'Cr', 'FeT', 'Mn',
-            'P', 'U', 'V', 'Zn', 'Zr', 'AT', 'ALB', 'APA', 'APR', 'DP', 'ET', 'HI', 'PPT', 'PPT_TOT_5', 'PPT_TOT_10',
-            'PPT_TOT_60', 'PPT_TOT_DAY', 'UV_IN', 'UV_REF', 'NIR_IN', 'NIR_REF', 'PAR_IN', 'PAR_REF', 'LWIR_IN',
-            'LWIR_REF', 'BB_IN', 'BB_REF', 'RH', 'SD', 'SWE', 'SDEN', 'ST', 'PPT_SF', 'SRAD', 'WBT', 'W_DIR', 'W_GS',
-            'W_SPD', 'W_CH', 'ERT', 'CEC', 'EXAFS', 'SEQ', 'XRD', 'XRF', 'XANES', 'HCND', 'SED_SIZE', 'HCND_Sat',
-            'Porosity', 'SBD', 'SYD', 'HCND_Unsat', 'RET_CUR', 'LSE', 'GWF', 'Well logs', 'SAT', 'SDE', 'SMO', 'STM',
-            'SWP', 'SEC', 'FDOM', 'DO', 'EC', 'SC', 'SAL', 'GWL', 'PH', 'ORP', 'RDC', 'SWL', 'WLH', 'WLE', 'WT', 'TDS',
-            'TSS', 'TRB', 'STO_RES', 'LAI', 'PLT_HT', 'PAI', 'PFT', 'SAP', 'RGB', 'GCC', 'NDVI'] == [i.basin3d_id for
-                                                                                                     i in
-                                                                                                     catalog.find_observed_property_variables()]
-
-    # This test creates a catalog
-    def test_create_catalog():
-        from tests.testplugins import alpha
-        from basin3d.core.catalog import CatalogTinyDb
-        catalog = CatalogTinyDb()
-        plugins = [alpha.AlphaSourcePlugin(catalog)]
-        catalog.initialize(plugins)
-        results = [{'basin3d_id': 'ACT',
-                    'datasource': {'credentials': {},
-                                   'id': 'Alpha',
-                                   'id_prefix': 'A',
-                                   'location': 'https://asource.foo/',
-                                   'name': ''},
-                    'datasource_id': 'Alpha',
-                    'datasource_variable_id': 'Acetate',
-                    'observed_property': {'datasource': {'credentials': {},
-                                                         'id': 'Alpha',
-                                                         'id_prefix': 'A',
-                                                         'location': 'https://asource.foo/',
-                                                         'name': 'Alpha'},
-                                          'datasource_description': '',
-                                          'datasource_variable': 'Acetate',
-                                          'observed_property_variable': {'basin3d_id': 'ACT',
-                                                                         'categories': ['Biogeochemistry',
-                                                                                        'Anions'],
-                                                                         'full_name': 'Acetate '
-                                                                                      '(CH3COO)',
-                                                                         'units': 'mM'},
-                                          'sampling_medium': 'WATER'},
-                    'observed_property_variable': {'basin3d_id': 'ACT',
-                                                   'categories': ['Biogeochemistry', 'Anions'],
-                                                   'full_name': 'Acetate (CH3COO)',
-                                                   'units': 'mM'}},
-                   {'basin3d_id': 'Ag',
-                    'datasource': {'credentials': {},
-                                   'id': 'Alpha',
-                                   'id_prefix': 'A',
-                                   'location': 'https://asource.foo/',
-                                   'name': ''},
-                    'datasource_id': 'Alpha',
-                    'datasource_variable_id': 'Ag',
-                    'observed_property': {'datasource': {'credentials': {},
-                                                         'id': 'Alpha',
-                                                         'id_prefix': 'A',
-                                                         'location': 'https://asource.foo/',
-                                                         'name': 'Alpha'},
-                                          'datasource_description': '',
-                                          'datasource_variable': 'Ag',
-                                          'observed_property_variable': {'basin3d_id': 'Ag',
-                                                                         'categories': ['Biogeochemistry',
-                                                                                        'Trace '
-                                                                                        'elements'],
-                                                                         'full_name': 'Silver '
-                                                                                      '(Ag)',
-                                                                         'units': 'mg/L'},
-                                          'sampling_medium': 'WATER'},
-                    'observed_property_variable': {'basin3d_id': 'Ag',
-                                                   'categories': ['Biogeochemistry',
-                                                                  'Trace elements'],
-                                                   'full_name': 'Silver (Ag)',
-                                                   'units': 'mg/L'}},
-                   {'datasource_id': 'Alpha', 'datasource': {},
-                    'datasource_variable_id': 'Aluminum', 'observed_property': {},
-                    'basin3d_id': 'Al', 'observed_property_variable': {}}]
-        for item in catalog.in_memory_db:
-            idx = item.doc_id - 1
-            assert results[idx] == item
-
-
-@pytest.mark.parametrize("plugins, query, expected",
-                         [([usgs.USGSDataSourcePlugin],
-                           {'variable_name': 'ACT', "datasource_id": 'Alpha'}, None),
-                          ([usgs.USGSDataSourcePlugin,
-                            alpha.AlphaSourcePlugin],
-                           {"datasource_id": 'USGS', 'variable_name': 'Hg'}, {'datasource': {'credentials': {},
-                                                                                             'id': 'USGS',
-                                                                                             'id_prefix': 'USGS',
-                                                                                             'location': 'https://waterservices.usgs.gov/nwis/',
-                                                                                             'name': 'USGS'},
-                                                                              'datasource_description': 'Mercury, water, filtered, nanograms per liter',
-                                                                              'datasource_variable': '50287',
-                                                                              'observed_property_variable': {
-                                                                                  'basin3d_id': 'Hg',
-                                                                                  'categories': ['Biogeochemistry',
-                                                                                                 'Trace elements'],
-                                                                                  'full_name': 'Mercury (Hg)',
-                                                                                  'units': 'mg/L'},
-                                                                              'sampling_medium': 'WATER'}),
-                          ([usgs.USGSDataSourcePlugin,
-                            alpha.AlphaSourcePlugin],
-                           {"datasource_id": 'Alpha', 'variable_name': 'ACT'}, {'datasource': {'credentials': {},
-                                                                                               'id': 'Alpha',
-                                                                                               'id_prefix': 'A',
-                                                                                               'location': 'https://asource.foo/',
-                                                                                               'name': 'Alpha'},
-                                                                                'datasource_description': '',
-                                                                                'datasource_variable': 'Acetate',
-                                                                                'observed_property_variable': {
-                                                                                    'basin3d_id': 'ACT',
-                                                                                    'categories': ['Biogeochemistry',
-                                                                                                   'Anions'],
-                                                                                    'full_name': 'Acetate (CH3COO)',
-                                                                                    'units': 'mM'},
-                                                                                'sampling_medium': 'WATER'}),
-                          ([usgs.USGSDataSourcePlugin,
-                            alpha.AlphaSourcePlugin], {"datasource_id": 'FOO', 'variable_name': 'ACT'},
-                           None)
-                          ],
-                         ids=['Wrong-Alpha', 'USGS-plus', 'Alpha-plus', 'Bad-DataSource'])
-def test_observed_property(plugins, query, expected):
-    """ Test observed property """
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
-    catalog.initialize([p(catalog) for p in plugins])
-
-    observed_property = catalog.find_observed_property(**query)
-    if expected is None:
-        assert observed_property == expected
-    else:
-        assert observed_property.to_dict() == expected
diff --git a/tests/test_core_catalog.py b/tests/test_core_catalog.py
new file mode 100644
index 0000000..16fc3a2
--- /dev/null
+++ b/tests/test_core_catalog.py
@@ -0,0 +1,428 @@
+import logging
+from unittest.mock import Mock
+
+import pytest
+from tinydb import TinyDB
+
+from basin3d.core.catalog import CatalogException
+from basin3d.core.models import DataSource, ObservedProperty, AttributeMapping
+from basin3d.core.schema.enum import SamplingMediumEnum, StatisticEnum
+from tests.testplugins import alpha, complexmap
+from basin3d.plugins import usgs
+
+log = logging.Logger(__name__)
+
+
+# ---------------------------------------
+# Helper and mock functions
+
+def mock_plugin_get_meta():
+    class PluginMeta:
+        id = 'Alpha'
+        name = 'Alpha'
+        location = 'https://asource.foo/'
+        id_prefix = 'A'
+
+    return PluginMeta
+
+
+def mock_init_catalog(dummyself):
+    test_db_filepath = './tests/resources/test.json'
+    test_catalog = TinyDB(test_db_filepath)
+    test_catalog.truncate()
+    return test_catalog
+
+
+def get_datasource_from_id(id, id_prefix, location='test.location'):
+    """Helper file to turn id into datasource"""
+    return DataSource(id=id, name=id, id_prefix=id_prefix, location=location)
+
+
+# ---------------------------------------
+# Fixtures
+
+
+@pytest.fixture
+def test_catalog():
+    return mock_init_catalog('dummyself')
+
+
+@pytest.fixture
+def catalog(monkeypatch):
+    from basin3d.core.catalog import CatalogTinyDb
+
+    catalog = CatalogTinyDb()
+    catalog.plugin_dir = 'tests.testplugins'
+
+    catalog.initialize([])
+
+    return catalog
+
+
+# ---------------------------------------
+# Tests
+# ********* ORDER of tests matters!! see comments
+# ********* The catalog is only first created in the test_create_catalog function below.
+
+# _get_attribute_enum (catalogBASE)
+def test_get_attribute_enum():
+    from basin3d.core.catalog import CatalogBase
+    catalog = CatalogBase()
+    assert catalog._get_attribute_enum('WATER', SamplingMediumEnum) == SamplingMediumEnum.WATER
+    assert catalog._get_attribute_enum('FOO', SamplingMediumEnum) is None
+
+
+@pytest.mark.parametrize("plugins", [(["foo"]), [Mock()]])
+def test_catalog_error(plugins):
+    """Test Catalog Error"""
+    from basin3d.core.catalog import CatalogTinyDb, CatalogException
+    catalog = CatalogTinyDb()
+    pytest.raises(CatalogException, catalog.initialize, plugins)
+
+
+# Test with different variable files.
+# Last test uses the default basin3d hydrology variables which is needed for rest of tests.
+def test_gen_basin3d_variable_store(catalog, caplog):
+    caplog.set_level(logging.INFO, logger=__name__)
+    catalog.variable_dir = 'tests.resources'
+
+    caplog.clear()
+    catalog.variable_filename = 'basin3d_variables_duplicate.csv'
+    catalog._gen_variable_store()
+    assert catalog._get_observed_property('Br').to_dict() == ObservedProperty(
+        basin3d_vocab='Br', full_name='Bromide (Br)',
+        categories=['Biogeochemistry', 'Anions'], units='mM').to_dict()
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Duplicate BASIN-3D variable Br found. Skipping duplicate.' in log_msgs
+
+    catalog.variable_filename = 'basin3d_variables_wrong_header.csv'
+    from basin3d.core.catalog import CatalogException
+    with pytest.raises(CatalogException):
+        catalog._gen_variable_store()
+
+    # KEEP this test last: it resets the variable file.
+    catalog.variable_filename = 'basin3d_observed_property_vocabulary.csv'
+    catalog.variable_dir = 'basin3d.data'
+    catalog._gen_variable_store()
+    assert catalog._get_observed_property('ACT') == ObservedProperty(
+        basin3d_vocab='ACT', full_name='Acetate (CH3COO)',
+        categories=['Biogeochemistry', 'Anions'], units='mM')
+    assert catalog._get_observed_property('heya') is None
+
+
+# catalog.find_observed_property (TinyDB)
+def test_find_observed_property(catalog):
+    assert catalog.find_observed_property('ACT') == ObservedProperty(basin3d_vocab='ACT',
+                                                                     full_name='Acetate (CH3COO)',
+                                                                     categories=['Biogeochemistry' ,'Anions'],
+                                                                     units='mM')
+    assert catalog.find_observed_property('FOO') is None
+
+
+def test_not_initialized_errors(caplog):
+    from basin3d.core.catalog import CatalogTinyDb, CatalogException
+    new_catalog = CatalogTinyDb()
+
+    caplog.clear()
+    with pytest.raises(CatalogException):
+        new_catalog._insert('Foo')
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Could not insert record. Catalog not initialized.' in log_msgs
+
+    caplog.clear()
+    del log_msgs
+    with pytest.raises(CatalogException):
+        new_catalog.find_observed_property('FOO')
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Variable Store has not been initialized.' in log_msgs
+
+    caplog.clear()
+    del log_msgs
+    with pytest.raises(CatalogException):
+        test_gen = new_catalog.find_observed_properties()
+        for op in test_gen:
+            pass
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Variable Store has not been initialized.' in log_msgs
+
+    caplog.clear()
+    del log_msgs
+    with pytest.raises(CatalogException):
+        new_catalog.find_datasource_attribute_mapping('Foo', 'FOO', 'Foo')
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Attribute Store has not been initialized.' in log_msgs
+
+    caplog.clear()
+    del log_msgs
+    with pytest.raises(CatalogException):
+        test_gen = new_catalog.find_attribute_mappings()
+        for attr_map in test_gen:
+            pass
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Attribute Store has not been initialized.' in log_msgs
+
+    caplog.clear()
+    del log_msgs
+    with pytest.raises(CatalogException):
+        test_gen = new_catalog.find_attribute_mappings()
+        for attr_map in test_gen:
+            pass
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Attribute Store has not been initialized.' in log_msgs
+
+
+def test_find_observed_properties(caplog, catalog):
+    """Test the observed property search works"""
+    caplog.set_level(logging.INFO)
+
+    # all observed_properties are returned
+    assert ['ACT', 'Br', 'Cl', 'DIN', 'DTN', 'F', 'NO3', 'NO2', 'PO4', 'SO4', 'S2', 'S2O3', 'HCO3', 'DIC', 'DOC',
+            'NPOC', 'POC', 'TIC', 'TOC', 'd13C', 'CO2_d13C_soilgas', 'CH4_d13C_soilgas', 'd15N', 'N2O_d15N_soilgas',
+            'CO2_d18O_soilgas', 'N2O_d18O_soilgas', 'SO4_d34S', 'U235', 'H2O_d18O', 'H2O_dD', 'NH4', 'Ca', 'Mg', 'K',
+            'Si', 'Na', 'MICRO_COV', 'MGO1', 'MGO2', 'MGO3', 'GENE_TRAN', 'MICRO_PEP', 'MICRO_PLFA', 'NH3', 'CO2', 'H2',
+            'H2S', 'CH4', 'N2', 'N2O', 'O2', 'Al', 'Sb', 'As', 'Ba', 'Be', 'B', 'Cd', 'Cs', 'Co', 'Cu', 'Eu', 'Ge',
+            'Fe2', 'Hg', 'Pb', 'Li', 'Mn2', 'Mo', 'Ni', 'Rb', 'Se', 'Ag', 'Sr', 'Th', 'Sn', 'Ti', 'Cr', 'FeT', 'Mn',
+            'P', 'U', 'V', 'Zn', 'Zr', 'AT', 'ALB', 'APA', 'APR', 'DP', 'ET', 'HI', 'PPT', 'PPT_TOT_5', 'PPT_TOT_10',
+            'PPT_TOT_60', 'PPT_TOT_DAY', 'UV_IN', 'UV_REF', 'NIR_IN', 'NIR_REF', 'PAR_IN', 'PAR_REF', 'LWIR_IN',
+            'LWIR_REF', 'BB_IN', 'BB_REF', 'RH', 'SD', 'SWE', 'SDEN', 'ST', 'PPT_SF', 'SRAD', 'WBT', 'W_DIR', 'W_GS',
+            'W_SPD', 'W_CH', 'ERT', 'CEC', 'EXAFS', 'SEQ', 'XRD', 'XRF', 'XANES', 'HCND', 'SED_SIZE', 'HCND_Sat',
+            'Porosity', 'SBD', 'SYD', 'HCND_Unsat', 'RET_CUR', 'LSE', 'GWF', 'Well logs', 'SAT', 'SDE', 'SMO', 'STM',
+            'SWP', 'SEC', 'FDOM', 'DO', 'EC', 'SC', 'SAL', 'GWL', 'PH', 'ORP', 'RDC', 'SWL', 'WLH', 'WLE', 'WT', 'TDS',
+            'TSS', 'TRB', 'STO_RES', 'LAI', 'PLT_HT', 'PAI', 'PFT', 'SAP', 'RGB', 'GCC', 'NDVI'] == [i.basin3d_vocab for
+                                                                                                     i in
+                                                                                                     catalog.find_observed_properties()]
+
+    # specified valid observed properties returned
+    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'Br'])]
+
+    # on invalid observed properties is not returned and warning message generated.
+    caplog.clear()
+    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'FOO', 'Br'])]
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'BASIN-3D does not support variable FOO' in log_msgs
+
+
+def test_process_plugin_attr_mapping(catalog, caplog):
+    """Test a bad mapping file with the wrong header"""
+    from tests.testplugins import alpha
+    from basin3d.core.catalog import CatalogException
+    with pytest.raises(CatalogException):
+        catalog._process_plugin_attr_mapping(
+            alpha.AlphaSourcePlugin(catalog), filename='mapping_alpha_wrong_header.csv', datasource=DataSource())
+
+    caplog.set_level(logging.INFO)
+    caplog.clear()
+    from tests.testplugins import beta
+    catalog._process_plugin_attr_mapping(beta.BetaSourcePlugin(catalog), filename='beta_mapping.csv', datasource=DataSource(id='Beta'))
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'Datasource Beta: Attribute type STAT is not supported. Skipping mapping.' in log_msgs
+    assert 'Datasource Beta: basin3d_vocab MAXIMUM for attr_type STATISTIC is not a valid BASIN-3D vocabulary. Skipping attribute mapping.' in log_msgs
+
+
+@pytest.mark.parametrize("plugins, query, expected",
+                         # Wrong-plugin-inititalized
+                         [([usgs.USGSDataSourcePlugin],
+                           {'datasource_id': 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'ACT'},
+                           AttributeMapping(
+                               attr_type='OBSERVED_PROPERTY',
+                               basin3d_vocab='NOT_SUPPORTED',
+                               basin3d_desc=[],
+                               datasource_vocab='ACT',
+                               datasource_desc='No datasource was found for id "Alpha".',
+                               datasource=DataSource())
+                           ),
+                          # USGS
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
+                           {'datasource_id': 'USGS', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': '00095'},
+                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                                            basin3d_vocab='SC:WATER',
+                                            basin3d_desc=[ObservedProperty(basin3d_vocab='SC', full_name='Specific Conductance (SC)',
+                                                                           categories=['Hydrogeology', 'Water Physical/Quality Parameters'], units='uS/cm'),
+                                                          SamplingMediumEnum.WATER],
+                                            datasource_vocab='00095',
+                                            datasource_desc='Specific conductance, water, unfiltered, microsiemens per centimeter at 25 degrees Celsius',
+                                            datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS',
+                                                                  location='https://waterservices.usgs.gov/nwis/', credentials={}))
+                           ),
+                          # Alpha
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
+                           {'datasource_id': 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Acetate'},
+                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                                            basin3d_vocab='ACT:WATER',
+                                            basin3d_desc=[ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM'),
+                                                          SamplingMediumEnum.WATER],
+                                            datasource_vocab='Acetate',
+                                            datasource_desc='acetate',
+                                            datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))
+                           ),
+                          # Complexmap
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin, complexmap.ComplexmapSourcePlugin],
+                           {'datasource_id': 'Complexmap', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Mean Al'},
+                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC',
+                                            basin3d_vocab='Al:WATER:MEAN',
+                                            basin3d_desc=[ObservedProperty(basin3d_vocab='Al', full_name='Aluminum (Al)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
+                                                          SamplingMediumEnum.WATER, StatisticEnum.MEAN],
+                                            datasource_vocab='Mean Al',
+                                            datasource_desc='aluminum (Al) concentration in water',
+                                            datasource=DataSource(id='Complexmap', name='Complexmap', id_prefix='C', location='https://asource.foo/', credentials={}))
+                           ),
+                          # Bad-DataSource
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
+                           {'datasource_id': 'FOO', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Acetate'},
+                           AttributeMapping(attr_type='OBSERVED_PROPERTY',
+                                            basin3d_vocab='NOT_SUPPORTED',
+                                            basin3d_desc=[],
+                                            datasource_vocab='Acetate',
+                                            datasource_desc='No datasource was found for id "FOO".',
+                                            datasource=DataSource())
+                           )
+                          ],
+                         ids=['Wrong-plugin-initialized', 'USGS', 'Alpha', 'Complexmap', 'Bad-DataSource'])
+def test_find_datasource_attribute_mapping(plugins, query, expected):
+    """ Test attribute mapping """
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in plugins])
+
+    attribute_mapping = catalog.find_datasource_attribute_mapping(**query)
+    assert attribute_mapping == expected
+
+
+# currently these are the implemented uses:
+# -- BASIN-3D vocab to datasource vocab:
+#    -- fully specified: e.g. Al:WATER:MAX
+#    -- with wildcard: e.g. one or more attribute is specified but not all: Al:WATER:.*
+#    -- partially specified: e.g. OBSERVED_PROPERTY = Al for compound vocabs like Al:WATER:MAX (note partial specification may not be beginning text)
+#    -- could be a list or a single BASIN-3D vocab. Note: Result is ALWAYS a list
+# -- datasource vocab to BASIN-3D
+#    -- single datasource vocab (with attr_type specified) with expected single BASIN-3D
+# -- all mappings: to find complex mappings
+# catalog.find_attribute_mappings (TinyDB)
+@pytest.mark.parametrize('plugins, query, expected_count, expected_list, expected_msg',
+                         # ds_id-attr_type-attr_vocab-from_basin3d--USGS-OBSERVED_PROPERTY-Hg--compound
+                         [
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
+                           {'datasource_id': 'USGS', 'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': 'Hg', 'from_basin3d': True}, 1,
+                           [AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                                             basin3d_vocab='Hg:WATER',
+                                             basin3d_desc=[ObservedProperty(basin3d_vocab='Hg', full_name='Mercury (Hg)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
+                                                           SamplingMediumEnum.WATER],
+                                             datasource_vocab='50287',
+                                             datasource_desc='Mercury, water, filtered, nanograms per liter',
+                                             datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/', credentials={}))],
+                           []),
+                          # datasource_id-only-Alpha-all
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha'}, 14, [], []),
+                          # no-params
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {}, 67, [], []),
+                          # ds_id-attr_type-Alpha-STATISTIC
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_type': 'STATISTIC'}, 3, [], []),
+                          # ds_id-attr_type-attr_vocab--Alpha-STATISTIC-mean
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_type': 'STATISTIC', 'attr_vocab': 'mean'}, 1, [], []),
+                          # ds_id-attr_type_attr_vocab-from_basin3d--Alpha-STATISTIC-MEAN
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_type': 'STATISTIC', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 1, [], []),
+                          # ds_id-attr_vocab--Alpha-mean
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_vocab': 'mean'}, 1,
+                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                                             datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
+                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                           []),
+                          # ds_id-attr_vocab-from_basin3d--Alpha-MEAN
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 1,
+                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                                             datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
+                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                           []),
+                          # attr_type_attr_vocab--STATISTIC-mean
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_type': 'STATISTIC', 'attr_vocab': 'mean'}, 1,
+                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                                             datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
+                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                           []),
+                          # attr_type_attr_vocab-from_basin3d--STATISTIC-MEAN
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_type': 'STATISTIC', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 2,
+                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                                             datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
+                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={})),
+                            AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='00003',
+                                             datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
+                                             datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/', credentials={}))],
+                           []),
+                          # ds_id-attr_type_attr_vocab-from_basin3d-similar-vocab--USGS-RESULT_QUALITY
+                          ([usgs.USGSDataSourcePlugin], {'datasource_id': 'USGS', 'attr_type': 'RESULT_QUALITY', 'attr_vocab': ['ESTIMATED', 'VALIDATED'], 'from_basin3d': True}, 3, [], []),
+                          # attr_type--AGGREGATION_TYPE
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_type': 'AGGREGATION_DURATION'}, 4, [], []),
+                          # attr_vocab--Aluminum
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': 'Aluminum'}, 1, [], []),
+                          # attr_vocab-from_basin3d--Al
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': 'Al', 'from_basin3d': True}, 3, [], []),
+                          # attr_vocabs--Aluminum,Acetate
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': ['Aluminum', 'Acetate']}, 2, [], []),
+                          # attr_vocabs-from_basin3d--ACT,Al
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': ['Al', 'ACT'], 'from_basin3d': True}, 4, [], []),
+                          # attr_vocabs-some_bad--Aluminum,Acetate,Foo
+                          ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': ['Aluminum', 'Acetate', 'Foo']}, 2, [],
+                           ['No attribute mappings found for the following datasource vocabularies: Foo. Note: specified datasource id = None and attribute type = None']),
+                          # BAD-ds_id
+                          ([alpha.AlphaSourcePlugin], {'datasource_id': 'Foo'}, -1, [], []),
+                          # BAD-attr_type
+                          ([alpha.AlphaSourcePlugin], {'attr_type': 'BAD_ATTR_TYPE'}, -1, [], []),
+                          # BAD-attr_vocab_type
+                          ([alpha.AlphaSourcePlugin], {'attr_vocab': {'foo': 'foo'}}, -1, [], []),
+                          # No-results
+                          ([alpha.AlphaSourcePlugin], {'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': 'Hg'}, 0, [],
+                           ['No attribute mappings found for specified parameters: datasource id = "None", attribute type = "OBSERVED_PROPERTY", datasource vocabularies: Hg.']),
+                          # No-results-from_basin3d
+                          ([alpha.AlphaSourcePlugin], {'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': 'Hg', 'from_basin3d': True}, 0, [],
+                           ['No attribute mappings found for specified parameters: datasource id = "None", attribute type = "OBSERVED_PROPERTY", BASIN-3D vocabularies: Hg.']),
+                          # complexmap-attr_vocabs-from_basin3d--ACT,Al
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_vocab': ['Al', 'ACT'], 'from_basin3d': True}, 5, [], []),
+                          # complexmap-attr_vocabs-wildcard-from_basin3d--Al
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_vocab': ['Al:.*:.*'], 'from_basin3d': True}, 3, [], []),
+                          # complexmap-attr_vocab-Al
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_vocab': ['Mean Al']}, 1, [], []),
+                          # complexmap-attr_vocab-bothAl
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_vocab': ['Mean Al', 'Mean Aluminum']}, 2, [], []),
+                          # complexmap-attr_vocabs-single-wildcard-from_basin3d--Al
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_vocab': ['Al:.*:MEAN', 'Al:.*:MAX'], 'from_basin3d': True}, 2, [], []),
+                          # complexmap-attr_type-attr_vocabs-from_basin3d--MEAN, MAX
+                          ([complexmap.ComplexmapSourcePlugin], {'attr_type': 'STATISTIC', 'attr_vocab': ['MEAN', 'MAX'], 'from_basin3d': True}, 5, [], []),
+                         ],
+                         ids=['USGS-from_basin3d', 'datasource_id-only-Alpha-all', 'no-params-ALL', 'ds_id-attr_type-Alpha-STATISTIC',
+                              'ds_id-attr_type-attr_vocab--Alpha-STATISTIC-mean', 'ds_id-attr_type_attr_vocab-from_basin3d--Alpha-STATISTIC-MEAN',
+                              'ds_id-attr_vocab--Alpha-mean', 'ds_id-attr_vocab-from_basin3d--Alpha-MEAN', 'attr_type_attr_vocab--STATISTIC-mean',
+                              'attr_type_attr_vocab-from_basin3d--STATISTIC-MEAN', 'ds_id-attr_type_attr_vocab-from_basin3d-similar-vocab', 'attr_type--AGGREGATION_TYPE', 'attr_vocab--Aluminum',
+                              'attr_vocab-from_basin3d--Al', 'attr_vocabs--Aluminum-Acetate', 'attr_vocabs-from_basin3d--ACT-Al',
+                              'attr_vocabs-some_bad--Aluminum-Acetate-Foo', 'BAD-ds_id', 'BAD-attr_type', 'BAD-attr_vocab_type', 'No-results', 'No-results-from_basin3d',
+                              'complexmap-attr_vocabs-from_basin3d', 'complexmap-attr_vocabs-wildcard-from_basin3d', 'complexmap-attr_vocab-Al', 'complexmap-attr_vocab-bothAl',
+                              'complexmap-attr_vocabs-single-wildcard-from_basin3d', 'complexmap-attr_type-attr_vocabs-from_basin3d'])
+def test_find_attribute_mappings(caplog, plugins, query, expected_count, expected_list, expected_msg):
+    """ Test attribute mapping """
+    caplog.set_level(logging.INFO)
+
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in plugins])
+    caplog.clear()
+
+    attribute_mappings = catalog.find_attribute_mappings(**query)
+
+    actual_list = []
+    if expected_count >= 0:
+        count = 0
+        for attr_mapping in attribute_mappings:
+            count += 1
+            if expected_list:
+                actual_list.append(attr_mapping)
+        assert count == expected_count
+        if expected_list:
+            for expected_attr_mapping in expected_list:
+                assert expected_attr_mapping in actual_list
+        if expected_msg:
+            log_msgs = [rec.message for rec in caplog.records]
+            for msg in expected_msg:
+                assert msg in log_msgs
+    else:
+        with pytest.raises(CatalogException):
+            for attr_mapping in attribute_mappings:
+                pass
diff --git a/tests/test_core_models.py b/tests/test_core_models.py
index 1c9754b..1d415a5 100644
--- a/tests/test_core_models.py
+++ b/tests/test_core_models.py
@@ -1,27 +1,87 @@
 import pytest
 
 from basin3d.core.models import AbsoluteCoordinate, AltitudeCoordinate, Coordinate, DepthCoordinate, \
-    GeographicCoordinate, MeasurementTimeseriesTVPObservation, MonitoringFeature, Observation, ObservedProperty, \
-    ObservedPropertyVariable, RelatedSamplingFeature, RepresentativeCoordinate, TimeValuePair, ResultListTVP, \
-    VerticalCoordinate, DataSource, Person
-from basin3d.core.schema.enum import FeatureTypeEnum, ResultQualityEnum
-from basin3d.core.types import SamplingMedium, SpatialSamplingShapes
+    GeographicCoordinate, MeasurementTimeseriesTVPObservation, MonitoringFeature, Observation, \
+    ObservedProperty, RelatedSamplingFeature, RepresentativeCoordinate, TimeValuePair, ResultListTVP, \
+    VerticalCoordinate, MappedAttribute, AttributeMapping, Person
+from basin3d.core.schema.enum import FeatureTypeEnum, ResultQualityEnum, StatisticEnum, SamplingMediumEnum
+from basin3d.core.types import SpatialSamplingShapes
 
 
 @pytest.fixture
-def observed_property_var():
+def observed_property():
     """
     Load some fake data to use in the tests
     """
-    return ObservedPropertyVariable(basin3d_id='FH2O', full_name='Groundwater Flux',
-                                    categories=['Hydrology', 'Subsurface'], units='m3/m/s')
+    return ObservedProperty(basin3d_vocab='FH2O', full_name='Groundwater Flux',
+                            categories=['Hydrology', 'Subsurface'], units='m3/m/s')
 
 
 @pytest.fixture
-def observed_property(datasource, observed_property_var):
-    return ObservedProperty(datasource_variable='water_flux', observed_property_variable=observed_property_var,
-                            sampling_medium=SamplingMedium.WATER, datasource=datasource,
-                            datasource_description='a test variable')
+def mapped_attribute_op_ag(datasource):
+    return MappedAttribute(
+        attr_type='OBSERVED_PROPERTY',
+        attr_mapping=AttributeMapping(
+            attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+            basin3d_vocab='Ag:WATER',
+            basin3d_desc=[
+                ObservedProperty(basin3d_vocab='Ag', full_name='Silver (Ag)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
+                SamplingMediumEnum.WATER],
+            datasource_vocab='Ag',
+            datasource_desc='sliver concentration in water',
+            datasource=datasource))
+
+
+@pytest.fixture
+def mapped_attribute_op_act(datasource):
+    return MappedAttribute(
+        attr_type='OBSERVED_PROPERTY',
+        attr_mapping=AttributeMapping(
+            attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+            basin3d_vocab='ACT:WATER',
+            basin3d_desc=[ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM'),
+                          SamplingMediumEnum.WATER],
+            datasource_vocab='Acetate',
+            datasource_desc='acetate',
+            datasource=datasource))
+
+
+@pytest.fixture
+def mapped_attribute_sampling_medium_act(datasource):
+    return MappedAttribute(
+        attr_type='SAMPLING_MEDIUM',
+        attr_mapping=AttributeMapping(
+            attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+            basin3d_vocab='ACT:WATER',
+            basin3d_desc=[ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM'),
+                          SamplingMediumEnum.WATER],
+            datasource_vocab='Acetate',
+            datasource_desc='acetate',
+            datasource=datasource))
+
+
+@pytest.fixture
+def mapped_attribute_result_quality_validated(datasource):
+    return MappedAttribute(attr_type='RESULT_QUALITY',
+                           attr_mapping=AttributeMapping(
+                               attr_type='RESULT_QUALITY',
+                               basin3d_vocab='VALIDATED',
+                               basin3d_desc=[ResultQualityEnum.VALIDATED],
+                               datasource_vocab='VALIDATED',
+                               datasource_desc='',
+                               datasource=datasource))
+
+
+@pytest.fixture
+def mapped_attribute_agg_dur_not_supported(datasource):
+    return MappedAttribute(attr_type='AGGREGATION_DURATION',
+                           attr_mapping=AttributeMapping(
+                               attr_type='AGGREGATION_DURATION',
+                               basin3d_vocab='NOT_SUPPORTED',
+                               basin3d_desc=[],
+                               datasource_vocab='daily',
+                               datasource_desc='No mapping was found for datasource vocab: "daily" in datasource: "Alpha".',
+                               datasource=datasource))
 
 
 def test_person():
@@ -36,6 +96,7 @@ def test_person():
     assert person.institution == "The Simpsons"
     assert person.role == "Grand Pooba"
 
+
 def test_data_source_model(datasource):
     """Test DataSource model"""
 
@@ -45,23 +106,42 @@ def test_data_source_model(datasource):
     assert datasource.location == 'https://asource.foo/'
 
 
-def test_observed_property_create(observed_property, observed_property_var, datasource):
-    """ Was the object created correctly? """
+def test_observed_property_create(observed_property):
+    """ create the object and test attributes """
 
-    assert observed_property.sampling_medium == SamplingMedium.WATER
-    assert observed_property.datasource_variable == 'water_flux'
-    assert observed_property.observed_property_variable == observed_property_var
-    assert observed_property.datasource == datasource
-    assert observed_property.datasource_description == 'a test variable'
+    assert observed_property.basin3d_vocab == 'FH2O'
+    assert observed_property.full_name == 'Groundwater Flux'
+    assert observed_property.categories == ['Hydrology', 'Subsurface']
+    assert observed_property.units == 'm3/m/s'
 
 
-def test_observed_property_variable_create(observed_property_var):
-    """ create the object and test attributes """
+def test_attribute_mapping(mapped_attribute_op_act, datasource):
+    attr_mapping = mapped_attribute_op_act.attr_mapping
+    assert attr_mapping.attr_type == 'OBSERVED_PROPERTY:SAMPLING_MEDIUM'
+    assert attr_mapping.basin3d_vocab == 'ACT:WATER'
+    assert attr_mapping.basin3d_desc[0] == ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM')
+    assert attr_mapping.basin3d_desc[1] == SamplingMediumEnum.WATER
+    assert attr_mapping.datasource_vocab == 'Acetate'
+    assert attr_mapping.datasource_desc == 'acetate'
+    assert attr_mapping.datasource == datasource
 
-    assert observed_property_var.basin3d_id == 'FH2O'
-    assert observed_property_var.full_name == 'Groundwater Flux'
-    assert observed_property_var.categories == ['Hydrology', 'Subsurface']
-    assert observed_property_var.units == 'm3/m/s'
+
+def test_mapped_attribute(mapped_attribute_op_act):
+    assert mapped_attribute_op_act.attr_type == 'OBSERVED_PROPERTY'
+    assert isinstance(mapped_attribute_op_act.attr_mapping, AttributeMapping) is True
+    assert mapped_attribute_op_act.get_basin3d_vocab() == 'ACT'
+    assert mapped_attribute_op_act.get_basin3d_desc() == ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM')
+
+
+def test_mapped_attribute_not_supported(mapped_attribute_agg_dur_not_supported, datasource):
+    assert mapped_attribute_agg_dur_not_supported.attr_type == 'AGGREGATION_DURATION'
+    attr_mapping = mapped_attribute_agg_dur_not_supported.attr_mapping
+    assert attr_mapping.attr_type == 'AGGREGATION_DURATION'
+    assert attr_mapping.basin3d_vocab == 'NOT_SUPPORTED'
+    assert attr_mapping.basin3d_desc == []
+    assert attr_mapping.datasource_vocab == 'daily'
+    assert attr_mapping.datasource_desc == 'No mapping was found for datasource vocab: "daily" in datasource: "Alpha".'
+    assert attr_mapping.datasource == datasource
 
 
 def test_representative_coordinate():
@@ -112,7 +192,7 @@ def test_absolute_coordinate():
     assert a_coord.distance_units == VerticalCoordinate.DISTANCE_UNITS_FEET
 
 
-def test_monitoring_feature_create(plugin_access_alpha):
+def test_monitoring_feature_create(plugin_access_alpha, mapped_attribute_op_ag, mapped_attribute_op_act):
     """Test instance of monitoring feature"""
 
     a_region = MonitoringFeature(
@@ -174,7 +254,7 @@ def test_monitoring_feature_create(plugin_access_alpha):
                     value=-0.5, distance_units=VerticalCoordinate.DISTANCE_UNITS_METERS)
             )
         ),
-        observed_property_variables=['Ag', 'Acetate'],
+        observed_properties=['Ag', 'Acetate'],
         related_sampling_feature_complex=[
             RelatedSamplingFeature(plugin_access=plugin_access_alpha,
                                    related_sampling_feature='Region1',
@@ -202,12 +282,13 @@ def test_monitoring_feature_create(plugin_access_alpha):
         VerticalCoordinate.DISTANCE_UNITS_METERS
     assert a_point.coordinates.representative.vertical_position.datum == \
         DepthCoordinate.DATUM_LOCAL_SURFACE
-    assert a_point.observed_property_variables == ['ACT', 'Ag']
+    assert a_point.observed_properties[0] == mapped_attribute_op_ag
+    assert a_point.observed_properties[1] == mapped_attribute_op_act
     assert a_point.related_sampling_feature_complex[0].related_sampling_feature == 'A-Region1'
     assert a_point.related_sampling_feature_complex[0].role == 'PARENT'
 
 
-def test_observation_create(plugin_access_alpha):
+def test_observation_create(plugin_access_alpha, mapped_attribute_result_quality_validated):
     """
     Test instance of observation model class
     NOTE: In practice, the Observation should not be used stand alone
@@ -217,7 +298,7 @@ def test_observation_create(plugin_access_alpha):
         id='timeseries01',
         utc_offset='9',
         phenomenon_time='20180201',
-        result_quality=ResultQualityEnum.VALIDATED,
+        result_quality=['VALIDATED'],
         feature_of_interest='Point011')
 
     assert obs01.datasource.id == 'Alpha'
@@ -225,11 +306,13 @@ def test_observation_create(plugin_access_alpha):
     assert obs01.utc_offset == '9'
     assert obs01.phenomenon_time == '20180201'
     assert obs01.observed_property is None
-    assert obs01.result_quality == ResultQualityEnum.VALIDATED
+    assert obs01.result_quality == [mapped_attribute_result_quality_validated]
     assert obs01.feature_of_interest == 'Point011'
 
 
-def test_measurement_timeseries_tvp_observation_create(plugin_access_alpha):
+def test_measurement_timeseries_tvp_observation_create(
+        plugin_access_alpha, datasource,
+        mapped_attribute_op_act, mapped_attribute_sampling_medium_act, mapped_attribute_result_quality_validated, mapped_attribute_agg_dur_not_supported):
     """Test instance of Measurement Timeseries TVP Observation"""
 
     obs01 = MeasurementTimeseriesTVPObservation(
@@ -237,39 +320,39 @@ def test_measurement_timeseries_tvp_observation_create(plugin_access_alpha):
         id='timeseries01',
         utc_offset='9',
         phenomenon_time='20180201',
-        result_quality=[ResultQualityEnum.VALIDATED],
+        result_quality=['VALIDATED'],
         feature_of_interest='Point011',
         feature_of_interest_type=FeatureTypeEnum.POINT,
         aggregation_duration='daily',
         time_reference_position='start',
-        observed_property_variable='Acetate',
+        observed_property='Acetate',
         statistic='mean',
-        result=ResultListTVP(value=[TimeValuePair('201802030100', '5.32')],
-                             quality=[ResultQualityEnum.VALIDATED]),
+        result=ResultListTVP(plugin_access=plugin_access_alpha,
+                             value=[TimeValuePair('201802030100', '5.32')],
+                             result_quality=['VALIDATED']),
         unit_of_measurement='m'
     )
 
     assert obs01.id == 'A-timeseries01'
     assert obs01.utc_offset == '9'
     assert obs01.phenomenon_time == '20180201'
-    assert obs01.observed_property == ObservedProperty(
-        datasource_variable='Acetate',
-        observed_property_variable=ObservedPropertyVariable(
-            basin3d_id='ACT', full_name='Acetate (CH3COO)',
-            categories=['Biogeochemistry', 'Anions'], units='mM'),
-        sampling_medium=SamplingMedium.WATER,
-        datasource=DataSource(
-            id='Alpha', name='Alpha', id_prefix='A',
-            location='https://asource.foo/', credentials={}),
-        datasource_description='')
-    assert obs01.observed_property_variable == 'ACT'
-    assert obs01.result_quality == [ResultQualityEnum.VALIDATED]
+    assert obs01.observed_property == mapped_attribute_op_act
+    assert obs01.sampling_medium == mapped_attribute_sampling_medium_act
+    assert obs01.result_quality == [mapped_attribute_result_quality_validated]
     assert obs01.feature_of_interest == 'Point011'
     assert obs01.feature_of_interest_type == FeatureTypeEnum.POINT
-    assert obs01.aggregation_duration == 'daily'
+    assert obs01.aggregation_duration == mapped_attribute_agg_dur_not_supported
     assert obs01.time_reference_position == 'start'
-    assert obs01.statistic == 'mean'
+    assert obs01.statistic == MappedAttribute(attr_type='STATISTIC',
+                                              attr_mapping=AttributeMapping(
+                                                  attr_type='STATISTIC',
+                                                  basin3d_vocab='MEAN',
+                                                  basin3d_desc=[StatisticEnum.MEAN],
+                                                  datasource_vocab='mean',
+                                                  datasource_desc='',
+                                                  datasource=datasource))
     assert obs01.unit_of_measurement == 'm'
     assert obs01.datasource.id == 'Alpha'
     assert obs01.result.value[0] == TimeValuePair('201802030100', '5.32')
-    assert obs01.result.quality[0] == ResultQualityEnum.VALIDATED
+    assert obs01.result.result_quality[0] == mapped_attribute_result_quality_validated
+
diff --git a/tests/test_core_plugins.py b/tests/test_core_plugins.py
index 8b76f85..ca13ba8 100644
--- a/tests/test_core_plugins.py
+++ b/tests/test_core_plugins.py
@@ -87,41 +87,44 @@ def test_get_feature_type(feature_name, return_format, result):
 
 
 @pytest.mark.parametrize("plugin, messages, synthesis_call, synthesis_args", [
+    # ErrorSource.MF
     ("tests.testplugins.plugin_error.ErrorSourcePlugin",
-     [{'msg': 'Unexpected Error(Exception): This is a list_monitoring_features exception',
-       'level': 'ERROR',
-       'where': ['Error', 'MonitoringFeature']}], "monitoring_features", {}),
-    ("tests.testplugins.plugin_error.ErrorSourcePlugin", [{'level': 'ERROR',
-                                                           'msg': 'Unexpected Error(Exception): This is a '
-                                                                  'find_measurement_timeseries_tvp_observations error',
-                                                           'where': ['Error', 'MeasurementTimeseriesTVPObservation']}],
+     [{'msg': 'Unexpected Error(Exception): This is a list_monitoring_features exception', 'level': 'ERROR', 'where': ['Error', 'MonitoringFeature']}],
+     "monitoring_features",
+     {}),
+    # ErrorSource.TVP
+    ("tests.testplugins.plugin_error.ErrorSourcePlugin",
+     [{'level': 'ERROR', 'msg': 'Unexpected Error(Exception): This is a find_measurement_timeseries_tvp_observations error', 'where': ['Error', 'MeasurementTimeseriesTVPObservation']}],
+     "measurement_timeseries_tvp_observations",
+     {'start_date': '2019-10-01', 'observed_property': ["Ag"], 'monitoring_feature': ['E-region']}),
+    # AlphaSource.MF
+    ("tests.testplugins.alpha.AlphaSourcePlugin",
+     [{'level': 'ERROR', 'msg': 'DataSource not not found for id E-1234', 'where': None}],
+     "monitoring_features",
+     {"id": 'E-1234'}),
+    # USGS.MR
+
+    ("basin3d.plugins.usgs.USGSDataSourcePlugin",
+     [{'level': 'ERROR', 'msg': 'DataSource not not found for id E-1234', 'where': None}],
+     "monitoring_features",
+     {"id": 'E-1234'}),
+    # NoPluginViews.TVP
+    ("tests.testplugins.no_plugin_views.NoPluginViewsPlugin",
+     [{'level': 'WARN', 'msg': 'Plugin view does not exist', 'where': ['NoPluginView', 'MeasurementTimeseriesTVPObservation']}],
      "measurement_timeseries_tvp_observations",
-     {'start_date': '2019-10-01', 'observed_property_variables': ["Ag"],
-      'monitoring_features': ['region']}),
-    ("tests.testplugins.alpha.AlphaSourcePlugin", [{'level': 'ERROR',
-                                                    'msg': 'DataSource not not found for id E-1234',
-                                                    'where': None}], "monitoring_features", {"id": 'E-1234'}),
-    ("basin3d.plugins.usgs.USGSDataSourcePlugin", [{'level': 'ERROR',
-                                                    'msg': 'DataSource not not found for id E-1234',
-                                                    'where': None}], "monitoring_features", {"id": 'E-1234'}),
-    ("tests.testplugins.no_plugin_views.NoPluginViewsPlugin", [{'level': 'WARN',
-                                                                'msg': 'Plugin view does not exist',
-                                                                'where': ['NoPluginView',
-                                                                          'MeasurementTimeseriesTVPObservation']}],
-     "measurement_timeseries_tvp_observations", {'start_date': '2019-10-01', 'observed_property_variables': ["Ca"],
-                                                 'monitoring_features': ['region']}),
-    ("tests.testplugins.no_plugin_views.NoPluginViewsPlugin", [{'level': 'WARN',
-                                                                'msg': 'Plugin view does not exist',
-                                                                'where': ['NoPluginView', 'MonitoringFeature']}],
-     "monitoring_features", {'id': 'NPV-01'}),
-    ("tests.testplugins.alpha.AlphaSourcePlugin", [{'level': 'WARN',
-                                                    'msg': 'Synthesis generated warnings but they are in the wrong format',
-                                                    'where': ['Alpha', 'MeasurementTimeseriesTVPObservation']}],
+     {'start_date': '2019-10-01', 'observed_property': ["Ca"], 'monitoring_feature': ['NPV-region']}),
+    # NoPluginViews.MF
+    ("tests.testplugins.no_plugin_views.NoPluginViewsPlugin",
+     [{'level': 'WARN', 'msg': 'Plugin view does not exist', 'where': ['NoPluginView', 'MonitoringFeature']}],
+     "monitoring_features",
+     {'id': 'NPV-01'}),
+    # AlphaSource.TVP
+    ("tests.testplugins.alpha.AlphaSourcePlugin",
+     [{'level': 'WARN', 'msg': 'Synthesis generated warnings but they are in the wrong format', 'where': ['Alpha', 'MeasurementTimeseriesTVPObservation']}],
      "measurement_timeseries_tvp_observations",
-     {'start_date': '2019-10-01', 'observed_property_variables': ["Ag"],
-      'monitoring_features': ['A-region']}),
-], ids=["ErrorSource.MF", "ErrorSource.TVP", "AlphaSource.MF", "USGS.MR", "NoPluginViews.TVP", "NoPluginViews.MF",
-        "AlphaSource.TVP"])
+     {'start_date': '2019-10-01', 'observed_property': ["Ag"], 'monitoring_feature': ['A-region']}),
+    ],
+    ids=["ErrorSource.MF", "ErrorSource.TVP", "AlphaSource.MF", "USGS.MR", "NoPluginViews.TVP", "NoPluginViews.MF", "AlphaSource.TVP"])
 def test_plugin_exceptions(plugin, messages, synthesis_call, synthesis_args):
     """Test that basin3d handles unexpected exceptions"""
 
diff --git a/tests/test_core_schema.py b/tests/test_core_schema.py
index b61c5d4..f021b5a 100644
--- a/tests/test_core_schema.py
+++ b/tests/test_core_schema.py
@@ -9,10 +9,10 @@
 
 @pytest.mark.parametrize("params, error", [({}, False),
                                            ({"datasource": ["FOO"],
-                                             "monitoring_features": []}, False),
+                                             "monitoring_feature": []}, False),
                                            ({"datasource": ["FOO"],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "parent_features": ['moo']}, False),
+                                             "monitoring_feature": ['bar', 'base'],
+                                             "parent_feature": ['moo']}, False),
                                            ({"feature_type": 'POINT'}, False),
                                            ({"feature_type": 'FOO'}, True),
                                            ({"datasource": 'FOO'}, False)],
@@ -23,7 +23,7 @@ def test_query_monitoring_feature(params, error):
     if not error:
         query = QueryMonitoringFeature(**params)
 
-        for p in ["datasource", "monitoring_features", "parent_features",
+        for p in ["datasource", "monitoring_feature", "parent_feature",
                   "feature_type"]:
 
             if p not in params:
@@ -34,61 +34,78 @@ def test_query_monitoring_feature(params, error):
         pytest.raises(ValidationError, QueryMonitoringFeature, **params)
 
 
-@pytest.mark.parametrize("params, error", [({}, True),
-                                           ({"datasource": ["FOO"],
-                                             "monitoring_features": [],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, True),
-                                           ({"datasource": ["FOO"],
-                                             "monitoring_features": [],
-                                             "aggregation_duration": 'MONTH',
-                                             "start_date": "2021-01-01"}, True),
-                                           ({"datasource": ["FOO"],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, False),
-                                           ({"aggregation_duration": 'FOO',
-                                             "monitoring_features": ['bar', 'base'],
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, True),
-                                           ({"statistic": ['MEAN'],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, False),
-                                           ({"result_quality": ['VALIDATED'],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, False),
-                                           ({"statistic": ['FOO'],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, True),
-                                           ({"result_quality": ['BAR'],
-                                             "monitoring_features": ['bar', 'base'],
-                                             "aggregation_duration": 'MONTH',
-                                             "observed_property_variables":['FOO', 'BAR'],
-                                             "start_date": "2021-01-01"}, True)],
-                         ids=["empty-invalid", "monitoring-feature-invalid", "observed-property-variables-missing", "valid1",
-                              "aggregation-duration-invalid",
-                              "statistic-valid", "result-quality-valid",
-                              "statistic-invalid", "result-quality-invalid"])
+@pytest.mark.parametrize(
+    "params, error", [
+                      # empty-invalid
+                      ({}, True),
+                      # monitoring-feature-invalid
+                      ({"datasource": ["FOO"],
+                        "monitoring_feature": [],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, True),
+                      # observed-property-missing
+                      ({"datasource": ["FOO"],
+                        "monitoring_feature": [],
+                        "aggregation_duration": 'MONTH',
+                        "start_date": "2021-01-01"}, True),
+                      # valid1
+                      ({"datasource": ["FOO"],
+                        "monitoring_feature": ['bar', 'base'],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, False),
+                      # aggregation-duration-invalid
+                      ({"aggregation_duration": 'FOO',
+                        "monitoring_feature": ['bar', 'base'],
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, True),
+                      # aggregation-duration-valid-none
+                      ({"aggregation_duration": None,
+                        "monitoring_feature": ['bar', 'base'],
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, False),
+                      # statistic-valid
+                      ({"statistic": ['MEAN'],
+                        "monitoring_feature": ['bar', 'base'],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, False),
+                      # result-quality-valid
+                      ({"result_quality": ['VALIDATED'],
+                        "monitoring_feature": ['bar', 'base'],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, False),
+                      # statistic-invalid
+                      ({"statistic": ['FOO'],
+                        "monitoring_feature": ['bar', 'base'],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, True),
+                      # result-quality-invalid
+                      ({"result_quality": ['BAR'],
+                        "monitoring_feature": ['bar', 'base'],
+                        "aggregation_duration": 'MONTH',
+                        "observed_property":['FOO', 'BAR'],
+                        "start_date": "2021-01-01"}, True)],
+    ids=["empty-invalid", "monitoring-feature-invalid", "observed-property-missing", "valid1",
+         "aggregation-duration-invalid", "aggregation-duration-valid-none",
+         "statistic-valid", "result-quality-valid", "statistic-invalid", "result-quality-invalid"])
 def test_query_measurement_timeseries_tvp(params, error):
     """Test the measurement timeseries tvp query dataclass"""
     if not error:
         query = QueryMeasurementTimeseriesTVP(**params)
 
-        for p in ["datasource", "aggregation_duration", "monitoring_features", "observed_property_variables",
+        for p in ["datasource", "aggregation_duration", "monitoring_feature", "observed_property",
                   "start_date", "end_date", "statistic", "result_quality"]:
 
             query_json = json.loads(query.json())
             if p not in params:
                 assert query_json[p] is None
             else:
+                if p == "aggregation_duration" and params[p] is None:
+                    params[p] = 'DAY'
                 assert query_json[p] == params[p]
 
             if 'aggregation_duration' not in query_json.keys():
@@ -104,3 +121,6 @@ def test_enumerations(enum):
 
     assert enum.names()
     assert enum.values()
+
+
+# ToDo: test_set_mapped_attribute_enum_type
diff --git a/tests/test_core_synthesis.py b/tests/test_core_synthesis.py
index 4443c36..ca1b5ef 100644
--- a/tests/test_core_synthesis.py
+++ b/tests/test_core_synthesis.py
@@ -1,16 +1,44 @@
 import pytest
 
-from basin3d.core.synthesis import _synthesize_query_identifiers
+from basin3d.core.models import DataSource
+from basin3d.core.plugin import DataSourcePluginAccess
+from basin3d.core.schema.query import QueryMeasurementTimeseriesTVP
+from basin3d.core.synthesis import MeasurementTimeseriesTVPObservationAccess
 
+from tests.testplugins import alpha
 
-@pytest.mark.parametrize("values, filtered_params",
-                         [(["F-9237"], ['9237']),
-                          (["F-9237", "R-8e38e8", "F-00000"], ['9237', '00000']),
-                          ("R-9237", [])],
-                         ids=["single", "multiple", "none"])
-def test_extract_query_param_ids(values, filtered_params):
-    """Filtering of query arguments"""
 
-    synthesized_values = _synthesize_query_identifiers(values, "F")
-    assert synthesized_values == filtered_params
+@pytest.fixture
+def alpha_plugin_access():
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
+    alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
 
+    return DataSourcePluginAccess(alpha_ds, catalog)
+
+
+@pytest.mark.parametrize('input_query, expected_result',
+                         [
+                          # change-to-day
+                          ({'datasource': ['A'], 'monitoring_feature': ['bar', 'base'], 'observed_property':['FOO', 'BAR'],
+                            'start_date': '2021-01-01', 'aggregation_duration': 'MONTH'}, ['DAY']),
+                          # NONE
+                          ({'datasource': ['A'], 'monitoring_feature': ['bar', 'base'], 'observed_property':['FOO', 'BAR'],
+                            'start_date': '2021-01-01', 'aggregation_duration': 'NONE'}, ['NONE']),
+                          # not-specified
+                          ({'datasource': ['A'], 'monitoring_feature': ['bar', 'base'], 'observed_property':['FOO', 'BAR'],
+                            'start_date': '2021-01-01'}, ['DAY']),
+                          ], ids=['change-to-day', 'NONE', 'not-specified'])
+def test_measurement_timeseries_TVP_observation_access_synthesize_query(input_query, expected_result, alpha_plugin_access):
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
+    alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
+    alpha_access = MeasurementTimeseriesTVPObservationAccess(alpha_ds, catalog)
+
+    query = QueryMeasurementTimeseriesTVP(**input_query)
+
+    result = alpha_access.synthesize_query(alpha_plugin_access, query)
+
+    assert result.aggregation_duration == expected_result
diff --git a/tests/test_core_translate.py b/tests/test_core_translate.py
new file mode 100644
index 0000000..ee62440
--- /dev/null
+++ b/tests/test_core_translate.py
@@ -0,0 +1,233 @@
+import logging
+import pytest
+
+from basin3d.core import translate
+from basin3d.core.models import DataSource
+from basin3d.core.plugin import DataSourcePluginAccess
+from basin3d.core.schema.enum import NO_MAPPING_TEXT
+from basin3d.core.schema.query import QueryMonitoringFeature, QueryMeasurementTimeseriesTVP
+from tests.testplugins import alpha, complexmap
+
+
+@pytest.fixture
+def alpha_plugin_access():
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
+    alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
+
+    return DataSourcePluginAccess(alpha_ds, catalog)
+
+
+@pytest.fixture
+def complex_plugin_access():
+    from basin3d.core.catalog import CatalogTinyDb
+    catalog = CatalogTinyDb()
+    catalog.initialize([p(catalog) for p in [complexmap.ComplexmapSourcePlugin]])
+    complex_ds = DataSource(id='Complexmap', name='Complexmap', id_prefix='C', location='https://asource.foo/')
+
+    return DataSourcePluginAccess(complex_ds, catalog)
+
+
+# ToDo: add test for triple compound mapping
+@pytest.mark.parametrize(
+    'attr_type, basin3d_vocab, basin3d_query, expected_results, expected_msgs',
+    # non-compound
+    [('statistic', 'MEAN', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['ACT'], start_date='2020-01-01', statistic=['MEAN']),
+      ['mean'], []),
+     # non-compound-no-match
+     ('statistic', 'ESTIMATED', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['ACT'], start_date='2020-01-01', statistic=['MEAN']),
+      ['NOT_SUPPORTED'], ['Datasource "Alpha" did not have matches for attr_type STATISTIC and BASIN-3D vocab ESTIMATED.']),
+     # compound-simple_query
+     ('observed_property', 'ACT', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['ACT'], start_date='2020-01-01'),
+      ['Acetate'], []),
+     # compound-simple_query-multimap
+     ('observed_property', 'Al', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Al'], start_date='2020-01-01'),
+      ['Al', 'Aluminum'], []),
+     # compound-compound_query-multimap
+     ('observed_property', 'Al', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Al'], start_date='2020-01-01', sampling_medium=['WATER']),
+      ['Al', 'Aluminum'], []),
+     # compound-compound_query
+     ('observed_property', 'Ag', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Ag'], start_date='2020-01-01', sampling_medium=['WATER']),
+      ['Ag'], []),
+     # compound-compound_query-no_compound_match
+     ('observed_property', 'Al', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Al'], start_date='2020-01-01', sampling_medium=['GAS']),
+      ['NOT_SUPPORTED'], ['Datasource "Alpha" did not have matches for attr_type OBSERVED_PROPERTY:SAMPLING_MEDIUM and BASIN-3D vocab Al:GAS.']),
+     # compound-compound_query_lists
+     ('observed_property', 'Ag', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Ag', 'Al'], start_date='2020-01-01', sampling_medium=['WATER', 'GAS']),
+      ['Ag', 'Ag_gas'], []),
+     # compound-compound_query_no_match
+     ('observed_property', 'Hg', QueryMeasurementTimeseriesTVP(monitoring_feature=['A-1'], observed_property=['Ag', 'Al', 'Hg'], start_date='2020-01-01'),
+      ['NOT_SUPPORTED'], ['Datasource "Alpha" did not have matches for attr_type OBSERVED_PROPERTY:SAMPLING_MEDIUM and BASIN-3D vocab Hg:.*.']),
+     # non-compound_non-query-class
+     ('statistic', 'MEAN', {'statistic': 'MEAN'}, ['mean'], []),
+     # compound-simple_query_non-query-class
+     ('observed_property', 'ACT', {'observed_property': 'ACT'}, ['Acetate'], []),
+     # compound-compound_query_non-query-class-lists
+     ('observed_property', 'Ag', {'observed_property': ['Ag'], 'sampling_medium': ['WATER']}, ['Ag'], []),
+     # compound_non-query-class_str-value
+     ('observed_property', 'Ag', {'observed_property': 'Ag'}, ['Ag', 'Ag_gas'], []),
+     ],
+    ids=['non-compound', 'non-compound-no-match', 'compound-simple_query', 'compound-simple_query-multimap',
+         'compound-compound_query-multimap', 'compound-compound_query', 'compound-compound_query-no_compound_match',
+         'compound-compound_query_lists', 'compound-compound_query_no_match',
+         'non-compound_non-query-class', 'compound-simple_query_non-query-class',
+         'compound-compound_query_non-query-class-lists', 'compound_non-query-class_str-value'])
+def test_find_datasource_vocab(alpha_plugin_access, caplog, attr_type, basin3d_vocab, basin3d_query, expected_results, expected_msgs):
+    caplog.set_level(logging.INFO)
+    caplog.clear()
+
+    results = translate._translate_to_datasource_vocab(alpha_plugin_access, attr_type, basin3d_vocab, basin3d_query)
+    assert sorted(results) == sorted(expected_results)
+
+    if expected_msgs:
+        log_msgs = [rec.message for rec in caplog.records]
+        for msg in expected_msgs:
+            assert msg in log_msgs
+
+
+@pytest.mark.parametrize('ds, query, expected_results, set_transformed_attr',
+                         [
+                          # single-compound
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Ag_gas'], sampling_medium=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # both-compound
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], sampling_medium=['GAS'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag_gas'], sampling_medium=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # single-compound+non-compound
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], statistic=['MEAN'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Ag_gas'], statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY'], 'statistic': ['mean']}),
+                          # not_supported
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], statistic=['INSTANT'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Ag_gas'], statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY'], 'statistic': [NO_MAPPING_TEXT]}),
+                          # not_supported-opv
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'RDC'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Ag_gas', NO_MAPPING_TEXT], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # multi-mapped
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Aluminum', 'Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # multi-mapped-complex
+                          ('Alpha',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Al'], sampling_medium=['WATER'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Aluminum', 'Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-single-compound
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Ag', 'Min Ag_gas'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-two-compound
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], sampling_medium=['GAS'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Min Ag_gas'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-other-two-compound
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Al'], statistic=['MEAN'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Aluminum', 'Mean Al'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-all-compound
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], sampling_medium=['GAS'], statistic=['MIN'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Min Ag_gas'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-all-compound-no-mapping
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag'], sampling_medium=['WATER'], statistic=['MIN'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=[NO_MAPPING_TEXT], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          # complex-all-compound-multiple-values
+                          ('Complexmap',
+                           QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Al'], sampling_medium=['WATER', 'GAS'], statistic=['MIN', 'MEAN'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Ag', 'Min Ag_gas', 'Minimum Aluminum', 'Mean Aluminum', 'Mean Al'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': ['DAY']}),
+                          ],
+                         ids=['single-compound', 'both-compound', 'single-compound+non-compound', 'not_supported', 'not_supported-opv', 'multi-mapped', 'multi-mapped-complex',
+                              'complex-single-compound', 'complex-two-compound', 'complex-other-two-compound', 'complex-all-compound', 'complex-all-compound-no-mapping',
+                              'complex-all-compound-multiple-values'])
+def test_translator_translate_mapped_query_attrs(alpha_plugin_access, complex_plugin_access, ds, query, expected_results, set_transformed_attr):
+    plugin_access = complex_plugin_access if ds == 'Complexmap' else alpha_plugin_access
+    results = translate._translate_mapped_query_attrs(plugin_access, query)
+    for attr, value in set_transformed_attr.items():
+        setattr(expected_results, attr, value)
+    assert results == expected_results
+
+
+def test_translator_order_mapped_fields(alpha_plugin_access):
+    ordered_fields = translate._order_mapped_fields(alpha_plugin_access, ['statistic', 'aggregation_duration', 'sampling_medium', 'observed_property'])
+    assert ordered_fields == ['observed_property', 'sampling_medium', 'statistic', 'aggregation_duration']
+
+
+@pytest.mark.parametrize('query, set_translated_attr, expected_result',
+                         [(QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {}, True),
+                          (QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'observed_property': [NO_MAPPING_TEXT]}, False),
+                          (QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'observed_property': ['Ag', NO_MAPPING_TEXT]}, True),
+                          (QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'start_date': NO_MAPPING_TEXT}, True),
+                          (QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': NO_MAPPING_TEXT}, False),
+                          (QueryMeasurementTimeseriesTVP(observed_property=['Ag'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           {'aggregation_duration': {'key', 'value'}}, None),
+                          ],
+                         ids=['valid', 'invalid_only-list-not_supported', 'valid-2', 'valid-not-mapped-field',
+                              'invalid-single-not-supported', 'invalid_attr_value_type'])
+def test_translator_is_translated_query_valid(query, set_translated_attr, expected_result):
+    translated_query = query.copy()
+    for attr, value in set_translated_attr.items():
+        setattr(translated_query, attr, value)
+    assert translate._is_translated_query_valid('Alpha', query, translated_query) is expected_result
+
+
+@pytest.mark.parametrize("query, set_translated_attr",
+                         [(QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237'], observed_property=['Ag'], start_date='2019-01-01'),
+                           {'monitoring_feature': ['9237']}),
+                          (QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237', 'R-8e3838', 'A-00000'], observed_property=['Ag'], start_date='2019-01-01'),
+                           {'monitoring_feature': ['9237', '00000']}),
+                          (QueryMeasurementTimeseriesTVP(monitoring_feature=['F-9237'], observed_property=['Ag'], start_date='2019-01-01'),
+                           {'monitoring_feature': []}),
+                          (QueryMonitoringFeature(monitoring_feature=['A-9237', 'R-8e3838'], parent_feature=['A-00000']),
+                           {'monitoring_feature': ['9237'], 'parent_feature': ['00000']}),
+                          ],
+                         ids=["single", "multiple", "none", "monitoring_feature_query"])
+def test_translator_prefixed_query_attrs(alpha_plugin_access, query, set_translated_attr):
+    """Filtering of query arguments"""
+    translated_query = query.copy()
+    for attr, value in set_translated_attr.items():
+        setattr(translated_query, attr, value)
+    assert translate._translate_prefixed_query_attrs(alpha_plugin_access, query) == translated_query
+
+
+@pytest.mark.parametrize('query, set_translated_attr, set_cleaned_query_attr',
+                         [(QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237'], observed_property=['Ag'], start_date='2019-01-01'),
+                           {}, {}),
+                          (QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237'], observed_property=['Ag', NO_MAPPING_TEXT], start_date='2019-01-01'),
+                           {}, {'observed_property': ['Ag']}),
+                          (QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237'], observed_property=[NO_MAPPING_TEXT, NO_MAPPING_TEXT], start_date='2019-01-01'),
+                           {}, {'observed_property': []}),
+                          (QueryMeasurementTimeseriesTVP(monitoring_feature=['A-9237'], observed_property=['Ag'], start_date='2019-01-01'),
+                           {'observed_property': NO_MAPPING_TEXT}, {'observed_property': None}),
+                          ],
+                         ids=['no-change', 'rm-one-list', 'rm-all-list', 'rm-string'])
+def test_translator_clean_query(query, set_translated_attr, set_cleaned_query_attr):
+    translated_query = query.copy()
+    cleaned_query = query.copy()
+    for attr, value in set_translated_attr.items():
+        setattr(translated_query, attr, value)
+    for attr, value in set_cleaned_query_attr.items():
+        setattr(cleaned_query, attr, value)
+    assert translate._clean_query(translated_query) == cleaned_query
diff --git a/tests/test_plugins_usgs.py b/tests/test_plugins_usgs.py
index 32b1e21..a45308a 100644
--- a/tests/test_plugins_usgs.py
+++ b/tests/test_plugins_usgs.py
@@ -20,6 +20,7 @@ def get_url(data, status=200):
     """
     Creates a get_url call for mocking with the specified return data
     :param data:
+    :param status:
     :return:
     """
     return type('Dummy', (object,), {
@@ -32,6 +33,7 @@ def get_url_text(text, status=200):
     """
     Creates a get_url_text call for mocking with the specified return data
     :param text:
+    :param status:
     :return:
     """
 
@@ -42,8 +44,8 @@ def get_url_text(text, status=200):
 
 
 @pytest.mark.parametrize('additional_query_params',
-                         [({"monitoring_features": ["USGS-09110990", "USGS-09111250"], "observed_property_variables": []}),
-                          ({"observed_property_variables": ["RDC"]})],
+                         [({"monitoring_feature": ["USGS-09110990", "USGS-09111250"], "observed_property": []}),
+                          ({"observed_property": ["RDC"]})],
                          ids=['missing-variables', 'missing-monitoring_features'])
 def test_measurement_timeseries_tvp_observations_usgs_errors(additional_query_params, monkeypatch):
     """ Test USGS Timeseries data query"""
@@ -68,18 +70,18 @@ def test_measurement_timeseries_tvp_observations_usgs_errors(additional_query_pa
 @pytest.mark.parametrize('additional_filters, usgs_response, expected_results',
                          [
                           # all-good
-                          ({"monitoring_features": ["USGS-09110990", "USGS-09111250"], "observed_property_variables": ["RDC"], "result_quality": [ResultQualityEnum.VALIDATED]},
+                          ({"monitoring_feature": ["USGS-09110990", "USGS-09111250"], "observed_property": ["RDC"], "result_quality": [ResultQualityEnum.VALIDATED]},
                            "usgs_nwis_dv_p00060_l09110990_l09111250.json", {"statistic": StatisticEnum.MEAN, "result_quality": [ResultQualityEnum.VALIDATED], "count": 2}),
                           # some-quality-filtered-data
-                          ({"monitoring_features": ["USGS-09110990"], "observed_property_variables": ["WT"], "result_quality": [ResultQualityEnum.UNVALIDATED]},
+                          ({"monitoring_feature": ["USGS-09110990"], "observed_property": ["WT"], "result_quality": [ResultQualityEnum.UNVALIDATED]},
                            "usgs_get_data_09110000_VALIDATED_UNVALIDATED_WT_only.json",
                            {"statistic": StatisticEnum.MEAN, "result_quality": [ResultQualityEnum.UNVALIDATED], "count": 1, "synthesis_msgs": ['09110000 - 00010: 2 timestamps did not match data quality query.']}),
                           # all-data-filtered
-                          ({"monitoring_features": ["USGS-09110990"], "observed_property_variables": ["WT"], "result_quality": [ResultQualityEnum.REJECTED]},
+                          ({"monitoring_feature": ["USGS-09110990"], "observed_property": ["WT"], "result_quality": [ResultQualityEnum.REJECTED]},
                            "usgs_get_data_09110000_VALIDATED_UNVALIDATED_WT_only.json",
-                           {"statistic": StatisticEnum.MEAN, "result_quality": [], "count": 1, "synthesis_msgs": ['09110000 - 00010: 4 timestamps did not match data quality query.', '09110000 had no valid data values for 00010 that match the query.']})
+                           {"count": 0, "synthesis_msgs": []})
                          ],
-                         ids=['all-good', 'some-quality-filtered-data', 'all-data-filtered'])
+                         ids=['all-good', 'some-quality-filtered-data', 'missing-mapping'])
 def test_measurement_timeseries_tvp_observations_usgs(additional_filters, usgs_response, expected_results, monkeypatch):
     """ Test USGS Timeseries data query"""
 
@@ -104,8 +106,9 @@ def test_measurement_timeseries_tvp_observations_usgs(additional_filters, usgs_r
         for timeseries in measurement_timeseries_tvp_observations:
             data = json.loads(timeseries.to_json())
             count += 1
-            assert data["statistic"] == expected_results.get("statistic")
-            assert data["result_quality"] == expected_results.get("result_quality")
+            assert data["statistic"]["attr_mapping"]["basin3d_vocab"] == expected_results.get("statistic")
+            for idx, result_quality in enumerate(data["result_quality"]):
+                assert result_quality["attr_mapping"]["basin3d_vocab"] == expected_results.get("result_quality")[idx]
         assert count == expected_results.get("count")
         if expected_results.get('synthesis_msgs'):
             expected_msgs = expected_results.get('synthesis_msgs')
@@ -161,7 +164,7 @@ def test_usgs_monitoring_feature2(query, feature_type, monkeypatch):
 
 
 @pytest.mark.parametrize("query, expected_count", [({"datasource": "USGS"}, 2889),
-                                                   ({"monitoring_features": ['USGS-02']}, 1),
+                                                   ({"monitoring_feature": ['USGS-02']}, 1),
                                                    ({"feature_type": "region"}, 21),
                                                    ({"feature_type": "subregion"}, 222),
                                                    ({"feature_type": "basin"}, 379),
@@ -172,8 +175,8 @@ def test_usgs_monitoring_feature2(query, feature_type, monkeypatch):
                                                    ({"feature_type": "plot"}, 0),
                                                    ({"feature_type": "vertical path"}, 0),
                                                    ({"feature_type": "horizontal path"}, 0),
-                                                   ({"parent_features": ['USGS-02']}, 118),
-                                                   ({"parent_features": ['USGS-0202'], "feature_type": "subbasin"}, 8)],
+                                                   ({"parent_feature": ['USGS-02']}, 118),
+                                                   ({"parent_feature": ['USGS-0202'], "feature_type": "subbasin"}, 8)],
                          ids=["all", "region_by_id", "region", "subregion", "basin", "subbasin", "watershed", "subwatershed",
                               "site", "plot", "vertical_path", "horizontal_path", "all_by_region", "subbasin_by_subregion"])
 def test_usgs_monitoring_features(query, expected_count, monkeypatch):
@@ -199,8 +202,7 @@ def test_usgs_monitoring_features(query, expected_count, monkeypatch):
     assert count == expected_count
 
 
-@pytest.mark.parametrize("query, expected_count", [
-    ({"parent_features": ['USGS-02020004'], "feature_type": "point"}, 52)],
+@pytest.mark.parametrize("query, expected_count", [({"parent_feature": ['USGS-02020004'], "feature_type": "point"}, 52)],
                          ids=["points_by_subbasin"])
 def test_usgs_monitoring_features2(query, expected_count, monkeypatch):
     """Test USGS search by region  """
@@ -224,7 +226,7 @@ def test_usgs_monitoring_features2(query, expected_count, monkeypatch):
     assert count == expected_count
 
 
-@pytest.mark.parametrize("query, expected_count", [({"monitoring_features": ["USGS-09129600"], "feature_type": "point"}, 1)],
+@pytest.mark.parametrize("query, expected_count", [({"monitoring_feature": ["USGS-09129600"], "feature_type": "point"}, 1)],
                          ids=["point_by_id"])
 def test_usgs_monitoring_features3(query, expected_count, monkeypatch):
     """Test USGS search by region  """
@@ -249,8 +251,8 @@ def test_usgs_monitoring_features3(query, expected_count, monkeypatch):
 
 
 @pytest.mark.parametrize("query, expected_count", [({"feature_type": "point"}, 0),
-                                                  ({"parent_features": ['USGS-020200'], "feature_type": "point"}, 0)],
-                        ids=["point", "invalid_points"])
+                                                   ({"parent_feature": ['USGS-020200'], "feature_type": "point"}, 0)],
+                         ids=["point", "invalid_points"])
 def test_usgs_monitoring_features_invalid_query(query, expected_count, monkeypatch):
     """Test USGS search by region  """
 
diff --git a/tests/test_synthesis.py b/tests/test_synthesis.py
index ff81217..16dd3a4 100644
--- a/tests/test_synthesis.py
+++ b/tests/test_synthesis.py
@@ -1,8 +1,9 @@
 import datetime
-
+import logging
 import pytest
 
 import basin3d
+from basin3d.core.catalog import CatalogException
 from basin3d.core.schema.query import QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
 from basin3d.core.synthesis import DataSourceModelIterator
 from basin3d.synthesis import register, SynthesisException
@@ -36,7 +37,6 @@ def test_register_implicit(monkeypatch):
     assert datasources[0].location == 'https://waterservices.usgs.gov/nwis/'
 
 
-
 def test_register():
     """Test basic plugin registration"""
 
@@ -79,8 +79,9 @@ def test_monitoring_features_found():
                                                                   'messages': [],
                                                                   'query': {'datasource': None,
                                                                             'feature_type': None,
-                                                                            'monitoring_features': None,
-                                                                            'parent_features': None}}
+                                                                            'is_valid_translated_query': None,
+                                                                            'monitoring_feature': None,
+                                                                            'parent_feature': None}}
         assert isinstance(monitoring_featurues.synthesis_response.query, QueryMonitoringFeature)
 
         for mf in monitoring_featurues:
@@ -102,8 +103,9 @@ def test_monitoring_features_found():
                                                                                           'MonitoringFeature']}],
                                                                   'query': {'datasource': None,
                                                                             'feature_type': None,
-                                                                            'monitoring_features': None,
-                                                                            'parent_features': None}}
+                                                                            'is_valid_translated_query': None,
+                                                                            'monitoring_feature': None,
+                                                                            'parent_feature': None}}
 
         assert count == 2
     else:
@@ -124,70 +126,61 @@ def test_measurement_timeseries_tvp_observations_count():
 
     synthesizer = register(['tests.testplugins.alpha.AlphaSourcePlugin'])
     measurement_timeseries_tvp_observations = synthesizer.measurement_timeseries_tvp_observations(
-        monitoring_features=['test'], observed_property_variables=['test'], start_date='2016-02-01')
+        monitoring_feature=['A-3'], observed_property=['Al'], start_date='2016-02-01')
     if isinstance(measurement_timeseries_tvp_observations, DataSourceModelIterator):
         count = 0
         assert measurement_timeseries_tvp_observations.synthesis_response is not None
         assert measurement_timeseries_tvp_observations.synthesis_response.query is not None
         assert isinstance(measurement_timeseries_tvp_observations.synthesis_response.query,
                           QueryMeasurementTimeseriesTVP)
-        assert measurement_timeseries_tvp_observations.synthesis_response.query.monitoring_features == ['test']
-        assert measurement_timeseries_tvp_observations.synthesis_response.query.observed_property_variables == ['test']
+        assert measurement_timeseries_tvp_observations.synthesis_response.query.monitoring_feature == ['A-3']
+        assert measurement_timeseries_tvp_observations.synthesis_response.query.observed_property == ['Al']
         assert measurement_timeseries_tvp_observations.synthesis_response.query.start_date == datetime.date(2016, 2, 1)
 
         for mf in measurement_timeseries_tvp_observations:
             count += 1
 
-        assert count == 4
+        assert count == 1
     else:
         assert measurement_timeseries_tvp_observations is not None
 
 
 @pytest.mark.parametrize("plugins, query, expected_count",
-                         [(['basin3d.plugins.usgs.USGSDataSourcePlugin'], {}, 43),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'USGS'}, 43),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha'}, 4),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'FOO'}, 0)
+                         [(['basin3d.plugins.usgs.USGSDataSourcePlugin'], {}, 53),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'USGS'}, 53),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha'}, 14),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha', 'attr_type': 'OBSERVED_PROPERTY'}, 6),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': ['Ag']}, 1),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': ['Ag'], 'from_basin3d': True}, 2),
+                          (['basin3d.plugins.usgs.USGSDataSourcePlugin', 'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'FOO'}, -1)
                           ],
-                         ids=['USGS-only', 'USGS-plus', 'Alpha-plus', 'Bad-DataSource'])
-def test_observed_properties(plugins, query, expected_count):
-    """Test observed properties search"""
+                         ids=['USGS-only', 'USGS-plus', 'Alpha-plus', 'Alpha-OP', 'Alpha-OP-Ag-datasource', 'Alpha-OP-Ag-basin3d', 'Bad-DataSource'])
+def test_attribute_mappings(plugins, query, expected_count):
+    """Test attribute_mappings search"""
 
     synthesizer = register(plugins)
-    observed_properties = synthesizer.observed_properties(**query)
+    results = synthesizer.attribute_mappings(**query)
 
-    # TODO are there other things to test?
-    count = 0
-    for op in observed_properties:
-        print(op)
-        count += 1
+    if expected_count > 0:
+        count = 0
+        for attr_mapping in results:
+            count += 1
 
-    assert count == expected_count
+        assert count == expected_count
 
+    else:
+        with pytest.raises(CatalogException):
+            for attr_mapping in results:
+                pass
 
-@pytest.mark.parametrize("plugins, query, expected_count",
-                         [(['basin3d.plugins.usgs.USGSDataSourcePlugin'], {}, 168),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'USGS'}, 43),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'Alpha'}, 4),
-                          (['basin3d.plugins.usgs.USGSDataSourcePlugin',
-                            'tests.testplugins.alpha.AlphaSourcePlugin'], {"datasource_id": 'FOO'}, 0)
-                          ],
-                         ids=['USGS-only', 'USGS-plus', 'Alpha-plus', 'Bad-DataSource'])
-def test_observed_property_variables(plugins, query, expected_count):
-    """ Test observed property variables """
 
-    synthesizer = register(plugins)
-    observed_properties = synthesizer.observed_property_variables(**query)
+def test_observed_properties():
+    """Test observed_properties method"""
+    synthesizer = register(['tests.testplugins.alpha.AlphaSourcePlugin'])
+    results = synthesizer.observed_properties()
 
-    # TODO are there other things to test?
     count = 0
-    for op in observed_properties:
-        print(op)
+    for op in results:
         count += 1
 
-    assert count == expected_count
+    assert count == 168
diff --git a/tests/testplugins/alpha.py b/tests/testplugins/alpha.py
index 10a7836..f0383c5 100644
--- a/tests/testplugins/alpha.py
+++ b/tests/testplugins/alpha.py
@@ -5,7 +5,7 @@
     GeographicCoordinate, MeasurementTimeseriesTVPObservation, MonitoringFeature, RelatedSamplingFeature, \
     RepresentativeCoordinate, SpatialSamplingShapes, VerticalCoordinate, ResultListTVP
 from basin3d.core.plugin import DataSourcePluginPoint, basin3d_plugin, basin3d_plugin_access
-from basin3d.core.schema.enum import FeatureTypeEnum, TimeFrequencyEnum, ResultQualityEnum
+from basin3d.core.schema.enum import FeatureTypeEnum, TimeFrequencyEnum
 from basin3d.core.schema.query import QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
 
 logger = logging.getLogger(__name__)
@@ -56,16 +56,26 @@ def find_measurement_timeseries_tvp_observations(self, query: QueryMeasurementTi
     data: List[Any] = []
     quality: List[Any] = []
 
-    if query.monitoring_features == ['region']:
+    if query.monitoring_feature == ['region']:
         return StopIteration({"message": "FOO"})
 
+    supported_monitoring_features = [f'{num}' for num in range(1, 5)]
+
+    if not any([loc_id in supported_monitoring_features for loc_id in query.monitoring_feature]):
+        return StopIteration({"message": "No data from data source matches monitoring features specified."})
+
+    location_indices = []
+    for loc_id in query.monitoring_feature:
+        if loc_id in supported_monitoring_features:
+            location_indices.append(int(loc_id.split('-')[-1]))
+
     from datetime import datetime
     for num in range(1, 10):
         data.append((datetime(2016, 2, num), num * 0.3454))
     data = [data, data, [], data]
-    rqe1 = ResultQualityEnum.VALIDATED
-    rqe2 = ResultQualityEnum.UNVALIDATED
-    rqe3 = ResultQualityEnum.REJECTED
+    rqe1 = 'VALIDATED'
+    rqe2 = 'UNVALIDATED'
+    rqe3 = 'REJECTED'
     quality = [[rqe1, rqe1, rqe1, rqe1, rqe1, rqe1, rqe1, rqe1, rqe1],
                [rqe2, rqe2, rqe2, rqe2, rqe2, rqe2, rqe2, rqe3, rqe3],
                [],
@@ -74,17 +84,16 @@ def find_measurement_timeseries_tvp_observations(self, query: QueryMeasurementTi
                  [rqe2, rqe3],
                  [],
                  [rqe1, rqe2, rqe3]]
-    observed_property_variables = ["Acetate", "Acetate", "Aluminum", "Aluminum"]
+    observed_property_variables = ["Acetate", "Acetate", "Aluminum", "Al"]
     units = ['nm', 'nm', 'mg/L', 'mg/L']
-    statistics = ['MEAN', 'MAX', 'MEAN', 'MAX']
+    statistics = ['mean', 'max', 'mean', 'max']
 
-    for num in range(1, 5):
+    for num in location_indices:
         observed_property_variable = observed_property_variables[num - 1]
         feature_id = f'A-{str(num - 1)}'
         if query:
-            if query.monitoring_features:
-                if str(num) not in query.monitoring_features:
-                    continue
+            if observed_property_variable not in query.observed_property:
+                continue
             if query.statistic:
                 if statistics[num - 1] not in query.statistic:
                     continue
@@ -121,7 +130,7 @@ def find_measurement_timeseries_tvp_observations(self, query: QueryMeasurementTi
         yield MeasurementTimeseriesTVPObservation(
             plugin_access=self,
             id=num,
-            observed_property_variable=observed_property_variable,
+            observed_property=observed_property_variable,
             utc_offset=-8 - num,
             feature_of_interest=MonitoringFeature(
                 plugin_access=self,
@@ -146,7 +155,7 @@ def find_measurement_timeseries_tvp_observations(self, query: QueryMeasurementTi
                             distance_units=VerticalCoordinate.DISTANCE_UNITS_METERS)
                     )
                 ),
-                observed_property_variables=["Ag", "Acetate", "Aluminum"],
+                observed_properties=["Ag", "Acetate", "Aluminum", "Al"],
                 related_sampling_feature_complex=[
                     RelatedSamplingFeature(plugin_access=self,
                                            related_sampling_feature="Region1",
@@ -159,7 +168,7 @@ def find_measurement_timeseries_tvp_observations(self, query: QueryMeasurementTi
             result_quality=result_qualities,
             time_reference_position=None,
             statistic=statistics[num - 1],
-            result=ResultListTVP(value=result_value, quality=result_value_quality)
+            result=ResultListTVP(plugin_access=self, value=result_value, result_quality=result_value_quality)
         )
 
     return StopIteration(synthesis_messages)
@@ -230,7 +239,7 @@ def list_monitoring_features(self, query: QueryMonitoringFeature):
                     distance_units=VerticalCoordinate.DISTANCE_UNITS_METERS)
             )
         ),
-        observed_property_variables=["Ag", "Acetate"],
+        observed_properties=["Ag", "Acetate"],
         related_sampling_feature_complex=[
             RelatedSamplingFeature(plugin_access=self,
                                    related_sampling_feature="Region1",
diff --git a/tests/testplugins/alpha_mapping.csv b/tests/testplugins/alpha_mapping.csv
new file mode 100644
index 0000000..c09bf74
--- /dev/null
+++ b/tests/testplugins/alpha_mapping.csv
@@ -0,0 +1,15 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,ACT:WATER,Acetate,acetate
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ag:WATER,Ag,sliver concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Ag:GAS,Ag_gas,silver concentration vaporized (bogus)
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Al:WATER,Aluminum,aluminum concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,Al:WATER,Al,aluminum (Al) concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM,As:SOLID_PHASE,As,arsenic in soil
+STATISTIC,MEAN,mean,
+STATISTIC,MAX,max,
+STATISTIC,MIN,min,
+RESULT_QUALITY,VALIDATED,VALIDATED,
+RESULT_QUALITY,UNVALIDATED,UNVALIDATED,
+RESULT_QUALITY,REJECTED,REJECTED,
+AGGREGATION_DURATION,DAY,DAY,
+AGGREGATION_DURATION,NONE,NONE,
diff --git a/tests/testplugins/beta_mapping.csv b/tests/testplugins/beta_mapping.csv
new file mode 100644
index 0000000..05cd2b8
--- /dev/null
+++ b/tests/testplugins/beta_mapping.csv
@@ -0,0 +1,3 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+STAT,MIN,min,
+STATISTIC,MAXIMUM,max
\ No newline at end of file
diff --git a/tests/testplugins/complexmap.py b/tests/testplugins/complexmap.py
new file mode 100644
index 0000000..9736f69
--- /dev/null
+++ b/tests/testplugins/complexmap.py
@@ -0,0 +1,9 @@
+from basin3d.core.plugin import DataSourcePluginPoint
+
+
+class ComplexmapSourcePlugin(DataSourcePluginPoint):
+    class DataSourceMeta:
+        location = 'https://asource.foo/'
+        id = 'Complexmap'  # unique id for the datasource
+        id_prefix = 'C'
+        name = id  # Human Friendly Data Source Name
diff --git a/tests/testplugins/complexmap_mapping.csv b/tests/testplugins/complexmap_mapping.csv
new file mode 100644
index 0000000..3921598
--- /dev/null
+++ b/tests/testplugins/complexmap_mapping.csv
@@ -0,0 +1,13 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,ACT:WATER:MEAN,Mean acetate,acetate
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,ACT:WATER:MIN,Min acetate,acetate
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,Ag:WATER:MEAN,Mean Ag,sliver concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,Ag:GAS:MIN,Min Ag_gas,silver concentration vaporized (bogus)
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,Al:WATER:MEAN,Mean Aluminum,aluminum concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,Al:WATER:MIN,Minimum Aluminum,aluminum concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,Al:WATER:MEAN,Mean Al,aluminum (Al) concentration in water
+OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC,As:SOLID_PHASE:MEAN,Mean As,arsenic in soil
+RESULT_QUALITY,VALIDATED,VALIDATED,
+RESULT_QUALITY,UNVALIDATED,UNVALIDATED,
+RESULT_QUALITY,REJECTED,REJECTED,
+AGGREGATION_DURATION,DAY,DAY,
\ No newline at end of file
diff --git a/tests/testplugins/error_mapping.csv b/tests/testplugins/error_mapping.csv
new file mode 100644
index 0000000..71e1ea3
--- /dev/null
+++ b/tests/testplugins/error_mapping.csv
@@ -0,0 +1,7 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+OBSERVED_PROPERTY,ACT,Acetate,
+OBSERVED_PROPERTY,Ag,Ag,
+OBSERVED_PROPERTY,Ag,Ag_gas,
+OBSERVED_PROPERTY,Al,Aluminum,
+OBSERVED_PROPERTY,As,As,
+AGGREGATION_DURATION,DAY,DAY,
\ No newline at end of file
diff --git a/tests/testplugins/mapping_alpha.csv b/tests/testplugins/mapping_alpha.csv
deleted file mode 100644
index 4b4a162..0000000
--- a/tests/testplugins/mapping_alpha.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-basin3d_id,datasource_variable,sampling_medium,description
-ACT,Acetate,WATER,
-Ag,Ag,WATER,
-Ag,Ag_gas,GAS,
-Al,Aluminum,WATER,
-As,As,DIRT,
\ No newline at end of file
diff --git a/tests/testplugins/mapping_alpha_wrong_header.csv b/tests/testplugins/mapping_alpha_wrong_header.csv
index 49bbe09..975bbdd 100644
--- a/tests/testplugins/mapping_alpha_wrong_header.csv
+++ b/tests/testplugins/mapping_alpha_wrong_header.csv
@@ -1,5 +1,5 @@
-the_id,datasource_name,sampling_medium,description
-ACT,Acetate,WATER,
-Ag,Ag,WATER,
-Al,Al,WATER,
-As,As,WATER,
\ No newline at end of file
+attr_type,the_id,datasource_vocab,datasource_description
+OBSERVED_PROPERTY,ACT,Acetate,
+OBSERVED_PROPERTY,Ag,Ag,
+OBSERVED_PROPERTY,Al,Al,
+OBSERVED_PROPERTY,As,As,'As description'
\ No newline at end of file
diff --git a/tests/testplugins/mapping_error.csv b/tests/testplugins/mapping_error.csv
deleted file mode 100644
index 4b4a162..0000000
--- a/tests/testplugins/mapping_error.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-basin3d_id,datasource_variable,sampling_medium,description
-ACT,Acetate,WATER,
-Ag,Ag,WATER,
-Ag,Ag_gas,GAS,
-Al,Aluminum,WATER,
-As,As,DIRT,
\ No newline at end of file
diff --git a/tests/testplugins/mapping_nopluginview.csv b/tests/testplugins/mapping_nopluginview.csv
deleted file mode 100644
index 4b4a162..0000000
--- a/tests/testplugins/mapping_nopluginview.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-basin3d_id,datasource_variable,sampling_medium,description
-ACT,Acetate,WATER,
-Ag,Ag,WATER,
-Ag,Ag_gas,GAS,
-Al,Aluminum,WATER,
-As,As,DIRT,
\ No newline at end of file
diff --git a/tests/testplugins/no_plugin_views.py b/tests/testplugins/no_plugin_views.py
index 912e624..8e3de83 100644
--- a/tests/testplugins/no_plugin_views.py
+++ b/tests/testplugins/no_plugin_views.py
@@ -1,12 +1,6 @@
 import logging
-from typing import Any, List
 
-from basin3d.core.models import AbsoluteCoordinate, AltitudeCoordinate, Coordinate, DepthCoordinate, \
-    GeographicCoordinate, MeasurementTimeseriesTVPObservation, MonitoringFeature, RelatedSamplingFeature, \
-    RepresentativeCoordinate, SpatialSamplingShapes, VerticalCoordinate
 from basin3d.core.plugin import DataSourcePluginPoint, basin3d_plugin, basin3d_plugin_access
-from basin3d.core.schema.enum import FeatureTypeEnum, TimeFrequencyEnum
-from basin3d.core.schema.query import QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
 
 logger = logging.getLogger(__name__)
 
diff --git a/tests/testplugins/nopluginview_mapping.csv b/tests/testplugins/nopluginview_mapping.csv
new file mode 100644
index 0000000..397e449
--- /dev/null
+++ b/tests/testplugins/nopluginview_mapping.csv
@@ -0,0 +1,7 @@
+attr_type,basin3d_vocab,datasource_vocab,datasource_desc
+OBSERVED_PROPERTY,ACT,Acetate,
+OBSERVED_PROPERTY,Ag,Ag,
+OBSERVED_PROPERTY,Ag,Ag_gas,
+OBSERVED_PROPERTY,Al,Aluminum,
+OBSERVED_PROPERTY,As,As,
+AGGREGATION_DURATION,DAY,DAY
\ No newline at end of file
diff --git a/tests/testplugins/plugin_error.py b/tests/testplugins/plugin_error.py
index 7a7d1f0..e1a8e7a 100644
--- a/tests/testplugins/plugin_error.py
+++ b/tests/testplugins/plugin_error.py
@@ -1,11 +1,7 @@
 import logging
-from typing import Any, List
 
-from basin3d.core.models import AbsoluteCoordinate, AltitudeCoordinate, Coordinate, DepthCoordinate, \
-    GeographicCoordinate, MeasurementTimeseriesTVPObservation, MonitoringFeature, RelatedSamplingFeature, \
-    RepresentativeCoordinate, SpatialSamplingShapes, VerticalCoordinate
+from basin3d.core.models import MeasurementTimeseriesTVPObservation, MonitoringFeature
 from basin3d.core.plugin import DataSourcePluginPoint, basin3d_plugin, basin3d_plugin_access
-from basin3d.core.schema.enum import FeatureTypeEnum, TimeFrequencyEnum
 from basin3d.core.schema.query import QueryById, QueryMeasurementTimeseriesTVP, QueryMonitoringFeature
 
 logger = logging.getLogger(__name__)