From e2b0127a0d784ed9740fd316c3f823d06410f386 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 6 Apr 2026 11:51:37 -0500 Subject: [PATCH 1/6] doc fixes --- README.rst | 2 +- docs/api/utils.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8570956..5c46c51 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,6 @@ Getting Started pip install git+https://github.com/onesixsolutions/torchcast.git#egg=torchcast -``torchcast`` requires Python >= 3.8 and PyTorch >= 1.8. +``torchcast`` requires Python >= 3.9 and PyTorch >= 1.12. See the `Quick Start `_ for a simple example that will get you up to speed, or delve into the `examples `_ or the `API `_. diff --git a/docs/api/utils.rst b/docs/api/utils.rst index 77457e5..1aff719 100644 --- a/docs/api/utils.rst +++ b/docs/api/utils.rst @@ -4,4 +4,4 @@ Utils .. include:: ../macros.hrst .. automodule:: torchcast.utils - :members: TimeSeriesDataset, TimeSeriesDataLoader, add_season_features, complete_times, make_baseline, SimpleTrainer, StateSpaceTrainer, SeasonalEmbeddingsTrainer, Stopping \ No newline at end of file + :members: TimeSeriesDataset, TimeSeriesDataLoader, add_season_features, complete_times, make_baseline, SimpleTrainer, StateSpaceTrainer, ModelMatEmbeddingsTrainer, Stopping \ No newline at end of file From 12de3ad4e487d6a5a5190f10ee667a3fa92d8c5f Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 16 Apr 2026 18:29:30 -0500 Subject: [PATCH 2/6] use more stable softplus --- torchcast/process/regression.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/torchcast/process/regression.py b/torchcast/process/regression.py index 7eabb2b..8c9c67d 100644 --- a/torchcast/process/regression.py +++ b/torchcast/process/regression.py @@ -107,7 +107,7 @@ def __init__(self, def _init_state_elements(self, predictors: Sequence[str], fixed: Sequence[str]) -> Sequence[StateElement]: - assert 'ceiling' not in predictors, f"`ceiling` is a reserved name for {type(self).__name__}" + assert '_ceiling' not in predictors, f"`_ceiling` is a reserved name for {type(self).__name__}" coefs = [ StateElement(name=p, measure_multi=None, has_process_variance=p not in fixed) for p in predictors @@ -137,7 +137,7 @@ def get_measured_mean(self, mean: torch.Tensor, time: int, cache: dict) -> torch coefs = mean[:, :self.num_predictors] ceiling = mean[:, self.num_predictors] cache['yhat'] = (X * coefs).sum(-1) - return cache['yhat'] - torch.log1p(torch.exp(cache['yhat'] - ceiling)) + return cache['yhat'] - torch.nn.functional.softplus(cache['yhat'] - ceiling) def get_measurement_jacobian(self, mean: torch.Tensor, time: int, cache: dict) -> torch.Tensor: # TODO: reparameterize From 3a9312c125a8c5146e5eb7e7d7120f923d5cd8ab Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 16 Apr 2026 18:29:49 -0500 Subject: [PATCH 3/6] warn if model measures arent in dataset --- torchcast/state_space/predictions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/torchcast/state_space/predictions.py b/torchcast/state_space/predictions.py index a2cc729..cf2ca17 100644 --- a/torchcast/state_space/predictions.py +++ b/torchcast/state_space/predictions.py @@ -368,6 +368,12 @@ def _to_dataframe(self, if m not in by_measure: continue actuals[m] = tens[..., mgroup.index(m)] + missing = set(by_measure) - set(dataset.all_measures) + if missing: + warn( + f"The following measures in your model are not present in your dataset, please double-check that " + f"the names you passed to the dataset match the `measures` you passed to the model:\n{missing}" + ) out = [] times = TimeSeriesDataset.get_dataset_times( dataset.start_offsets, num_timesteps=self.state_means.shape[1], dt_unit=dataset.dt_unit From 76762e05da6b62f311dd64104d634c5a5689595b Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 16 Apr 2026 18:30:00 -0500 Subject: [PATCH 4/6] fix docstring --- torchcast/state_space/state_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchcast/state_space/state_space.py b/torchcast/state_space/state_space.py index 0c03390..df44e66 100644 --- a/torchcast/state_space/state_space.py +++ b/torchcast/state_space/state_space.py @@ -381,7 +381,7 @@ def fit(self, :param optimizer: The optimizer to use. Can also pass a function which takes the parameters and returns an optimizer instance. Default is :class:`torch.optim.LBFGS` with ``(line_search_fn='strong_wolfe', max_iter=1)``. :param stopping: Controls stopping/convergence rules; should be a :class:`torchcast.utils.Stopping` instance, or - a dict of keyword-args to one. Example: ``stopping={'abstol' : .001, 'monitor' : 'params'}`` + a dict of keyword-args to one. Example: ``stopping={'abstol' : .001, 'monitor_params' : True}`` :param verbose: If True (default) will print the loss and epoch. :param callbacks: A list of functions that will be called at the end of each epoch, which take the current epoch's loss value. From b6c7c619737f84d05f50625574b9597a64c8ea10 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 17 Apr 2026 09:41:54 -0500 Subject: [PATCH 5/6] update default optimizer to handle pytorch 2.10 -> 2.11 change --- torchcast/state_space/state_space.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/torchcast/state_space/state_space.py b/torchcast/state_space/state_space.py index df44e66..99cd709 100644 --- a/torchcast/state_space/state_space.py +++ b/torchcast/state_space/state_space.py @@ -379,7 +379,8 @@ def fit(self, :param y: A tensor containing the batch of time-series(es), see :func:`StateSpaceModel.forward()`. :param optimizer: The optimizer to use. Can also pass a function which takes the parameters and returns an - optimizer instance. Default is :class:`torch.optim.LBFGS` with ``(line_search_fn='strong_wolfe', max_iter=1)``. + optimizer instance. Default is :class:`torch.optim.LBFGS` with + ``(line_search_fn='strong_wolfe', max_iter=1, max_eval=25)``. :param stopping: Controls stopping/convergence rules; should be a :class:`torchcast.utils.Stopping` instance, or a dict of keyword-args to one. Example: ``stopping={'abstol' : .001, 'monitor_params' : True}`` :param verbose: If True (default) will print the loss and epoch. @@ -402,9 +403,12 @@ def fit(self, optimizer = optimizer([p for p in self.parameters() if p.requires_grad]) elif optimizer is None: optimizer = torch.optim.LBFGS( + # only pass params that require grad: [p for p in self.parameters() if p.requires_grad], - # https://discuss.pytorch.org/t/unclear-purpose-of-max-iter-kwarg-in-the-lbfgs-optimizer/65695/4 + # see https://discuss.pytorch.org/t/unclear-purpose-of-max-iter-kwarg-in-the-lbfgs-optimizer/65695/4 max_iter=1, + # see https://github.com/pytorch/pytorch/pull/161488 + max_eval=25, line_search_fn='strong_wolfe' ) From ccd99a5a0bb01c399e8fe946dfa86a15fe9969c5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 17 Apr 2026 10:23:55 -0500 Subject: [PATCH 6/6] Bump to v1.1.1 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 ++++++ torchcast/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55dc6f4..4c05213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## v1.1.1 (2026-04-17) + +### Bug fix: LBFGS default optimizer regression with PyTorch >= 2.10 + +The default LBFGS optimizer now explicitly sets `max_eval=25`. This restores correct training behavior after [pytorch/pytorch#161488](https://github.com/pytorch/pytorch/pull/161488) (shipped in PyTorch 2.10) fixed a bug where `max_eval` was silently ignored by the strong Wolfe line search. Prior to that fix, `max_eval` defaulted to `2` (from `max_iter * 1.25 + 1` with `max_iter=1`), which was effectively ignored — the line search ran freely. After the fix, the cap was correctly enforced, causing the optimizer to converge after only a handful of epochs with a poor loss. + ## v1.1.0 (2026-04-06) ### Refactor of `Process` API and internals diff --git a/torchcast/__init__.py b/torchcast/__init__.py index 1a72d32..b3ddbc4 100644 --- a/torchcast/__init__.py +++ b/torchcast/__init__.py @@ -1 +1 @@ -__version__ = '1.1.0' +__version__ = '1.1.1'