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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.strong.io/torchcast/quick_start.html>`_ for a simple example that will get you up to speed, or delve into the `examples <https://docs.strong.io/torchcast/examples/examples.html>`_ or the `API <https://docs.strong.io/torchcast/api/api.html>`_.
2 changes: 1 addition & 1 deletion docs/api/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
:members: TimeSeriesDataset, TimeSeriesDataLoader, add_season_features, complete_times, make_baseline, SimpleTrainer, StateSpaceTrainer, ModelMatEmbeddingsTrainer, Stopping
2 changes: 1 addition & 1 deletion torchcast/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.0'
__version__ = '1.1.1'
4 changes: 2 additions & 2 deletions torchcast/process/regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions torchcast/state_space/predictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions torchcast/state_space/state_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,10 @@ 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'}``
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.
Expand All @@ -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'
)

Expand Down
Loading