openfisca / openfisca-core

OpenFisca core engine. See other repositories for countries-specific code & data.
https://openfisca.org
GNU Affero General Public License v3.0
168 stars 75 forks source link

Cycle checking results in inconsistent calculations #749

Closed Morendil closed 5 years ago

Morendil commented 5 years ago

Minimal repro:

# -*- coding: utf-8 -*-
import openfisca_france
from openfisca_core.simulations import Simulation

tax_benefit_system = openfisca_france.CountryTaxBenefitSystem()
situation = {
    "individus": {
        "demandeur": {
            "date_naissance": {
                "2018-10": "1952-01-05",
                "2018-09": "1952-01-05",
                "2018-08": "1952-01-05",
                "2018-07": "1952-01-05"
            },
            "salaire_imposable": {
                "2018": 1000,
                "2017": 1000,
                "2016": 1000
            }
        }
    },
    "familles": {
        "_": {
            "parents": [
                "demandeur"
            ],
            "enfants": [],
        }
    },
    "foyers_fiscaux": {
        "_": {
            "declarants": [
                "demandeur"
            ],
            "personnes_a_charge": []
        }
    },
    "menages": {
        "_": {
            "personne_de_reference": [
                "demandeur"
            ],
            "enfants": []
        }
    }
}

allMonths = ['2018-09']

calculs_ok = [
{'variable': 'rfr', 'periods': ['2016']},
{'variable': 'ass', 'periods': allMonths}
]

calculs_ko = [
{'variable': 'ass', 'periods': allMonths},
{'variable': 'rfr', 'periods': ['2016']}
]

simulation_actuelle = Simulation(
tax_benefit_system=tax_benefit_system,
simulation_json=situation)

for computation in calculs_ok:
    for period in computation['periods']:
        data = simulation_actuelle.calculate(computation['variable'], period)
        print (computation['variable'])
        print (data)

simulation_actuelle = Simulation(
tax_benefit_system=tax_benefit_system,
simulation_json=situation)

for computation in calculs_ko:
    for period in computation['periods']:
        data = simulation_actuelle.calculate(computation['variable'], period)
        print (computation['variable'])
        print (data)

The results of computing ass and rfr should not depend on the order of evaluation, but they do:

rfr
[573.99994]
ass
[0.]
ass
[0.]
rfr
[0.]

This is impacting France and indirectly Mes-Aides (preventing an upgrade to the latest version of France in production). The France model is very ramified and cycles abound; on inspection I think the current solution (max_nb_cycles) is misguided.

Connected to openfisca/openfisca-france#1211

Morendil commented 5 years ago

One thing that makes this hard to investigate is that it manifests in many ways, some of which can be seen on a small scale (as in the test above) and some of which have to be embraced at a larger scale. I've created a gist with the trace of a calculation based on a version of Core that removes cycle checks altogether. This trace is from executing this Mes Aides test. It's 10K lines long - that's how deep the stack reaches.

One of the things that's going in there is that we calculate salaire_imposable 6 times for different periods, to calculate autonomie_financiere. If you search for autonomie_financiere in the trace it quickly becomes apparent that we compute it at ever increasing depths of stack, on each occasion for a period reaching further and further back in time.

The hunch I'm investigating is that this isn't about detecting "cycles" at all. It seems clear for this particular observation that the problem can be also framed as why are we even attempting to compute salaire_imposable in the first place, when we don't have a salaire_de_base in the first place ? This is a semantic determination, not a purely syntactic one. I, a human, know that it's pointless to try and compute a taxable salary when there is no salary information in the situation we are computing.

Is there a clue we could follow ? It seems like there is:

class salaire_de_base(Variable):
    value_type = float
    entity = Individu
    label = u"Salaire de base, en général appelé salaire brut, la 1ère ligne sur la fiche de paie"
    set_input = set_input_divide_by_period
    reference = u'http://www.insee.fr/fr/methodes/default.asp?page=definitions/salaire-mensuel-base-smb.htm'
    definition_period = MONTH

Well, it doesn't have a default value! That makes sense, because there's really no "default" base salary in salary computation - but there can be the absence of a salary. And in fact we can short-circuit a bunch of calculations if we could detect this. Unfortunately, the code for variables as written has a notion of a "default default value", and so we don't take advantage of this clue.

However, this doesn't save us yet. I've tried forcing Core to skip the computation if the variable being computed was salaire_imposable - and we still hit a cycle - but a different one, which is a clue that cycles in fact abound in the France model. (A good hint that we should solve this problem in a general way and not piecemeal, I think.)

Here is the resulting trace. This looks more like a "true" circular definition: we have the eligibility for AAH computed based on a bunch of income sources which include another aid called ASI/ASPA, for which eligibility in turn hinges on a bunch of income sources including AAH itself.

However, it's important to note that it's not a real cycle in the sense that each time around we shift the period of evaluation towards the past. It would be reasonable to compute a situation in which we want to know, for instance, what fluctuations exist over time as a result of being granted aids in the past which causes someone's income to go over a threshold which causes them not to be eligible for this aid.

What doesn't really make sense in the context of the test is this: we are evaluating eligibility for ASI/ASPA at infinite depths for a situation where its age condition is not fulfilled. Again we have a semantic understanding that we could short-circuit a computation - this time not because a variable is absent but because a variable is present.

This is not a new issue and is linked to vectorization of the computations. In short, we've assumed so far that vector computation necessarily means eager evaluation. We are evaluating ASI/ASPA for a 38-year-old on the chance that the array might contain elderly people. We've tried to improve this with the max_nb_cycles parameter, but this has a huge problem: max_nb_cycles only stops a computation when a cycle is detected, which means that we have been at least once through the loop of unnecessary computations.

This is absurd in the singular case, but it also makes kind of an absurdity, unless I'm very mistaken about the execution behaviour, of our performance story for vector computations. The bulk of the population is under 65, so just how much performance we're throwing away even in a vector context to run one loop of superfluous evaluations for everyone, relative to what we'd be throwing away by performing the computation sequentially, is an open question.

The good news is that we don't have to choose between the two evils of sequential computation or vast amounts of unnecessary computation, at least I don't believe we do. There seem to be technical solutions (lazyarray, cytoolz), or in the worst case we could syntactically elevate eligibility conditions to a special status, perform them always before computing income bases (or other expensive aggregates), and filter the arrays post-eligibility to perform the income basis computation only on entities that pass the first level of eligibility.

But anyway, the TLDR here is: I'd like to reframe this problem from detecting cycles to short-circuiting computation.

Morendil commented 5 years ago

Continuing on from the previous episode…

I've investigated short-circuiting as suggested above. One problem that immediately arises is that it's not clear how to change the "shape" of a call to simulation.calculate(), given that

What I'd initially envisioned was something like the following scenario, using aah as an example:

My rationale for this approach was that if, for instance, AAH eligibility and ASI eligibility have something that syntactically looks like a circular dependency, but that in practice AAH and ASI eligibility are mutually exclusive OR dependent on inputs that only satisfy them in particular periods (which is in fact the case), then this ensures that the vector eventually becomes empty, and the computation terminates.

The problem here is that we do not have a way to (temporarily, in stack-like fashion) change the vector being operated on into another, because this context is not part of the execution stack, so this fails at step 3.

At this point I decided to step back from solving the question "how to short-circuit computations", and instead go back to investigating the original problem report: figuring out why rfr is zeroed out by computing ass first in the above test.

The reason this happens is two-fold:

Instrumenting the code in simulations.py to figure out when the rfr variable was being computed and cached, and how this interacted with cycle detection, resulted in this:

...
Formula result for rfr [0.] 2014
Caching rfr [0.] 2014
in cycle with retraite_imposable 2016-01
in cycle with revenu_assimile_pension 2016
in cycle with revenu_assimile_pension_apres_abattements 2016
in cycle with traitements_salaires_pensions_rentes 2016
in cycle with rev_cat_tspr 2016
in cycle with rev_cat 2016
in cycle with rbg 2016
in cycle with rng 2016
in cycle with rni 2016
ultimate cycle error for rfr 2016
Formula result for rfr None 2016
Default for rfr [0.] 2016
Caching rfr [0.] 2016
...

We see that there is a cycle error in retraite_imposable, and this "bubbles up" the stack for a while. The first time we reach, moving up the stack, a computation that sets max_nb_cycles is this line: rfr = individu.foyer_fiscal('rfr', period = period.n_2, max_nb_cycles = 1)

This is why it's rfr that gets zeroed out and not some other variable.

On branch simplify-cycle-detection, we can see the effect of removing the second rule above (re-raise the error until we hit a line with a max_nb_cycles): all of the tests keep passing (except the unit tests that checked this rule of cycle detection). So we might have a quick-and-dirty fix for Mes Aides' problem until we figure out a more permanent solution.

@guillett Would you please test your problematic calculations from Mes Aides against this particular branch of core ?

bonjourmauko commented 5 years ago

Also note that by defaulting max_nb_cycles to 0:

# simulations.py:166
max_nb_cycles = parameters.get('max_nb_cycles')

if max_nb_cycles is not None:
    self.max_nb_cycles = max_nb_cycles
else:
    self.max_nb_cycles = max_nb_cycles = 0

We obtain the desired result:

rfr
[573.99994]
ass
[0.]
ass
[0.]
rfr
[573.99994]

This increases stack's maximum recursion depth from 242 to 268 (~10%).

This makes 7 out of 1508 France's tests fail:

$ openfisca-run-test --country-package openfisca_france tests
======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 48, in assert_near
    diff, absolute_error_margin)
AssertionError: b'revenu_disponible@2017: '[37042.36] differs from [36355.] with an absolute margin [687.3594] > 1
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: revenu_disponible.yaml: f1aw_f4ba_2017 - 2017
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 48, in assert_near
    diff, absolute_error_margin)
AssertionError: b'revenu_disponible@2017: '[71194.37] differs from [70507.] with an absolute margin [687.3672] > 1
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: revenu_disponible.yaml: acomptes_ir_2017 - 2017
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 48, in assert_near
    diff, absolute_error_margin)
AssertionError: b'revenu_disponible@2018: '[2958671.8] differs from [2957973.8] with an absolute margin [698.] > 1
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: revenu_disponible.yaml: pfu_av_2018 - 2018
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 48, in assert_near
    diff, absolute_error_margin)
AssertionError: b'revenu_disponible@2018: '[18597.934] differs from [17900.] with an absolute margin [697.9336] > 1
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: revenu_disponible.yaml: pfu_epargne_non_solidaire_etats_non_cooperatifs_2018 - 2018
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 48, in assert_near
    diff, absolute_error_margin)
AssertionError: b'revenu_disponible@2018: '[203457.94] differs from [202760.] with an absolute margin [697.9375] > 1
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: revenu_disponible.yaml: pfu_hors_av_epargne_non_solidaire_etats_non_cooperatifs_2018 - 2018
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 52, in assert_near
    diff, abs(relative_error_margin * target_value))
AssertionError: b'rsa@2017-01: '[0.] differs from [335.17] with a relative margin [335.17] > [3.3517]
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: rsa_2017.yaml: Les derniers benefices agricoles et autres TNS sont pris en compte dans la BR du RSA - 2017-01
--------------------- >> end captured logging << ---------------------

======================================================================
FAIL: unittest.case.FunctionTestCase (check)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 152, in check
    _run_test(period_str, test, verbose, only_variables, ignore_variables, options)
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/test_runner.py", line 249, in _run_test
    relative_error_margin = relative_error_margin,
  File "/Users/hyperion/.pyenv/versions/3.7.0/envs/openfisca-france-3.7.0/lib/python3.7/site-packages/openfisca_core/tools/__init__.py", line 52, in assert_near
    diff, abs(relative_error_margin * target_value))
AssertionError: b'rsa@2017-01: '[0.] differs from [135.17] with a relative margin [135.17] > [1.3517]
-------------------- >> begin captured logging << --------------------
openfisca_core.tools.test_runner: ERROR: rsa_2017.yaml: Les derniers benefices TNS sont pris en compte dans la BR du RSA - 2017-01
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 1508 tests in 495.242s

FAILED (failures=7)

By defaulting max_nb_cycles to 1:

# simulations.py:166
max_nb_cycles = parameters.get('max_nb_cycles')

if max_nb_cycles is not None:
    self.max_nb_cycles = max_nb_cycles
else:
    self.max_nb_cycles = max_nb_cycles = 1

We also obtain the desired result:

rfr
[573.99994]
ass
[0.]
ass
[0.]
rfr
[573.99994]

But this increases stack's maximum recursion depth from 242 to 961 (~397%)!!!

Morendil commented 5 years ago

Making progress on short-circuiting.

Consider the following method we might add to entities.py: https://github.com/openfisca/openfisca-core/blob/6ba2e556ff27abfa41e3d103354719850398bb04/openfisca_core/entities.py#L205-L210

This allows a new syntax-like construct for formulas:

class aah_base(Variable):
    calculate_output = calculate_output_add
    value_type = float
    entity = Individu
    definition_period = MONTH

    def formula(individu, period, parameters):
        return individu.condition('aah_eligible', 'aah_base_montant', period)

Obviously this requires a bit of work on existing formulas. Is it worth it though ?

I tried a change of this sort in just two places, the eligibility for AAH and that for PAJE. (No particular reason, they're the ones that popped up first on looking for likely instances of the general pattern "take the array-wise logical AND of an eligibility condition and an amount".)

The results are impressive - I get a 35% gain on execution time for the Mes Aides tests (from 315s to 206s) and a 20% total gain on the entire test suite. Not so bad for so few lines.

An obvious objection is that the above implementation does not realize these performance gains in the general vector case (n > 1), only for singleton simulations or those where these eligibility conditions are homogenous.

The gains in the singleton case alone could be worth it just in terms of helping local test execution or CI load. But I think we can realize the gains in the general vector case as well by partitioning computation. Say we start with a vector of 1000, and encounter an eligibility condition which 25% only pass. Then we can run the cheaper computation on 75% of the vector and restrict the more expensive one to that 25%. Of that 25%, the ones that require deeper computation are also probably going to encounter some more eligibility conditions, all of which will reduce the vector further.

bonjourmauko commented 5 years ago

What I'd initially envisioned was something like the following scenario, using aah as an example:

compute the vector aah_eligible, a relatively shallow computation count how many items in that vector are True and note the indices create a new sub-vector of the vector being operated on, containing these indices pass this (smaller) population vector on to the formula for aah_base_ressources "unpack" this small vector upon return, into a larger vector, filling the indices in the original that correspond to eligible individuals

You could achieve this by boolean indexing and fancy indexing, however memory performance should be tested as both create copies of the arrays, instead of just views. An example:

import numpy                                                                                                                                                                                        

montant_aah = numpy.zeros(int(1e5))                                                                                                                                                                                                                                                                                                                                                      
# array([0., 0., 0., ..., 0., 0., 0.])

aah_eligible = numpy.random.choice(numpy.array([True, False]), int(1e5))                                                                                                                                                                                                                                                                                                                
# array([ True,  True,  True, ...,  True, False,  True])

ahh_eligible_indexes = numpy.where(aah_eligible)[0]                                                                                                                                                 
# array([    0,     1,     2, ..., 99994, 99997, 99999])

# let's say this is the formula for now
aah_base_resource_formula = 100                                                                                                                                                                     

montant_aah[ahh_eligible_indexes] += aah_base_resource_formula                                                                                                                                      
# array([100., 100., 100., ..., 100., 100., 100.])

montant_aah                                                                                                                                                                                        
# array([100., 100., 100., ..., 100.,   0., 100.])
bonjourmauko commented 5 years ago

Hi @Morendil, thanks for the investigation on the possible scenarios.

The gains in the singleton case alone could be worth it just in terms of helping local test execution or CI load. But I think we can realize the gains in the general vector case as well by partitioning computation.

This is all theoretical, but let's say for:

random = numpy.random.rand(n) 
condition = numpy.random.choice(numpy.array([True, False]), n)
random - random
eligible = random[numpy.where(condition)[0]]
random[numpy.where(condition)[0]] = eligible + eligible 

These are the results for different values of n:

n formula A formula B condition.any()
10 428 ns 975 ns 1.21 µs
100 444 ns 1.1 µs 1.2 µs
1000 745 ns 2.91 µs 1.31 µs
10000 3.42 µs 30 µs 1.39 µs
100000 135 µs 481 µs 2.21 µs
1000000 733 µs 5.89 ms 10.6 µs
10000000 27.7 ms 75 ms 111 µs

We can see that:

Of course, all of this has to be tested against actual populations (we could generate with https://github.com/openfisca/openfisca-survey-manager/blob/master/openfisca_survey_manager/tests/test_scenario.py for testing).

Morendil commented 5 years ago

Linking to this old discussion on short-circuiting: #363.

Morendil commented 5 years ago

To recap some progress in the few weeks since this was opened, some avenues we explored for cracking this were the following:

What we are lacking, it seems to me, is some clarity on how OpenFisca is supposed to work; a specification of any kind, formal or informal, would probably help a lot.

One of the examples that @guillett came up with was:

class pension(Variable):
  definition_period = MONTH

  formula(entity, period):
    return entity.calculate('pension', period.last_month)

While this is pathological, it's also a simplified example of the kind of thing we have been writing without meaning to. It is not properly speaking a cycle since pension for the previous month isn't the same as pension for this month (we've been informally calling them "spirals").

In fact, this code will terminate if there is an input for pension at some previous month relative to the requested period. If that is not the case, we could still statically reason our way out of an infinite loop in the following way:

Computing the dependency graph can help us statically assess the situation, and would have other uses, such as helping visualize the model, helping navigate variables in the API and its exploration UI.

Our lack of precision in defining "what we talk about when we talk about OpenFisca formulas" may have led us to write formulas that will stop making sense when we reexamine them in the light of a more precise framework that incorporates notions of "free variables" and "calculable formulas". The ease of using 0 as a default value for just about everything, the fact that variables with formulas can also be used as inputs at the drop of a hat, and other "conveniences" in Core might mean that we have some (or a lot of) work to make the models conform to the calculable specification a posteriori. But it might be well worth it in terms of accuracy and quality of our computations.

To take an example from France, one of the cycles we explicitly disallow is: aide_logement_neutralisation_rsa -> rsa -> aide_logement -> aide_logement_neutralisation_rsa

However, the other dependency of aide_logement_neutralisation_rsa, which is revenu_assimile_salaire_apres_abattements, is never evaluated if we allow indefinite cycles. This is silly because that variable is essentially salaire_imposable, the taxable salary, which is rather easy to assess as non-calculable when there is no input of a net or base salary.

Once the calculation of aide_logement_neutralisation_rsa is triggered it takes close to 5000 intermediate calculations before closing the cycle - which we could simply skip if we looked at the free variables before starting.

benjello commented 5 years ago

I am not sure I understand everything but I can give you some input of how we got there for france.

Morendil commented 5 years ago

provided by the user when initializing its simulation.

Well, if we're looking at what the user provides, we can also look at all the inputs; after all if we can make a decision on whether a formula is calculable or not based on that data, we shouldn't have the user do any work. If we can't make that decision, then it's hard to tell the user how to make it. What advice would we give the user ?

allow the user be able to specify a backstop date before which every a variable could be reset to its default value or a specified value

The first option ("try the oldest variable") is what I experimented my initial investigations after discussing the problem with @guillett and it didn't seem to solve the problem very effectively. In some cases the dependencies are so deep that a few cycles are enough to overflow the stack. So if you have a wide range of dates, you're stuck with the problem.

Admittedly, I did this for all variables and not on a variable-by-variable basis. On the other hand if you have to do this for all variables, it could get very tedious to do it by hand…

I agree with the logic that we should keep the formulas in the country package agnostic of how they're going to be computed, and rely on the inputs. But I think we should be able to "follow the values back in time" automatically, not force the user to do it; and I think we should give strong guarantees that computations always terminate, without ever pushing on the user the responsibility for termination (which is what max_nb_cycles also does). We should be able to raise an error statically if a computation is going to overflow the stack.

fpagnoux commented 5 years ago

Thanks @Morendil and @maukoquiroga for all this great investigation, this is really interesting and could offer a lot of potential improvements for OpenFisca!

the fact that variables with formulas can also be used as inputs at the drop of a hat

This is caused by the fact an OpenFisca model can be used for different usecases. For instance, a "net salary" is usually an input if you're calculating social benefits, but an output if you're calculation social contributions.

fpagnoux commented 5 years ago

Also, FYI, there is already something in Core to print the trace of a calculation:

simulation = Simulation(tax_benefit_system, simulation_dict, trace = True)
simulation.calculate('disposable_income', '2017-01')
simulation.tracer.print_computation_log()

will print

  disposable_income<2017-01> >> [3920. 2675.]
    salary<2017-01> >> [4000. 2500.]
    basic_income<2017-01> >> [600. 600.]
      age<2017-01> >> [37 33]
        birth<2017-01> >> ['1980-01-01' '1984-01-01']
    income_tax<2017-01> >> [600. 375.]
      salary<2017-01> >> [4000. 2500.]
    social_security_contribution<2017-01> >> [80. 50.]
      salary<2017-01> >> [4000. 2500.]
Morendil commented 5 years ago

Fixed in #817