Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 14 additions & 54 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)
77 changes: 77 additions & 0 deletions gitpr.py
Original file line number Diff line number Diff line change
@@ -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)
44 changes: 44 additions & 0 deletions gitsync.py
Original file line number Diff line number Diff line change
@@ -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)