diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f3deb78..3cbfcff59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,70 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Fixes Issue [#1202](https://github.com/PSLmodels/OG-Core/issues/1202): - `get_r_gov` clipped the interest rate on government debt at zero, so a - sovereign that genuinely pays a negative real rate could not be modelled. - The bound is now a parameter, `r_gov_floor`, with a default of 0.0 that - reproduces the previous behaviour exactly. +- Fixes Issue [#1202](https://github.com/PSLmodels/OG-Core/issues/1202): `get_r_gov` clipped the interest rate on government debt at zero, so a sovereign that genuinely pays a negative real rate could not be modelled. The bound is now a parameter, `r_gov_floor`, with a default of 0.0 that reproduces the previous behaviour exactly. +- `npv_table` in `output_tables.py` (Issue #1131): builds a table of the net present value of the reform-minus-baseline change in flow variables (e.g. `Y`) over a horizon, evaluated at a list of discount rates. Values are un-stationarized by default so the NPV is taken over the actual (trend-inclusive) level path. +- New resource constraint error message, noting the maximum absolute resource-constraint error, the period it occurs in, and the tolerance, plus a one-line note on how to read it +- Scatter parameters object once per SS solve instead of one per residual evaluation. Pure overhead removal on any Dask-backed SS solve; the larger the Specifications object and the more workers, the more it saves. No numerical change — same solves, same results. + ### Bug Fixes -- Fixes Issue [#1200](https://github.com/PSLmodels/OG-Core/issues/1200): - `replacement_rate_adjust` was read only inside `SS_amount`, so it applied - to the US-Style Social Security system and was silently ignored under - Defined Benefits, Notional Defined Contribution, and Points System. The - adjustment is now applied to those three systems in `pension_amount`, - via a `replacement_rate_adjustment` helper that mirrors the indexing - `SS_amount` already uses, including the per-cohort `t + tt` offset along - the time path. `SS_amount` is unchanged, so US-Style results cannot move. - -### Added -- `npv_table` in `output_tables.py` (Issue #1131): builds a table of the net present value of the reform-minus-baseline change in flow variables (e.g. `Y`) over a horizon, evaluated at a list of discount rates. Values are un-stationarized by default so the NPV is taken over the actual (trend-inclusive) level path. +- Fixes Issue [#1200](https://github.com/PSLmodels/OG-Core/issues/1200): `replacement_rate_adjust` was read only inside `SS_amount`, so it applied to the US-Style Social Security system and was silently ignored under Defined Benefits, Notional Defined Contribution, and Points System. The adjustment is now applied to those three systems in `pension_amount`, via a `replacement_rate_adjustment` helper that mirrors the indexing `SS_amount` already uses, including the per-cohort `t + tt` offset along the time path. `SS_amount` is unchanged, so US-Style results cannot move. ## [0.20.0] - 2026-08-13 12:00:00 ### Added -- Addresses Issue [#1205](https://github.com/PSLmodels/OG-Core/issues/1205): - the UN Data Portal API token can now come from a `un_token` argument to - `demographics.get_un_data`, from a `UN_API_TOKEN` environment variable, or - from a single per-user file (`$XDG_CONFIG_HOME/og/un_api_token.txt`, or - `%APPDATA%\og\un_api_token.txt` on Windows). Sources are tried in that - order, then `un_api_token.txt` in the working directory. This gives one - token per user instead of one per directory. -- An `og-token` command, OG-Core's first console script, to manage that - token without hunting for the file: `og-token set` saves one, - `og-token show` reports where it lives and which source wins, and - `og-token rm` deletes it. The token is read without echoing and is never - printed back. +- Addresses Issue [#1205](https://github.com/PSLmodels/OG-Core/issues/1205): the UN Data Portal API token can now come from a `un_token` argument to `demographics.get_un_data`, from a `UN_API_TOKEN` environment variable, or from a single per-user file (`$XDG_CONFIG_HOME/og/un_api_token.txt`, or `%APPDATA%\og\un_api_token.txt` on Windows). Sources are tried in that order, then `un_api_token.txt` in the working directory. This gives one token per user instead of one per directory. +- An `og-token` command, OG-Core's first console script, to manage that token without hunting for the file: `og-token set` saves one, `og-token show` reports where it lives and which source wins, and `og-token rm` deletes it. The token is read without echoing and is never printed back. ### Changed -- Answering the UN API token prompt now saves the token to the per-user file - rather than to the current working directory, which used to leave a copy of - the token in every directory a model was run from. An existing - `un_api_token.txt` in the working directory is still read, with a notice - that the location is deprecated. -- The token prompt now names both ways forward: where to generate a free - token, and that pressing return uses the archived copy of the same data - instead. -- The token is read with `getpass` rather than `input`, so it is no longer - echoed into the terminal and its scrollback while being typed or pasted. -- An expired token is now named as the reason for falling back to the - archived data, instead of failing the same way as a missing one. The - portal issues JSON Web Tokens, so the expiry date is read locally with - no extra request, and `og-token show` reports whether the stored token - is still current. A token that is not a readable JSON Web Token is used - as before, with no expiry reported. -- A run that falls back to the archived data now says once, not once per - series, how to register a token: where to get one, the full path of the - `og-token` command belonging to the running interpreter, and the token - file to write. The full path matters because `og-token` is installed - beside the interpreter and is normally not on the shell's PATH, so - someone who obtains a token days later has something they can paste - rather than a command that reports "not found". -- The token prompt is skipped when standard input is not interactive, so - scheduled and scripted runs fall back to the Population-Data archive - instead of waiting on input. +- Answering the UN API token prompt now saves the token to the per-user file rather than to the current working directory, which used to leave a copy of the token in every directory a model was run from. An existing `un_api_token.txt` in the working directory is still read, with a notice that the location is deprecated. +- The token prompt now names both ways forward: where to generate a free token, and that pressing return uses the archived copy of the same data instead. +- The token is read with `getpass` rather than `input`, so it is no longer echoed into the terminal and its scrollback while being typed or pasted. +- An expired token is now named as the reason for falling back to the archived data, instead of failing the same way as a missing one. The portal issues JSON Web Tokens, so the expiry date is read locally with no extra request, and `og-token show` reports whether the stored token is still current. A token that is not a readable JSON Web Token is used as before, with no expiry reported. +- A run that falls back to the archived data now says once, not once per series, how to register a token: where to get one, the full path of the `og-token` command belonging to the running interpreter, and the token file to write. The full path matters because `og-token` is installed beside the interpreter and is normally not on the shell's PATH, so someone who obtains a token days later has something they can paste rather than a command that reports "not found". +- The token prompt is skipped when standard input is not interactive, so scheduled and scripted runs fall back to the Population-Data archive instead of waiting on input. ## [0.19.2] - 2026-08-18 17:00:00 diff --git a/Makefile b/Makefile index 2c4c8fcbb..23e2d9cd6 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ # GNU Makefile that documents and automates common development operations # using the GNU make tool (version >= 3.81) -# Development is typically conducted on Linux or Max OS X (with the Xcode -# command-line tools installed), so this Makefile is designed -# to work in that environment (and not on Windows). +# Development is typically conducted on Linux or Mac OS X (with the Xcode +# command-line tools installed). The Git helper targets also +# work on Windows when GNU Make and uv are available. # USAGE: OG-Core$ make [TARGET] .PHONY: help @@ -17,6 +17,8 @@ help: @echo "coverage : generate test coverage report" @echo "documentation : build new Jupyter Book documentation files" @echo "format : format code using ruff and linecheck" + @echo "git-sync : synchronize local, origin, and upstream Git repos" + @echo "git-pr N=n : create local pr-n branch containing upstream PR" .PHONY: clean clean: @@ -72,3 +74,11 @@ format: .PHONY: pip-package pip-package: uv build + +.PHONY: git-sync +git-sync: + @uv run python ./gitsync + +.PHONY: git-pr +git-pr: + @uv run python ./gitpr $(N) diff --git a/gitpr.py b/gitpr.py new file mode 100644 index 000000000..e2a4e91fe --- /dev/null +++ b/gitpr.py @@ -0,0 +1,77 @@ +"""Create or update a local branch for an OG-Core upstream pull request.""" + +from __future__ import annotations + +import subprocess +import sys + + +def git( + *args: str, check: bool = True, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ("git", *args), + check=check, + text=True, + capture_output=capture_output, + ) + + +def current_branch() -> str: + return git("branch", "--show-current", capture_output=True).stdout.strip() + + +def main() -> int: + if len(sys.argv) != 2 or not sys.argv[1].isdigit() or int(sys.argv[1]) < 1: + print( + "ERROR: specify one positive pull-request number: make " + "git-pr N=123", + file=sys.stderr, + ) + return 1 + + branch = current_branch() + if branch not in {"master", "main"}: + print( + "STOP: switch to your local master or main branch first.", + file=sys.stderr, + ) + return 1 + + number = sys.argv[1] + pr_branch = f"pr-{number}" + git("fetch", "upstream", f"refs/pull/{number}/head") + + exists = ( + git( + "show-ref", + "--verify", + "--quiet", + f"refs/heads/{pr_branch}", + check=False, + ).returncode + == 0 + ) + if exists: + git("switch", pr_branch) + result = git("merge", "--ff-only", "FETCH_HEAD", check=False) + if result.returncode: + print( + "STOP: {pr_branch} cannot fast-forward to upstream " + f"PR #{number}. " + "Delete or reconcile the local branch, then try again.", + file=sys.stderr, + ) + return result.returncode + else: + git("switch", "--create", pr_branch, "FETCH_HEAD") + + git("status", "--short", "--branch") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) diff --git a/gitsync.py b/gitsync.py new file mode 100644 index 000000000..b65998c25 --- /dev/null +++ b/gitsync.py @@ -0,0 +1,44 @@ +"""Fast-forward a local master or main branch from upstream/master.""" + +from __future__ import annotations + +import subprocess +import sys + + +def git( + *args: str, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ("git", *args), + check=True, + text=True, + capture_output=capture_output, + ) + + +def current_branch() -> str: + return git("branch", "--show-current", capture_output=True).stdout.strip() + + +def main() -> int: + branch = current_branch() + if branch not in {"master", "main"}: + print( + "STOP: switch to your local master or main branch first.", + file=sys.stderr, + ) + return 1 + + git("fetch", "upstream") + # A sync should not silently create a merge commit when branches diverge. + git("merge", "--ff-only", "upstream/master") + git("push", "origin", branch) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode)