docs: def iter_penalty_model_from_specification(cur, specification):
      """Iterate through all penalty models in the cache matching the
      given specification.
  
      Args:
          cur (:class:`sqlite3.Cursor`): An sqlite3 cursor. This function
              is meant to be run within a :obj:`with` statement.
          specification (:class:`penaltymodel.Specification`): A specification
              for a penalty model.
  
      Yields:
          :class:`penaltymodel.PenaltyModel`
  
      """
      encoded_data = {}
  
      nodelist = sorted(specification.graph)
      edgelist = sorted(sorted(edge) for edge in specification.graph.edges)
      encoded_data['num_nodes'] = len(nodelist)
      encoded_data['num_edges'] = len(edgelist)
      encoded_data['edges'] = json.dumps(edgelist, separators=(',', ':'))
      encoded_data['num_variables'] = len(next(iter(specification.feasible_configurations)))
      encoded_data['num_feasible_configurations'] = len(specification.feasible_configurations)
  
      encoded = {_serialize_config(config): en for config, en in specification.feasible_configurations.items()}
      configs, energies = zip(*sorted(encoded.items()))
      encoded_data['feasible_configurations'] = json.dumps(configs, separators=(',', ':'))
      encoded_data['energies'] = json.dumps(energies, separators=(',', ':'))
  
      encoded_data['decision_variables'] = json.dumps(specification.decision_variables, separators=(',', ':'))
      encoded_data['classical_gap'] = json.dumps(specification.min_classical_gap, separators=(',', ':'))
  
      select = \
          """
          SELECT
              linear_biases,
              quadratic_biases,
              offset,
              decision_variables,
              classical_gap,
              ground_energy
          FROM penalty_model_view
          WHERE
              -- graph:
              num_nodes = :num_nodes AND
              num_edges = :num_edges AND
              edges = :edges AND
              -- feasible_configurations:
              num_variables = :num_variables AND
              num_feasible_configurations = :num_feasible_configurations AND
              feasible_configurations = :feasible_configurations AND
              energies = :energies AND
              -- decision variables:
              decision_variables = :decision_variables AND
              -- we could apply filters based on the energy ranges but in practice this seems slower
              classical_gap >= :classical_gap
          ORDER BY classical_gap DESC;
          """
  
      for row in cur.execute(select, encoded_data):
          # we need to build the model
          linear = _decode_linear_biases(row['linear_biases'], nodelist)
          quadratic = _decode_quadratic_biases(row['quadratic_biases'], edgelist)
  
          model = dimod.BinaryQuadraticModel(linear, quadratic, row['offset'], dimod.SPIN)  # always spin
  
          yield pm.PenaltyModel.from_specification(specification, model, row['classical_gap'], row['ground_energy'])