Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add time clock to runtime #170

Merged
merged 41 commits into from
Mar 8, 2021
Merged

add time clock to runtime #170

merged 41 commits into from
Mar 8, 2021

Conversation

feefladder
Copy link
Contributor

added placeholder, should work now

@benbovy benbovy mentioned this pull request Feb 9, 2021
4 tasks
@benbovy
Copy link
Member

benbovy commented Feb 9, 2021

Thanks @joeperdefloep! However, I don't think MAIN_CLOCK will add anything really useful in this PR.

The "placeholder" that I suggest in https://github.com/benbovy/xarray-simlab/issues/155#issuecomment-775507738 is for addressing the case where we want to use the main clock as an explicit dimension when declaring xarray-simlab variables in process classes. It is a different (but related) problem than this PR. We would need something a bit more advanced than a string, i.e., a custom type that xarray-simlab understands so that it can "replace" it with the actual dimension label corresponding to the main clock (later set by the user) when creating xarray Dataset objects. Although for an entirely different purpose, an example of such type (sentinel class, singleton) is attr.NOTHING.

Updating runtime context entries with the main clock values (this PR) should be addressed afterwards IMO. Runtime context entries are already hard-coded, so we can just use the _main_clock and main_clock strings.

@benbovy
Copy link
Member

benbovy commented Feb 9, 2021

Hmm I'm not sure to understand what you have implemented in your last commit.

Just to be sure, what we'd like to do is something like in the small example below or do you have something else in mind?

@xs.process
class Foo:
    var = xs.variable(dims=xs.MAIN_CLOCK, intent='out')

    @xs.runtime(args='main_clock_array')
    def initialize(self, main_clock):
        self.var = main_clock * 2


m = xs.Model({'foo': Foo})

ds_in = xs.create_setup(
    model=m,
    clocks={'time': range(5)},
    output_vars={'foo__var': None}
)

ds_out = ds_in.xsimlab.run(model=m)

# should print a DataArray with dimension 'time'
# and values [0, 2, 4, 6, 8]
print(ds_out.foo__var)

For this example, we need to implement new functionality in xarray-simlab in order to support the two lines below:

  1. var = xs.variable(dims=xs.MAIN_CLOCK, intent='out')
  2. @xs.runtime(args='main_clock_array')

This PR implements 2, and I suggest implementing 1 in a separate PR (Ideally, we should address 1 first)

What I have in mind for 1 is just a very basic placeholder xs.MAIN_CLOCK that does nothing else than tell xarray-simlab to get the main clock dimension label and use it to create the foo__var output variable in the simulation store. xs.MAIN_CLOCK thus don't hold any actual dimension label and would never be used in any xarray object.

@feefladder
Copy link
Contributor Author

feefladder commented Feb 9, 2021

Just to be sure, what we'd like to do is something like in the small example below or do you have something else in mind?

Yes, that was exactly what I wanted to implement! Well, actually a bit more:
I also implemented 2, to be able to debug with a small example:

@xs.process
class dBiomass:
    """calculates biomass change based on light intensity
    """
    maxrad = xs.variable(dims=xs.MAIN_CLOCK,intent='out')
    biomass = xs.variable(intent='out')
    
    @xs.runtime(args=['main_clock_values','main_clock_array'])
    def initialize(self,clock_values,clock_array):
        print(clock_values)
        print(clock_array[xs.MAIN_CLOCK])
        self.biomass=0
        self.maxrad = main_clock
    
    @xs.runtime(args='step_delta')
    def run_step(self,dt):
        self._d_biomass = dt*self.maxrad
        
    def finalize_step(self):
        self.biomass += self._d_biomass

biomass_model_raw = xs.Model({'dBiomass':dBiomass})

ds_in = xs.create_setup(
    model=biomass_model_raw,
    clocks={
        'day':range(100)
    },
    master_clock='day',
    input_vars={},
    output_vars={'dBiomass__biomass':'day'}
)

maybe end-to-end testing is not the most efficient way of adding a new feature, how do you normally tackle such things, or how would I go about debugging 1. without first implementing 2? Writing tests maybe? 🙊 Serious question though...

this implements the following lines:

  1. maxrad = xs.variable(dims=xs.MAIN_CLOCK,intent='out') This works correctly, since xs.MAIN_CLOCK is updated to the master_clock_dim in the update_vars step. You could print(xs.MAIN_CLOCK) in any process step and get day.
  2. @xs.runtime(args='main_clock_values'): access to main clock as a raw array and subsequent processing. Here are some problems: Since normally only time-dependent variables are set as pre-processing, we need to add logic that expands the vars along the time dimension if a time dimension is set in the var. This could lead to problems if:
    1. multiple options are given, such as dims=[(x,y),(x,xs.MAIN_CLOCK)]
    2. the dimension of y is as long as xs.MAIN_CLOCK
      Again, we could check for this, in the expand-along-time-dimension logic that should go in the initialize step. I thought this could maybe be avoided, if the user passes a xr.DataArray, with explicit dimensions, which brings us to the next:
  3. @xs.runtime(args='main_clock_array'): access to main clock as a DataArray. This also implies use of clock.array[xs.MAIN_CLOCK]`. This may only be necessary when Use xarray data structures inside models #141 is fully addressed, but it is the reason for the singleton class in favour of a simple string placeholder that is replaced by the actual string. This will not be updated accordingly between definition and problem run. Indeed, maybe this should be a separate pull request, (but maybe it is nice to have it for when Use xarray data structures inside models #141 is to be implemented?)

There is another possible problem when a process dimension happens to have the same name as the main clock dimension e.g. xs.variable(dims=(xs.MAIN_CLOCK,'day) with the above example

@benbovy
Copy link
Member

benbovy commented Feb 9, 2021

how do you normally tackle such things, or how would I go about debugging 1. without first implementing 2?

Isn't this PR and #155 initially about implementing 2? You could test 2 without having 1 implemented if that's what you mean, e.g.,

@xs.process
class Foo:
    var = xs.variable(dims=(), intent='out')

    @xs.runtime(args='main_clock_array')
    def initialize(self, main_clock):
        self.var = np.sum(main_clock)


m = xs.Model({'foo': Foo})

ds_in = xs.create_setup(
    model=m,
    clocks={'time': range(5)},
    output_vars={'foo__var': None}
)

ds_out = ds_in.xsimlab.run(model=m)

assert ds_out.foo__var == 10

This works correctly, since xs.MAIN_CLOCK is updated to the master_clock_dim in the update_vars step.

One major drawback is that this approach is not thread safe.

Since normally only time-dependent variables are set as pre-processing, we need to add logic that expands the vars along the time dimension if a time dimension is set in the var.

Not sure I understand, but I guess you mean that we should take care of the case where users provide time-varying values for a variable like xs.variable(dims=xs.MAIN_CLOCK, intent='in')? We probably should. I suggest that xs.variable(dims=xs.MAIN_CLOCK, intent='in') implicitly implies xs.variable(dims=xs.MAIN_CLOCK, intent='in', static=True) (or just check that static=True if we want to avoid being implicit), so that no time-varying input value is allowed. (Note: by time-varying value I mean provide an input array with one additional dimension that is not defined in the variable dims).

multiple options are given, such as dims=[(x,y),(x,xs.MAIN_CLOCK)]

This is not possible in xarray-simlab, each option must have a different number of dimensions.

@xs.runtime(args='main_clock_array'): access to main clock as a DataArray. This also implies use of clock.array[xs.MAIN_CLOCK]`

It would certainly be useful to access the main clock as a DataArray, but why clock.array[xs.MAIN_CLOCK]? Why not just setting main_clock_array=dataset.xsimlab.main_clock_coord here?

There is another possible problem when a process dimension happens to have the same name as the main clock dimension e.g. xs.variable(dims=(xs.MAIN_CLOCK,'day'))

We should raise an error in this case. It either to the model user to choose another label for the main clock, or the model developer to use another, potentially less conflicting label in process classes.

@feefladder
Copy link
Contributor Author

feefladder commented Feb 10, 2021

Isn't this PR and #155 initially about implementing 2? You could test 2 without having 1 implemented if that's what you mean, e.g.,

Well, it is about making the simple example work. The question was how to test 1 without 2. But I think the following example does that (testing 1 without access to main_clock_values):

time_array = np.array([0,1,2,3])

@xs.process
class Foo:
    #intent cannot be 'in' if we want to set it in initialize
    bar = xs.variable(dims=xs.MAIN_CLOCK,intent='in',static=True)
    baz = xs.variable(intent='out')
    
    def initialize(self):
        self.bar = time_array*2
        self.baz=0
    
    @xs.runtime(args='step_delta')
    def run_step(self,dt):
        #now self.bar is actually [0,2,4,6]
        #should be 0-6 in different steps
        self.baz += dt*self.bar

m = xs.Model({'foo':Foo})


ds_in = xs.create_setup(
    model=m,
    clocks={
        'time':time_array
    },
    input_vars={
        'foo__bar': 5,
    },
    output_vars={
        #add output var to get error: dims don't match
#         'Biomass__biomass':'time'
    }
)

ds_in.xsimlab.run(m)

This works correctly, since xs.MAIN_CLOCK is updated to the master_clock_dim in the update_vars step.

One major drawback is that this approach is not thread safe.

ok yes, if a user was to create multiple models with multiple with different main_clock dimensions, and processes that refer to xs.MAIN_CLOCK, then these different threads would be setting xs.MAIN_CLOCK to different values. However, this implementation is safe against the normal multiprocessing, since it is set in preprocessing.
An alternative would be to add MAIN_CLOCK to each process, so that:

  1. xs.MAIN_CLOCK is instantiated when a model is created. All earlier references to xs.MAIN_CLOCK now point to that class instance. this sounds like becoming really complex to me: How do we know which references are from our model and which are not? Should we put the MAIN_CLOCK in the process class?
  2. it is still updated in update_clocks to match the main_clock_dim. This is a preprocessing step, and therefore safe for batch model runs

Since normally only time-dependent variables are set as pre-processing, we need to add logic that expands the vars along the time dimension if a time dimension is set in the var.

Not sure I understand, but I guess you mean that we should take care of the case where users provide time-varying values for a variable like xs.variable(dims=xs.MAIN_CLOCK, intent='in')? We probably should. I suggest that xs.variable(dims=xs.MAIN_CLOCK, intent='in') implicitly implies xs.variable(dims=xs.MAIN_CLOCK, intent='in', static=True) (or just check that static=True if we want to avoid being implicit), so that no time-varying input value is allowed. (Note: by time-varying value I mean provide an input array with one additional dimension that is not defined in the variable dims).

actually, setting intent='in' variables is not allowed in the initialize step (or any step). I think we should keep it that way, and solve #155 for 'out' or 'inout' variables, since we calculate and update them (in the initialize step). In fact, a user cannot access time-varying inputs in the initialize step of a model(footnote).

What I meant here is that in the above example, in initialize, we set the value as an array, whereas in run_step, the values should be accessed as individual values. Therefore, in (after) the initialize step, variables that have xs.MAIN_CLOCK in their dims, should expand it over that axes, so that individual values are passed in run_step.

It would certainly be useful to access the main clock as a DataArray, but why clock.array[xs.MAIN_CLOCK]? Why not just setting main_clock_array=dataset.xsimlab.main_clock_coord here?

The comment was from a user-perspective. For example if the user has clock in a datetime and wants to be able to clock_array[xs.MAIN_CLOCK + '.dayofyear'] or use any operation such as .loc at runtime.

There is another possible problem when a process dimension happens to have the same name as the main clock dimension e.g. xs.variable(dims=(xs.MAIN_CLOCK,'day'))

We should raise an error in this case. It either to the model user to choose another label for the main clock, or the model developer to use another, potentially less conflicting label in process classes.

footnote:
Keyerror when accessing time-varying data in initialize step:

@xs.process
class Foo:
    bar = xs.variable(intent='in')
    baz = xs.variable(intent='out')
    
    def initialize(self):
        #this gives a keyerror on time-varying data
        #could be solved with #155
        self.baz=self.bar
    
    @xs.runtime(args='step_delta')
    def run_step(self,dt):
        self.baz += dt*self.bar

m = xs.Model({'foo':Foo})


ds_in = xs.create_setup(
    model=m,
    clocks={
        'time':time_array
    },
    input_vars={
        'foo__bar': 5,
    },
    output_vars={
        'foo__baz':'time'
    }
)

# ds_in = ds_in.xsimlab.update_vars(model=m,input_vars={'foo':{'bar':ds_in.time*5}})
ds_in.xsimlab.run(m).foo__baz.plot()

@benbovy
Copy link
Member

benbovy commented Feb 10, 2021

Looks like we don't really agree on what should be xs.MAIN_CLOCK...

Having a singleton class instantiated as xs.MAIN_CLOCK that has a main_clock_dim attribute is roughly equivalent to have a global, mutable variable defined in xarray-simlab that is used and updated when running any simulation. Honestly, this approach is bad design IMO.

Moreover, updating xs.MAIN_CLOCK.main_clock_dim in update_clocks does not prevent it to be updated between model setup and run, or not updated at all:

import xarray as xr
import xsimlab as xs

ds_in = xr.load_dataset('some_file.nc')

# xs.MAIN_CLOCK.main_clock_dim may not be correct
ds_in.xsimlab.run()

A much better approach would be to have xs.MAIN_CLOCK that doesn't hold any mutable attribute + something like

dim_labels = [self.mclock_dim if d is MAIN_CLOCK else d for d in dim_labels]

put here. If we want to allow using xs.MAIN_CLOCK for input variables too (for whatever use case), then we would need to add something similar here.

@benbovy
Copy link
Member

benbovy commented Feb 10, 2021

The comment was from a user-perspective. For example if the user has clock in a datetime and wants to be able to clock_array[xs.MAIN_CLOCK + '.dayofyear'] or use any operation such as .loc at runtime.

If clock_array is a DataArray with one dimension that corresponds to the main clock, we could just get it from clock_array.dims, no?

What I meant here is that in the above example, in initialize, we set the value as an array, whereas in run_step, the values should be accessed as individual values. Therefore, in (after) the initialize step, variables that have xs.MAIN_CLOCK in their dims, should expand it over that axes, so that individual values are passed in run_step.

Ok I see. Allow declaring input variables with a xs.MAIN_CLOCK dimension would potentially bring a lot of issues. I really need to think more about this. As you say it's probably wiser to allow it for output variables only.

For the case of output variables set in initialize, if we want to access the array element value for the current step in run_step, we could just reuse @xs.runtime(args='step').

@feefladder
Copy link
Contributor Author

feefladder commented Feb 10, 2021

Ok, so I properly implemented the singleton class (thanks for pointing out the right places! I was really at a loss there, as to how to implement it, it was indeed still very ugly).

If clock_array is a DataArray with one dimension that corresponds to the main clock, we could just get it from clock_array.dims, no?

hmm, maybe you're right. I added a link to the dim in drivers, can remove it again if you want. EDIT:removed it

What I meant here is that in the above example, in initialize, we set the value as an array, whereas in run_step, the values should be accessed as individual values. Therefore, in (after) the initialize step, variables that have xs.MAIN_CLOCK in their dims, should expand it over that axes, so that individual values are passed in run_step.

Ok I see. Allow declaring input variables with a xs.MAIN_CLOCK dimension would potentially bring a lot of issues. I really need to think more about this. As you say it's probably wiser to allow it for output variables only.

I added a check/ValueError in _maybe_transpose, that is always called to check the dims. to warn the user.

For the case of output variables set in initialize, if we want to access the array element value for the current step in run_step, we could just reuse @xs.runtime(args='step').

Yes that works! (and it makes sense, since the user also defined it as a dimension.) It felt a bit weird at first, because I thought I wanted the same behaviour as time-dependent inputs

Now I am a bit lost as to making tests... I think I could make a full example in test_model.py

@feefladder
Copy link
Contributor Author

feefladder commented Feb 10, 2021

So most things work, but there is now the problem that an output variable will show up with two dimensions in the output:
this example:

@xs.process
class Foo:
    a = xs.variable(intent="out", dims=(xs.MAIN_CLOCK))

    @xs.runtime(args="main_clock_values")
    def initialize(self, clock_values):
        self.a = clock_values

    @xs.runtime(args="step")
    def run_step(self, step):
        self.a[step] += 1


model = xs.Model({"foo": Foo})
ds_in = xs.create_setup(
    model=model,
    clocks={"clock": range(5)},
    input_vars={},
    output_vars={"foo__a": "clock"},
)
print(ds_in.xsimlab.run(model=model).foo__a)

returns:

<xarray.DataArray 'foo__a' (clock: 5)>
array([[1., 1., 2., 3., 4.],
       [1., 2., 2., 3., 4.],
       [1., 2., 3., 3., 4.],
       [1., 2., 3., 4., 4.],
       [1., 2., 3., 4., 4.]])
Coordinates:
  * clock    (clock) int64 0 1 2 3 4

where we would want a:

<xarray.DataArray 'foo__a' (clock: 5)>
array([1.,2.,3.,4.,4.,])
Coordinates:
  * clock    (clock) int64 0 1 2 3 4

@feefladder feefladder closed this Feb 10, 2021
@feefladder feefladder reopened this Feb 10, 2021
Copy link
Member

@benbovy benbovy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!!

In your last example, you definitely want to save only one snapshot for foo__a instead of saving snapshots for this unchanged value at each time step, i.e.,

ds_in = xs.create_setup(
    ...
    output_vars={"foo__a": None},
)

I don't know if there is any use case where it would make sense to update the values a variable with xs.MAIN_CLOCK dimension in run_step and save multiple snapshots. If there's no case, we could raise when users want to save multiple snapshots, but we can implement this later (let's see how it goes).

@@ -210,6 +209,9 @@ def _maybe_transpose(dataset, model, check_dims, batch_dim):
if xr_var is None:
continue

if any([MAIN_CLOCK in d for d in model.cache[var_key]["metadata"]["dims"]]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's preferable for now that we raise an error when we create an xs.variable if it has xs.MAIN_CLOCK and its dimensions and intent != 'out', so that we catch this unsupported (yet?) case earlier.

@@ -241,6 +241,14 @@ def _create_zarr_dataset(
f"its accepted dimension(s): {var_info['metadata']['dims']}"
)

# set MAIN_CLOCK placeholder to main_clock dimension
if self.mclock_dim in dim_labels and MAIN_CLOCK in dim_labels:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't allow declaring input variables with xs.MAIN_CLOCK, this case shouldn't happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can still happen: when a user declares a variabke with dims
`(xs.MAIN_CLOCK,'clock')'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes! Hmm then we would probably want to check in Dataset.xsimlab.update_clocks that the given clock labels (main clock and other clocks if provided) don't conflict with the dimension labels set in the model. I don't think we do this already.

For convenience we could add a property in xs.Model that returns a list of all (unique) dimension labels declared in the model.

Let's fix that in another PR..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if that is the right place though: Am I right that a user may add a process after updating clocks, thus not catching it?

xsimlab/utils.py Outdated
Comment on lines 19 to 21
"""singleton class to be used as main clock dimension: update on runtime
it has all behaviour that dimensions in a `xr.DataArray` normally have.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""singleton class to be used as main clock dimension: update on runtime
it has all behaviour that dimensions in a `xr.DataArray` normally have.
"""
"""Singleton class to be used as a placeholder of the main clock
dimension.
It will be replaced by the actual dimension label set during simulation setup
(i.e., ``main_clock`` argument).
"""

xsimlab/utils.py Outdated Show resolved Hide resolved
xsimlab/variable.py Outdated Show resolved Hide resolved
feefladder and others added 6 commits February 11, 2021 09:57
Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de>
Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de>
Copy link
Member

@benbovy benbovy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just left some minor comments.

@@ -29,7 +33,7 @@ def __new__(cls):
return _MainClockDim._singleton

def __repr__(self):
return "MAIN_CLOCK (uninitialized)"
return "MAIN_CLOCK (undefined)"


MAIN_CLOCK = _MainClockDim()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to add some docstrings for this attribute, e.g.,

MAIN_CLOCK = _MainClockDim()
"""
Sentinel to indicate simulation's main clock dimension, to be
replaced by the actual dimension label set in input/output datasets.
"""

and add it in the docs (probably in API references in the variables section, since we would use it when declaring variables).

xsimlab/variable.py Outdated Show resolved Hide resolved
xsimlab/xr_accessor.py Outdated Show resolved Hide resolved
xsimlab/tests/test_model.py Outdated Show resolved Hide resolved
self.b = clock_array * 2
assert clock_array.dims[0] == "clock"
assert all(clock_array[clock_array.dims[0]].data == [0, 1, 2, 3])

@xs.runtime(args=["step_delta", "step"])
def run_step(self, dt, n):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove dt if you don't use it.

xsimlab/tests/test_model.py Outdated Show resolved Hide resolved
xsimlab/tests/test_model.py Outdated Show resolved Hide resolved
feefladder and others added 2 commits February 19, 2021 20:25
Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de>
Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de>
@feefladder
Copy link
Contributor Author

Now, when we print the model, the MAIN_CLOCK (undefined) still shows up in the dimensions. Not sure how to solve that (and it sounds like a minor issue)?

@benbovy
Copy link
Member

benbovy commented Feb 22, 2021

Now, when we print the model, the MAIN_CLOCK (undefined) still shows up in the dimensions.

That's intended behavior. The main clock label is not defined yet and may be different from one simulation to another.

Copy link
Member

@benbovy benbovy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the long wait @joeperdefloep. This looks all good to me! Just one remaining issue with the doc build. Maybe we can remove MAIN_CLOCK from api.rst here and address this later.

doc/api.rst Outdated
@@ -154,7 +154,7 @@ Variable

.. autosummary::
:toctree: _api_generated/

MAIN_CLOCK
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, adding MAIN_CLOCK here yields an error when building the docs with Sphinx (as a result the whole Variable section is empty).

Copy link
Contributor Author

@feefladder feefladder Mar 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I've checked that, and I don't get an error. That is, I did get a "could not import module MAIN_CLOCK" or something, when I didn't first python -m pip install ., but after that it built properly.
e.g. make html worked properly without errors.
link

Copy link
Member

@benbovy benbovy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two last things and it should be all good:

  • comment below
  • could you update whats_new.rst, please?

@@ -0,0 +1,3 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove this file, please? It should be ignored in the next PRs (#169).

@benbovy
Copy link
Member

benbovy commented Mar 8, 2021

Excellent @joeperdefloep! Many thanks for your great work here, and thanks for your patience!

@benbovy benbovy merged commit a6494d0 into xarray-contrib:master Mar 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add access to all master clock values in runtime context
2 participants