QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

LazyObject receiving an update during calculate() will stay in state "not calculated"

Open pcaspers opened this issue 2 months ago • 6 comments

We have a case where a lazy object receives an update() from one of its observables during calculate(). Since calculate() sets calculated_ to true early, see here,

    inline void LazyObject::calculate() const {
        if (!calculated_ && !frozen_) {
            calculated_ = true;   // prevent infinite recursion in
                                  // case of bootstrapping
            try {
                performCalculations();
            } catch (...) {
                calculated_ = false;
                throw;
            }
        }
    }

and the update() is received during performCalculations() the member variable calculated_ will be set to false in LazyObject::update().

I would like the LazyObject to be in state calculated_ = true instead, i.e. I am tempted to change the code to

    inline void LazyObject::calculate() const {
        if (!calculated_ && !frozen_) {
            calculated_ = true;   // prevent infinite recursion in
                                  // case of bootstrapping
            try {
                performCalculations();
            } catch (...) {
                calculated_ = false;
                throw;
            }
            calculated_ = true; // might have been set to false in between
        }
    }

I guess you could argue that the update that is received means that the LazyObject should be calculated on the next call to calculate(). However at least in our case, the update is already incorporated in the current calculation.

I am checking whether we can simplify the setup so that the code change is not necessary. On the other hand the change could actually be a potential fix and prevent unnecessary calculations.

Just want to get opinions on what the "correct" or "expected" behavior would be? Maybe there is no unique answer and we rather leave the behavior as is in order not to break something.

pcaspers avatar Oct 01 '25 16:10 pcaspers

I'm a bit puzzled. If this happens, isn't it a hint that there's an observability cycle in there somewhere?

lballabio avatar Oct 02 '25 11:10 lballabio

Actually no, but there is a non-standard element in our setup alright: We have a class A inheriting von LazyObject. An instance of class A observes an instance of class B which is also a LazyObject. During the calculation of A we call recalculate() on B. It's a separate discussion whether this is required (I am also looking into this part), but assuming it's legit to do this, B will notify A during the calculation.

pcaspers avatar Oct 02 '25 11:10 pcaspers

I mean the doc of recalculate() says

            \note Explicit invocation of this method is <b>not</b>
                  necessary if the object registered itself as
                  observer with the structures on which such results
                  depend.  It is strongly advised to follow this
                  policy when possible.

Although it's not the reason why we call recalculate() I can see why this is not the expected way to go.

pcaspers avatar Oct 02 '25 12:10 pcaspers

So are we saying that if calculated_ is false at the end of LazyObject::calculate() that this is a hint for "wrong" usage of classes in some sense? On the other hand we do expose LazyObject::recalculate() and if we allow to use this method we should consequently set calculated_ to true at the end of LazyObject::calculate?

I don't know the history of LazyObject::calculate() but the comment about setting calculated_ to true early seems to suggest that this was mainly done for the bootstrapping use case. And LazyObject::recalculate() was introduced for bootstrapping, too, I think?

pcaspers avatar Oct 03 '25 17:10 pcaspers

Ok, I see. I suppose there's no harm in assigning true again, but I'm still puzzled as to why you have to call recalculate() on your dependency. Without knowing your setup, I would have guessed that during the calculation of A you would call some methods of B, and B should know on its own whether or not to recalculate...

In any case, yes, recalculate() is used during bootstrapping; the instruments inside the helpers don't register with the curve being bootstrapped and must be told to recalculate when the nodes are moved.

lballabio avatar Oct 07 '25 17:10 lballabio

Yes, I'll try to make this change in our library and test that for a while, I might open a PR against QuantLib then.

Our setup is not mature here, we call recalculate() on an instrument to make sure that the associated pricing engine has the arguments and results associated to the instrument. Then we call a proprietary method on the pricing engine to calculate forward prices of the underlying instrument, which feed into an "outer" instrument. Bad design and to be reviewed, but the best we have at the moment.

pcaspers avatar Oct 10 '25 15:10 pcaspers

This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.

github-actions[bot] avatar Dec 10 '25 02:12 github-actions[bot]