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

Create abmtime function for StandardABM #941

Closed
Tortar opened this issue Dec 2, 2023 · 17 comments · Fixed by #942
Closed

Create abmtime function for StandardABM #941

Tortar opened this issue Dec 2, 2023 · 17 comments · Fixed by #942
Labels
breaking design Design-related discussions for APIs, Interfaces, etc. enhancement New feature or request
Milestone

Comments

@Tortar
Copy link
Member

Tortar commented Dec 2, 2023

This is practically costless, and actually somewhat needed: run! and offline_run! restart from 0 in the step column each time they are called. Apart from that, this is a function some people would implement by themselves anyway to track the time of the ABM, and implementing it would make StandardABM more homogeneous with the new EventQueueABM which has one.

This means that this is a breaking change though if we change those two functions to instead use the time of the model

cc @Datseris

@Tortar Tortar added this to the v6.0 milestone Dec 2, 2023
@Tortar Tortar added design Design-related discussions for APIs, Interfaces, etc. enhancement New feature or request breaking labels Dec 2, 2023
@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

yes i've been thinking about this as well, to add a timer in standardABM as well. Now run! and co should start from the model timer which itself starts at 0 but is never reset (unless re-creating the model of course).

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

and so I guess also step! with a function should change:

Step the model forwards until f(model, t) returns true,
where t is the current amount of time the model has been evolved
for, starting from 0.

(the docstring is even not very clear in my opinion)

I would remove the t dependency and just do f(model), the user could use abmtime inside if wants to. Notice that this could be non breaking, since we can easily enough detect which one of the two signatures is called, it is just a deprecation

@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

Okay, but it has to be breaking. It is bad for performance to check if a user gave in a function expecting 1 or 2 arguments. Type unstable. (I know from DynamicalSystems.jl)

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

actually it seems not bad with this trick:

julia> h(x) = x
h (generic function with 1 method)

julia> h(x, y) = x + y
h (generic function with 2 methods)

julia> t(f) = try; f(1); return true; catch; f(1,2); return false; end;

julia> using BenchmarkTools

julia> @btime t($h)
  1.182 ns (0 allocations: 0 bytes)
true

(same when h(x) is not defined)

@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

this is a missleading benchmark because you are passing constant literals to f: 1, 2. The compiler optimizes everything away and understands that f is called with 1, whose value is true. Everything is inlined away to the trivial t(f) = true.

@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

it is well known (stated in the manual in performance tips) that one should not use try blocks inside performance critical code, doubly more so not inside a for loop which is what step! does. Here the try block is simply compiled away!

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

good observation, but I guess we are still in a similar situation since I think model is constant propagated, and I would use 0 for t, let's revise this in the future

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

notice also that we would call it outside the for loop inside step!, at the start of the function, just a warning there is enough

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

applicable is probably more robust though:

julia> f(x) = x
f (generic function with 1 method)

julia> t(f, x, y) = applicable(f, x, y) ? true : false
t (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime t($f, $1, $1)
  93.312 ns (0 allocations: 0 bytes)
false

(the try catch trick takes microseconds when the catch block is executed instead...otherwise it is very fast though, so it could become a good reason for the user to update the code :D)

@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

or, we could simply break nothing. Make it so that the API is: f is user input, and f(model, t) returns true is when evolution stops. t is always the total amount of time elapsed since the start of the run! call. So, it does not care about the current model time.

Or, t is the same as the model time. Up to us to choose.

Since the model time is a new concept, either choice is non-breaking, given that we didn't explain this well enough originally. (What is breaking or not is defined by what the docs say)

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

Or, t is the same as the model time. Up to us to choose.

In this case:

  • it is redundant to pass it since it can be accessed with abmtime(model)
  • (!) it actually changes the behaviour to use t as the model time, even if it is not stated in the docs, so this would be a silent breaking change, even if the docs weren't clear
  • it is a bit more sensible to me f(model) since the stopping condition would probably be something unrelated to the time

On the other hand, leaving t as it is now it would be better in a sense since something like this is more easy to implement:

f(m, t) = stopping_condition_unrelated_to_time(m) || t > 1000

so in the end I think that keeping f(model, t) as it is now, and just improving the docs should be enough 👍

@Datseris
Copy link
Member

Datseris commented Dec 2, 2023

it is redundant to pass it since it can be accessed with abmtime(model)

Sure, but it keeps the API the same, so it is non breaking in this case.

  • even if it is not stated in the docs, so this would be a silent breaking change, even if the docs weren't clear

I don't think this is a breaking change. This is what I am trying to say: whether something is "breaking" or not is defined by the docs. No by what the actual code does. If something was ambiguous in the docs and could be interpreted either way, it does not count as a breaking change to enforce one of the ways it could be interpreted.

  • since the stopping condition would probably be something unrelated to the time

I disagree! And that's the reason time was given as an argument in the first place. I would never use this functionality without an if t < 10000.0 or something condition, to avoid infinite while loops that never stop. But you already said the same thing in the second part of the answer.

@Tortar
Copy link
Member Author

Tortar commented Dec 2, 2023

Yes, I'm more on the side to keep the current way since if you want to call twice step! with a function, it should be easier to implement a condition related to the number of steps if the time is relative to step!

On the breaking change part I don't agree but the subject is controversial :D : https://dev.to/turnerj/should-behavioural-changes-be-considered-breaking-changes-under-semver-2d5n

@Tortar
Copy link
Member Author

Tortar commented Dec 4, 2023

So what is your opinion @Datseris on all this? I'm for keeping the time relative when a function is passed for the advantage I stated in the previous comment.

p.s. I can't help on Agents.jl for a while (some weeks) since I'm really short on time with uni work D:

@Datseris
Copy link
Member

Datseris commented Dec 5, 2023

My opinion is the same. Keep the API as is and time is relative to model initial time.

No worries about the lack of time. I'll try to finish the event queue but also no promises.

@Tortar
Copy link
Member Author

Tortar commented Dec 5, 2023 via email

@Datseris
Copy link
Member

Datseris commented Dec 6, 2023

Yes, I mean that the counter inside step! always starts from 0 each time step is called. It measures time relative to model initial time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking design Design-related discussions for APIs, Interfaces, etc. enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants