code: def explore_properties(
      jsonschema_properties: Dict[str, Any],
      parent_parameter_path: str,
      dq: Deque[ConfigOption],
      allow_list: List[str] = [],
  ) -> Deque[Tuple[Dict, bool]]:
      """Recursively explores the `properties` part of any subsection of the schema.
  
      Args:
          jsonschema_properties: any properties section of the schema.
          parent_parameter_path: period-delimited list of parent dictionary keys up to the given jsonschema_properties
              (e.g. defaults.number.preprocessing)
          dq: dequeue data structure that stores tuples of (config_options, fully_explored).
              config_options: Dict[str, List], fully_explored: bool is a dictionary is a dictionary of parameter name to
              list of values to explore.
              fully_explored is a boolean value indicating whether all subsections of the properties dictionary have been
              explored.
          allow_list: list of top level keys of the properties dictionary to skip.
  
      Returns:
          A deque of (dict, bool) tuples.
          - The first element of the tuple contains a dictionary of config options, which maps from a ludwig
              config parameter to a list of the values to be explored for that parameter. Here's an example:
                  trainer.batch_size: ["auto", 2, 43]
                  trainer.learning_rate: ["auto", 0.1, 0.00002, 0.32424]
                  ...
          - The second element of the tuple is whether we've explored this "config path"
              fully. This is important to track when recursing into nested structures.
      """
      # processed_dq will contain complete config options with all the parameters in the properties dictionary
      # dq will contain configs options that are still being completed.
      processed_dq = deque()
      while dq and not dq[0].fully_explored:
          for (
              parameter_name_or_section,
              jsonschema_property,
          ) in jsonschema_properties.items():
              if (
                  allow_list
                  and parameter_name_or_section not in allow_list
              ):
                  continue
  
              parameter_path = (
                  f"{parent_parameter_path}.{parameter_name_or_section}"
                  if parent_parameter_path
                  else parameter_name_or_section
              )
              config_options, _ = dq.popleft()
  
              if (
                  "properties" in jsonschema_property
                  and "allOf" in jsonschema_property
              ):
                  for child_item in jsonschema_property["allOf"]:
                      expanded_config_options_dq = explore_from_all_of(
                          config_options=copy.deepcopy(config_options),
                          item=child_item,
                          key_so_far=parameter_path,
                      )
                      # add returned child config options to the deque to be processed.
                      dq.extend(expanded_config_options_dq)
  
              elif (
                  "properties" in jsonschema_property
                  and "allOf" not in jsonschema_property
              ):
                  # This is the case where we don't have a list of properties, just a properties
                  # dictionary nested inside another.
                  child_properties = jsonschema_property["properties"]
                  # a new dequeue to be passed to explore parameters from
                  raw_entry = deque(
                      [
                          ConfigOption(
                              copy.deepcopy(config_options), False
                          )
                      ]
                  )
                  child_config_options_dq = explore_properties(
                      child_properties, parameter_path, raw_entry
                  )
                  merged_config_options_dq = merge_dq(
                      config_options, child_config_options_dq
                  )
                  # add returned config options to the deque to be processed.
                  dq.extend(merged_config_options_dq)
  
              else:
                  # this is the base case.
                  parameter_samples = get_samples(jsonschema_property)
                  if parameter_samples:
                      config_options[parameter_path] = parameter_samples
  
                  # add config_options back to queue. fully_explored = False because we still didn't finish
                  # exploring all the keys in the properties dictionary.
                  dq.appendleft(ConfigOption(config_options, False))
  
          # at this point, we finished exploring all keys of the properties dictionary. Add all config options
          # to the processed queue.
          while dq:
              config_options, _ = dq.popleft()
              processed_dq.append(ConfigOption(config_options, True))
  
      return processed_dq
  
imports: import copy
  import random
  from collections import deque, namedtuple
  from typing import Any, Deque, Dict, List, Tuple, Union
  import pandas
  from ludwig.config_sampling.parameter_sampling import handle_property_type, ParameterBaseTypes
  from ludwig.constants import SEQUENCE, TEXT, TIMESERIES
  from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df
  from ludwig.schema.model_types.base import ModelConfig
  from ludwig.types import ModelConfigDict
  from ludwig.utils.misc_utils import merge_dict
label_desc: Use image node_id 1 for calling a global function with example usage: explore_properties(jsonschema_properties, parent_parameter_path, dq, allow_list) and returns: processed_dq