docs: protected void updateDescendentsForExec(int execStep, VarId executedVar) {
          String varName = executedVar.getVariable();
          Variable var = sameDiff.getVariables().get(executedVar.getVariable());
          //Find any ops (or variables with control dependencies) that this is required for execution of and check if now available for exec
          List<String> l = sameDiff.getVariables().get(executedVar.getVariable()).getInputsForOp();
          String[] inputForOps = l == null ? null : l.toArray(new String[l.size()]);  //Just executed variable is input to these ops
          List<String> controlDepForVars = var.getControlDepsForVar();                //Just executed variable is a control dependency for these variables
          List<String> controlDepForOps = var.getControlDepsForOp();                  //Just executed variable is a control dependency for these ops
  
  
          SDVariable v = var.getVariable();
          boolean isConstOrPhInput = v.isPlaceHolder() || v.isConstant();
  
          //After a variable becomes available, we should look at the ops this is an input to, and check if we can execute this op now...
          if (inputForOps != null) {
              for (String opName : inputForOps) {
  
                  DifferentialFunction fn = sameDiff.getFunctionById(opName);
                  if (fn instanceof Merge) {
                      //Merge op: available for execution when *any* of its inputs are available. But only mark it for exec once...
                      List<String> opOutputs = sameDiff.getOps().get(opName).getOutputsOfOp();
                      Preconditions.checkState(opOutputs.size() == 1, "Expected only 1 output variable for merge op, got %s", opOutputs);
                      VarId outVarId = newVarId(opOutputs.get(0), executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                      if (!nodeOutputs.containsKey(outVarId) && subgraph.contains(outVarId.getVariable()) && !availableForExecSet.contains(outVarId)) {
                          availableForExec.add(outVarId);
                          availableForExecSet.add(outVarId);
                          log.trace("Marked merge op ({}) variable {} as available for execution: input {} is now available", opName, outVarId, executedVar);
                      }
  
                      //Mark that we need the specified input to calculate this output
                      addToExecInputs(isConstOrPhInput, executedVar, outVarId);
                      continue;
                  } else if (fn instanceof Enter) {
                      //Enter node: available for exec when any of its inputs are available for exec
                      // Note input feeds from one frame to another
                      List<String> opOutputs = sameDiff.getOps().get(opName).getOutputsOfOp();
                      Preconditions.checkState(opOutputs.size() == 1, "Expected only 1 output variable for enter op, got %s", opOutputs);
                      Enter e = (Enter) fn;
                      boolean isConstant = e.isConstant();
                      VarId outVarId = newVarId(opOutputs.get(0), e.getFrameName(), 0, executedVar.toFrameIter());     //Note: parent frame of output op is enter var's *current* frame
  
                      if(isConstant && executedVar.getParentFrame() != null){
                          //For enter nodes that are constants, we want iteration 0 in all frames in the heirarchy
                          //For example, const -> Enter(a) -> Enter(b) -> op; in this case, the input to Op (at any frame/iteration) should should
                          // be the constant value - which is recorded as (frame="a",iter=0,parent=(frame="b",iter=0))
                          outVarId.setParentFrame(outVarId.getParentFrame().clone());
                          FrameIter fi = outVarId.getParentFrame();
                          while(fi != null){
                              fi.setIteration(0);
                              fi = fi.getParentFrame();
                          }
                      }
  
                      if (!nodeOutputs.containsKey(outVarId) && subgraph.contains(outVarId.getVariable()) && !availableForExecSet.contains(outVarId)) {
                          availableForExec.add(outVarId);
                          availableForExecSet.add(outVarId);
                          log.trace("Marked enter op ({}) variable {} as available for execution: input {} is now available", opName, outVarId, executedVar);
                      }
  
                      //Also record the parent frame: we'll need this when we get to the corresponding exit ops
                      frameParents.put(e.getFrameName(), executedVar.toFrameIter());
  
                      //Mark that we need the specified input to calculate this output
                      addToExecInputs(isConstOrPhInput, executedVar, outVarId);
                      continue;
                  } else if (fn instanceof Exit) {
                      //Exit node forwards input to parent frame
                      List<String> opOutputs = sameDiff.getOps().get(opName).getOutputsOfOp();
                      FrameIter parentFrame = frameParents.get(executedVar.getFrame());
                      Preconditions.checkNotNull(parentFrame, "Parent frame must not be null for exit op: variable to exec is %s", executedVar);
  
                      VarId outVarId = new VarId(opOutputs.get(0), parentFrame.getFrame(), parentFrame.getIteration(), executedVar.getParentFrame().getParentFrame());    //Parent frame of output is parent of current parent
                      if (!nodeOutputs.containsKey(outVarId) && subgraph.contains(outVarId.getVariable()) && !availableForExecSet.contains(outVarId)) {
                          availableForExec.add(outVarId);
                          availableForExecSet.add(outVarId);
                          log.trace("Marked Exit op ({}) variable {} as available for execution: input {} is now available", opName, outVarId, executedVar);
                      }
  
                      addToExecInputs(isConstOrPhInput, executedVar, outVarId);
                      continue;
                  } else if (fn instanceof NextIteration) {
                      //NextIteration is available for execution when its single input is available
                      //NextIteration op: forwards its single input to the output of the current frame, but increments the iteration number
                      List<String> opOutputs = sameDiff.getOps().get(opName).getOutputsOfOp();
                      Preconditions.checkState(opOutputs.size() == 1, "Expected exactly 1 output for NextIteration op: got %s", opOutputs);
                      VarId outVarId = newVarId(opOutputs.get(0), executedVar.getFrame(), executedVar.getIteration() + 1, executedVar.getParentFrame());
  
                      if (!nodeOutputs.containsKey(outVarId) && subgraph.contains(outVarId.getVariable()) && !availableForExecSet.contains(outVarId)) {
                          availableForExec.add(outVarId);
                          availableForExecSet.add(outVarId);
                          log.trace("Marked NextIteration op ({}) variable {} as available for execution: input {} is now available", opName, outVarId, executedVar);
                      }
  
                      //Mark that we need the specified input to calculate this output
                      addToExecInputs(isConstOrPhInput, executedVar, outVarId);
                      continue;
                  }
                  //Note for LoopCond: just forwards input to output - so basically handle it the same as other ops here
  
  
                  //Can execute this op - and hence get it's output variables - if all inputs (and control deps) are available
                  String[] inputsThisOp = fn.argNames();
                  boolean allInputsAvailable = true;
                  if (inputsThisOp != null) {
                      allInputsAvailable = allInputsAvailable(execStep, inputsThisOp, executedVar);
                  }
  
                  //Check Op control dependencies
                  List<String> opControlDeps = sameDiff.getOps().get(opName).getControlDeps();
                  if (opControlDeps != null && allInputsAvailable) {
                      for (String cd : opControlDeps) {
                          VarId vcd = newVarId(cd, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                          if (!nodeOutputs.containsKey(vcd)) {
                              allInputsAvailable = false;
                              break;
                          }
                      }
                  }
  
                  List<String> opOutputs = sameDiff.getOps().get(opName).getOutputsOfOp();
                  if (opOutputs != null) {
  
                      for (String s : opOutputs) {
                          //The input (for normal ops - not Enter/Exit/NextIteration) have the same frame and iteration number as the just executed var
                          //Exception 1 to this: constants. If variable is a constant, then it's always iteration 0 of the main frame  (unless variable control dep exists)
                          //Exception 2 to this: placeholders. As above
                          SDVariable sdv = sameDiff.getVariable(s);
                          Variable variable = sameDiff.getVariables().get(s);
                          VarId outVarId;
                          if (sdv.isConstant() || sdv.isPlaceHolder()) {
                              //Constant
                              if(variable.getControlDeps() == null || var.getControlDeps().isEmpty()){
                                  //Standard case - do a lookup of placeholder/constant
                                  outVarId = newVarId(s, OUTER_FRAME, 0, null);
                              } else {
                                  //Edge case: control dependency x -> constant exists
                                  //We should look up based on x's frame/iteration
                                  outVarId = newVarId(s, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                              }
                          } else {
                              //Normal (non-constant)
                              outVarId = newVarId(s, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                          }
  
                          //Mark that we need the specified input to calculate this output
                          addToExecInputs(isConstOrPhInput, executedVar, outVarId);
  
                          //Check variable control dependencies, for each of the op outputs
                          if(allInputsAvailable && variable.getControlDeps() != null && !variable.getControlDeps().isEmpty()){
                              //If one of the op outputs has a control dependency input, make sure this is available
                              // before executing the op
                              //For example, if z=add(x,y) and control dependency A->z exists, then don't execute op until A is available
                              for(String cd : variable.getControlDeps()){
                                  Variable cdVar = sameDiff.getVariables().get(cd);
                                  VarId cdVarId = null;
                                  if (cdVar.getVariable().isConstant() || cdVar.getVariable().isPlaceHolder()) {
                                      //Constant
                                      if(variable.getControlDeps() == null || var.getControlDeps().isEmpty()){
                                          //Standard case - do a lookup of placeholder/constant
                                          cdVarId = newVarId(cd, OUTER_FRAME, 0, null);
                                      } else {
                                          //Edge case: control dependency x -> constant -> thisOutput exists
                                          //We should look up based on x's frame/iteration
                                          cdVarId = newVarId(cd, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                                      }
                                  } else {
                                      //Normal (non-constant)
                                      cdVarId = newVarId(cd, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                                  }
                                  allInputsAvailable &= nodeOutputs.containsKey(cdVarId);
                                  if(!allInputsAvailable)
                                      break;
                              }
                          }
                      }
  
                      if (allInputsAvailable) {
                          //Op can be executed -> variables as output are available for exec
  
                          for (String s : opOutputs) {
                              if (!subgraph.contains(s))
                                  continue;       //Don't need this variable to calculate requested outputs - so don't mark as available for execution
                              VarId vid = newVarId(s, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                              if(!availableForExecSet.contains(vid)) {
                                  availableForExec.add(vid);
                                  availableForExecSet.add(vid);
                                  log.trace("Marked variable as available for execution: {} - output of op {} ({}) with op inputs {}", vid, opName,
                                          fn.getClass().getSimpleName(), (inputsThisOp == null ? "<none>" : Arrays.toString(inputsThisOp)));
                              }
                          }
                      }
                  }
  
              }
          }
  
          //Also check variable control dependencies... if control dependency varX->varY exists and varY is a constant/placeholder/variable,
          // then it's not going to be triggered by the op-based check above
          if(controlDepForVars != null){
              for(String s : controlDepForVars){
                  if (!subgraph.contains(s))
                      continue;       //Don't need this variable to calculate requested outputs - so don't mark as available for execution
  
                  SDVariable depFor = sameDiff.getVariable(s);
                  if(depFor.getVariableType() != VariableType.ARRAY){
                      //Control dependency executedVar -> s exists, where "s" is not the output of an op
                      //Even thought this is a constant, we'll inherit the frame and iteration from the control dependency
                      // otherwise, we lose this frame/iteration information for any downstream variables using the constant within a frame
                      VarId outVarId = newVarId(s, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                      if(!availableForExecSet.contains(outVarId)) {
                          availableForExec.add(outVarId);
                          availableForExecSet.add(outVarId);
                          log.trace("Marked variable as available for execution: {} - control dependency {} -> {} exists", outVarId, executedVar.getVariable(), s);
                      }
                  } else {
                      //Another edge case: OpX has output varY (with no inputs), and control dependency executedVar -> varY exists
                      //We should check if OpX is now available for execution...
                      //Similarly, if we have OpX with inputs, but we're only waiting on a varible control dependency Z -> X
                      // then we might not get triggered as available for exec above either
                      String opName = sameDiff.getVariables().get(s).getOutputOfOp();
                      if(opName != null){
                          SameDiffOp op = sameDiff.getOps().get(opName);
                          boolean allInputsAvailable = true;
                          if(op.getInputsToOp() != null && !op.getInputsToOp().isEmpty()){
                              List<String> inputList = op.getInputsToOp();
                              allInputsAvailable = allInputsAvailable(execStep, inputList.toArray(new String[inputList.size()]), executedVar);
                          }
  
                          if(allInputsAvailable && op.getControlDeps() != null){
                              for(String cd : op.getControlDeps()){
                                  VarId vid = newVarId(cd, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());     //Note: is array type, therefore has same frame/iter as parent
                                  allInputsAvailable &= nodeOutputs.containsKey(vid);
                                  if(!allInputsAvailable)
                                      break;
                              }
                          }
                          if(allInputsAvailable){
                              for(String opOutput : op.getOutputsOfOp()){
                                  Variable v2 = sameDiff.getVariables().get(opOutput);
                                  if(v2.getControlDeps() != null){
                                      for(String s2 : v2.getControlDeps()){
                                          VarId vid = newVarId(s2, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());     //Note: is array type, therefore has same frame/iter as parent
                                          allInputsAvailable &= nodeOutputs.containsKey(vid);
                                          if(!allInputsAvailable)
                                              break;
                                      }
                                  }
                              }
                          }
  
                          if(allInputsAvailable){
                              VarId outVarId = newVarId(s, executedVar.getFrame(), executedVar.getIteration(), executedVar.getParentFrame());
                              if(!availableForExecSet.contains(outVarId)) {
                                  availableForExec.add(outVarId);
                                  log.trace("Marked variable as available for execution: {} - is output of op {} with no inputs (but has control dependencies)", outVarId, op.getName());
                              }
                          }
                      }
                  }
              }
          }
  
          //Edge case: if control dependency varX->opY exists, and opY doesn't have any inputs, it also can't be triggeered
          // (made available for execution) by any of the previous checks. For any ops that DO have inputs, they will
          // be triggered already
          if(controlDepForOps != null){
              for(String opName : controlDepForOps){
                  SameDiffOp op = sameDiff.getOps().get(opName);
                  if(op.getInputsToOp() == null || op.getInputsToOp().isEmpty()){
                      for(String out : op.getOutputsOfOp()){
                          if (!subgraph.contains(out))
                              continue;       //Don't need this variable to calculate requested outputs - so don't mark as available for execution
  
                          //TODO is it possible to have both variable and op control dependencies??
                          VarId outVarId = newVarId(out, OUTER_FRAME, 0, null);
                          if(!availableForExecSet.contains(outVarId)) {
                              availableForExec.add(outVarId);
                              availableForExecSet.add(outVarId);
                              log.trace("Marked variable as available for execution: {} - op control dependency variable {} -> op {} exists", outVarId, executedVar.getVariable(), opName);
                          }
                      }
                  }
              }
          }
      }