code: def generate_expanded_graph(graph_in):
      """Generates an expanded graph based on node parameterization
  
      Parameterization is controlled using the `iterables` field of the
      pipeline elements.  Thus if there are two nodes with iterables a=[1,2]
      and b=[3,4] this procedure will generate a graph with sub-graphs
      parameterized as (a=1,b=3), (a=1,b=4), (a=2,b=3) and (a=2,b=4).
      """
      import networkx as nx
  
      try:
          dfs_preorder = nx.dfs_preorder
      except AttributeError:
          dfs_preorder = nx.dfs_preorder_nodes
  
      logger.debug("PE: expanding iterables")
      graph_in = _remove_nonjoin_identity_nodes(
          graph_in, keep_iterables=True
      )
      # standardize the iterables as {(field, function)} dictionaries
      for node in graph_in.nodes():
          if node.iterables:
              _standardize_iterables(node)
      allprefixes = list("abcdefghijklmnopqrstuvwxyz")
  
      # the iterable nodes
      inodes = _iterable_nodes(graph_in)
      logger.debug("Detected iterable nodes %s", inodes)
      # while there is an iterable node, expand the iterable node's
      # subgraphs
      while inodes:
          inode = inodes[0]
          logger.debug("Expanding the iterable node %s...", inode)
  
          # the join successor nodes of the current iterable node
          jnodes = [
              node
              for node in graph_in.nodes()
              if hasattr(node, "joinsource")
              and inode.name == node.joinsource
              and nx.has_path(graph_in, inode, node)
          ]
  
          # excise the join in-edges. save the excised edges in a
          # {jnode: {source name: (destination name, edge data)}}
          # dictionary
          jedge_dict = {}
          for jnode in jnodes:
              in_edges = jedge_dict[jnode] = {}
              edges2remove = []
              for src, dest, data in graph_in.in_edges(jnode, True):
                  in_edges[src.itername] = data
                  edges2remove.append((src, dest))
  
              for src, dest in edges2remove:
                  graph_in.remove_edge(src, dest)
                  logger.debug(
                      "Excised the %s -> %s join node in-edge.",
                      src,
                      dest,
                  )
  
          if inode.itersource:
              # the itersource is a (node name, fields) tuple
              src_name, src_fields = inode.itersource
              # convert a single field to a list
              if isinstance(src_fields, (str, bytes)):
                  src_fields = [src_fields]
              # find the unique iterable source node in the graph
              try:
                  iter_src = next(
                      node
                      for node in graph_in.nodes()
                      if node.name == src_name
                      and nx.has_path(graph_in, node, inode)
                  )
              except StopIteration:
                  raise ValueError(
                      "The node %s itersource %s was not found"
                      " among the iterable predecessor nodes"
                      % (inode, src_name)
                  )
              logger.debug(
                  "The node %s has iterable source node %s",
                  inode,
                  iter_src,
              )
              # look up the iterables for this particular itersource descendant
              # using the iterable source ancestor values as a key
              iterables = {}
              # the source node iterables values
              src_values = [
                  getattr(iter_src.inputs, field)
                  for field in src_fields
              ]
              # if there is one source field, then the key is the source value,
              # otherwise the key is the tuple of source values
              if len(src_values) == 1:
                  key = src_values[0]
              else:
                  key = tuple(src_values)
              # The itersource iterables is a {field: lookup} dictionary, where the
              # lookup is a {source key: iteration list} dictionary. Look up the
              # current iterable value using the predecessor itersource input values.
              iter_dict = {
                  field: lookup[key]
                  for field, lookup in inode.iterables
                  if key in lookup
              }
  
              # convert the iterables to the standard {field: function} format
  
              def make_field_func(*pair):
                  return pair[0], lambda: pair[1]
  
              iterables = dict(
                  [
                      make_field_func(*pair)
                      for pair in list(iter_dict.items())
                  ]
              )
          else:
              iterables = inode.iterables.copy()
          inode.iterables = None
          logger.debug("node: %s iterables: %s", inode, iterables)
  
          # collect the subnodes to expand
          subnodes = [s for s in dfs_preorder(graph_in, inode)]
          prior_prefix = [
              re.findall(r"\.(.)I", s._id) for s in subnodes if s._id
          ]
          prior_prefix = sorted(
              [l for item in prior_prefix for l in item]
          )
          if not prior_prefix:
              iterable_prefix = "a"
          else:
              if prior_prefix[-1] == "z":
                  raise ValueError("Too many iterables in the workflow")
              iterable_prefix = allprefixes[
                  allprefixes.index(prior_prefix[-1]) + 1
              ]
          logger.debug(("subnodes:", subnodes))
  
          # append a suffix to the iterable node id
          inode._id += ".%sI" % iterable_prefix
  
          # merge the iterated subgraphs
          subgraph = graph_in.subgraph(subnodes).copy()
          graph_in = _merge_graphs(
              graph_in,
              subnodes,
              subgraph,
              inode._hierarchy + inode._id,
              iterables,
              iterable_prefix,
              inode.synchronize,
          )
  
          # reconnect the join nodes
          for jnode in jnodes:
              # the {node id: edge data} dictionary for edges connecting
              # to the join node in the unexpanded graph
              old_edge_dict = jedge_dict[jnode]
              # the edge source node replicates
              expansions = defaultdict(list)
              for node in graph_in:
                  for src_id in list(old_edge_dict.keys()):
                      # Drop the original JoinNodes; only concerned with
                      # generated Nodes
                      itername = node.itername
                      if (
                          hasattr(node, "joinfield")
                          and itername == src_id
                      ):
                          continue
                      # Patterns:
                      #   - src_id : Non-iterable node
                      #   - src_id.[a-z]\d+ :
                      #       IdentityInterface w/ iterables or nested JoinNode
                      #   - src_id.[a-z]I.[a-z]\d+ :
                      #       Non-IdentityInterface w/ iterables
                      #   - src_idJ\d+ : JoinNode(IdentityInterface)
                      if itername.startswith(src_id):
                          suffix = itername[len(src_id) :]
                          if re.fullmatch(
                              r"((\.[a-z](I\.[a-z])?|J)\d+)?", suffix
                          ):
                              expansions[src_id].append(node)
              for in_id, in_nodes in list(expansions.items()):
                  logger.debug(
                      "The join node %s input %s was expanded to %d nodes.",
                      jnode,
                      in_id,
                      len(in_nodes),
                  )
              # preserve the node iteration order by sorting on the node id
              for in_nodes in list(expansions.values()):
                  in_nodes.sort(key=lambda node: node._id)
  
              # the number of join source replicates.
              iter_cnt = count_iterables(iterables, inode.synchronize)
              # make new join node fields to connect to each replicated
              # join in-edge source node.
              slot_dicts = [
                  jnode._add_join_item_fields() for _ in range(iter_cnt)
              ]
              # for each join in-edge, connect every expanded source node
              # which matches on the in-edge source name to the destination
              # join node. Qualify each edge connect join field name by
              # appending the next join slot index, e.g. the connect
              # from two expanded nodes from field 'out_file' to join
              # field 'in' are qualified as ('out_file', 'in1') and
              # ('out_file', 'in2'), resp. This preserves connection port
              # integrity.
              for old_id, in_nodes in list(expansions.items()):
                  # reconnect each replication of the current join in-edge
                  # source
                  for in_idx, in_node in enumerate(in_nodes):
                      olddata = old_edge_dict[old_id]
                      newdata = deepcopy(olddata)
                      # the (source, destination) field tuples
                      connects = newdata["connect"]
                      # the join fields connected to the source
                      join_fields = [
                          field
                          for _, field in connects
                          if field in jnode.joinfield
                      ]
                      # the {field: slot fields} maps assigned to the input
                      # node, e.g. {'image': 'imageJ3', 'mask': 'maskJ3'}
                      # for the third join source expansion replicate of a
                      # join node with join fields image and mask
                      slots = slot_dicts[in_idx]
                      for con_idx, connect in enumerate(connects):
                          src_field, dest_field = connect
                          # qualify a join destination field name
                          if dest_field in slots:
                              slot_field = slots[dest_field]
                              connects[con_idx] = (
                                  src_field,
                                  slot_field,
                              )
                              logger.debug(
                                  "Qualified the %s -> %s join field %s as %s.",
                                  in_node,
                                  jnode,
                                  dest_field,
                                  slot_field,
                              )
                      graph_in.add_edge(in_node, jnode, **newdata)
                      logger.debug(
                          "Connected the join node %s subgraph to the"
                          " expanded join point %s",
                          jnode,
                          in_node,
                      )
  
          # nx.write_dot(graph_in, '%s_post.dot' % node)
          # the remaining iterable nodes
          inodes = _iterable_nodes(graph_in)
  
      for node in graph_in.nodes():
          if node.parameterization:
              node.parameterization = [
                  param for _, param in sorted(node.parameterization)
              ]
      logger.debug("PE: expanding iterables ... done")
  
      return _remove_nonjoin_identity_nodes(graph_in)
  
imports: import os
  import sys
  import pickle
  from collections import defaultdict
  import re
  from copy import deepcopy
  from glob import glob
  from pathlib import Path
  from traceback import format_exception
  from hashlib import sha1
  from functools import reduce
  import numpy
  from ...None import logging, config
  from ...utils.filemanip import indirectory, relpath, fname_presuffix, ensure_list, get_related_files, save_json, savepkl, loadpkl, write_rst_header, write_rst_dict, write_rst_list
  from ...utils.misc import str2bool
  from ...utils.functions import create_function_from_source
  from ...interfaces.base.traits_extension import rebase_path_traits, resolve_path_traits, OutputMultiPath, isdefined, Undefined
  from ...interfaces.base.support import Bunch, InterfaceResult
  from ...interfaces.base import CommandLine
  from ...interfaces.utility import IdentityInterface
  from ...utils.provenance import ProvStore, pm, nipype_ns, get_id
  from inspect import signature
label_desc: Use image node_id 31 for calling a global function with example usage: generate_expanded_graph(graph_in) and returns: _remove_nonjoin_identity_nodes, pair,  function