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 constructor architecture #1410

Merged
merged 45 commits into from May 21, 2024
Merged

Add constructor architecture #1410

merged 45 commits into from May 21, 2024

Conversation

mnwhite
Copy link
Contributor

@mnwhite mnwhite commented Apr 15, 2024

Beginning work on the construct method of AgentType. As of this moment, a basic prototype works for income distributions. Still a lot more to develop and test.

  • Tests for new functionality/models or Tests to reproduce the bug-fix in code.
  • Updated documentation of features that add new functionality.
  • Update CHANGELOG.md with major/minor changes.

Completely untested, will adapt construct_income_process on next commit as test example.
Holy crap, construct() worked on the first try! Only IncShkDstn has been tested, and none of the "error catching" features have been looked at. But hey, it works!
Should pass these tests now, but formatting is probably still wrong.
@mnwhite
Copy link
Contributor Author

mnwhite commented Apr 15, 2024

As noted in the issue where I proposed this, there are two significant design points to work out:

  1. How to handle situations where it's natural to construct multiple objects in the same function. The most obvious case is the first example I implemented: IncShkDstn, PermShkDstn, and TranShkDstn. I put in an ugly workaround, but something else should be done before this is merged.

  2. Whether and how we want constructors to indicate whether they've made a time-varying or time-invariant object. I'm beginning to come around to the idea that these concepts (to the extent they're in HARK now and will be until several more overhaul steps are completed) should not be intended to be changed at the instance level. Rather, they're a fixed fact of the AgentType. There's a very specific reason they were defined at the instance level, but that reason no longer exists.

Can now collapse a DiscreteDistribution to a single dimension; PermShkDstn and TranShkDstn are made this way now. Moved bespoke constructor dictionary lines out of init. Future plan: each AgentType subclass should have default constructors dictionary in its parameter dictionary.
One test was failing due to lack of seed.
@mnwhite
Copy link
Contributor Author

mnwhite commented Apr 24, 2024

Can someone tell me how to run the pre-commit routine locally? I can run black, but I don't know how to make lint work.

@sbenthall
Copy link
Contributor

$ pre-commit run --all-files

@sbenthall
Copy link
Contributor

more info in the developer guide here: https://docs.econ-ark.org/guides/contributing.html#pre-commit-hooks

@alanlujan91
Copy link
Member

pip install pre-commit # install 
pre-commit install # start-up
git add * # stage your changes
pre-commit # run the hooks

@alanlujan91
Copy link
Member

$ pre-commit run --all-files

you probably don't want to run all files, only your changes

All tests pass except for calc_jacobian, which needs a separate fix.
Missed this one. Jacobian test will still be failing.
All test should pass now. Added new optional input for AgentType.solve() that allows the user to decline to run the presolve routine. For most of our classes, presolve might update the terminal solution and/or run update/constructor methods, but this is fatal to what calc_jacobian wants to do.
I think something about pytest just changed while I was working on this branch, because three tests needed to have a [0] added to them (to reference the only element in the array) in order to be correct. Nothing changed, but the result of these tests did. I'm confused.
One assertEqual test was off by 1e-16 on Python 3.9, but not 3.10. No idea why.
Also added retro support for using [None] rather than None for aXtraExtra.
@mnwhite
Copy link
Contributor Author

mnwhite commented Apr 27, 2024

A bunch of tests needed syntax changes, but everything passes now. The other things that I'm moving to constructors will be much easier from here on out, and should be completed on Monday.

For this PR, I'm going to set the goal at getting everything in constructor functions that should be (plus some alternates that can be swapped in). After this is merged, in a separate branch/PR, I'm going to tackle undoing/simplifying the custom init methods for each AgentType subclass, and get them all just using AgentType.init ... That also means that the default dictionary should be a class attribute.

@alanlujan91
Copy link
Member

We should be doing super().init everywhere

@mnwhite
Copy link
Contributor Author

mnwhite commented Apr 27, 2024

Even where super().__init__ is used, there can be problems when update() is called by a parent class as part of init before the child class is "ready" for it, leading to unexpected results. In the not-too-distant future, I hope all our AgentType subclasses inherit directly from AgentType, and don't have any special init stuff.

pLvlGrid and pLvlNextFunc are now built with the constructors framework. This means that the *only* difference between the AgentType subclasses in ConsGenIncProcessModel is their default dictionary. GenInc tests pass locally, committing to see what else I broke.
@mnwhite
Copy link
Contributor Author

mnwhite commented May 7, 2024

If tests pass on this commit, the next commit will change exactly one line of code, and revise tests to fit. As is, the "construct pLvlGrid by simulation" method draws on AgentCount, which is fine as long as AgentCount is actually large. The number of eventual simulated agents shouldn't be used in the solution of the problem, even extremely indirectly. So I'm just going to have it use a large number of agents no matter what. This is also why the hardcoded seeds don't matter in this function/method.

Never make last second changes before committing, kids. You will never get it right and only embarrass yourself through the commit history.
Minor errors in code and dictionaries due to changes in prior commit.
@mnwhite
Copy link
Contributor Author

mnwhite commented May 10, 2024

Still behind schedule, as there were more terminal solvers than I remembered. Still to do the following small tasks:

  • make some example MrkvArray constructors
  • a few "alternate example" constructor functions, to demonstrate swappability

for j in range(len(args_needed)):
this_arg = args_needed[j]
if hasattr(self, this_arg):
temp_dict[this_arg] = getattr(self, this_arg)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to do this without hasattr/getattr/setattr, using manipulations of a data structure rather than an object's attributes?

if not any_missing:
temp = constructor(**temp_dict)
setattr(self, key, temp)
self.parameters[key] = temp
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these parameters, or model constructs, or both?

what if there was a self.constructs dictionary that was separate from parameters?

including their names, the function that constructs them, the names of
those functions inputs, and whether those inputs are present.

Parameters
Copy link
Contributor

Choose a reason for hiding this comment

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

Python conventions for printed representations of things is to create a __repr__ method, or to display docstrings.

HARK/core.py Outdated

out = ""
for i in range(N_keys):
key = keys[i]
Copy link
Contributor

Choose a reason for hiding this comment

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

could try for key in keys: here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do use i below, because I track which work items have been completed in a boolean vector.

HARK/core.py Outdated
out = ""
for i in range(N_keys):
key = keys[i]
has_val = hasattr(self, key)
Copy link
Contributor

Choose a reason for hiding this comment

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

could be looking up in parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, construct() does this but I forgot to put that line in here. Will fix.

Returns
-------
None.
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

This method create functionality that operates on arbitrary functions, since any arbitrary function is a constructor.

Is a constructor a type?

Or a constructor-list a type/data structure with extra functionality?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we want to leave it general/simple that any function can be a constructor, to keep barriers to entry low.

Also had to change dictionary import in tests. Notebooks might break.
Too much copy-pasta, something ended up zero that should have been one.
One is an extremely basic binary state specification, the other is a "ratchet" specification in which transitions only go one direction, and only one step at a time.
Same issue as in earlier commit, but in an example notebook. Just imported and used correct dictionary.
Construct method is now more robust, with an option to force through errors and record them in a separate dictionary. Also made other small changes/fixes.
This commit adds one missing (trivial) constructor and adds a few "counterexample" constructors that *could* be used for other parameter specifications. There are many, many more that could be written.
@mnwhite
Copy link
Contributor Author

mnwhite commented May 14, 2024

Once the current tests finish, I'll be pushing a commit that only has documentation / changelog updates. I've added a small number of "alternate" constructors that can be substituted in, but there's an arbitrarily large number of other ones that could be written as well.

Following yesterday's discussion with Seb and Alan, I added an additional features to construct: the option to force it to continue running even if it encounters exceptions. In the default (force=False), any exception other than simply not finding an argument will raise an exception as normal; reaching the end of the method without having completed its work is an exception. Under force=True, it records each exception to _constructor_errors and then ignores it, terminating only when it runs out of work to do, as usual. This option allows the user to (e.g.) run construct(force=True) and then simply examine ThisType._constructor_errors (and do ThisType.describe_constructors()) to get a quick read of what happened when they tried to build all of their constructed inputs.

This PR is ready for others to look at. No external behavior has changed. The only changes in this PR are how complex objects get created.

Also added one missing line of documentation.
@mnwhite mnwhite changed the title WIP: Add constructor architecture Add constructor architecture May 14, 2024
if param_name in self.parameters:
del self.parameters[param_name]
if hasattr(self, param_name):
delattr(self, param_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any way to avoid this redundant storage of the constructs?

Maybe constructs should not be stored in parameters, but in self.constructs (or something)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's for deleting things. Right now, I've written things to be intentionally redundant, because we're "between two worlds". The medium run goal is to remove the top level namespace at all, only putting them in parameters.

I don't want a separate constructs dictionary, because there's no meaningful distinction between parameters and constructs. It might be that parameters was the wrong name for that dictionary.

@sbenthall
Copy link
Contributor

This all looks very good to me.

The one thing which I think could be improved is the confusing way in which constructed objects are stored, both (!) as an entry in parameters and as a new attribute on the model object. Neither of these seem quite right to me.

What about a self.constructs dictionary?

@sbenthall
Copy link
Contributor

Ok, I understand that removing object attribute storage entirely is a goal, but something to be implemented in a different PR.

I'll leave further notes on #889

@sbenthall
Copy link
Contributor

There is a conflicting file with CHANGELOG

Otherwise, I believe this is ready to merge?

@mnwhite
Copy link
Contributor Author

mnwhite commented May 21, 2024

Fixed CHANGELOG

@sbenthall sbenthall merged commit c318576 into master May 21, 2024
18 checks passed
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.

None yet

4 participants