The basics#

Before anything else, let’s import the classes we need:

try:
    from respace import ResultSet
except ImportError:
    !pip install respace
    from respace import ResultSet

The ResultSet class#

Now let’s build the simplest possible ResultSet: it contains only one result "result" that depends on a single parameter "parameter". And let’s make it verbose, so we see more of what’s going on:

def add_one(parameter): return parameter + 1
rs = ResultSet({"result": add_one}, {"parameter": 1}, verbose=True)
rs
<xarray.Dataset>
Dimensions:    (parameter: 1)
Coordinates:
  * parameter  (parameter) int64 1
Data variables:
    result     (parameter) int64 -1

Here you can see what’s displayed is an xarray.Dataset instance, which is a representation of your parameter space (the param_space attribute of rs). You can see all the data you’ve entered in there, except add_one, the computing function[1]. So where is it? Let’s select the result and see what we got:

rs["result"]
<xarray.DataArray 'result' (parameter: 1)>
array([-1])
Coordinates:
  * parameter  (parameter) int64 1
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       []
    compute_times:         []
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

There it is, under Attributes! And there’s some other stuff there too. But we’ll get back to that. First let’s look at the “values” of the result displayed there: an array with just a -1. But what is this doing there? Nothing was computed. Well, to find out, let’s compute() a value of "result":

res = rs.compute("result", {})
print(res)
rs["result"]
Computing result for the following parameter values:
{'parameter': 1}
2
<xarray.DataArray 'result' (parameter: 1)>
array([0])
Coordinates:
  * parameter  (parameter) int64 1
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2]
    compute_times:         [1.6689300537109375e-06]
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

Many things have changed in here, but let’s start with the value in the array: it became 0. So what does it mean? It means that the result for parameter = 1 is located at index 0 of the computed_values attribute: there is the 2 resulting from the addition. But how did it know to add 1 + 1? Well since parameter was not provided in the dictionary passed as second argument to rs.compute(), its default value was taken. Here since the parameter has only one possible value, that’s the default. Otherwise, the first value along the parameter axis will be the default. Let’s now add more parameter values to see how that goes.

Changing the parameters#

rs.add_param_values({'parameter': [2, 3, 4]})
rs["result"]
<xarray.DataArray 'result' (parameter: 4)>
array([ 0, -1, -1, -1])
Coordinates:
  * parameter  (parameter) int64 1 2 3 4
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2]
    compute_times:         [1.6689300537109375e-06]
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

As you can see, the new parameter values are there in the Coordinates, and the array’s size increased along the exising axis, with the 0 still at the coordinate corresponding to parameter = 1, and -1 elsewhere. Let’s see what happens if we now try to get() the result as we did for compute above:

res = rs.get("result", {})
print(res)
rs["result"]
2
<xarray.DataArray 'result' (parameter: 4)>
array([ 0, -1, -1, -1])
Coordinates:
  * parameter  (parameter) int64 1 2 3 4
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2]
    compute_times:         [1.6689300537109375e-06]
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

Here’s the 2 again, and we didn’t get any message saying a new value was computed. That’s because it wasn’t, since the value was already computed it was just retrieved from the right position in computed_values. Now if we get for a different "parameter" value:

res = rs.get("result", {"parameter": 3})
print(res)
rs["result"]
Computing result for the following parameter values:
{'parameter': 3}
4
<xarray.DataArray 'result' (parameter: 4)>
array([ 0, -1,  1, -1])
Coordinates:
  * parameter  (parameter) int64 1 2 3 4
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2, 4]
    compute_times:         [1.6689300537109375e-06, 1.1920928955078125e-06]
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

As expected, here it computed the result for the new value.

Note

Now if we want to see only the part of the parameter space where values have been computed, we can use the populated_space property:

rs.populated_space["result"]
<xarray.DataArray 'result' (parameter: 2)>
array([0., 1.])
Coordinates:
  * parameter  (parameter) int64 1 3
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2, 4]
    compute_times:         [1.6689300537109375e-06, 1.1920928955078125e-06]
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

And what happens if we try to make a computation for a parameter value that’s not in the parameter space?

res = rs.get("result", {"parameter": 5})
print(res)
rs["result"]
Hide code cell output
Computing result for the following parameter values:
{'parameter': 5}
6
<xarray.DataArray 'result' (parameter: 5)>
array([ 0, -1,  1, -1,  2])
Coordinates:
  * parameter  (parameter) int64 1 2 3 4 5
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2, 4, 6]
    compute_times:         [1.6689300537109375e-06, 1.1920928955078125e-06, 9...
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

Well it’s simply added to the set and the computation goes through.

Adding parameters#

What if we need to add some new parameters at some point? That’s what the add_params() method is for. Here are different ways to use it that show some of the types of parameters that can be added:

from datetime import date
from respace import Parameter
rs.add_params({"date": [date(2000, 1, 1), date.today()], "constant": 4})
rs.add_params(Parameter("letter", default="c", values=["a", "b", "c"]))
rs["result"]
<xarray.DataArray 'result' (constant: 1, date: 2, letter: 3, parameter: 5)>
array([[[[ 0, -1,  1, -1,  2],
         [ 0, -1,  1, -1,  2],
         [ 0, -1,  1, -1,  2]],

        [[ 0, -1,  1, -1,  2],
         [ 0, -1,  1, -1,  2],
         [ 0, -1,  1, -1,  2]]]])
Coordinates:
  * letter     (letter) object 'c' 'a' 'b'
  * constant   (constant) int64 4
  * date       (date) object 2000-01-01 2023-03-19
  * parameter  (parameter) int64 1 2 3 4 5
Attributes:
    tracking_compute_fun:  <function add_one at 0x7fbbd43f5990>
    computed_values:       [2, 4, 6]
    compute_times:         [1.6689300537109375e-06, 1.1920928955078125e-06, 9...
    name:                  result
    compute_fun:           <function add_one at 0x7fbc012df370>
    save_fun:              <function save_pickle at 0x7fbbd43d37f0>
    load_fun:              <function load_pickle at 0x7fbbd43d3880>
    save_suffix:           .pickle
    save_path_fmt:         None

Note

Note how dimensions have been added to the array, and how the default value for "letter" was shifted to the first position: that is so we always know which value is the default.

Warning

Beware that the existing result values are then assumed to have been computed for the default value of the added parameters. So you should always make sure (and that’s usually a good programming practice!) that for the new parameters set at their default value, the behaviour of the computing function is unchanged. Also, if needed, don’t forget to update it accordingly. If parameters are absent from the signature of the function, the default behaviour implemented in ReSpace is to silently ignore these parameters[2].

Adding results#

Equivalently, you have the add_results() method to introduce new results in the set. Here’s how you use it:

from respace import ResultMetadata

rs.add_results({"other_result": lambda parameter, constant: parameter - constant})
rs.add_results([ResultMetadata("c", lambda: 1, save_path_fmt="c")])
rs
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[12], line 3
      1 from respace import ResultMetadata
----> 3 rs.add_results({"other_result": lambda parameter, constant: parameter - constant})
      4 rs.add_results([ResultMetadata("c", lambda: 1, save_path_fmt="c")])
      5 rs

File ~/checkouts/readthedocs.org/user_builds/respace/checkouts/stable/src/respace/result.py:831, in ResultSet.add_results(self, results_metadata)
    827 data = -np.ones([len(p.values) for p in self.parameters], dtype="int")
    828 add_data_vars = {
    829     r.name: self._make_res_var(r, dims, data) for r in results_metadata
    830 }
--> 831 self.param_space = self.param_space.assign(variables=add_data_vars)

File ~/checkouts/readthedocs.org/user_builds/respace/envs/stable/lib/python3.10/site-packages/xarray/core/dataset.py:6080, in Dataset.assign(self, variables, **variables_kwargs)
   6078 data.coords._maybe_drop_multiindex_coords(set(results.keys()))
   6079 # ... and then assign
-> 6080 data.update(results)
   6081 return data

File ~/checkouts/readthedocs.org/user_builds/respace/envs/stable/lib/python3.10/site-packages/xarray/core/dataset.py:4946, in Dataset.update(self, other)
   4910 def update(self: T_Dataset, other: CoercibleMapping) -> T_Dataset:
   4911     """Update this dataset's variables with those from another dataset.
   4912 
   4913     Just like :py:meth:`dict.update` this is a in-place operation.
   (...)
   4944     Dataset.merge
   4945     """
-> 4946     merge_result = dataset_update_method(self, other)
   4947     return self._replace(inplace=True, **merge_result._asdict())

File ~/checkouts/readthedocs.org/user_builds/respace/envs/stable/lib/python3.10/site-packages/xarray/core/merge.py:1104, in dataset_update_method(dataset, other)
   1101             if coord_names:
   1102                 other[key] = value.drop_vars(coord_names)
-> 1104 return merge_core(
   1105     [dataset, other],
   1106     priority_arg=1,
   1107     indexes=dataset.xindexes,
   1108     combine_attrs="override",
   1109 )

File ~/checkouts/readthedocs.org/user_builds/respace/envs/stable/lib/python3.10/site-packages/xarray/core/merge.py:761, in merge_core(objects, compat, join, combine_attrs, priority_arg, explicit_coords, indexes, fill_value)
    756 prioritized = _get_priority_vars_and_indexes(aligned, priority_arg, compat=compat)
    757 variables, out_indexes = merge_collected(
    758     collected, prioritized, compat=compat, combine_attrs=combine_attrs
    759 )
--> 761 dims = calculate_dimensions(variables)
    763 coord_names, noncoord_names = determine_coords(coerced)
    764 if explicit_coords is not None:

File ~/checkouts/readthedocs.org/user_builds/respace/envs/stable/lib/python3.10/site-packages/xarray/core/variable.py:3216, in calculate_dimensions(variables)
   3214             last_used[dim] = k
   3215         elif dims[dim] != size:
-> 3216             raise ValueError(
   3217                 f"conflicting sizes for dimension {dim!r}: "
   3218                 f"length {size} on {k!r} and length {dims[dim]} on {last_used!r}"
   3219             )
   3220 return dims

ValueError: conflicting sizes for dimension 'letter': length 1 on 'other_result' and length 3 on {'letter': 'letter', 'constant': 'constant', 'date': 'date', 'parameter': 'parameter'}

Saving results#

ReSpace also makes it super easy for you to save your results, let’s have a look:

_ = rs.save("result", {"parameter": 5})
_ = rs.save("c", {})
Saving result at result_letter=c_constant=4_date=2000-01-01_parameter=5.pickle.
Computing c for the following parameter values:
{'letter': 'c', 'constant': 4, 'date': datetime.date(2000, 1, 1), 'parameter': 1}
Saving c at c.pickle.

The result "c" was saved according to the save path format we passed it. More interestingly, "result" was saved at a path indicating first its name, and then a string giving the name of the parameters and their values.