Repository: dwavesystems/dwave-cloud-client

Issue:
Client.from_config() inconsistency for legacy .dwrc behavior
legacy_load_config() says, "Assuming the new config file dwave.conf is not found...
```
>>> client = dwave.cloud.Client.from_config(profile='profile-b')
# Will try to connect with the url `https://two.com` and the token `token-two`.""
```

For when users have only a legacy .dwrc file,  client = dwave.cloud.Client.from_config() works as expected:
```
>>> import dwave.cloud as dc
>>> client = dwave.cloud.Client.from_config()    # doctest: +SKIP
>>> client.endpoint   # doctest: +SKIP
'https://one.com'
```

But specifying a profile does not work consistently to the above:

```
In [87]: import dwave.cloud as dc

In [88]: client = dc.Client.from_config(profile='profile-a')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-88-b5ececaca8fe> in <module>()
----> 1 client = dc.Client.from_config(profile='profile-a')

C:\Users\jpasvolsky\!git_DocsOS\env\ADTT\CloudClient\dwave\cloud\client.pyc in from_config(cls, config_file, profile, client, endpoint, token, solver, proxy, legacy_config_fallback, **kwargs)
    360         config = load_config(
    361             config_file=config_file, profile=profile, client=client,
--> 362             endpoint=endpoint, token=token, solver=solver, proxy=proxy)
    363
    364         # fallback to legacy `.dwrc` if key variables missing

C:\Users\jpasvolsky\!git_DocsOS\env\ADTT\CloudClient\dwave\cloud\config.py in load_config(config_file, profile, client, endpoint, token, solver, proxy)
    575             config_file = os.getenv("DWAVE_CONFIG_FILE")
    576         section = load_profile_from_files(
--> 577             [config_file] if config_file else None, profile)
    578
    579     # override a selected subset of values via env or kwargs,

C:\Users\jpasvolsky\!git_DocsOS\env\ADTT\CloudClient\dwave\cloud\config.py in load_profile_from_files(filenames, profile)
    355             section = dict(config[profile])
    356         except KeyError:
--> 357             raise ValueError("Config profile {!r} not found".format(profile))
    358     else:
    359         # as the very last resort (unspecified profile name and

ValueError: Config profile 'profile-a' not found
```

I think this is because when no profile is specified, load_config_from_files()'s line "filenames = get_configfile_paths()" returns empty so the function returns a placeholder config (<backports.configparser.ConfigParser at 0x8a37160>) to load_profile_from_files(), which returns a placeholder section to load_config(), which returns it to Client.from_config(), which will then call legacy_load_config(). 
But when a profile is specified, load_profile_from_files(profile='profile-a') creates an OrderedDict() out of the placeholder config that raises a key error in:

```
if profile:
        try:
            section = dict(config[profile])
        except KeyError:
            raise ValueError("Config profile {!r} not found".format(profile))
```
 


Discussion:

Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 5544722..0cdd555 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -102,7 +102,9 @@ InstanceAttributeDocumenter.add_directive_header = iad_add_directive_header
 
 
 # Link to Python standard lib objects
-intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
-    'qbsolv': ('https://docs.ocean.dwavesys.com/projects/qbsolv/en/latest/', None),
-    'oceandocs': ('https://docs.ocean.dwavesys.com/en/latest/', None),
-    'sysdocs_gettingstarted': ('https://docs.dwavesys.com/docs/latest/', None)}
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3', None),
+    'numpy': ('https://numpy.org/doc/stable/', None),
+    'oceandocs': ('https://docs.ocean.dwavesys.com/en/stable/', None),
+    'sysdocs_gettingstarted': ('https://docs.dwavesys.com/docs/latest/', None),
+}
diff --git a/docs/index.rst b/docs/index.rst
index c0032f6..5f437b9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -46,7 +46,7 @@ Documentation
 
   Ocean Home <https://ocean.dwavesys.com/>
   Ocean Documentation <https://docs.ocean.dwavesys.com>
-  Ocean Glossary <https://docs.ocean.dwavesys.com/en/latest/glossary.html>
+  Ocean Glossary <https://docs.ocean.dwavesys.com/en/stable/concepts/index.html>
 
 .. toctree::
   :caption: D-Wave
diff --git a/docs/intro.rst b/docs/intro.rst
index 04f1cde..5404dea 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -24,9 +24,9 @@ Configuration
 It's recommended you set up your D-Wave Cloud Client configuration through the
 :std:doc:`interactive CLI utility <oceandocs:docs_cli>`.
 
-As described in the :std:doc:`Using a D-Wave System <oceandocs:overview/dwavesys>` section
-of Ocean Documentation, for your code to access remote D-Wave compute resources, you must
-configure communication through SAPI; for example, your code needs your API
+As described in the :std:doc:`Configuring Access to D-Wave Solvers <oceandocs:overview/sapi>`
+section of Ocean Documentation, for your code to access remote D-Wave compute resources,
+you must configure communication through SAPI; for example, your code needs your API
 token for authentication. D-Wave Cloud Client provides multiple options for configuring
 the required information:
 
@@ -166,75 +166,3 @@ A typical workflow may include the following steps:
 3. Submit your problem, using your solver, and then process the returned
    :class:`~dwave.cloud.computation.Future`, instantiated by your solver to handle
    remotely executed problem solving.
-
-Terminology
-===========
-
-.. glossary::
-
-    Ising
-         Traditionally used in statistical mechanics. Variables are "spin up"
-         (:math:`\uparrow`) and "spin down" (:math:`\downarrow`), states that
-         correspond to :math:`+1` and :math:`-1` values. Relationships between
-         the spins, represented by couplings, are correlations or anti-correlations.
-         The objective function expressed as an Ising model is as follows:
-
-         .. math::
-
-                  \begin{equation}
-                       \text{E}_{ising}(\pmb{s}) = \sum_{i=1}^N h_i s_i + \sum_{i=1}^N \sum_{j=i+1}^N J_{i,j} s_i s_j
-                  \end{equation}
-
-         where the linear coefficients corresponding to qubit biases
-         are :math:`h_i`, and the quadratic coefficients corresponding to coupling
-         strengths are :math:`J_{i,j}`.
-
-    model
-        A collection of variables with associated linear and
-        quadratic biases.
-
-    QUBO
-         Quadratic unconstrained binary optimization.
-         QUBO problems are traditionally used in computer science. Variables
-         are TRUE and FALSE, states that correspond to 1 and 0 values.
-         A QUBO problem is defined using an upper-diagonal matrix :math:`Q`,
-         which is an :math:`N` x :math:`N` upper-triangular matrix of real weights,
-         and :math:`x`, a vector of binary variables, as minimizing the function
-
-         .. math::
-
-            \begin{equation}
-              f(x) = \sum_{i} {Q_{i,i}}{x_i} + \sum_{i<j} {Q_{i,j}}{x_i}{x_j}
-            \end{equation}
-
-         where the diagonal terms :math:`Q_{i,i}` are the linear coefficients and
-         the nonzero off-diagonal terms are the quadratic coefficients
-         :math:`Q_{i,j}`.
-         This can be expressed more concisely as
-
-         .. math::
-
-            \begin{equation}
-              \min_{{x} \in {\{0,1\}^n}} {x}^{T} {Q}{x}.
-            \end{equation}
-
-         In scalar notation, the objective function expressed as a QUBO
-         is as follows:
-
-         .. math::
-
-            \begin{equation}
-                        \text{E}_{qubo}(a_i, b_{i,j}; q_i) = \sum_{i} a_i q_i + \sum_{i<j} b_{i,j} q_i q_j.
-            \end{equation}
-
-    sampler
-        A process that samples from low energy states of a problem’s objective function.
-        A binary quadratic model (BQM) sampler samples from low energy states in models such
-        as those defined by an Ising equation or a Quadratic Unconstrained Binary Optimization
-        (QUBO) problem and returns an iterable of samples, in order of increasing energy. A dimod
-        sampler provides ‘sample_qubo’ and ‘sample_ising’ methods as well as the generic
-        BQM sampler method.
-
-    solver
-        A resource that runs a problem. Some solvers interface to the QPU; others leverage CPU
-        and GPU resources.
diff --git a/dwave/cloud/cli.py b/dwave/cloud/cli.py
index f0a44f1..bb833df 100644
--- a/dwave/cloud/cli.py
+++ b/dwave/cloud/cli.py
@@ -31,7 +31,7 @@ from dwave.cloud.utils import (
     default_text_input, click_info_switch, generate_random_ising_problem,
     datetime_to_timestamp, utcnow, strtrunc, CLIError, set_loglevel,
     get_contrib_packages, user_agent)
-from dwave.cloud.coders import encode_problem_as_bq
+from dwave.cloud.coders import bqm_as_file
 from dwave.cloud.package_info import __title__, __version__
 from dwave.cloud.exceptions import (
     SolverAuthenticationError, InvalidAPIResponseError, UnsupportedSolverError,
@@ -473,8 +473,8 @@ def sample(config_file, profile, solver_def, biases, couplings, random_problem,
               help='Connection profile (section) name')
 @click.option('--problem-id', '-i', default=None,
               help='Problem ID (optional)')
-@click.option('--format', '-f', default='bq-zlib',
-              type=click.Choice(['coo', 'bq-zlib'], case_sensitive=False),
+@click.option('--format', '-f', default='dimodbqm',
+              type=click.Choice(['coo', 'dimodbqm'], case_sensitive=False),
               help='Problem data encoding')
 @click.argument('input_file', metavar='FILE', type=click.File('rb'))
 def upload(config_file, profile, problem_id, format, input_file):
@@ -495,7 +495,7 @@ def upload(config_file, profile, problem_id, format, input_file):
                 "in {!r} format.").format(input_file.name, format))
 
     if format == 'coo':
-        click.echo("Transcoding 'coo' to 'bq-zlib'.")
+        click.echo("Transcoding 'coo' to 'dimodbqm'.")
 
         try:
             import dimod
@@ -506,17 +506,17 @@ def upload(config_file, profile, problem_id, format, input_file):
         # note: `BQM.from_coo` doesn't support files opened in binary (yet);
         # fallback to reopen for now
         with open(input_file.name, 'rt') as fp:
-            bqm = dimod.BinaryQuadraticModel.from_coo(fp)
-            problem = encode_problem_as_bq(bqm, compress=True)['data']
+            bqm = dimod.AdjVectorBQM.from_coo(fp)
+            problem_file = bqm_as_file(bqm)
 
-    elif format == 'bq-zlib':
-        problem = input_file
+    elif format == 'dimodbqm':
+        problem_file = input_file
 
     click.echo("Uploading...")
 
     try:
         future = client.upload_problem_encoded(
-            problem=problem, problem_id=problem_id)
+            problem=problem_file, problem_id=problem_id)
         remote_problem_id = future.result()
     except Exception as e:
         click.echo(e)
diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py
index 1c7b7e0..d52c57e 100644
--- a/dwave/cloud/client.py
+++ b/dwave/cloud/client.py
@@ -48,7 +48,6 @@ import threading
 import requests
 import warnings
 import operator
-import collections
 import queue
 
 import base64
@@ -58,6 +57,7 @@ import concurrent.futures
 
 from itertools import chain
 from functools import partial, wraps
+from collections import abc, namedtuple, OrderedDict
 from concurrent.futures import ThreadPoolExecutor
 
 from dateutil.parser import parse as parse_datetime
@@ -66,7 +66,7 @@ from plucky import pluck
 from dwave.cloud.package_info import __packagename__, __version__
 from dwave.cloud.exceptions import *
 from dwave.cloud.computation import Future
-from dwave.cloud.config import load_config, legacy_load_config, parse_float
+from dwave.cloud.config import load_config, parse_float
 from dwave.cloud.solver import Solver, available_solvers
 from dwave.cloud.concurrency import PriorityThreadPoolExecutor
 from dwave.cloud.upload import ChunkedData
@@ -186,7 +186,7 @@ class Client(object):
     @classmethod
     def from_config(cls, config_file=None, profile=None, client=None,
                     endpoint=None, token=None, solver=None, proxy=None,
-                    headers=None, legacy_config_fallback=False, **kwargs):
+                    headers=None, **kwargs):
         """Client factory method to instantiate a client instance from configuration.
 
         Configuration values can be specified in multiple ways, ranked in the following
@@ -277,10 +277,6 @@ class Client(object):
                 Newline-separated additional HTTP headers to include with each
                 API request, or a dictionary of (key, value) pairs.
 
-            legacy_config_fallback (bool, default=False):
-                If True and loading from a standard D-Wave Cloud Client configuration
-                file (``dwave.conf``) fails, tries loading a legacy configuration file (``~/.dwrc``).
-
         Other Parameters:
             Unrecognized keys (str):
                 All unrecognized keys are passed through to the appropriate client class constructor
@@ -321,19 +317,6 @@ class Client(object):
             headers=headers)
         logger.debug("Config loaded: %r", config)
 
-        # fallback to legacy `.dwrc` if key variables missing
-        if legacy_config_fallback:
-            warnings.warn(
-                "'legacy_config_fallback' is deprecated, and it will be removed "
-                "in 0.7.0. please convert your legacy .dwrc file to the new "
-                "config format.", DeprecationWarning)
-
-            if not config.get('token'):
-                config = legacy_load_config(
-                    profile=profile, client=client, endpoint=endpoint,
-                    token=token, solver=solver, proxy=proxy, headers=headers)
-                logger.debug("Legacy config loaded: %r", config)
-
         # manual override of other (client-custom) arguments
         config.update(kwargs)
 
@@ -401,7 +384,7 @@ class Client(object):
         # parse solver
         if not solver:
             solver_def = {}
-        elif isinstance(solver, collections.Mapping):
+        elif isinstance(solver, abc.Mapping):
             solver_def = solver
         elif isinstance(solver, str):
             # support features dict encoded as JSON in our config INI file
@@ -422,7 +405,7 @@ class Client(object):
         # parse headers
         if not headers:
             headers_dict = {}
-        elif isinstance(headers, collections.Mapping):
+        elif isinstance(headers, abc.Mapping):
             headers_dict = headers
         elif isinstance(headers, str):
             try:
@@ -1037,8 +1020,14 @@ class Client(object):
         return solvers
 
     def solvers(self, refresh=False, **filters):
-        """Deprecated in favor of :meth:`.get_solvers`."""
-        warnings.warn("'solvers' is deprecated in favor of 'get_solvers'.", DeprecationWarning)
+        """Deprecated in favor of :meth:`.get_solvers`.
+
+        Scheduled for removal in 0.9.0.
+        """
+        warnings.warn(
+            "'solvers' is deprecated, and it will be removed "
+            "in 0.9.0. please convert your code to use 'get_solvers'",
+            DeprecationWarning)
         return self.get_solvers(refresh=refresh, **filters)
 
     def get_solver(self, name=None, refresh=False, **filters):
@@ -1122,7 +1111,7 @@ class Client(object):
         This method is thread safe.
         """
         self._submission_queue.put(self._submit.Message(body, future))
-    _submit.Message = collections.namedtuple('Message', ['body', 'future'])
+    _submit.Message = namedtuple('Message', ['body', 'future'])
 
     def _do_submit_problems(self):
         """Pull problems from the submission queue and submit them.
diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py
index 7d498ab..2b69cbe 100644
--- a/dwave/cloud/coders.py
+++ b/dwave/cloud/coders.py
@@ -21,7 +21,12 @@ import codecs
 from dwave.cloud.utils import (
     uniform_iterator, uniform_get, strip_tail, active_qubits)
 
-__all__ = ['encode_problem_as_qp', 'decode_qp', 'decode_qp_numpy']
+__all__ = [
+    'encode_problem_as_qp', 'decode_qp', 'decode_qp_numpy',
+    'encode_problem_as_bq', 'decode_bq',
+    'encode_problem_as_ref',
+    'bqm_as_file',
+]
 
 
 def encode_problem_as_qp(solver, linear, quadratic, offset=0,
@@ -46,7 +51,7 @@ def encode_problem_as_qp(solver, linear, quadratic, offset=0,
             Are (quadratic) biases specified on undirected edges?
 
     Returns:
-        encoded submission dictionary
+        Encoded submission dictionary.
     """
     active = active_qubits(linear, quadratic)
 
@@ -157,10 +162,11 @@ def _decode_byte(byte):
     """Helper for decode_qp, turns a single byte into a list of bits.
 
     Args:
-        byte: byte to be decoded
+        byte (int):
+            Byte to be decoded.
 
     Returns:
-        list of bits corresponding to byte
+        List of bits corresponding to byte.
     """
     bits = []
     for _ in range(8):
@@ -175,6 +181,13 @@ def _decode_ints(message):
     The int array is stored as little endian 32 bit integers.
     The array has then been base64 encoded. Since we are decoding we do these
     steps in reverse.
+
+    Args:
+        message (str):
+            The int array, base-64 encoded.
+
+    Returns:
+        Decoded double array.
     """
     binary = base64.b64decode(message)
     return struct.unpack('<' + ('i' * (len(binary) // 4)), binary)
@@ -188,10 +201,11 @@ def _decode_doubles(message):
     steps in reverse.
 
     Args:
-        message: the double array
+        message (str):
+            The double array, base-64 encoded.
 
     Returns:
-        decoded double array
+        Decoded double array.
     """
     binary = base64.b64decode(message)
     return struct.unpack('<' + ('d' * (len(binary) // 8)), binary)
@@ -278,52 +292,52 @@ def decode_qp_numpy(msg, return_matrix=True):
     return result
 
 
-def _encode_problem_as_bq_ref(problem):
-    assert isinstance(problem, str)
-
-    return problem
+def encode_problem_as_ref(problem):
+    """Encode the problem given via reference for submission in the `ref` data
+    format.
 
-def _encode_problem_as_bq_json(problem):
-    assert hasattr(problem, 'to_serializable')
+    Args:
+        problem (str):
+            A reference to an uploaded problem (problem ID).
 
-    return problem.to_serializable(use_bytes=False)
+    Returns:
+        Encoded submission dictionary.
+    """
 
-def _encode_problem_as_bq_json_zlib(problem):
-    assert hasattr(problem, 'to_serializable')
+    if not isinstance(problem, str):
+        raise TypeError("unsupported problem reference type")
 
-    bqm_dict = _encode_problem_as_bq_json(problem)
-    return zlib.compress(codecs.encode(json.dumps(bqm_dict), "ascii"))
+    return {
+        'format': 'ref',
+        'data': problem
+    }
 
 
-def encode_problem_as_bq(problem, compress=False):
+def encode_problem_as_bq(problem):
     """Encode the binary quadratic problem for submission in the `bq` data
     format.
 
     Args:
-        problem (:class:`~dimod.BinaryQuadraticModel`/str):
-            A binary quadratic model, or a reference to one (Problem ID).
+        problem (:class:`~dimod.BinaryQuadraticModel`):
+            A binary quadratic model.
 
     Returns:
-        encoded submission dictionary
-    """
+        Encoded submission dictionary.
 
-    if isinstance(problem, str):
-        return {
-            'format': 'bqm-ref',
-            'data': _encode_problem_as_bq_ref(problem)
-        }
+    Note:
+        The `bq` format assumes the complete BQM is sent embedded in the sample
+        job submit data, something none of the production solvers currently
+        support.
+    """
+    # NOTE: semi-deprecated format; see `bqm_as_file`.
 
-    if compress:
-        return {
-            'format': 'bq-zlib',
-            'data': _encode_problem_as_bq_json_zlib(problem)
-        }
+    if not hasattr(problem, 'to_serializable'):
+        raise TypeError("unsupported problem type")
 
-    else:
-        return {
-            'format': 'bq',
-            'data': _encode_problem_as_bq_json(problem)
-        }
+    return {
+        'format': 'bq',
+        'data': problem.to_serializable(use_bytes=False)
+    }
 
 
 def decode_bq(msg):
@@ -346,3 +360,40 @@ def decode_bq(msg):
     result['problem_type'] = 'bqm'
 
     return result
+
+
+def bqm_as_file(bqm, **options):
+    """Encode in-memory BQM as DIMODBQM binary file format.
+
+    Args:
+        bqm (:class:`~dimod.BQM`):
+            Binary quadratic model.
+
+        **options (dict):
+            :class:`~dimod.serialization.fileview.FileView` options.
+
+    Returns:
+        file-like:
+            Binary stream with BQM encoded in DIMODBQM format.
+    """
+    # This now the preferred way of BQM binary serialization.
+
+    # XXX: temporary implemented here, until something like
+    # dwavesystems/dimod#599 is available.
+
+    try:
+        import dimod
+        from dimod.serialization.fileview import FileView as BQMFileView
+    except ImportError: # pragma: no cover
+        raise RuntimeError("Can't encode BQM without 'dimod'. "
+                           "Re-install the library with 'bqm' support.")
+
+    if isinstance(bqm, BQMFileView):
+        return bqm
+
+    # test explicitly to avoid copy on cast if possible
+    fileviewable = (dimod.AdjArrayBQM, dimod.AdjVectorBQM, dimod.AdjMapBQM)
+    if not isinstance(bqm, fileviewable):
+        bqm = AdjVectorBQM(bqm)
+
+    return BQMFileView(bqm, **options)
diff --git a/dwave/cloud/computation.py b/dwave/cloud/computation.py
index 2d7ad04..3c0f11a 100644
--- a/dwave/cloud/computation.py
+++ b/dwave/cloud/computation.py
@@ -27,15 +27,17 @@ Some :class:`Future` methods are blocking.
 
 """
 
-import threading
 import time
+import threading
 import functools
 import warnings
 
+from operator import itemgetter
 from dateutil.parser import parse
 from concurrent.futures import TimeoutError
 
-from dwave.cloud.utils import utcnow, datetime_to_timestamp
+from dwave.cloud.utils import (
+    utcnow, datetime_to_timestamp, aliasdict, deprecated)
 from dwave.cloud.exceptions import InvalidAPIResponseError
 
 # Use numpy if available for fast decoding
@@ -568,7 +570,7 @@ class Future(object):
             them anymore, on client side. For QPU solvers, please replace
             `'samples'` with `'solutions'` and `'occurrences'` with
             `'num_occurrences'`. Better yet, use :meth:`Future.samples` and
-            :meth:`Future.occurrences` instead.
+            :meth:`Future.num_occurrences` instead.
 
         Examples:
             This example creates a solver using the local system's default
@@ -695,18 +697,17 @@ class Future(object):
 
         raise InvalidAPIResponseError("Active variables not present in the response")
 
-    # XXX: rename to num_occurrences, alias as occurrences, but deprecate it
     @property
-    def occurrences(self):
-        """Occurrences buffer for the submitted job.
+    def num_occurrences(self):
+        """Number of sample occurrences buffer for the submitted job.
 
         First calls to access data of a :class:`Future` object are blocking;
         subsequent access to this property is non-blocking.
 
         Returns:
-            list or NumPy matrix of doubles: Occurrences. When returned results
-            are ordered in a histogram, `occurrences` indicates the number of
-            times a particular solution recurred.
+            list or NumPy matrix of doubles: number of occurrences. When
+            returned results are ordered in a histogram, `num_occurrences`
+            indicates the number of times a particular solution recurred.
 
         Examples:
             This example creates a solver using the local system's default
@@ -721,10 +722,10 @@ class Future(object):
             ...     solver = client.get_solver()
             ...     quad = {(16, 20): -1, (17, 20): 1, (16, 21): 1, (17, 21): 1}
             ...     computation = solver.sample_ising({}, quad, num_reads=500, answer_mode='histogram')
-            ...     for i in range(len(computation.occurrences)):
+            ...     for i in range(len(computation.num_occurrences)):
             ...         print(computation.samples[i][16], computation.samples[i][17],
             ...               computation.samples[i][20], computation.samples[i][21],
-                              ' --> ', computation.energies[i], computation.occurrences[i])
+            ...               ' --> ', computation.energies[i], computation.num_occurrences[i])
             ...
             (-1, 1, -1, -1, ' --> ', -2.0, 41)
             (-1, -1, -1, 1, ' --> ', -2.0, 53)
@@ -751,6 +752,20 @@ class Future(object):
         else:
             return [1] * len(result['solutions'])
 
+    # TODO: remove in 0.10.0+
+    @property
+    def occurrences(self):
+        """Deprecated in favor of Future.num_occurrences property.
+
+        Scheduled for removal in 0.10.0.
+        """
+        warnings.warn(
+            "'Future.occurrences' is deprecated, and it will be removed "
+            "in 0.10.0+. Please convert your code to use 'Future.num_occurrences'",
+            DeprecationWarning)
+
+        return self.num_occurrences
+
     def wait_sampleset(self):
         """Blocking sampleset getter."""
 
@@ -779,7 +794,7 @@ class Future(object):
 
         sampleset = dimod.SampleSet.from_samples(
             (samples, variables), vartype=vartype,
-            energy=self.energies, num_occurrences=self.occurrences,
+            energy=self.energies, num_occurrences=self.num_occurrences,
             info=info, sort_labels=True)
 
         # this means that samplesets retrieved BEFORE this function are called
@@ -900,19 +915,24 @@ class Future(object):
         self.parse_time = time.time() - start
         return self._result
 
+    # TODO: schedule for removal
     def _alias_result(self):
-        """Create aliases for some of the keys in the results dict. Eventually,
-        those will be renamed on the server side.
+        """Alias `solutions` and `num_occurrences`.
 
-        Deprecated since version 0.6.0. Will be removed in 0.7.0.
+        Deprecated in version 0.8.0.
         """
         if not self._result:
             return
 
-        aliases = {'samples': 'solutions',
-                   'occurrences': 'num_occurrences'}
-        for alias, original in aliases.items():
-            if original in self._result and alias not in self._result:
-                self._result[alias] = self._result[original]
+        msg = "'{}' alias has been deprecated in favor of '{}'"
+        samples_msg = msg.format('samples', 'solutions')
+        occurrences_msg = msg.format('occurrences', 'num_occurrences')
+
+        aliases = dict(
+            samples=deprecated(samples_msg)(itemgetter('solutions')),
+            occurrences=deprecated(occurrences_msg)(itemgetter('num_occurrences')))
+
+        self._result = aliasdict(self._result)
+        self._result.alias(aliases)
 
         return self._result
diff --git a/dwave/cloud/config.py b/dwave/cloud/config.py
index 8fd5c2e..1c8800c 100644
--- a/dwave/cloud/config.py
+++ b/dwave/cloud/config.py
@@ -190,10 +190,10 @@ Examples:
     variable. Next, the second profile is selected
     with the explicitly named solver overriding the environment variable setting.
 
-    >>> import dwave.cloud as dc
+    >>> import dwave.cloud
     >>> import os
     >>> os.environ['DWAVE_API_SOLVER'] = 'EXAMPLE_2000Q_SYSTEM'   # doctest: +SKIP
-    >>> dc.config.load_config("./dwave_c.conf")   # doctest: +SKIP
+    >>> dwave.cloud.config.load_config("./dwave_c.conf")   # doctest: +SKIP
     {'client': 'sw',
      'endpoint': 'https://url.of.some.dwavesystem.com/sapi',
      'proxy': None,
@@ -220,8 +220,7 @@ from dwave.cloud.utils import uniform_get
 from dwave.cloud.exceptions import ConfigFileReadError, ConfigFileParseError
 
 __all__ = ['get_configfile_paths', 'get_configfile_path', 'get_default_configfile_path',
-           'load_config_from_files', 'load_profile_from_files',
-           'load_config', 'legacy_load_config']
+           'load_config_from_files', 'load_profile_from_files', 'load_config']
 
 CONF_APP = "dwave"
 CONF_AUTHOR = "dwavesystem"
@@ -270,14 +269,14 @@ def get_configfile_paths(system=True, user=True, local=True, only_existing=True)
         This example displays all paths to configuration files on a Windows system
         running Python 2.7 and then finds the single existing configuration file.
 
-        >>> import dwave.cloud as dc
+        >>> from dwave.cloud.config import get_configfile_paths
         >>> # Display paths
-        >>> dc.config.get_configfile_paths(only_existing=False)   # doctest: +SKIP
+        >>> get_configfile_paths(only_existing=False)   # doctest: +SKIP
         ['C:\\ProgramData\\dwavesystem\\dwave\\dwave.conf',
          'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf',
          '.\\dwave.conf']
         >>> # Find existing files
-        >>> dc.config.get_configfile_paths()   # doctest: +SKIP
+        >>> get_configfile_paths()   # doctest: +SKIP
         ['C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf']
 
     """
@@ -318,17 +317,17 @@ def get_configfile_path():
             Configuration file path.
 
     Examples:
-        This example displays the highest-priority configuration file on a Windows system
-        running Python 2.7.
+        This example displays the highest-priority configuration file on a
+        Windows system.
 
-        >>> import dwave.cloud as dc
+        >>> from dwave.cloud import config
         >>> # Display paths
-        >>> dc.config.get_configfile_paths(only_existing=False)   # doctest: +SKIP
+        >>> config.get_configfile_paths(only_existing=False)   # doctest: +SKIP
         ['C:\\ProgramData\\dwavesystem\\dwave\\dwave.conf',
          'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf',
          '.\\dwave.conf']
         >>> # Find highest-priority local configuration file
-        >>> dc.config.get_configfile_path()   # doctest: +SKIP
+        >>> config.get_configfile_path()   # doctest: +SKIP
         'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf'
 
     """
@@ -347,19 +346,19 @@ def get_default_configfile_path():
             Configuration file path.
 
     Examples:
-        This example displays the default configuration file on an Ubuntu Unix system
-        running IPython 2.7.
+        This example displays the default configuration file on an Ubuntu Linux
+        system.
 
-        >>> import dwave.cloud as dc
+        >>> from dwave.cloud import config
         >>> # Display paths
-        >>> dc.config.get_configfile_paths(only_existing=False)   # doctest: +SKIP
+        >>> config.get_configfile_paths(only_existing=False)   # doctest: +SKIP
         ['/etc/xdg/xdg-ubuntu/dwave/dwave.conf',
          '/usr/share/upstart/xdg/dwave/dwave.conf',
          '/etc/xdg/dwave/dwave.conf',
          '/home/mary/.config/dwave/dwave.conf',
          './dwave.conf']
         >>> # Find default configuration path
-        >>> dc.config.get_default_configfile_path()   # doctest: +SKIP
+        >>> config.get_default_configfile_path()   # doctest: +SKIP
         '/home/mary/.config/dwave/dwave.conf'
 
     """
@@ -788,150 +787,3 @@ def load_config(config_file=None, profile=None, client=None,
     section['headers'] = headers or os.getenv("DWAVE_API_HEADERS", section.get('headers'))
 
     return section
-
-
-def legacy_load_config(profile=None, endpoint=None, token=None, solver=None,
-                       proxy=None, **kwargs):
-    """Load configured URLs and token for the SAPI server.
-
-    .. warning:: Included only for backward compatibility. Please use
-        :func:`load_config` or the client factory
-        :meth:`~dwave.cloud.client.Client.from_config` instead.
-
-    This method tries to load a legacy configuration file from ``~/.dwrc``, select a
-    specified `profile` (or, if not specified, the first profile), and override
-    individual keys with values read from environment variables or
-    specified explicitly as key values in the function.
-
-    Configuration values can be specified in multiple ways, ranked in the following
-    order (with 1 the highest ranked):
-
-    1. Values specified as keyword arguments in :func:`legacy_load_config()`
-    2. Values specified as environment variables
-    3. Values specified in the legacy ``~/.dwrc`` configuration file
-
-    Environment variables searched for are:
-
-     - ``DW_INTERNAL__HTTPLINK``
-     - ``DW_INTERNAL__TOKEN``
-     - ``DW_INTERNAL__HTTPPROXY``
-     - ``DW_INTERNAL__SOLVER``
-
-    Legacy configuration file format is a modified CSV where the first comma is
-    replaced with a bar character (``|``). Each line encodes a single profile. Its
-    columns are::
-
-        profile_name|endpoint_url,authentication_token,proxy_url,default_solver_name
-
-    All its fields after ``authentication_token`` are optional.
-
-    When there are multiple connections in a file, the first one is
-    the default. Any commas in the URLs must be percent-encoded.
-
-    Args:
-        profile (str):
-            Profile name in the legacy configuration file.
-
-        endpoint (str, default=None):
-            API endpoint URL.
-
-        token (str, default=None):
-            API authorization token.
-
-        solver (str, default=None):
-            Default solver to use in :meth:`~dwave.cloud.client.Client.get_solver`.
-            If undefined, all calls to :meth:`~dwave.cloud.client.Client.get_solver`
-            must explicitly specify the solver name/id.
-
-        proxy (str, default=None):
-            URL for proxy to use in connections to D-Wave API. Can include
-            username/password, port, scheme, etc. If undefined, client uses a
-            system-level proxy, if defined, or connects directly to the API.
-
-    Returns:
-        Dictionary with keys: endpoint, token, solver, and proxy.
-
-    Examples:
-        This example creates a client using the :meth:`~dwave.cloud.client.Client.from_config`
-        method, which falls back on the legacy file by default when it fails to
-        find a D-Wave Cloud Client configuration file (setting its `legacy_config_fallback`
-        parameter to False precludes this fall-back operation). For this example,
-        no D-Wave Cloud Client configuration file is present on the local system;
-        instead the following ``.dwrc`` legacy configuration file is present in the
-        user's home directory::
-
-            profile-a|https://one.com,token-one
-            profile-b|https://two.com,token-two
-
-        The following example code creates a client without explicitly specifying
-        key values, therefore auto-detection searches for existing (non-legacy) configuration
-        files in the standard directories of :func:`get_configfile_paths` and, failing to
-        find one, falls back on the existing legacy configuration file above.
-
-        >>> import dwave.cloud as dc
-        >>> client = dwave.cloud.Client.from_config()    # doctest: +SKIP
-        >>> client.endpoint   # doctest: +SKIP
-        'https://one.com'
-        >>> client.token    # doctest: +SKIP
-        'token-one'
-
-        The following examples specify a profile and/or token.
-
-        >>> # Explicitly specify a profile
-        >>> client = dwave.cloud.Client.from_config(profile='profile-b')  # doctest: +SKIP
-        ... # Will try to connect with the url `https://two.com` and the token `token-two`.
-        >>> client = dwave.cloud.Client.from_config(profile='profile-b', token='new-token')    # doctest: +SKIP
-        ... # Will try to connect with the url `https://two.com` and the token `new-token`.
-
-    """
-
-    def _parse_config(fp, filename):
-        fields = ('endpoint', 'token', 'proxy', 'solver')
-        config = OrderedDict()
-        for line in fp:
-            # strip whitespace, skip blank and comment lines
-            line = line.strip()
-            if not line or line.startswith('#'):
-                continue
-            # parse each record, store in dict with label as key
-            try:
-                label, data = line.split('|', 1)
-                values = [v.strip() or None for v in data.split(',')]
-                config[label] = dict(zip(fields, values))
-            except:
-                raise ConfigFileParseError(
-                    "Failed to parse {!r}, line {!r}".format(filename, line))
-        return config
-
-    def _read_config(filename):
-        try:
-            with open(filename, 'r') as f:
-                return _parse_config(f, filename)
-        except (IOError, OSError):
-            raise ConfigFileReadError("Failed to read {!r}".format(filename))
-
-    config = {}
-    filename = os.path.expanduser('~/.dwrc')
-    if os.path.exists(filename):
-        config = _read_config(filename)
-
-    # load profile if specified, or first one in file
-    if profile:
-        try:
-            section = config[profile]
-        except KeyError:
-            raise ValueError("Config profile {!r} not found".format(profile))
-    else:
-        try:
-            _, section = next(iter(config.items()))
-        except StopIteration:
-            section = {}
-
-    # override config variables (if any) with environment and then with arguments
-    section['endpoint'] = endpoint or os.getenv("DW_INTERNAL__HTTPLINK", section.get('endpoint'))
-    section['token'] = token or os.getenv("DW_INTERNAL__TOKEN", section.get('token'))
-    section['proxy'] = proxy or os.getenv("DW_INTERNAL__HTTPPROXY", section.get('proxy'))
-    section['solver'] = solver or os.getenv("DW_INTERNAL__SOLVER", section.get('solver'))
-    section.update(kwargs)
-
-    return section
diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py
index 57f0d57..dbb2984 100644
--- a/dwave/cloud/solver.py
+++ b/dwave/cloud/solver.py
@@ -35,8 +35,8 @@ from collections import abc
 
 from dwave.cloud.exceptions import *
 from dwave.cloud.coders import (
-    encode_problem_as_qp, encode_problem_as_bq,
-    decode_qp_numpy, decode_qp, decode_bq)
+    encode_problem_as_qp, encode_problem_as_ref,
+    decode_qp_numpy, decode_qp, decode_bq, bqm_as_file)
 from dwave.cloud.utils import uniform_iterator, reformat_qubo_as_ising
 from dwave.cloud.computation import Future
 from dwave.cloud.concurrency import Present
@@ -234,24 +234,6 @@ class BaseSolver(object):
             # TODO: remove when all production solvers are updated
             return self.id.startswith('hybrid')
 
-    @property
-    def is_qpu(self):
-        warnings.warn("'is_qpu' property is deprecated in favor of 'qpu'."
-                      "It will be removed in 0.8.0.", DeprecationWarning)
-        return self.qpu
-
-    @property
-    def is_software(self):
-        warnings.warn("'is_software' property is deprecated in favor of 'software'."
-                      "It will be removed in 0.8.0.", DeprecationWarning)
-        return self.software
-
-    @property
-    def is_online(self):
-        warnings.warn("'is_online' property is deprecated in favor of 'online'."
-                      "It will be removed in 0.8.0.", DeprecationWarning)
-        return self.online
-
 
 class UnstructuredSolver(BaseSolver):
     """Class for D-Wave unstructured solvers.
@@ -341,8 +323,8 @@ class UnstructuredSolver(BaseSolver):
         bqm = dimod.BinaryQuadraticModel.from_qubo(qubo, offset)
         return self.sample_bqm(bqm, **params)
 
-    def _encode_any_problem_as_bqm_ref(self, problem, params):
-        """Encode `problem` for submitting in `bqm-ref` format. Upload the
+    def _encode_problem_as_ref(self, problem, params):
+        """Encode `problem` for submitting in `ref` format. Upload the
         problem if it's not already uploaded.
 
         Args:
@@ -355,19 +337,18 @@ class UnstructuredSolver(BaseSolver):
         Returns:
             str:
                 JSON-encoded problem submit body
-
         """
 
         if isinstance(problem, str):
             problem_id = problem
         else:
-            logger.debug("To encode the problem for submit via 'bqm-ref', "
+            logger.debug("To encode the problem for submit in the 'ref' format, "
                          "we need to upload it first.")
             problem_id = self.upload_bqm(problem).result()
 
         body = json.dumps({
             'solver': self.id,
-            'data': encode_problem_as_bq(problem_id),
+            'data': encode_problem_as_ref(problem_id),
             'type': 'bqm',
             'params': params
         })
@@ -395,7 +376,7 @@ class UnstructuredSolver(BaseSolver):
 
         # encode the request (body as future)
         body = self.client._encode_problem_executor.submit(
-            self._encode_any_problem_as_bqm_ref,
+            self._encode_problem_as_ref,
             problem=bqm, params=params)
 
         # computation future holds a reference to the remote job
@@ -406,31 +387,6 @@ class UnstructuredSolver(BaseSolver):
 
         return computation
 
-    @staticmethod
-    def _bqm_as_fileview(bqm):
-        # New preferred BQM binary serialization.
-        # XXX: temporary until something like dwavesystems/dimod#599 is implemented.
-
-        try:
-            from dimod.serialization.fileview import FileView as BQMFileView
-            from dimod import AdjVectorBQM
-        except ImportError: # pragma: no cover
-            return
-
-        if isinstance(bqm, BQMFileView):
-            return bqm
-
-        try:
-            if not isinstance(bqm, AdjVectorBQM):
-                bqm = AdjVectorBQM(bqm)
-        except:
-            return
-
-        try:
-            return BQMFileView(bqm)
-        except:
-            return
-
     def upload_bqm(self, bqm):
         """Upload the specified :term:`BQM` to SAPI, returning a Problem ID
         that can be used to submit the BQM to this solver (i.e. call the
@@ -452,14 +408,13 @@ class UnstructuredSolver(BaseSolver):
             To use this method, dimod package has to be installed.
         """
 
-        data = self._bqm_as_fileview(bqm)
-        if data is None:
-            if hasattr(bqm, 'to_serializable'):
-                # soon to be deprecated
-                data = encode_problem_as_bq(bqm, compress=True)['data']
-            else:
-                # raw data (ready for upload) in `bqm`
-                data = bqm
+        try:
+            data = bqm_as_file(bqm)
+        except Exception as e:
+            logger.debug("BQM conversion to file failed with %r, "
+                         "assuming data already encoded.", e)
+            # assume `bqm` given as file, ready for upload
+            data = bqm
 
         return self.client.upload_problem_encoded(data)
 
diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py
index 41a026c..161be65 100644
--- a/dwave/cloud/utils.py
+++ b/dwave/cloud/utils.py
@@ -19,6 +19,7 @@ import logging
 import platform
 import itertools
 import numbers
+import warnings
 
 from collections import abc, OrderedDict
 from urllib.parse import urljoin
@@ -484,6 +485,31 @@ class tictoc(object):
         self.dt = time.perf_counter() - self.tick
 
 
+class deprecated(object):
+    """Decorator that issues a deprecation message on each call of the
+    decorated function.
+    """
+
+    def __init__(self, msg=None):
+        self.msg = msg
+
+    def __call__(self, fn):
+        if not callable(fn):
+            raise TypeError("decorated object must be callable")
+
+        @wraps(fn)
+        def wrapped(*args, **kwargs):
+            msg = self.msg
+            if not msg:
+                fn_name = getattr(fn, '__name__', 'unnamed')
+                msg = "{}() has been deprecated".format(fn_name)
+            warnings.warn(msg, DeprecationWarning)
+
+            return fn(*args, **kwargs)
+
+        return wrapped
+
+
 def parse_loglevel(level_name, default=logging.NOTSET):
     """Resolve numeric and symbolic log level names to numeric levels."""
 
@@ -547,3 +573,99 @@ def get_platform_tags():
     fs = [ep.load() for ep in iter_entry_points('dwave.common.platform.tags')]
     tags = list(filter(None, [f() for f in fs]))
     return tags
+
+
+class aliasdict(dict):
+    """A dict subclass with support for item aliasing -- when you want to allow
+    explicit access to some keys, but not to store them in the dict.
+
+    :class:`aliasdict` can be used as a stand-in replacement for :class:`dict`.
+    If no aliases are added, behavior is identical to :class:`dict`.
+
+    Alias items added can be explicitly accessed, but they are not visible
+    otherwise via the dict interface. Aliases shadow original keys, and their
+    values can be computed on access only.
+
+    Aliases are added with :meth:`.alias`, and they are stored in the
+    :attr:`.aliases` class instance dictionary.
+
+    Example:
+        >>> from operator import itemgetter
+        >>> from dwave.cloud.utils import aliasdict
+
+        >>> d = aliasdict(a=1, b=2)
+        >>> d.alias(c=itemgetter('a'))
+        >>> d
+        {'a': 1, 'b': 2}
+        >>> 'c' in d
+        True
+        >>> d['c']
+        1
+
+    """
+    __slots__ = ('aliases', )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # keep alias keys and reference values separate from the base dict
+        self.aliases = {}
+
+    def alias(self, *args, **kwargs):
+        """Update aliases dictionary with the key/value pairs from ``other``,
+        overwriting existing keys.
+
+        Args:
+            other (dict/Iterable[(key,value)]):
+                Either another dictionary object or an iterable of key/value
+                pairs (as tuples or other iterables of length two). If keyword
+                arguments are specified, the dictionary is then updated with
+                those key/value pairs ``d.alias(red=1, blue=2)``.
+
+        Note:
+            Alias key will become available via item getter, but it will not
+            be listed in the container.
+
+            Alias value can be a concrete value for the alias key, or it can be
+            a callable that is evaluated on the aliasdict instance, on each
+            access.
+
+        """
+        self.aliases.update(*args, **kwargs)
+
+    def _alias_value(self, key):
+        value = self.aliases[key]
+        if callable(value):
+            value = value(self)
+        return value
+
+    def __getitem__(self, key):
+        if key in self.aliases:
+            return self._alias_value(key)
+        return super().__getitem__(key)
+
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError:
+            return default
+
+    def __setitem__(self, key, value):
+        if key in self.aliases:
+            return self.aliases.__setitem__(key, value)
+        return super().__setitem__(key, value)
+
+    def __delitem__(self, key):
+        if key in self.aliases:
+            return self.aliases.__delitem__(key)
+        return super().__delitem__(key)
+
+    def __contains__(self, key):
+        if key in self.aliases:
+            return True
+        return super().__contains__(key)
+
+    def copy(self):
+        new = type(self)(self)
+        new.alias(self.aliases)
+        return new


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 887f6e1..7c07618 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -318,7 +318,7 @@ class TestCli(unittest.TestCase):
     def test_upload(self):
         config_file = 'dwave.conf'
         profile = 'profile'
-        format = 'bq-zlib'
+        format = 'dimodbqm'
         problem_id = 'prob:lem:id'
         filename = 'filename'
 
diff --git a/tests/test_client.py b/tests/test_client.py
index 0ceef46..a502057 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -233,21 +233,6 @@ class ClientFactory(unittest.TestCase):
                 init.assert_called_once_with(
                     endpoint='endpoint', token='token', custom='new-custom')
 
-    def test_legacy_config_load_fallback(self):
-        conf = {k: k for k in 'endpoint token proxy solver'.split()}
-        with mock.patch("dwave.cloud.client.load_config", return_value={}):
-            with mock.patch("dwave.cloud.client.legacy_load_config", lambda **kwargs: conf):
-                # test fallback works (legacy config is loaded)
-                with dwave.cloud.Client.from_config(legacy_config_fallback=True) as client:
-                    self.assertEqual(client.endpoint, 'endpoint')
-                    self.assertEqual(client.token, 'token')
-                    self.assertEqual(client.default_solver, {"name__eq": "solver"})
-                    self.assertEqual(client.session.proxies['http'], 'proxy')
-
-                # test fallback is avoided (legacy config skipped)
-                self.assertRaises(
-                    ValueError, dwave.cloud.Client.from_config, legacy_config_fallback=False)
-
     def test_solver_features_from_config(self):
         solver_def = {"qpu": True}
         conf = {k: k for k in 'endpoint token'.split()}
diff --git a/tests/test_coders.py b/tests/test_coders.py
index fdb184d..223430e 100644
--- a/tests/test_coders.py
+++ b/tests/test_coders.py
@@ -24,7 +24,7 @@ from plucky import pluck
 
 from dwave.cloud.coders import (
     encode_problem_as_qp, decode_qp, decode_qp_numpy,
-    encode_problem_as_bq, decode_bq)
+    encode_problem_as_bq, decode_bq, encode_problem_as_ref)
 from dwave.cloud.solver import StructuredSolver, UnstructuredSolver
 from dwave.cloud.utils import generate_const_ising_problem
 
@@ -227,7 +227,7 @@ class TestQPDecodersWithOffset(TestQPDecoders):
 
 class TestBQCoders(unittest.TestCase):
 
-    def test_bq_request_encoding_empty_bqm(self):
+    def test_bq_encodes_empty_bqm(self):
         """Empty BQM has to be trivially encoded."""
 
         bqm = dimod.BQM.from_qubo({})
@@ -238,7 +238,7 @@ class TestBQCoders(unittest.TestCase):
         self.assertEqual(pluck(req, 'data.num_variables'), 0)
         self.assertEqual(pluck(req, 'data.num_interactions'), 0)
 
-    def test_bq_request_encoding_ising_bqm(self):
+    def test_bq_encodes_ising_bqm(self):
         """Simple Ising BQM properly encoded."""
 
         bqm = dimod.BQM.from_ising({0: 1}, {(0, 1): 1})
@@ -250,7 +250,7 @@ class TestBQCoders(unittest.TestCase):
         self.assertEqual(pluck(req, 'data.num_variables'), 2)
         self.assertEqual(pluck(req, 'data.num_interactions'), 1)
 
-    def test_bq_request_encoding_qubo_bqm(self):
+    def test_bq_encodes_qubo_bqm(self):
         """Simple Qubo BQM properly encoded."""
 
         bqm = dimod.BQM.from_qubo({(0, 1): 1})
@@ -262,7 +262,7 @@ class TestBQCoders(unittest.TestCase):
         self.assertEqual(pluck(req, 'data.num_variables'), 2)
         self.assertEqual(pluck(req, 'data.num_interactions'), 1)
 
-    def test_bq_request_encoding_bqm_named_vars(self):
+    def test_bq_encodes_bqm_with_named_vars(self):
         """BQM with named variable properly encoded."""
 
         bqm = dimod.BQM.from_ising({}, {'ab': 1, 'bc': 1, 'ca': 1})
@@ -275,6 +275,23 @@ class TestBQCoders(unittest.TestCase):
         self.assertEqual(pluck(req, 'data.num_interactions'), 3)
         self.assertEqual(pluck(req, 'data.variable_labels'), list('abc'))
 
+    def test_ref_encoder(self):
+        problem_id = '123'
+
+        req = encode_problem_as_ref(problem_id)
+
+        self.assertEqual(req.get('format'), 'ref')
+        self.assertEqual(req.get('data'), problem_id)
+
+    def test_bq_ref_input_validation(self):
+        problem_id = '123'
+        with self.assertRaises(TypeError):
+            encode_problem_as_bq(problem_id)
+
+        bqm = dimod.BQM.from_qubo({})
+        with self.assertRaises(TypeError):
+            encode_problem_as_ref(bqm)
+
     def test_bq_response_decoding(self):
         """Answer to simple problem properly decoded."""
 
diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py
index 3fdc8ec..e6fa54e 100644
--- a/tests/test_mock_solver_loading.py
+++ b/tests/test_mock_solver_loading.py
@@ -29,7 +29,7 @@ from dwave.cloud.hybrid import Client as HybridClient
 from dwave.cloud.exceptions import (
     SolverPropertyMissingError, ConfigFileReadError, ConfigFileParseError,
     SolverError, SolverNotFoundError)
-from dwave.cloud.config import legacy_load_config, load_config
+from dwave.cloud.config import load_config
 from dwave.cloud.testing import iterable_mock_open
 
 
@@ -319,11 +319,6 @@ class RequestEvent(Exception):
         raise RequestEvent(method, url, *args, **kwargs)
 
 
-legacy_config_body = """
-prod|file-prod-url,file-prod-token
-alpha|file-alpha-url,file-alpha-token,,alpha-solver
-"""
-
 config_body = """
 [prod]
 endpoint = http://file-prod.url
@@ -343,103 +338,6 @@ polling_timeout = 180
 """
 
 
-# patch the new config loading mechanism, to test only legacy config loading
-@mock.patch("dwave.cloud.config.get_configfile_paths", lambda: [])
-# patch Session.request to raise RequestEvent with the URL requested
-@mock.patch.object(requests.Session, 'request', RequestEvent.request)
-class MockLegacyConfiguration(unittest.TestCase):
-    """Ensure that the precedence of configuration sources is followed."""
-
-    endpoint = "http://custom-endpoint.url"
-
-    def setUp(self):
-        # clear `config_load`-relevant environment variables before testing, so
-        # we only need to patch the ones that we are currently testing
-        for key in frozenset(os.environ.keys()):
-            if key.startswith("DWAVE_") or key.startswith("DW_INTERNAL__"):
-                os.environ.pop(key, None)
-
-    def test_explicit_only(self):
-        """Specify information only through function arguments."""
-        with Client.from_config(endpoint=self.endpoint, token='arg-token') as client:
-            try:
-                client.get_solver('arg-solver')
-            except RequestEvent as event:
-                self.assertTrue(event.url.startswith(self.endpoint))
-                return
-        self.fail()
-
-    def test_nonexisting_file(self):
-        """With no values set, we should get an error when trying to create Client."""
-        with self.assertRaises(ConfigFileReadError):
-            with Client.from_config(config_file='nonexisting', legacy_config_fallback=False) as client:
-                pass
-
-    def test_explicit_with_file(self):
-        """With arguments and a config file, the config file should be ignored."""
-        with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
-            with Client.from_config(endpoint=self.endpoint, token='arg-token') as client:
-                try:
-                    client.get_solver('arg-solver')
-                except RequestEvent as event:
-                    self.assertTrue(event.url.startswith(self.endpoint))
-                    return
-                self.fail()
-
-    def test_only_file(self):
-        """With no arguments or environment variables, the default connection from the config file should be used."""
-        with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
-            with Client.from_config('config_file') as client:
-                try:
-                    client.get_solver('arg-solver')
-                except RequestEvent as event:
-                    self.assertTrue(event.url.startswith('http://file-prod.url/'))
-                    return
-                self.fail()
-
-    def test_only_file_key(self):
-        """If give a name from the config file the proper URL should be loaded."""
-        with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
-            with mock.patch("dwave.cloud.config.get_configfile_paths", lambda *x: ['file']):
-                with Client.from_config(profile='alpha') as client:
-                    try:
-                        client.get_solver('arg-solver')
-                    except RequestEvent as event:
-                        self.assertTrue(event.url.startswith('http://file-alpha.url/'))
-                        return
-                    self.fail()
-
-    def test_env_with_file_set(self):
-        """With environment variables and a config file, the config file should be ignored."""
-        with mock.patch("dwave.cloud.config.open", iterable_mock_open(legacy_config_body), create=True):
-            with mock.patch.dict(os.environ, {'DW_INTERNAL__HTTPLINK': 'http://env.url', 'DW_INTERNAL__TOKEN': 'env-token'}):
-                with Client.from_config(config_file=False, legacy_config_fallback=True) as client:
-                    try:
-                        client.get_solver('arg-solver')
-                    except RequestEvent as event:
-                        self.assertTrue(event.url.startswith('http://env.url/'))
-                        return
-                    self.fail()
-
-    def test_env_args_set(self):
-        """With arguments and environment variables, the environment variables should be ignored."""
-        with mock.patch.dict(os.environ, {'DW_INTERNAL__HTTPLINK': 'http://env.url', 'DW_INTERNAL__TOKEN': 'env-token'}):
-            with Client.from_config(endpoint=self.endpoint, token='args-token') as client:
-                try:
-                    client.get_solver('arg-solver')
-                except RequestEvent as event:
-                    self.assertTrue(event.url.startswith(self.endpoint))
-                    return
-                self.fail()
-
-    def test_file_read_error(self):
-        """On config file read error, we should fail with `ConfigFileReadError`,
-        but only if .dwrc actually exists on disk."""
-        with mock.patch("dwave.cloud.config.open", side_effect=OSError, create=True):
-            with mock.patch("os.path.exists", lambda fn: True):
-                self.assertRaises(ConfigFileReadError, legacy_load_config)
-
-
 class MockConfiguration(unittest.TestCase):
 
     def test_custom_options(self):
diff --git a/tests/test_mock_submission.py b/tests/test_mock_submission.py
index 76985aa..8d26c22 100644
--- a/tests/test_mock_submission.py
+++ b/tests/test_mock_submission.py
@@ -19,6 +19,7 @@ import json
 import unittest
 import itertools
 import threading
+import warnings
 import collections
 
 from unittest import mock
@@ -199,7 +200,12 @@ def choose_reply(path, replies, statuses=None, date=None):
 class _QueryTest(unittest.TestCase):
     def _check(self, results, linear, quad, offset=0, num_reads=1):
         # Did we get the right number of samples?
-        self.assertEqual(num_reads, sum(results.occurrences))
+        self.assertEqual(num_reads, sum(results.num_occurrences))
+
+        # verify .occurrences property still works, although is deprecated
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            self.assertEqual(100, sum(results.occurrences))
 
         # Make sure energies are correct in raw results
         for energy, state in zip(results.energies, results.samples):
@@ -1024,3 +1030,42 @@ class TestOffsetHandling(_QueryTest):
                 # but because the offset in answer is wrong, energies are off
                 with self.assertRaises(AssertionError):
                     self._check(results, linear, quadratic, offset=offset, **params)
+
+
+@mock.patch('time.sleep', lambda *x: None)
+class TestComputationDeprecations(_QueryTest):
+
+    def test_deprecations(self):
+        """Proper deprecation warnings are raised."""
+
+        def create_mock_session(client):
+            session = mock.Mock()
+            session.post = lambda a, _: choose_reply(a, {
+                'problems/': '[%s]' % complete_no_answer_reply(
+                    '123', 'abc123')})
+            session.get = lambda a: choose_reply(a, {
+                'problems/123/': complete_reply(
+                    '123', 'abc123')})
+            return session
+
+        with mock.patch.object(Client, 'create_session', create_mock_session):
+            with Client('endpoint', 'token') as client:
+                solver = Solver(client, solver_data('abc123'))
+
+                linear, quadratic = test_problem(solver)
+                params = dict(num_reads=100)
+                results = solver.sample_ising(linear, quadratic, **params)
+
+                # aliased keys are deprecated in 0.8.0
+                with self.assertWarns(DeprecationWarning):
+                    results['samples']
+                with self.assertWarns(DeprecationWarning):
+                    results['occurrences']
+
+                # .error is deprecated in 0.7.x, scheduled for removal in 0.9.0
+                with self.assertWarns(DeprecationWarning):
+                    results.error
+
+                # .occurrences is deprecated in 0.8.0, scheduled for removal in 0.10.0+
+                with self.assertWarns(DeprecationWarning):
+                    results.occurrences
diff --git a/tests/test_mock_unstructured_submission.py b/tests/test_mock_unstructured_submission.py
index 6c87804..b5af6f2 100644
--- a/tests/test_mock_unstructured_submission.py
+++ b/tests/test_mock_unstructured_submission.py
@@ -95,14 +95,14 @@ class TestUnstructuredSolver(unittest.TestCase):
                     numpy.testing.assert_array_equal(fut.sampleset, ss)
                     numpy.testing.assert_array_equal(fut.samples, ss.record.sample)
                     numpy.testing.assert_array_equal(fut.energies, ss.record.energy)
-                    numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences)
+                    numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences)
 
                     # submit of pre-uploaded bqm problem
                     fut = solver.sample_bqm(mock_problem_id)
                     numpy.testing.assert_array_equal(fut.sampleset, ss)
                     numpy.testing.assert_array_equal(fut.samples, ss.record.sample)
                     numpy.testing.assert_array_equal(fut.energies, ss.record.energy)
-                    numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences)
+                    numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences)
 
                     # ising sampling
                     lin, quad, _ = bqm.to_ising()
@@ -114,7 +114,7 @@ class TestUnstructuredSolver(unittest.TestCase):
                     numpy.testing.assert_array_equal(fut.sampleset, ss)
                     numpy.testing.assert_array_equal(fut.samples, ss.record.sample)
                     numpy.testing.assert_array_equal(fut.energies, ss.record.energy)
-                    numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences)
+                    numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences)
 
                     # qubo sampling
                     qubo, _ = bqm.to_qubo()
@@ -126,7 +126,7 @@ class TestUnstructuredSolver(unittest.TestCase):
                     numpy.testing.assert_array_equal(fut.sampleset, ss)
                     numpy.testing.assert_array_equal(fut.samples, ss.record.sample)
                     numpy.testing.assert_array_equal(fut.energies, ss.record.energy)
-                    numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences)
+                    numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences)
 
     def test_upload_failure(self):
         """Submit should gracefully fail if upload as part of submit fails."""
diff --git a/tests/test_solver.py b/tests/test_solver.py
index 8076db1..f28abc3 100644
--- a/tests/test_solver.py
+++ b/tests/test_solver.py
@@ -21,6 +21,7 @@ Note:
 
 import unittest
 import random
+import warnings
 from datetime import datetime
 
 import numpy
@@ -97,7 +98,12 @@ class _QueryTest(unittest.TestCase):
         results = solver.sample_ising(linear, quad, num_reads=100, **kwargs)
 
         # Did we get the right number of samples?
-        self.assertEqual(100, sum(results.occurrences))
+        self.assertEqual(100, sum(results.num_occurrences))
+
+        # verify .occurrences property still works, although is deprecated
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            self.assertEqual(100, sum(results.occurrences))
 
         # offset is optional
         offset = kwargs.get('offset', 0)
@@ -215,7 +221,7 @@ class Submission(_QueryTest):
             sampleset = response.sampleset
 
             # Did we get the right number of samples?
-            self.assertEqual(100, sum(response.occurrences))
+            self.assertEqual(100, sum(response.num_occurrences))
 
             # Make sure the number of occurrences and energies are all correct
             numpy.testing.assert_array_almost_equal(
@@ -237,7 +243,7 @@ class Submission(_QueryTest):
             sampleset = response.sampleset
 
             # Did we get the right number of samples?
-            self.assertEqual(100, sum(response.occurrences))
+            self.assertEqual(100, sum(response.num_occurrences))
 
             # Make sure the number of occurrences and energies are all correct
             numpy.testing.assert_array_almost_equal(
@@ -346,7 +352,7 @@ class Submission(_QueryTest):
 
             for results, linear, quad in result_list:
                 # Did we get the right number of samples?
-                self.assertEqual(10, sum(results.occurrences))
+                self.assertEqual(10, sum(results.num_occurrences))
 
                 # Make sure the number of occurrences and energies are all correct
                 for energy, state in zip(results.energies, results.samples):
@@ -373,7 +379,7 @@ class Submission(_QueryTest):
                 # Responses must be canceled or correct
                 try:
                     # Did we get the right number of samples?
-                    self.assertEqual(max_num_reads, sum(results.occurrences))
+                    self.assertEqual(max_num_reads, sum(results.num_occurrences))
 
                     # Make sure the number of occurrences and energies are all correct
                     for energy, state in zip(results.energies, results.samples):
@@ -402,7 +408,7 @@ class Submission(_QueryTest):
 
             for results, linear, quad in result_list:
                 # Did we get the right number of samples?
-                self.assertEqual(40, sum(results.occurrences))
+                self.assertEqual(40, sum(results.num_occurrences))
 
                 # Make sure the number of occurrences and energies are all correct
                 for energy, state in zip(results.energies, results.samples):
@@ -423,7 +429,7 @@ class Submission(_QueryTest):
             # Go over computations, one by one, as they're done and check they're OK
             for computation in dwave.cloud.computation.Future.as_completed(computations):
                 self.assertTrue(computation.done())
-                self.assertEqual(40, sum(computation.occurrences))
+                self.assertEqual(40, sum(computation.num_occurrences))
                 for energy, state in zip(computation.energies, computation.samples):
                     self.assertAlmostEqual(energy, evaluate_ising(linear, quad, state))
 
@@ -479,7 +485,7 @@ class DecodingMethod(_QueryTest):
             result = self._submit_and_check(solver, linear, quad)
             self.assertIsInstance(result.samples, numpy.ndarray)
             self.assertIsInstance(result.energies, numpy.ndarray)
-            self.assertIsInstance(result.occurrences, numpy.ndarray)
+            self.assertIsInstance(result.num_occurrences, numpy.ndarray)
 
     def test_request_list_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
@@ -543,7 +549,7 @@ class DecodingMethod(_QueryTest):
             result = self._submit_and_check(solver, linear, quad, answer_mode='raw')
             self.assertIsInstance(result.samples, numpy.ndarray)
             self.assertIsInstance(result.energies, numpy.ndarray)
-            self.assertIsInstance(result.occurrences, numpy.ndarray)
+            self.assertIsInstance(result.num_occurrences, numpy.ndarray)
 
     def test_request_raw_list_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2ec424f..a1a3dc5 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -12,8 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import copy
+import uuid
 import logging
 import unittest
+import warnings
 from unittest import mock
 from collections import OrderedDict
 from itertools import count
@@ -22,8 +25,8 @@ from datetime import datetime
 from dwave.cloud.utils import (
     uniform_iterator, uniform_get, strip_head, strip_tail,
     active_qubits, generate_random_ising_problem,
-    default_text_input, utcnow, cached, retried, parse_loglevel,
-    user_agent)
+    default_text_input, utcnow, cached, retried, deprecated, aliasdict,
+    parse_loglevel, user_agent)
 
 
 class TestSimpleUtils(unittest.TestCase):
@@ -342,5 +345,209 @@ class TestRetriedDecorator(unittest.TestCase):
             sleep.assert_has_calls(calls)
 
 
+class TestDeprecatedDecorator(unittest.TestCase):
+
+    def test_func_called(self):
+        """Wrapped function is called with correct arguments."""
+
+        @deprecated()
+        def f(a, b):
+            return a, b
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+
+            self.assertEqual(f(1, b=2), (1, 2))
+
+    def test_warning_raised(self):
+        """Correct deprecation message is raised."""
+
+        msg = "deprecation message"
+
+        @deprecated(msg)
+        def f():
+            return
+
+        with self.assertWarns(DeprecationWarning, msg=msg):
+            f()
+
+    def test_warning_raised_automsg(self):
+        """Deprecation message is auto-generated and raised."""
+
+        @deprecated()
+        def f():
+            return
+
+        automsg_regex = r'f\(\) has been deprecated'
+
+        with self.assertWarnsRegex(DeprecationWarning, automsg_regex):
+            f()
+
+    def test_decorator(self):
+        with self.assertRaises(TypeError):
+            deprecated()("not-a-function")
+
+
+class TestAliasdict(unittest.TestCase):
+
+    def assert_dict_interface(self, aliased, origin):
+        "Assert `aliased` behaves exactly as `origin` dict."
+
+        self.assertIsInstance(aliased, dict)
+        self.assertDictEqual(aliased, origin)
+
+        self.assertEqual(len(aliased), len(origin))
+        self.assertSetEqual(set(aliased), set(origin))
+        self.assertSetEqual(set(aliased.keys()), set(origin.keys()))
+        self.assertSetEqual(set(aliased.items()), set(origin.items()))
+        self.assertListEqual(list(aliased.values()), list(origin.values()))
+        for k in origin:
+            self.assertIn(k, aliased)
+        stranger = "unique-{}".format(''.join(origin))
+        self.assertNotIn(stranger, aliased)
+
+        self.assertSetEqual(set(iter(aliased)), set(origin))
+
+        # copy
+        new = aliased.copy()
+        self.assertIsNot(new, aliased)
+        self.assertDictEqual(new, aliased)
+
+        # dict copy constructor on aliasdict
+        new = dict(aliased)
+        self.assertDictEqual(new, origin)
+
+        # pop on copy
+        key = next(iter(origin))
+        new.pop(key)
+        self.assertSetEqual(set(new), set(origin).difference(key))
+        self.assertDictEqual(aliased, origin)
+
+        # get
+        self.assertEqual(aliased[key], origin[key])
+        self.assertEqual(aliased.get(key), origin.get(key))
+
+        # set
+        new = aliased.copy()
+        ref = origin.copy()
+        new[stranger] = 4
+        ref[stranger] = 4
+        self.assertDictEqual(new, ref)
+
+        # del
+        del new[stranger]
+        self.assertDictEqual(new, origin)
+
+    def test_construction(self):
+        # aliasdict can be constructed from a mapping/dict
+        src = dict(a=1)
+        ad = aliasdict(src)
+        self.assert_dict_interface(ad, src)
+
+        # aliasdict can be updated, without affecting the source dict
+        ad.update(b=2)
+        self.assertDictEqual(ad, dict(a=1, b=2))
+        self.assertDictEqual(src, dict(a=1))
+
+        # source dict can be updated without affecting the aliased dict
+        src.update(c=3)
+        self.assertDictEqual(ad, dict(a=1, b=2))
+        self.assertDictEqual(src, dict(a=1, c=3))
+
+    def test_dict_interface(self):
+        src = dict(a=1, b=2)
+        ad = aliasdict(**src)
+        self.assert_dict_interface(ad, src)
+
+    def test_alias_concrete(self):
+        src = dict(a=1)
+
+        ad = aliasdict(src)
+        ad.alias(b=2)
+
+        self.assert_dict_interface(ad, src)
+        self.assertEqual(ad['b'], 2)
+
+    def test_alias_callable(self):
+        src = dict(a=1)
+
+        ad = aliasdict(src)
+        ad.alias(b=lambda d: d.get('a'))
+
+        self.assert_dict_interface(ad, src)
+
+        # 'b' equals 'a'
+        self.assertEqual(ad['b'], ad['a'])
+        self.assertEqual(ad['b'], src['a'])
+        self.assertEqual(ad['b'], 1)
+
+        # it's dynamic, works also when 'a' changes
+        ad['a'] = 2
+        self.assertEqual(ad['b'], ad['a'])
+        self.assertNotEqual(ad['b'], src['a'])
+        self.assertEqual(ad['b'], 2)
+
+    def test_alias_get_set_del(self):
+        src = dict(a=1)
+        ad = aliasdict(src)
+
+        # getitem and get on alias
+        ad.alias(b=2)
+        self.assertEqual(ad['b'], 2)
+        self.assertEqual(ad.get('b'), 2)
+        self.assertEqual(ad.aliases['b'], 2)
+
+        # get with a default
+        randomkey = str(uuid.uuid4())
+        self.assertEqual(ad.get(randomkey), None)
+        self.assertEqual(ad.get(randomkey, 1), 1)
+
+        # set alias
+        ad['b'] = 3
+        self.assertEqual(ad['b'], 3)
+        self.assertEqual(ad.aliases['b'], 3)
+
+        # update alias
+        ad.alias(b=4)
+        self.assertEqual(ad['b'], 4)
+        self.assertEqual(ad.aliases['b'], 4)
+
+        # delete alias
+        del ad['b']
+        self.assertEqual(ad.get('b'), None)
+
+    def test_shadowing(self):
+        "Alias keys take precedence."
+
+        ad = aliasdict(a=1)
+        ad.alias(a=2)
+
+        self.assertEqual(ad['a'], 2)
+
+        self.assertEqual(dict.__getitem__(ad, 'a'), 1)
+
+    def test_copy(self):
+        src = dict(a=1)
+        aliases = dict(b=2)
+
+        ad = aliasdict(src)
+        ad.alias(**aliases)
+
+        self.assertDictEqual(ad, src)
+        self.assertDictEqual(ad.aliases, aliases)
+
+        new = ad.copy()
+        self.assertIsInstance(new, aliasdict)
+        self.assertIsNot(new, ad)
+        self.assertDictEqual(new, src)
+        self.assertDictEqual(new.aliases, aliases)
+
+        new = copy.deepcopy(ad)
+        self.assertIsInstance(new, aliasdict)
+        self.assertIsNot(new, ad)
+        self.assertDictEqual(new, src)
+        self.assertDictEqual(new.aliases, aliases)
+
+
 if __name__ == '__main__':
     unittest.main()