diff --git a/design/SP-3225/linktablestats.rst b/design/SP-3225/linktablestats.rst index be81cb71..abe7c607 100644 --- a/design/SP-3225/linktablestats.rst +++ b/design/SP-3225/linktablestats.rst @@ -59,6 +59,20 @@ New Module-Level Constants ``science``, ``fwhm``, ``mean_norm_teff``, ``visit_rate``, ``teff_rate``, ``targets``. +``RSS_PRENIGHT_DESC_FORMAT`` + A format string template used to populate RSS item descriptions for + ``lsstcam`` ``prenight`` entries. Uses distinct labels from + ``RSS_DESC_FORMAT`` to make it clear the data comes from a simulation + rather than actual observations: + + - "Simulated (science) visits" instead of "Total visits" (all prenight + visits are science, so no separate "Science visits" line is needed). + - Metric labels omit the "(science visits)" qualifier (redundant). + - "Mean visit rate" omits "(all visits on sky)" (redundant). + + Contains placeholders for: ``total``, ``fwhm``, ``visit_rate``, + ``mean_norm_teff``, ``teff_rate``, ``targets``. + ``EFF_TIME_BREAKDOWN_COLS`` A tuple of the three ``compute_tinysum`` output column names that carry the effective-time breakdown factors: @@ -308,10 +322,33 @@ Implementation Steps 5. **Indent** the XML tree and optionally write to file. -Helper: ``_format_summary_desc`` – effective-time breakdown -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Helper: ``_format_summary_desc`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + def _format_summary_desc( + tinysum: pd.DataFrame, + dayobs: int, + report: str, + instrument: str, + night: datetime.date, + desc_format: str = None, + ) -> str: + +``desc_format`` : ``str`` or ``None`` + The format string to use for the description. ``None`` defaults to + ``RSS_DESC_FORMAT``. Pass ``RSS_PRENIGHT_DESC_FORMAT`` for prenight + items. + +The function passes all computed keyword arguments (``report``, +``instrument``, ``night``, ``total``, ``science``, ``fwhm``, +``visit_rate``, ``mean_norm_teff``, ``teff_rate``, ``targets``) to +``desc_format.format()``. Format strings that use only a subset of these +keywords are valid — unused keywords are silently ignored by +``str.format()``. -Within ``_format_summary_desc``, the ``{mean_norm_teff}`` placeholder is +**Effective-time breakdown**: The ``{mean_norm_teff}`` placeholder is populated conditionally: - If all three columns in ``EFF_TIME_BREAKDOWN_COLS`` are present in the @@ -430,7 +467,7 @@ visits use ``t_eff`` and ``visitExposureTime`` and are passed unmodified. ... prenight_visits=prenight_visits, ... ) >>> desc = tree.getroot().find("channel/item/description").text ->>> "Total visits:" in desc +>>> "Simulated (science) visits:" in desc True @@ -466,8 +503,8 @@ Changes from ``main`` description when no simulation visits match the night. 5. **Removed** ``"preprogress"`` from the default ``report_columns`` tuple in ``make_report_link_table``. -6. **New constants**: ``RSS_DESC_FORMAT``, ``INT_SUMMARY_COLUMNS``, - ``FLOAT_SUMMARY_COLUMNS``, ``SUMMARY_COLUMNS``, +6. **New constants**: ``RSS_DESC_FORMAT``, ``RSS_PRENIGHT_DESC_FORMAT``, + ``INT_SUMMARY_COLUMNS``, ``FLOAT_SUMMARY_COLUMNS``, ``SUMMARY_COLUMNS``, ``EFF_TIME_BREAKDOWN_COLS``. 7. **New imports**: ``hashlib``, ``numpy``, ``rubin_scheduler.site_models.Almanac``, @@ -489,6 +526,11 @@ Changes from ``main`` line includes a bracketed breakdown of PSF, transparency, and sky background factors. When any column is absent, the line retains its original format. +12. **Distinct prenight RSS format**: ``_format_summary_desc`` now accepts a + ``desc_format`` parameter (default ``None`` → ``RSS_DESC_FORMAT``). The + prenight branch passes ``RSS_PRENIGHT_DESC_FORMAT``, which uses + "Simulated (science) visits" instead of "Total visits" and omits the + separate "Science visits" line and redundant qualifier text. Dependencies @@ -522,7 +564,8 @@ Tests are in ``tests/test_reports.py``. Passes a synthetic ``prenight_visits`` DataFrame (using the simulator ``t_eff``/``visitExposureTime`` columns) whose dayObs match the report fixtures, and verifies the ``lsstcam`` ``prenight`` item descriptions carry - a populated ``Total visits: N (...)`` summary with a band breakdown. + a populated ``Simulated (science) visits: N (...)`` summary with a band + breakdown (using the distinct ``RSS_PRENIGHT_DESC_FORMAT``). ``test_make_report_rss_feed_prenight_blank_when_no_visits`` Passes ``prenight_visits`` whose dayObs match no report night, and verifies diff --git a/schedview/reports.py b/schedview/reports.py index fd4ac831..bd1dd967 100644 --- a/schedview/reports.py +++ b/schedview/reports.py @@ -23,6 +23,15 @@ Science targets: {targets} """ +RSS_PRENIGHT_DESC_FORMAT = """ +Simulated (science) visits: {total}; +Median FWHM: {fwhm}; +Mean visit rate: {visit_rate} visits/hour; +Total eff_time / total exp_time: {mean_norm_teff}; +Total eff_time / total night time: {teff_rate}; +Science targets: {targets} +""" + def find_reports( report_dir: str = "/sdf/data/rubin/shared/scheduler/reports", @@ -183,7 +192,14 @@ def make_report_link_table( return report_table_html -def _format_summary_desc(tinysum: pd.DataFrame, dayobs: int, report: str, instrument: str, night) -> str: +def _format_summary_desc( + tinysum: pd.DataFrame, + dayobs: int, + report: str, + instrument: str, + night: datetime.date, + desc_format: str = None, +) -> str: """Format an RSS item description from a ``compute_tinysum`` row. Parameters @@ -199,11 +215,14 @@ def _format_summary_desc(tinysum: pd.DataFrame, dayobs: int, report: str, instru The instrument name. night : `datetime.date` The local calendar date of the night start. + desc_format: `str` or `None` + Format for the description field. None defaults + to ``RSS_DESC_FORMAT``. Returns ------- desc : `str` - The formatted ``RSS_DESC_FORMAT`` text for the night. + The formatted text for the night. """ try: teff_rate = np.round(tinysum.loc[dayobs, "teff/night duration"], 2) @@ -225,7 +244,10 @@ def _format_summary_desc(tinysum: pd.DataFrame, dayobs: int, report: str, instru else: mean_norm_teff_str = f"{norm_teff}" - return RSS_DESC_FORMAT.format( + if desc_format is None: + desc_format = RSS_DESC_FORMAT + + return desc_format.format( report=report, instrument=instrument, night=night, @@ -359,7 +381,12 @@ def make_report_rss_feed( # visits gets a completely blank description (no fallback text). if dayobs in prenight_tinysum.index: desc.text = _format_summary_desc( - prenight_tinysum, dayobs, report_row.report, instrument, report_row.night + prenight_tinysum, + dayobs, + report_row.report, + instrument, + report_row.night, + desc_format=RSS_PRENIGHT_DESC_FORMAT, ) else: desc.text = "" diff --git a/tests/test_design_docs.py b/tests/test_design_docs.py index 37118325..b0c4b496 100644 --- a/tests/test_design_docs.py +++ b/tests/test_design_docs.py @@ -1,5 +1,6 @@ """Tests for RST design documents under design/.""" +import os import subprocess import sys from pathlib import Path @@ -9,6 +10,12 @@ DESIGN_DIR = Path(__file__).parent.parent / "design" RST_FILES = sorted(DESIGN_DIR.rglob("*.rst")) +# rst2html calls locale.setlocale(locale.LC_ALL, '') on startup, which fails +# if the environment's locale (e.g. en_US.UTF-8) isn't installed on the host. +# Force a locale that is always available so the test doesn't depend on the +# host's locale configuration. +_RST_ENV = {**os.environ, "LC_ALL": "C.UTF-8", "LANG": "C.UTF-8"} + def _rst_id(rst_path): """Return a short test ID for a design-doc path.""" @@ -23,6 +30,7 @@ def test_rst_valid(rst_file): ["rst2html", "--halt=warning", str(rst_file), "/dev/null"], capture_output=True, text=True, + env=_RST_ENV, ) assert result.returncode == 0, f"RST validation failed for {rst_file.name}:\n{result.stderr}" diff --git a/tests/test_reports.py b/tests/test_reports.py index 8f790025..895375a7 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -151,11 +151,10 @@ def test_make_report_rss_feed_prenight_description(self): if category == "lsstcam_prenight": prenight_descs.append(item.findtext("description") or "") joined = "\n".join(prenight_descs) - # The prenight items must carry a populated summary description. - assert re.search(r"Total visits: \d+ \(\d+[ugrizy]", joined) - # All prenight visits count as science, so the science line is - # populated with its own band breakdown (not zero). - assert re.search(r"Science visits: \d+ \(\d+[ugrizy]", joined) + # The prenight items must carry a populated summary description + # using the distinct prenight format (no separate "Science visits" + # line since all prenight visits are science). + assert re.search(r"Simulated \(science\) visits: \d+ \(\d+[ugrizy]", joined) def test_make_report_rss_feed_prenight_blank_when_no_visits(self): rng = np.random.default_rng(0)