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
75 changes: 74 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,36 @@ on:
push:
branches:
- main
release:
types:
- published

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# Tests and docs can each be skipped on a PR, either with a label or with a
# flag in the head commit subject. Both are PR-only by design: pushes to main
# and published releases always run everything.
check_skip_flags:
name: Check skip flags
runs-on: ubuntu-latest
outputs:
head-commit-message:
${{ steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE }}
steps:
- uses: actions/checkout@v7
with:
# On a pull_request event github.sha is the *merge* commit, whose
# subject is "Merge <sha> into <sha>" - we want the branch head.
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Get head commit message
id: get_head_commit_message
run: echo "HEAD_COMMIT_MESSAGE=$(git show -s --format=%s)" >> "$GITHUB_OUTPUT"

# Always runs: it is the formatting gate, and it is cheap.
pre-commit:
name: Format
runs-on: ubuntu-latest
Expand All @@ -36,7 +60,12 @@ jobs:
checks:
name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
runs-on: ${{ matrix.runs-on }}
needs: [pre-commit]
needs: [pre-commit, check_skip_flags]
# Skip with the "skip tests" label or a "[skip tests]" commit subject.
if: >-
github.event_name != 'pull_request' ||
(!contains(github.event.pull_request.labels.*.name, 'skip tests') &&
!contains(needs.check_skip_flags.outputs.head-commit-message, '[skip tests]'))
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -65,3 +94,47 @@ jobs:
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}

# Notebooks are *not* executed here (nb_execution_mode = "off" in
# docs/conf.py), so this only checks that the docs build. Executing the
# tutorials is the separate, label-gated Tutorials workflow.
docs:
name: Docs
runs-on: ubuntu-latest
# Deliberately does not need pre-commit, so docs feedback arrives in
# parallel. Skip with the "skip docs" label or a "[skip docs]" commit
# subject.
needs: [check_skip_flags]
if: >-
github.event_name != 'pull_request' ||
(!contains(github.event.pull_request.labels.*.name, 'skip docs') &&
!contains(needs.check_skip_flags.outputs.head-commit-message, '[skip docs]'))
steps:
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python
run: uv python install

- name: Install dependencies
run: uv sync --no-default-groups --group docs

# docs/conf.py urlretrieves this at import time if it is missing, so
# fetching it here just means the runner cache serves it instead.
- name: Download APOGEE data
uses: ethanjli/cached-download-action@v0.1.4
with:
url: https://users.flatironinstitute.org/~apricewhelan/pollux/rgb-highSNR-1k-1chip.h5
destination: docs/_data/rgb-highSNR-1k-1chip.h5

# -W makes warnings errors, so a broken cross-reference or malformed
# docstring fails the build; --keep-going reports every one of them
# instead of stopping at the first.
- name: Build docs
run: >-
uv run --no-sync sphinx-build -W --keep-going -b html docs docs/_build/html
40 changes: 19 additions & 21 deletions .github/workflows/tutorials.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
name: Tutorials

on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
# "labeled" is required: without it, adding the `tutorials` label to an
# already-open PR would never trigger a run. Listing types overrides the
# default set, so the other three have to be spelled out too.
types:
- opened
- synchronize
- reopened
- labeled
release:
types:
- published

jobs:
check_skip_flags:
name: Check skip flags
runs-on: ubuntu-latest
outputs:
head-commit-message:
${{ steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
with:
ref: ${{ env.GITHUB_SHA }}
- name: Get head commit message
id: get_head_commit_message
run: echo "HEAD_COMMIT_MESSAGE=$(git show -s --format=%s)" >> "$GITHUB_OUTPUT"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# Executing the notebooks is the most expensive job in the repo, so on a PR it
# is opt-in via the "tutorials" label. Pushes to main and published releases
# always run it.
notebooks:
name: "Build the notebooks for the docs"
runs-on: ubuntu-latest
needs: check_skip_flags
if:
${{ github.event.pull_request.draft == false &&
!contains(github.event.pull_request.labels.*.name, 'skip tutorials') &&
!contains(needs.check_skip_flags.outputs.head-commit-message, '[skip tutorials]')
}}
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'tutorials')
steps:
- uses: actions/checkout@v7

Expand Down
19 changes: 19 additions & 0 deletions DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ The APOGEE tutorials need `docs/_data/rgb-highSNR-1k-1chip.h5`, which the `tutor
workflow downloads from <https://users.flatironinstitute.org/~apricewhelan/pollux/>. On
Read the Docs the notebooks come from that workflow's artifact, not from a local run.

## CI

`CI` runs pre-commit, the test matrix, and a Sphinx build; `Tutorials` executes the
notebooks. On a pull request each can be turned on or off:

| Job | PR default | How to change it |
| --------------------- | ----------- | ----------------------------------------------------------- |
| Tests | runs | `skip tests` label, or `[skip tests]` in the commit subject |
| Docs (Sphinx build) | runs | `skip docs` label, or `[skip docs]` in the commit subject |
| Tutorials (notebooks) | **skipped** | add the `tutorials` label to opt in |

The commit-subject flags are read from the branch head, not the PR merge commit. All of
this is PR-only: pushes to `main` and published releases always run everything, so
`[skip tests]` on a commit that lands on `main` has no effect.

Notebook execution is opt-in because it is the slowest job in the repo — and because
Read the Docs pulls the executed notebooks from that workflow's artifact, as described
above. The `Docs` job never executes notebooks, so it only checks that the docs build.

## Release

The version comes from the git tag via `hatch-vcs`, so there is no version number to
Expand Down
9 changes: 5 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"sphinxext.opengraph",
"sphinx_togglebutton",
# "sphinx_tippy",
"rtds_action",
]

python_use_unqualified_type_names = True
Expand Down Expand Up @@ -168,9 +167,14 @@

# -- rtds-action --

# Only load rtds_action when a token is available, i.e. on Read the Docs. Without
# one it queries the GitHub API for an empty repository name and warns "Can't list
# files (404)", which would fail the docs build under -W.
if "GITHUB_TOKEN" in os.environ:
print("GitHub Token found: retrieving artifact")

extensions.append("rtds_action")

# The name of your GitHub repository
rtds_action_github_repo = "adrn/pollux"

Expand All @@ -190,9 +194,6 @@

else:
print("No GitHub Token found: skipping artifact retrieval")
rtds_action_github_repo = ""
rtds_action_github_token = ""
rtds_action_path = ""


# -- Check for executed tutorials and only add to toctree if they exist ------
Expand Down
3 changes: 1 addition & 2 deletions docs/tutorials/LVM-math-notes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,7 @@
" including the iterative optimizer and the preprocessing step we skipped here.\n",
"- [Linearized, closed-form solves](../linear-solves.md) covers how the fit exploits the\n",
" bilinear structure discussed above.\n",
"- [Error models](LVM-error-models.ipynb) is about fitting an unknown intrinsic scatter $s$ for a data output.\n",
"- [Missing labels](LVM-hierarchical-missing-labels.ipynb) uses a trick to handle missing data."
"- [Error models](LVM-error-models.ipynb) is about fitting an unknown intrinsic scatter $s$ for a data output."
]
}
],
Expand Down
6 changes: 3 additions & 3 deletions src/pollux/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class OutputData(eqx.Module):
stored in a 2D array with shape (128, 2048). You also have the errors on the fluxes,
which are stored in a 2D array with the same shape. You can create an instance of
``OutputData`` to store this data. In the example below, we will generate some
random data to represent this case (for the sake of illustration)::
random data to represent this case (for the sake of illustration):

>>> import jax.numpy as jnp
>>> import jax.random as jrnd
Expand All @@ -56,15 +56,15 @@ class OutputData(eqx.Module):

We did not specify a preprocessor, so the data are not preprocessed even if we call
``.preprocess()``. In this case, the processed data should equal the unprocessed
data::
data:

>>> tmp = flux_data.preprocess()
>>> assert tmp.processed
>>> assert jnp.all(tmp.data == flux_data.data)

We can instead specify a data preprocessor to rescale and center the input data. For
this, we use the ``ShiftScalePreprocessor``, which centers the data on the specified
location and scales the data by default along ``axis=0``::
location and scales the data by default along ``axis=0``:

>>> from pollux.data import ShiftScalePreprocessor
>>> flux_data = OutputData(
Expand Down
6 changes: 3 additions & 3 deletions src/pollux/data/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class ShiftScalePreprocessor(AbstractPreprocessor):
--------
The default way of computing the preprocessing parameters uses the ``from_data()``
class method, which computes the mean and standard deviation of the data along
axis=0::
axis=0:

>>> import jax.numpy as jnp
>>> data = jnp.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
Expand All @@ -116,7 +116,7 @@ class method, which computes the mean and standard deviation of the data along
>>> assert jnp.allclose(jnp.std(processed_data, axis=0), 1.0, atol=1e-5)

To instead use the mean and standard deviation computed over all axes at the same
time, set the axis to None::
time, set the axis to None:

>>> preprocessor = ShiftScalePreprocessor.from_data(data, axis=None)
>>> processed_data = preprocessor.transform(data)
Expand All @@ -126,7 +126,7 @@ class method, which computes the mean and standard deviation of the data along
``from_data_percentiles()`` class method, which computes the median and the
difference between the specified percentiles as the scale. Here we will specify
using (1/2 times) the difference of the 84th and 16th percentile values as the
scale::
scale:

>>> preprocessor = ShiftScalePreprocessor.from_data_percentiles(
... data, scale_percentiles=(5.0, 95.0)
Expand Down
11 changes: 4 additions & 7 deletions src/pollux/models/cannon.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,6 @@ class Cannon(LVM):
:func:`~pollux.models.optimize_iterative` turns it into the ridge term of the
closed-form solve. Defaults to the standard unit Gaussian.

Attributes
----------
n_features
Number of polynomial features, computed from ``label_size``, ``poly_degree``
and ``include_bias``.

Examples
--------
A Cannon for 3 labels and 100 spectral pixels:
Expand Down Expand Up @@ -222,7 +216,10 @@ def scatter_for(name: str, size: int) -> ScatterTransform | None:

@property
def n_features(self) -> int:
"""Number of polynomial features."""
"""Number of polynomial features.

Computed from ``label_size``, ``poly_degree`` and ``include_bias``.
"""
return _compute_n_poly_features(
self.latent_size, self.poly_degree, self.include_bias
)
Expand Down
28 changes: 16 additions & 12 deletions src/pollux/models/lvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,18 +1082,21 @@ def unpack_numpyro_pars(
dict
A nested dictionary with keys as output names. Each output name is a key with
a dict value containing "data" and "err" keys:

- For single transforms, "data" values are parameter dictionaries
- For TransformSequence, "data" values are tuples of parameter dictionaries
- "err" values follow the same structure as "data" for the error transforms
- "err" will be an empty dict {} if there are no error parameters
- Non-output parameters (like "latents") are passed through at the top level

Example structure:
{
"flux": {"data": {...} or (...), "err": {}}, # err empty if no error pars
"label": {"data": {...}, "err": {...}},
"latents": array
}
Example structure::

{
# "err" is empty if the output has no error parameters
"flux": {"data": {...} or (...), "err": {}},
"label": {"data": {...}, "err": {...}},
"latents": array
}
"""
unpacked_pars: dict[str, Any] = {}

Expand Down Expand Up @@ -1143,12 +1146,13 @@ def pack_numpyro_pars(
parameter dictionaries. Non-output parameters (like "latents") can exist at
the top level.

Example structure:
{
"flux": {"data": {...} or (...)}, # err key optional
"label": {"data": {...}, "err": {...}}, # err key included
"latents": array
}
Example structure::

{
"flux": {"data": {...} or (...)}, # err key optional
"label": {"data": {...}, "err": {...}}, # err key included
"latents": array
}

Returns
-------
Expand Down
1 change: 1 addition & 0 deletions src/pollux/models/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ def apply(
"""Apply the sequence of transforms to input latent vectors.

Parameters can be provided in two ways:

1. As positional arguments: One dictionary per transform in sequence order
2. As keyword arguments: Using "{transform_index}:{param}" naming scheme, so a
parameter named "A" in transform 0 of the sequence would be "0:A".
Expand Down
Loading