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

model: Automatically increase steps counter #2223

Merged
merged 4 commits into from
Aug 29, 2024

Conversation

EwoutH
Copy link
Member

@EwoutH EwoutH commented Aug 18, 2024

This pull request adds automatically increasing of steps counter in the Mesa model, even if the users overwrites the Model.step() method. It also removes the _time variable.

Background

Previously, Mesa required users to manually increment the step counters using the _advance_time() method, or by modifying the model._time and model._step values directly. This use of an private method was non-ideal.

Changes

steps is now incremented automatically within the Model class itself. Each call to the step() method will increment the steps counter by 1.

The _time counter is removed (for motivation, read the discussion below).

Implementation details

The core change involves wrapping the user-defined step method with _wrapped_step which handles the incrementing process before executing the user's step logic. This ensures that all increment operations are centrally managed and transparent to the user.

I choose to increment the time before the user step, in the spirit of "a new step" (or a new day) has begun. Then the model does things during the step, and.

def step():
    # Time is increased automatically at the beginning of the step
    self.agents.shuffle().do("step")  # Do things during the step
    self.datacollector.collect()      # Collect data at the end of the step

Here's the updated version of your examples with the initial whitespaces removed from the code blocks:

Example Usage

You don't need to do anything to use this features. You can access steps at any moment.

class MyModel(Model):
    def step(self):
        # User logic here

my_model = MyModel()
my_model.step()

Todo:

Resolve:

@EwoutH EwoutH added the feature Release notes label label Aug 18, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
Schelling small 🟢 -4.2% [-4.5%, -3.9%] 🔵 +1.4% [+1.3%, +1.6%]
Schelling large 🟢 -5.3% [-6.0%, -4.7%] 🔵 +2.5% [+1.9%, +3.1%]
WolfSheep small 🟢 -5.5% [-6.6%, -4.2%] 🔵 -2.4% [-2.7%, -2.1%]
WolfSheep large 🟢 -6.1% [-6.4%, -5.8%] 🟢 -3.6% [-3.9%, -3.3%]
BoidFlockers small 🟢 -7.6% [-8.2%, -7.0%] 🔵 -3.2% [-3.9%, -2.5%]
BoidFlockers large 🟢 -7.0% [-7.5%, -6.4%] 🔵 -1.4% [-2.1%, -0.7%]

mesa/model.py Outdated
@@ -77,6 +77,20 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self._steps: int = 0
self._time: TimeT = 0 # the model's clock

# Wrap the user-defined step method
if hasattr(self, "step"):
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the user defines their step with a different name, e.g. in the simultaneous activation?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good edge case! I think when using a scheduler, you're also mainly using the scheduler's time, so model.schedule.time. And as long as one of the functions is a step(), the model time will also be increased.

Personally I'm in favor of moving away from the schedulers, but this PR won't break that.

Copy link
Contributor

@rht rht Aug 18, 2024

Choose a reason for hiding this comment

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

Can't leave up to chance. If the model doesn't have a step, it is more informative to raise an error with an informative message.

Alternatively, bumping the step & time whenever model.agents.do is called, is a possible option.

Copy link
Member Author

@EwoutH EwoutH Aug 18, 2024

Choose a reason for hiding this comment

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

If the model doesn't have a step, it is more informative to raise an error with an informative message.

If you want to use your own process without using Mesa's internal clock, it's perfectly valid. I think we can cover this with documentation, and this will be less and less a problem as we move away from schedulers.

I added an example of staged time increase to the PR start.

Alternatively, bumping the step & time whenever model.agents.do is called, is another alternative.

The problem is, how do you handle multiple agents.do calls? Or agents.do to a partial agentset? Or with a select in between?

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, bumping the step & time whenever model.agents.do is called, is another alternative.

The problem is, how do you handle multiple agents.do calls? Or agents.do to a partial agentset? Or with a select in between?

If we want to track the number of steps, it should be tied to model.step. It's a bad idea to tie it to the AgentSet. Conceptually, this would mix up two completely different things. Which, as @EwoutH indicates, gives rise to all kinds of knock-on problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to use your own process without using Mesa's internal clock, it's perfectly valid. I think we can cover this with documentation, and this will be less and less a problem as we move away from schedulers.

steps's need is hardcoded it the batch runner and visualization. Currently, they are not optional.

@quaquel
Copy link
Member

quaquel commented Aug 18, 2024

I'll try to look at the code later today, but just some quick first questions/thoughts/concerns.

  1. Do we want to maintain the distinction between step and time? Is this distinction not confusing to a novice user?
  2. Should time not be handled by the simulator?

@EwoutH
Copy link
Member Author

EwoutH commented Aug 18, 2024

  • Do we want to maintain the distinction between step and time? Is this distinction not confusing to a novice user?

I think both can be useful in some cases. Steps are just the number of times model.step() is called (by default). Time can be anything you want.

For example, if I have a model that runs from 8 o'clock in the morning to 22 o'clock in the evening, with 5 minute steps, I might do:

class MyModel(Model):
    def __init__(self):
        ...
        self._time = 8
    def step(self, time=1/12):
        ...

Then I have a human-readable value of the time of day, while the steps can be useful for plotting, datacollection and debugging. So one is human readable and one is an uninterrupted sequence of integers.

Of course, we could aim our documentation and tutorials to mainly use one.

  • Should time not be handled by the simulator?

In my opinion, a model should be runnable without too many additional fluff. A simulator or scheduler should not be required for a simple ABM (and projectmesa/mesa-examples#161 proves those aren't needed for the vast majority of models).

@quaquel
Copy link
Member

quaquel commented Aug 18, 2024

In my opinion, a model should be runnable without too many additional fluff. A simulator or scheduler should not be required for a simple ABM

Fair enough, but then I would keep support for tracking time / the number of ticks also minimal. What about having full time support in the simulator and only have number of ticks/steps in the model class?

@EwoutH
Copy link
Member Author

EwoutH commented Aug 18, 2024

To do stages / simultaneous activation it can be nice to separate steps and time (see example in PR start). Also steps is just a counter and time can be used for a lot more.

Maybe the experimental ABMSimulator is redundant with this PR, and we only need to keep the DEVSSimulator

@quaquel
Copy link
Member

quaquel commented Aug 18, 2024

Maybe the experimental ABMSimulator is redundant with this PR, and we only need to keep the DEVSSimulator

No, ABMSimulator supports event scheduling while also having fixed time advancement (i.e., ABM style ticks).

@quaquel
Copy link
Member

quaquel commented Aug 18, 2024

I have been thinking about this PR a bit more. I think having an automatic counter for the number of times model.step is called is useful.

However, I would keep the other stuff like time, running automatically for a given number of steps, etc. for the ABMSimulator which already supports most of this via run_for and run_until. Being able to increment steps with more than one, even though step is called only once, as is possible with this API, really makes no sense to me and is confusing. Likewise, if a model needs to translate between steps and wall clock time, why not simply multiply with a fixed constant? Making models where two ticks represent different periods of wall clock time is conceptually confusing and invites bad modeling.

Splitting the functionality between tracking the number of times steps is called in the model and having a separate class for more advanced time management is my preferred way forward. This makes it possible for a user to keep building simple models as currently done (via a for loop as is current practice), but if you want more advanced control over time and running, we have an optional class that can be used.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 18, 2024

Thanks for your insights. I will be thinking about them a bit more, but for now, I see it this way:

  • Steps is the most basic variant. It's like ticks in NetLogo. Just the number. Ideal for simple models that loop model.step().
    • I agree that maybe we need to leave it fixed to 1 (otherwise, what harm does it do if people find a good reason to change is?)
  • Time is the intermediate variant. You can adjust it, conditionally skip it or increase it, let it depend on model or agent variables. It offers a bit of flexibility when you need just a bit more control, or want a human readable value (like in the example).
  • The ABMSimulator is where you do things between ticks, but still want to have ticks and a model step as construct. Just to give some certainty.
  • The DEVS scheduler you can do whatever you want. Go wild.

The ABMSimulator sits (for me) now in a weird place. I accidentally started using the DEVSimulator, before I knew I also wanted a model.step, and getting a model.step back was one or two lines of code. So I don't know if we really need it.

Looking at code complexity, adding and tracking a time aside from step is exactly one line of code. The ABMSimulator is about 90, with additional code you need to understand, document, write tutorial for, etc.

So, my current standpoint:

  • Maybe fix steps to always be 1. But what's the harm from letting it be a nice little hook to mount in to?
  • Keep the time. It adds very little complexity, and I think the examples above show it can sometimes be useful.
  • Consider removing the ABMSimulator, and see if we can just document how to use the DEVSimulator with an Model step. In advanced use cases, implicit is better than explicit.
  • Document that if you start to use a simulator, you should use the simulator time.

Edit: We might allow time to be a full Datetime object. Can imagine some scenarios in which that would be very handy.

@rht
Copy link
Contributor

rht commented Aug 18, 2024

steps is bread and butter: used in the batch runner and the visualization (to detect if user has reset the simulation), so it can't be fixed to 1 by default.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 18, 2024

I didn't want to run too far ahead, but I was thinking of allowing the index of the DataCollector to be changeable. Default could be steps or time, but you could input anything you did like.

And maybe the same for visualisation.

(with fixed to 1, I meant to fix it to increase always by 1)

@quaquel
Copy link
Member

quaquel commented Aug 18, 2024

Time is the intermediate variant. You can adjust it, conditionally skip it or increase it, let it depend on model or agent variables. It offers a bit of flexibility when you need just a bit more control, or want a human readable value (like in the example)

Why skip it or increase it in a non-fixed way? from a modeling point of view, this makes no sense to me. If "for readability", why should this be supported by MESA?

Consider removing the ABMSimulator, and see if we can just document how to use the DEVSimulator with an Model step. In advanced use cases, implicit is better than explicit.

The ABMSimulator can indeed be done by DEVSSimulator as well. The main use case for me is fixed time advancement (so increment by 1 and automagically calls model.step each tick) while having integer-based event scheduling. Conceptually, it is a real devs-ABM hybrid.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 18, 2024

You can do cute stuff like conditionally modifying your time resolution, to perform more ticks at certain moments:

    def step(self, time=False):
        # Increase volatility and decrease time increment during opening and closing hours
        if 9 <= self._time < 10 or 15 <= self._time < 16:
            self.volatility = 0.03
            self._time += timedelta(minutes=5)
        else:
            self.volatility = 0.01
            self._time += timedelta(minutes=30)

        # Update stock prices
        for stock in self.agents:
            stock.update_price(self.volatility)

If you allow datetime, you can even go a bit further:

        # If we've passed 4:00 PM, move to 9:00 AM the next day
        if self._time >= 16:
            next_day = self._time.date() + timedelta(days=1)
            self._time = datetime.combine(next_day, datetime.min.time()) + timedelta(hours=9)

And imagine you have external data lookups that use the time. Or in your data collector you can aggerate by hour, while still having more resolution available.

It's basically just an additional built-in counter you can use before having to jump immediately to discrete even scheduling.

@quaquel
Copy link
Member

quaquel commented Aug 19, 2024

You can do cute stuff like conditionally modifying your time resolution, to perform more ticks at certain moments:

That's not cute stuff, but bad modeling. It breaks discrete time advancement, which is a core principle behind ABMs. If a model requires variable time steps, you should use DEVS and carefully check all code to ensure time units are handled correctly.

Also, and separately, is this use case so common that it should be supported in a core class of MESA?

@rht
Copy link
Contributor

rht commented Aug 19, 2024

Also, and separately, is this use case so common that it should be supported in a core class of MESA?

If you looked at the PR's timeline, the time attribute was added months before the DEVS PR https://github.com/projectmesa/mesa/pulls?q=is%3Apr+discrete+event+is%3Aclosed. At the time, it was the only official way of having time being tracked. But again, even if the DEVS did not exist, the time attribute doesn't necessarily have to be there by default: for users who need it, they can always define their time attribute as needed, and it would need only a few lines of code for the feature.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 19, 2024

That's not cute stuff, but bad modeling. It breaks discrete time advancement, which is a core principle behind ABMs.

And that's why it's not the default. I agree that it's not bad practice for most cases. But if an users finds a novel case or use, why deny the flexibility?


I'm going to summarize my points:

  • By default, model.time is increased with a fixed amount of 1 each step.
  • model.time can be used as a human-readable values and potentially Datetime object
    • This allows data lookups in time-based lookup tables, decision logic based on a human-readable value, etc.
  • model.time can be used to acknowledge that a step has different sub-steps. It can be increased by a custom (but fixed) amount if you want divide you steps in different substeps
    • This is exactly how StagedActivation now works:

      The scheduler also tracks steps and time separately, allowing fractional time increments based on the number of stages. Time advances in fractional increments of 1 / (# of stages), meaning that 1 step = 1 unit of time.

    class MyModel(Model):
        def step(self, time=0.25):
            model.agents.do("morning")
            model.time += 0.25
            model.agents.do("afternoon")
            model.time += 0.25
            model.agents.do("evening")
            model.time += 0.25
            model.agents.do("night")
    
    my_model = MyModel()
    my_model.step()
    • Which means we can even keep track of both the full steps and the sub-steps, without having to do any rounding or flooring or sorts.
  • It gives users an additional built-in variable to play with, offering a nice hook to tie into, like a proper library would.
  • It adds very little complexity (a single line of code in our codebase, and none for users that don't want to modify it).

@quaquel
Copy link
Member

quaquel commented Aug 19, 2024

I think my main objection is not to time attribute perse but to the use of time and step as optional keyword arguments to model.step. This is a new feature, might break existing models, and gives rise to a lot of the problems I have been alluding to. If users want to do something like this in their custom models, they can do so. But why support it within MESA itself given the conceptual issues I have been highlighting?

Staged activation has its own internal time attribute. Which is conceptually strange. As argued before, there should be a single truth for time. I have to think about substeps with staged activation. I am unsure about how to handle that properly but seperating step and time might indeed be defendable for this.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

Would removing the step=1 argument from the Model.step be an acceptable compromise for you, and then it will always be increased by 1?


Fun fact: I have a DEVS model in development right now where a steps counter would be very useful, since I still have a model step and want to do something once every N steps, while the remainder of the model is DEVS controlled.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

Wait there is one use case we haven't discussed: Some people might want to step at another place in the step, than only at the beginning of the step. Thereful it would be useful to turn the automatic step increase of.

We could make the step argument a bool, allowing it either to be False (interpreted as 0) or True (interpreted as 1).

But in general, I'm for a flexible library with sensible defaults. time=1, step=1 are very sensible defaults, while also being very flexible.

@quaquel
Copy link
Member

quaquel commented Aug 20, 2024

Fun fact: I have a DEVS model in development right now where a steps counter would be very useful, since I still have a model step and want to do something once every N steps, while the remainder of the model is DEVS controlled.

use devs_simulator.time? or swicht to the ABMSimulator and use its time attribute should fix this for you.

Wait there is one use case we haven't discussed: Some people might want to step at another place in the step, than only at the beginning of the step.

why? I think with clear documentation (so allways at the start or allways at the end) any use case can easily be accomodated.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

I think at this point we disagree on a philosophical level what a simulation library should offer.

@projectmesa/maintainers, I would love some fresh perspectives.

@wang-boyu
Copy link
Member

As an alternative example, in our agents-and-networks gis example, we keep a clock manually : https://github.com/projectmesa/mesa-examples/blob/0ebc4d11711eb59480942d24dd13e9a744744704/gis/agents_and_networks/src/model/model.py#L215

  • By default, model.time is increased with a fixed amount of 1 each step.

This default behavior seems a bit redundant to me. TBH I wasn't really aware of the _time attribute and always used _steps. Unitless time is essentially the same thing as step (or am I missing something?)

If a clock is really really needed, we probably need something more dedicated, like what AnyLogic offers: https://www.anylogic.com/upload/books/new-big-book/16-model-time-date-and-calendar.pdf which is also mentioned above:

We might allow time to be a full Datetime object. Can imagine some scenarios in which that would be very handy.

But this may not be needed by all models, especially those simple models. So it may be better to be optional.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

So it may be better to be optional.

Let me be perfectly clear: It's 100% optional. You can just do model.step(). Simple models don't need to adjust anything.

As an alternative example, in our agents-and-networks gis example, we keep a clock manually

That's quite interesting. It supports the need for a separate clock/time aside from model.steps.

@quaquel
Copy link
Member

quaquel commented Aug 20, 2024

I think at this point we disagree on a philosophical level what a simulation library should offer.

My last question was really about trying to understand the issue. So not philosopical at al.

In line with @wang-boyu, however, I am in favor of simply counting steps and leave detailed time management to an optional more advanced feature (possibly to be integrated into the simulator classes which offer much of what is described in the book chapter).

@rht
Copy link
Contributor

rht commented Aug 20, 2024

I choose to increment the time before the user step, in the spirit of "a new step" (or a new day) has begun. Then the model does things during the step, and.

But that means the steps start from 1 instead of 0. There is no action happening at steps 0, the zeroth-day. Wouldn't this introduce a breaking change in other area, e.g. batchrunner?

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

why? I think with clear documentation (so allways at the start or allways at the end) any use case can easily be accomodated.

Yeah on that point I incline to agree with you, I don't see many scenarios where you want to increase the step counter at another point in the step.

What I meant with philosophical differences, is that I think convention over configuration is in general a good principle for Mesa to follow. Why? Because it offers two things that perfectly fits our mixed target audience.

  1. Good defaults, that allow it to really do things with minimal code. Our starting modellers are not programmers, every complexity that they have to think about matters.
  2. Highly flexible configuration. We're a library. Our classes get literarily subclassed to extend them. We target a wide variety of researchers. What do they want? Nice code hooks where they can tie into, so save themselves work when implementing complex problems. That's why the AgentSet, DEVS, PropertyLayers and a lot of other stuff exists. This is just another hook people can tie into. And an extremely low complexity one on our side.

And are there use cases for this specific hook? In my opinion, yes:

  • model.time can be used as a human-readable values and potentially Datetime object
    • This allows data lookups in time-based lookup tables, decision logic based on a human-readable value, etc.
  • model.time can be used to acknowledge that a step has different sub-steps. It can be increased by a custom (but fixed) amount if you want divide you steps in different substeps
    • This is exactly how StagedActivation now works:

      The scheduler also tracks steps and time separately, allowing fractional time increments based on the number of stages. Time advances in fractional increments of 1 / (# of stages), meaning that 1 step = 1 unit of time.

    class MyModel(Model):
        def step(self, time=0.25):
            model.agents.do("morning")
            model.time += 0.25
            model.agents.do("afternoon")
            model.time += 0.25
            model.agents.do("evening")
            model.time += 0.25
            model.agents.do("night")
    
    my_model = MyModel()
    my_model.step()
    • Which means we can even keep track of both the full steps and the sub-steps, without having to do any rounding or flooring or sorts.

@EwoutH
Copy link
Member Author

EwoutH commented Aug 20, 2024

There is no action happening at steps 0.

The whole __init__ is happening at step 0. If you have N steps, step 0 is the init and step 1 to N are the steps. I think this would be a good convention for Mesa, also something what flexible and easily explainable is.

@EwoutH EwoutH force-pushed the increment_time_step branch from a9575ec to 80e32d1 Compare August 29, 2024 15:53
@EwoutH EwoutH added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Aug 29, 2024
@EwoutH EwoutH requested review from quaquel, Corvince and rht August 29, 2024 16:08
@EwoutH EwoutH changed the title model: Automatically increase time and step count model: Automatically increase step counter Aug 29, 2024
@EwoutH
Copy link
Member Author

EwoutH commented Aug 29, 2024

I believe this is ready for final review. I would like to merge myself.

@EwoutH EwoutH added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Aug 29, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🟢 -7.5% [-9.1%, -5.6%] 🔵 -0.0% [-0.2%, +0.1%]
BoltzmannWealth large 🔵 -1.6% [-31.6%, +34.0%] 🔵 -2.3% [-4.9%, +0.3%]
Schelling small 🔵 -0.4% [-0.6%, -0.1%] 🔵 +2.0% [+1.8%, +2.2%]
Schelling large 🔵 +0.3% [-0.4%, +0.9%] 🔵 +0.4% [-0.8%, +1.4%]
WolfSheep small 🔵 +1.2% [-0.3%, +2.7%] 🔵 +0.2% [-0.1%, +0.5%]
WolfSheep large 🔵 +0.6% [-0.1%, +1.5%] 🔵 +0.1% [-0.8%, +1.1%]
BoidFlockers small 🔵 -0.7% [-1.5%, +0.0%] 🔵 -1.6% [-2.2%, -0.9%]
BoidFlockers large 🔵 -0.2% [-0.5%, +0.1%] 🔵 -1.1% [-1.7%, -0.4%]

- Add a few tests
- Check that for each example model.steps == 10
@EwoutH EwoutH force-pushed the increment_time_step branch from bca99d0 to 95e1cd2 Compare August 29, 2024 18:34
@EwoutH EwoutH merged commit 95e1cd2 into projectmesa:main Aug 29, 2024
9 of 12 checks passed
@EwoutH
Copy link
Member Author

EwoutH commented Aug 29, 2024

There we go. Mesa is now incrementing time automatically.

After these two weeks I can conclude this effort with the honor of writing the 65st message in this PR (of course not counting all the discussions outside). Like a good friend of mine once said: "De rijst is de bestelling" 🍋

@EwoutH EwoutH added the breaking Release notes label label Aug 29, 2024
@EwoutH EwoutH changed the title model: Automatically increase step counter model: Automatically increase steps counter Aug 30, 2024
EwoutH added a commit that referenced this pull request Oct 12, 2024
This commit reverts PR #161 projectmesa/mesa-examples#161

That PR assumed that time advancement would be done automatically, like proposed in #2223

We encountered some underlying issues with time, which we couldn't resolve in time.
EwoutH added a commit that referenced this pull request Oct 12, 2024
This commit reverts PR #161 projectmesa/mesa-examples#161

That PR assumed that time advancement would be done automatically, like proposed in #2223

We encountered some underlying issues with time, which we couldn't resolve in time.
EwoutH added a commit to EwoutH/mesa that referenced this pull request Oct 15, 2024
…lity (projectmesa#170)

This commit reverts PR projectmesa#161 projectmesa/mesa-examples#161

That PR assumed that time advancement would be done automatically, like proposed in projectmesa#2223

We encountered some underlying issues with time, which we couldn't resolve in time.
EwoutH added a commit to EwoutH/mesa that referenced this pull request Oct 15, 2024
…lity (projectmesa#170)

This commit reverts PR projectmesa#161 projectmesa/mesa-examples#161

That PR assumed that time advancement would be done automatically, like proposed in projectmesa#2223

We encountered some underlying issues with time, which we couldn't resolve in time.
This was referenced Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Release notes label feature Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatic time and step incrementing
7 participants