Repository: microsoft/Qcodes

Issue:
Feature request: set_latest
Parameters roughly speaking work like this:

- To set the parameter value via instrument communication, use `set`
- To get the parameter value via instrument communication, use `get`
- To get the parameter value without instrument communication, use `get_latest`
- To set the parameter value without instrument communication, use... `_save_val`?

Shouldn't that be `set_latest`? Furthermore, `_save_val` currently does not quite work as expected in cases where a `Parameter` has no `get_cmd`, but does have a `val_mapping`. An example:

```python
p1 = Parameter('p1', val_mapping={'one': 1, 'two': 2}, set_cmd=lambda x: x)
p1._save_val('one')
p1.get()
```
Raises a TypeError, because the `get` calls a `lambda: self.raw_value`, but the `raw_value` is `None`, since `_save_val` does not update the `raw_value` if there is a `val_mapping`. Link to code:
https://github.com/QCoDeS/Qcodes/blob/b68979ef084a19373976daaef1d6a19ba5644ae9/qcodes/instrument/parameter.py#L341-L355

Ideally, I would like
```python
p1 = Parameter('p1', val_mapping={'one': 1, 'two': 2}, set_cmd=lambda x: x)
p1.set_latest('one')
```
to set `p1._latest` to `{'value': 'one', 'ts': datetime(blah blah), 'raw_value': 1}`. Similarly for cases with scaling, offset, and parsers.

@QCoDeS/core 

PS: This came up in #1218, so although slightly contrived, in **can** happen in the real world.


Discussion:

Resolving patch:
diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py
--- a/qcodes/instrument/parameter.py
+++ b/qcodes/instrument/parameter.py
@@ -84,9 +84,10 @@
 from typing import Optional, Sequence, TYPE_CHECKING, Union, Callable, List, \
     Dict, Any, Sized, Iterable, cast, Type, Tuple, Iterator
 from functools import partial, wraps
+
 import numpy
-from qcodes.utils.helpers import abstractmethod
 
+from qcodes.utils.helpers import abstractmethod
 from qcodes.utils.helpers import (permissive_range, is_sequence_of,
                                   DelegateAttributes, full_class, named_repr,
                                   warn_units)
@@ -99,11 +100,15 @@
 if TYPE_CHECKING:
     from .base import Instrument, InstrumentBase
 
+
 Number = Union[float, int]
+
+
 # for now the type the parameter may contain is not restricted at all
 ParamDataType = Any
 ParamRawDataType = Any
 
+
 log = logging.getLogger(__name__)
 
 
@@ -267,7 +272,6 @@ def __init__(self, name: str,
         self.step = step
         self.scale = scale
         self.offset = offset
-        self.raw_value = None
 
         self.inter_delay = inter_delay
         self.post_delay = post_delay
@@ -284,8 +288,10 @@ def __init__(self, name: str,
         # record of latest value and when it was set or measured
         # what exactly this means is different for different subclasses
         # but they all use the same attributes so snapshot is consistent.
-        self._latest: Dict[str, Optional[Union[ParamDataType, datetime]]] = \
-            {'value': None, 'ts': None, 'raw_value': None}
+        self._latest: Dict[str,Optional[Union[ParamDataType,
+                                              ParamRawDataType,
+                                              datetime]]]
+        self._latest = {'value': None, 'ts': None, 'raw_value': None}
         self.get_latest = GetLatest(self, max_val_age=max_val_age)
 
         if hasattr(self, 'get_raw') and not getattr(self.get_raw, '__qcodes_is_abstract_method__', False):
@@ -321,6 +327,39 @@ def __init__(self, name: str,
         # to perform a validation on get
         self._validate_on_get = False
 
+    @property
+    def raw_value(self) -> ParamRawDataType:
+        """
+        Represents the cached raw value of the parameter.
+
+        Note that this property will be deprecated soon.
+
+        :getter: Returns the cached raw value of the parameter.
+        :setter: Setting the ``raw_value`` is not recommended as it may lead to
+            inconsistent state of the parameter.
+        """
+        return self._get_cache_raw()
+
+    @raw_value.setter
+    def raw_value(self, new_raw_value: ParamRawDataType) -> None:
+        self._set_cache_raw(new_raw_value)
+
+    def _set_cache_raw(self, raw_value: ParamRawDataType) -> None:
+        """
+        Sets the cached raw value of the parameter.
+
+        This is a private method for internal QCoDeS use only.
+        """
+        self._latest["raw_value"] = raw_value
+
+    def _get_cache_raw(self) -> ParamRawDataType:
+        """
+        Returns the cached raw value of the parameter.
+
+        This is a private method for internal QCoDeS use only.
+        """
+        return self._latest["raw_value"]
+
     @abstractmethod
     def get_raw(self) -> ParamRawDataType:
         """
@@ -427,8 +466,75 @@ def snapshot_base(self, update: bool = True,
 
         return state
 
+    def _from_value_to_raw_value(self, value: ParamDataType
+                                 ) -> ParamRawDataType:
+        raw_value: ParamRawDataType
+
+        if self.val_mapping is not None:
+            # Convert set values using val_mapping dictionary
+            raw_value = self.val_mapping[value]
+        else:
+            raw_value = value
+
+        # transverse transformation in reverse order as compared to
+        # getter: apply scale first
+        if self.scale is not None:
+            if isinstance(self.scale, collections.abc.Iterable):
+                # Scale contains multiple elements, one for each value
+                raw_value = tuple(val * scale for val, scale
+                                  in zip(raw_value, self.scale))
+            else:
+                # Use single scale for all values
+                raw_value *= self.scale
+
+        # apply offset next
+        if self.offset is not None:
+            if isinstance(self.offset, collections.abc.Iterable):
+                # offset contains multiple elements, one for each value
+                raw_value = tuple(val + offset for val, offset
+                                  in zip(raw_value, self.offset))
+            else:
+                # Use single offset for all values
+                raw_value += self.offset
+
+        # parser last
+        if self.set_parser is not None:
+            raw_value = self.set_parser(raw_value)
+
+        return raw_value
+
+    def set_cache(self, value: ParamDataType) -> None:
+        """
+        Set the cached value of the parameter without invoking the
+        ``set_cmd`` of the parameter (if it has one). For example, in case of
+        an instrument parameter, calling :meth:`set_cache` as opposed to
+        calling ``set`` will only change the internally-stored value of
+        the parameter (that is available when calling ``get_latest``),
+        and will pass that value to the instrument.
+
+        Note that this method also respects all the validation, parsing,
+        offsetting, etc that the ``set`` method respects. However,
+        if the parameter has :attr:`step` defined, unlike the ``set`` method,
+        this method does not perform setting the parameter step-by-step.
+
+        Args:
+            value: new value for the parameter
+        """
+        self.validate(value)
+        raw_value = self._from_value_to_raw_value(value)
+        self._update_cache_with(value=value, raw_value=raw_value)
+
+    def _update_cache_with(self, *,
+                           value: ParamDataType,
+                           raw_value: ParamRawDataType) -> None:
+        self._latest = {'value': value,
+                        'raw_value': raw_value,
+                        'ts': datetime.now()}
+
     def _save_val(self, value: ParamDataType, validate: bool = False) -> None:
         """
+        Use ``set_cache`` instead of this method. It will be deprecated soon.
+
         Update latest
         """
         if validate:
@@ -438,9 +544,56 @@ def _save_val(self, value: ParamDataType, validate: bool = False) -> None:
                 self.val_mapping is None and
                 self.scale is None and
                 self.offset is None):
-            self.raw_value = value
-        self._latest = {'value': value, 'ts': datetime.now(),
-                        'raw_value': self.raw_value}
+            raw_value = value
+        else:
+            raw_value = self._get_cache_raw()
+        self._update_cache_with(value=value, raw_value=raw_value)
+
+    def _from_raw_value_to_value(self, raw_value: ParamRawDataType
+                                 ) -> ParamDataType:
+        value: ParamDataType
+
+        if self.get_parser is not None:
+            value = self.get_parser(raw_value)
+        else:
+            value = raw_value
+
+        # apply offset first (native scale)
+        if self.offset is not None:
+            # offset values
+            if isinstance(self.offset, collections.abc.Iterable):
+                # offset contains multiple elements, one for each value
+                value = tuple(val - offset for val, offset
+                              in zip(value, self.offset))
+            elif isinstance(value, collections.abc.Iterable):
+                # Use single offset for all values
+                value = tuple(val - self.offset for val in value)
+            else:
+                value -= self.offset
+
+        # scale second
+        if self.scale is not None:
+            # Scale values
+            if isinstance(self.scale, collections.abc.Iterable):
+                # Scale contains multiple elements, one for each value
+                value = tuple(val / scale for val, scale
+                              in zip(value, self.scale))
+            elif isinstance(value, collections.abc.Iterable):
+                # Use single scale for all values
+                value = tuple(val / self.scale for val in value)
+            else:
+                value /= self.scale
+
+        if self.inverse_val_mapping is not None:
+            if value in self.inverse_val_mapping:
+                value = self.inverse_val_mapping[value]
+            else:
+                try:
+                    value = self.inverse_val_mapping[int(value)]
+                except (ValueError, KeyError):
+                    raise KeyError(f"'{value}' not in val_mapping")
+
+        return value
 
     def _wrap_get(self, get_function: Callable[..., ParamDataType]) ->\
             Callable[..., ParamDataType]:
@@ -448,48 +601,17 @@ def _wrap_get(self, get_function: Callable[..., ParamDataType]) ->\
         def get_wrapper(*args: Any, **kwargs: Any) -> ParamDataType:
             try:
                 # There might be cases where a .get also has args/kwargs
-                value = get_function(*args, **kwargs)
-                self.raw_value = value
-
-                if self.get_parser is not None:
-                    value = self.get_parser(value)
-
-                # apply offset first (native scale)
-                if self.offset is not None:
-                    # offset values
-                    if isinstance(self.offset, collections.abc.Iterable):
-                        # offset contains multiple elements, one for each value
-                        value = tuple(val - offset for val, offset
-                                      in zip(value, self.offset))
-                    elif isinstance(value, collections.abc.Iterable):
-                        # Use single offset for all values
-                        value = tuple(val - self.offset for val in value)
-                    else:
-                        value -= self.offset
-
-                # scale second
-                if self.scale is not None:
-                    # Scale values
-                    if isinstance(self.scale, collections.abc.Iterable):
-                        # Scale contains multiple elements, one for each value
-                        value = tuple(val / scale for val, scale
-                                      in zip(value, self.scale))
-                    elif isinstance(value, collections.abc.Iterable):
-                        # Use single scale for all values
-                        value = tuple(val / self.scale for val in value)
-                    else:
-                        value /= self.scale
+                raw_value = get_function(*args, **kwargs)
+
+                value = self._from_raw_value_to_value(raw_value)
+
+                if self._validate_on_get:
+                    self.validate(value)
+
+                self._update_cache_with(value=value, raw_value=raw_value)
 
-                if self.inverse_val_mapping is not None:
-                    if value in self.inverse_val_mapping:
-                        value = self.inverse_val_mapping[value]
-                    else:
-                        try:
-                            value = self.inverse_val_mapping[int(value)]
-                        except (ValueError, KeyError):
-                            raise KeyError(f"'{value}' not in val_mapping")
-                self._save_val(value, validate=self._validate_on_get)
                 return value
+
             except Exception as e:
                 e.args = e.args + ('getting {}'.format(self),)
                 raise e
@@ -512,39 +634,8 @@ def set_wrapper(value: ParamDataType, **kwargs: Any) -> None:
                     # even if the final value is valid we may be generating
                     # steps that are not so validate them too
                     self.validate(val_step)
-                    if self.val_mapping is not None:
-                        # Convert set values using val_mapping dictionary
-                        raw_value = self.val_mapping[val_step]
-                    else:
-                        raw_value = val_step
-
-                    # transverse transformation in reverse order as compared to
-                    # getter:
-                    # apply scale first
-                    if self.scale is not None:
-                        if isinstance(self.scale, collections.abc.Iterable):
-                            # Scale contains multiple elements,
-                            # one for each value
-                            raw_value = tuple(val * scale for val, scale
-                                              in zip(raw_value, self.scale))
-                        else:
-                            # Use single scale for all values
-                            raw_value *= self.scale
-
-                    # apply offset next
-                    if self.offset is not None:
-                        if isinstance(self.offset, collections.abc.Iterable):
-                            # offset contains multiple elements,
-                            # one for each value
-                            raw_value = tuple(val + offset for val, offset
-                                              in zip(raw_value, self.offset))
-                        else:
-                            # Use single offset for all values
-                            raw_value += self.offset
-
-                    # parser last
-                    if self.set_parser is not None:
-                        raw_value = self.set_parser(raw_value)
+
+                    raw_val_step = self._from_value_to_raw_value(val_step)
 
                     # Check if delay between set operations is required
                     t_elapsed = time.perf_counter() - self._t_last_set
@@ -556,10 +647,10 @@ def set_wrapper(value: ParamDataType, **kwargs: Any) -> None:
                     # Start timer to measure execution time of set_function
                     t0 = time.perf_counter()
 
-                    set_function(raw_value, **kwargs)
-                    self.raw_value = raw_value
-                    self._save_val(val_step,
-                                   validate=False)
+                    set_function(raw_val_step, **kwargs)
+
+                    self._update_cache_with(value=val_step,
+                                            raw_value=raw_val_step)
 
                     # Update last set time (used for calculating delays)
                     self._t_last_set = time.perf_counter()
@@ -984,7 +1075,7 @@ def __init__(self, name: str,
         # get/set methods)
         if not hasattr(self, 'get') and get_cmd is not False:
             if get_cmd is None:
-                self.get_raw = lambda: self._latest['raw_value']   # type: ignore[assignment]
+                self.get_raw = self._get_cache_raw   # type: ignore[assignment]
             else:
                 exec_str_ask = getattr(instrument, "ask", None) \
                     if instrument else None
@@ -994,7 +1085,7 @@ def __init__(self, name: str,
 
         if not hasattr(self, 'set') and set_cmd is not False:
             if set_cmd is None:
-                self.set_raw: Callable = partial(self._save_val, validate=False)
+                self.set_raw: Callable = lambda x: x
             else:
                 exec_str_write = getattr(instrument, "write", None) \
                     if instrument else None


Test patch:
diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py
--- a/qcodes/tests/test_parameter.py
+++ b/qcodes/tests/test_parameter.py
@@ -201,6 +201,21 @@ def test_number_of_validations(self):
         self.assertEqual(p.vals.values_validated,
                          [0, 0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
 
+    def test_number_of_validations_for_set_cached(self):
+        p = Parameter('p', set_cmd=None,
+                      vals=BookkeepingValidator())
+        self.assertEqual(p.vals.values_validated, [])
+
+        p.set_cache(1)
+        self.assertEqual(p.vals.values_validated, [1])
+
+        p.set_cache(4)
+        self.assertEqual(p.vals.values_validated, [1, 4])
+
+        p.step = 1
+        p.set_cache(10)
+        self.assertEqual(p.vals.values_validated, [1, 4, 10])
+
     def test_snapshot_value(self):
         p_snapshot = Parameter('no_snapshot', set_cmd=None, get_cmd=None,
                                snapshot_value=True)
@@ -353,6 +368,18 @@ def __init__(self, *args, **kwargs):
         with self.assertRaises(RuntimeError):
             localparameter.get_latest()
 
+    def test_latest_dictionary_gets_updated_upon_set_of_memory_parameter(self):
+        p = Parameter('p', set_cmd=None, get_cmd=None)
+        assert p._latest['value'] is None
+        assert p._latest['raw_value'] is None
+        assert p._latest['ts'] is None
+
+        p(42)
+
+        assert p._latest['value'] == 42
+        assert p._latest['raw_value'] == 42
+        assert p._latest['ts'] is not None
+
     def test_has_set_get(self):
         # Create parameter that has no set_cmd, and get_cmd returns last value
         gettable_parameter = Parameter('one', set_cmd=False, get_cmd=None)
@@ -362,6 +389,9 @@ def test_has_set_get(self):
             gettable_parameter(1)
         # Initial value is None if not explicitly set
         self.assertIsNone(gettable_parameter())
+        # Assert the set_cache still works for non-settable parameter
+        gettable_parameter.set_cache(1)
+        self.assertEqual(gettable_parameter(), 1)
 
         # Create parameter that saves value during set, and has no get_cmd
         settable_parameter = Parameter('two', set_cmd=None, get_cmd=False)
@@ -414,6 +444,13 @@ def test_step_ramp(self):
         p(44.5)
         self.assertListEqual(p.set_values, [42, 43, 44, 44.5])
 
+        # Assert that stepping does not impact ``set_cache`` call, and that
+        # the value that is passed to ``set_cache`` call does not get
+        # propagated to parameter's ``set_cmd``
+        p.set_cache(40)
+        self.assertEqual(p.get_latest(), 40)
+        self.assertListEqual(p.set_values, [42, 43, 44, 44.5])
+
         # Test error conditions
         with self.assertLogs(level='WARN'):
             self.assertEqual(p.get_ramp_values("A", 1), [])
@@ -513,6 +550,69 @@ def test_scale_and_offset_raw_value_iterable(self, values, offsets, scales):
         if isinstance(scales, Iterable) and not isinstance(offsets, Iterable):
             event('Scale is array but not offset')
 
+    @settings(max_examples=300)
+    @given(
+        values=iterable_or_number(
+            TestFloats, SharedSize, ValuesScalar, True),
+        offsets=iterable_or_number(
+            TestFloats, SharedSize, ValuesScalar, False),
+        scales=iterable_or_number(
+            TestFloats, SharedSize, ValuesScalar, False))
+    def test_scale_and_offset_raw_value_iterable_for_set_cached(
+            self, values, offsets, scales):
+        p = Parameter(name='test_scale_and_offset_raw_value', set_cmd=None)
+
+        # test that scale and offset does not change the default behaviour
+        p.set_cache(values)
+        self.assertEqual(p.raw_value, values)
+
+        # test setting scale and offset does not change anything
+        p.scale = scales
+        p.offset = offsets
+        self.assertEqual(p.raw_value, values)
+
+        np_values = np.array(values)
+        np_offsets = np.array(offsets)
+        np_scales = np.array(scales)
+        np_get_latest_values = np.array(p.get_latest())
+        # Without a call to ``get``, ``get_latest`` will just return old
+        # cached values without applying the set scale and offset
+        np.testing.assert_allclose(np_get_latest_values, np_values)
+        np_get_values = np.array(p.get())
+        # Now that ``get`` is called, the returned values are the result of
+        # application of the scale and offset. Obviously, calling
+        # ``get_latest`` now will also return the values with the applied
+        # scale and offset
+        np.testing.assert_allclose(np_get_values,
+                                   (np_values - np_offsets) / np_scales)
+        np_get_latest_values_after_get = np.array(p.get_latest())
+        np.testing.assert_allclose(np_get_latest_values_after_get,
+                                   (np_values - np_offsets) / np_scales)
+
+        # test set_cache for scalar values
+        if not isinstance(values, Iterable):
+            p.set_cache(values)
+            np.testing.assert_allclose(np.array(p.raw_value),
+                                       np_values * np_scales + np_offsets)
+            # No set/get cmd performed
+
+            # testing conversion back and forth
+            p.set_cache(values)
+            np_get_latest_values = np.array(p.get_latest())
+            # No set/get cmd performed
+            np.testing.assert_allclose(np_get_latest_values, np_values)
+
+        # adding statistics
+        if isinstance(offsets, Iterable):
+            event('Offset is array')
+        if isinstance(scales, Iterable):
+            event('Scale is array')
+        if isinstance(values, Iterable):
+            event('Value is array')
+        if isinstance(scales, Iterable) and isinstance(offsets, Iterable):
+            event('Scale is array and also offset')
+        if isinstance(scales, Iterable) and not isinstance(offsets, Iterable):
+            event('Scale is array but not offset')
 
     @given(scale=hst.integers(1, 100),
            value=hst.floats(min_value=1e-9, max_value=10))
@@ -653,6 +753,70 @@ def get_function():
         assert a.get() == -10
 
 
+_P = Parameter
+
+
+@pytest.mark.parametrize(
+    argnames=('p', 'value', 'raw_value'),
+    argvalues=(
+        (_P('p', set_cmd=None, get_cmd=None), 4, 4),
+        (_P('p', set_cmd=False, get_cmd=None), 14, 14),
+        (_P('p', set_cmd=None, get_cmd=False), 14, 14),
+        (_P('p', set_cmd=None, get_cmd=None, vals=vals.OnOff()), 'on', 'on'),
+        (_P('p', set_cmd=None, get_cmd=None, val_mapping={'screw': 1}),
+         'screw', 1),
+        (_P('p', set_cmd=None, get_cmd=None, set_parser=str, get_parser=int),
+         14, '14'),
+        (_P('p', set_cmd=None, get_cmd=None, step=7), 14, 14),
+        (_P('p', set_cmd=None, get_cmd=None, offset=3), 14, 17),
+        (_P('p', set_cmd=None, get_cmd=None, scale=2), 14, 28),
+        (_P('p', set_cmd=None, get_cmd=None, offset=-3, scale=2), 14, 25),
+    ),
+    ids=(
+        'with_nothing_extra',
+        'without_set_cmd',
+        'without_get_cmd',
+        'with_on_off_validator',
+        'with_val_mapping',
+        'with_set_and_parsers',
+        'with_step',
+        'with_offset',
+        'with_scale',
+        'with_scale_and_offset',
+    )
+)
+def test_set_latest_works_for_plain_memory_parameter(p, value, raw_value):
+    # Set latest value of the parameter
+    p.set_cache(value)
+
+    # Assert the latest value and raw_value
+    assert p.get_latest() == value
+    assert p.raw_value == raw_value
+
+    # Assert latest value and raw_value via private attributes for strictness
+    assert p._latest['value'] == value
+    assert p._latest['raw_value'] == raw_value
+
+    # Now let's get the value of the parameter to ensure that the value that
+    # is set above gets picked up from the `_latest` dictionary (due to
+    # `get_cmd=None`)
+
+    if not hasattr(p, 'get'):
+        return  # finish the test here for non-gettable parameters
+
+    gotten_value = p.get()
+
+    assert gotten_value == value
+
+    # Assert the latest value and raw_value
+    assert p.get_latest() == value
+    assert p.raw_value == raw_value
+
+    # Assert latest value and raw_value via private attributes for strictness
+    assert p._latest['value'] == value
+    assert p._latest['raw_value'] == raw_value
+
+
 class TestValsandParseParameter(TestCase):
 
     def setUp(self):
@@ -784,6 +948,15 @@ def test_has_set_get(self):
         self.assertTrue(hasattr(p, 'get'))
         self.assertFalse(hasattr(p, 'set'))
 
+        # Yet, it's possible to set the cached value
+        p.set_cache([6, 7, 8])
+        self.assertListEqual(p.get_latest(), [6, 7, 8])
+        # However, due to the implementation of this ``SimpleArrayParam``
+        # test parameter it's ``get`` call will return the originally passed
+        # list
+        self.assertListEqual(p.get(), [1, 2, 3])
+        self.assertListEqual(p.get_latest(), [1, 2, 3])
+
         with self.assertRaises(AttributeError):
             SettableArray([1, 2, 3], name, shape)
 
@@ -938,20 +1111,33 @@ def test_has_set_get(self):
         with self.assertRaises(AttributeError):
             MultiParameter(name, names, shapes)
 
-        p = SimpleMultiParam([0, [1, 2, 3], [[4, 5], [6, 7]]],
-                             name, names, shapes)
+        original_value = [0, [1, 2, 3], [[4, 5], [6, 7]]]
+        p = SimpleMultiParam(original_value, name, names, shapes)
 
         self.assertTrue(hasattr(p, 'get'))
         self.assertFalse(hasattr(p, 'set'))
+        # Ensure that ``set_cache`` works
+        new_cache = [10, [10, 20, 30], [[40, 50], [60, 70]]]
+        p.set_cache(new_cache)
+        self.assertListEqual(p.get_latest(), new_cache)
+        # However, due to the implementation of this ``SimpleMultiParam``
+        # test parameter it's ``get`` call will return the originally passed
+        # list
+        self.assertListEqual(p.get(), original_value)
+        self.assertListEqual(p.get_latest(), original_value)
+
         # We allow creation of Multiparameters with set to support
         # instruments that already make use of them.
-
         p = SettableMulti([0, [1, 2, 3], [[4, 5], [6, 7]]], name, names, shapes)
         self.assertTrue(hasattr(p, 'get'))
         self.assertTrue(hasattr(p, 'set'))
         value_to_set = [2, [1, 5, 2], [[8, 2], [4, 9]]]
         p.set(value_to_set)
         assert p.get() == value_to_set
+        # Also, ``set_cache`` works as expected
+        p.set_cache(new_cache)
+        assert p.get_latest() == new_cache
+        assert p.get() == value_to_set
 
     def test_full_name_s(self):
         name = 'mixed_dimensions'
@@ -1042,6 +1228,14 @@ def test_param_cmd_with_parsing(self):
         self.assertEqual(self._p, '5')
         self.assertEqual(p(), 5)
 
+        p.set_cache(7)
+        self.assertEqual(p.get_latest(), 7)
+        # Nothing has been passed to the "instrument" at ``set_cache``
+        # call, hence the following assertions should hold
+        self.assertEqual(self._p, '5')
+        self.assertEqual(p(), 5)
+        self.assertEqual(p.get_latest(), 5)
+
     def test_settable(self):
         p = Parameter('p', set_cmd=self.set_p, get_cmd=False)
 
@@ -1053,6 +1247,11 @@ def test_settable(self):
         self.assertTrue(hasattr(p, 'set'))
         self.assertFalse(hasattr(p, 'get'))
 
+        # For settable-only parameters, using ``set_cache`` may not make
+        # sense, neertheless, it works
+        p.set_cache(7)
+        self.assertEqual(p.get_latest(), 7)
+
     def test_gettable(self):
         p = Parameter('p', get_cmd=self.get_p)
         self._p = 21
@@ -1066,6 +1265,14 @@ def test_gettable(self):
         self.assertTrue(hasattr(p, 'get'))
         self.assertFalse(hasattr(p, 'set'))
 
+        p.set_cache(7)
+        self.assertEqual(p.get_latest(), 7)
+        # Nothing has been passed to the "instrument" at ``set_cache``
+        # call, hence the following assertions should hold
+        self.assertEqual(self._p, 21)
+        self.assertEqual(p(), 21)
+        self.assertEqual(p.get_latest(), 21)
+
     def test_val_mapping_basic(self):
         p = Parameter('p', set_cmd=self.set_p, get_cmd=self.get_p,
                       val_mapping={'off': 0, 'on': 1},
@@ -1087,6 +1294,16 @@ def test_val_mapping_basic(self):
         with self.assertRaises(KeyError):
             p()
 
+        self._p = 1  # for further testing
+
+        p.set_cache('off')
+        self.assertEqual(p.get_latest(), 'off')
+        # Nothing has been passed to the "instrument" at ``set_cache``
+        # call, hence the following assertions should hold
+        self.assertEqual(self._p, 1)
+        self.assertEqual(p(), 'on')
+        self.assertEqual(p.get_latest(), 'on')
+
     def test_val_mapping_with_parsers(self):
         # set_parser with val_mapping
         Parameter('p', set_cmd=self.set_p, get_cmd=self.get_p,
@@ -1106,6 +1323,14 @@ def test_val_mapping_with_parsers(self):
         self._p = 'PVAL: 1'
         self.assertEqual(p(), 'on')
 
+        p.set_cache('off')
+        self.assertEqual(p.get_latest(), 'off')
+        # Nothing has been passed to the "instrument" at ``set_cache``
+        # call, hence the following assertions should hold
+        self.assertEqual(self._p, 'PVAL: 1')
+        self.assertEqual(p(), 'on')
+        self.assertEqual(p.get_latest(), 'on')
+
     def test_on_off_val_mapping(self):
         instrument_value_for_on = 'on_'
         instrument_value_for_off = 'off_'
@@ -1513,3 +1738,107 @@ def test_delegate_parameter_snapshot():
     assert source_snapshot == p.snapshot()
     assert snapshot['value'] == 2
     assert source_snapshot['value'] == 13
+
+
+def test_delegate_parameter_set_cached_for_memory_source_parameter():
+    initial_value = 1
+    source = Parameter('p', set_cmd=None, get_cmd=None,
+                       initial_value=initial_value, offset=1, scale=2)
+    delegate = DelegateParameter('d', source, offset=4, scale=5)
+
+    # Setting the cached value of the source parameter has no impact on the
+    # delegate parameter
+
+    new_source_value = 3
+    source.set_cache(new_source_value)
+
+    assert source.raw_value == new_source_value * 2 + 1
+    assert source.get_latest() == new_source_value
+
+    assert delegate.raw_value is None
+
+    # But then when the delegate parameter's ``get`` is called, the new
+    # value of the source propagates
+    gotten_delegate_value = delegate.get()
+
+    assert gotten_delegate_value == (new_source_value - 4) / 5
+    assert delegate.raw_value == new_source_value
+    assert delegate.get_latest() == (new_source_value - 4) / 5
+
+    # Setting the cached value of the delegate parameter has no impact on
+    # the source parameter
+
+    new_delegate_value = 2
+    delegate.set_cache(new_delegate_value)
+
+    assert delegate.raw_value == new_delegate_value * 5 + 4
+    assert delegate.get_latest() == new_delegate_value
+
+    assert source.raw_value == new_source_value * 2 + 1
+    assert source.get_latest() == new_source_value
+
+    pytest.xfail(reason="DelegateParameter's get_latest and set_cache should "
+                        "reflect the state of the source parameter as "
+                        "opposed to the behavior that this test exposes.")
+
+
+def test_delegate_parameter_set_cached_for_instrument_source_parameter():
+    instrument_value = -689
+
+    def get_instrument_value():
+        nonlocal instrument_value
+        return instrument_value
+
+    def set_instrument_value(value):
+        nonlocal instrument_value
+        instrument_value = value
+
+    initial_value = 1
+    source = Parameter('p',
+                       set_cmd=set_instrument_value,
+                       get_cmd=get_instrument_value,
+                       initial_value=initial_value, offset=1, scale=2)
+    delegate = DelegateParameter('d', source, offset=4, scale=5)
+
+    # Setting the cached value of the source parameter has no impact on the
+    # delegate parameter. It also has no impact on the instrument value.
+
+    new_source_value = 3
+    source.set_cache(new_source_value)
+
+    assert source.raw_value == new_source_value * 2 + 1
+    assert source.get_latest() == new_source_value
+
+    assert instrument_value == initial_value * 2 + 1
+
+    assert delegate.raw_value is None
+
+    # Even after the delegate parameter's ``get`` is called, source
+    # parameter and the instrument values remain unchanged
+
+    gotten_delegate_value = delegate.get()
+
+    assert gotten_delegate_value == (initial_value - 4) / 5
+
+    assert delegate.raw_value == initial_value
+    assert delegate.get_latest() == (initial_value - 4) / 5
+
+    assert instrument_value == initial_value * 2 + 1
+
+    # Setting the cached value of the delegate parameter has no impact on
+    # the source parameter, and on the instrument value
+
+    new_delegate_value = 2
+    delegate.set_cache(new_delegate_value)
+
+    assert delegate.raw_value == new_delegate_value * 5 + 4
+    assert delegate.get_latest() == new_delegate_value
+
+    assert source.raw_value == initial_value * 2 + 1
+    assert source.get_latest() == initial_value
+
+    assert instrument_value == initial_value * 2 + 1
+
+    pytest.xfail(reason="DelegateParameter's get_latest and set_cache should "
+                        "reflect the state of the source parameter as "
+                        "opposed to the behavior that this test exposes.")