From 1a6a9e496bda8fa57c3a4da065d088bdad77d225 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Mon, 9 Feb 2026 15:57:11 -0500 Subject: [PATCH 1/6] first iteration of landing page --- docs/source/_static/css/styles.css | 194 +++++++++++++++++++ docs/source/_static/javascript/javascript.js | 44 +++++ docs/source/_templates/index.html | 108 +++++++++++ docs/source/conf.py | 11 +- docs/source/index.rst | 51 ----- 5 files changed, 356 insertions(+), 52 deletions(-) create mode 100644 docs/source/_static/css/styles.css create mode 100644 docs/source/_static/javascript/javascript.js create mode 100644 docs/source/_templates/index.html diff --git a/docs/source/_static/css/styles.css b/docs/source/_static/css/styles.css new file mode 100644 index 00000000..c79eb665 --- /dev/null +++ b/docs/source/_static/css/styles.css @@ -0,0 +1,194 @@ +/* ========================================================================== + BLOP CUSTOM THEME STYLES (PyData Theme Compatible) + ========================================================================== */ + +/* RESET & BASE */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--pst-font-family-sans-serif); + background-color: var(--pst-color-background); + color: var(--pst-color-text-base); + line-height: 1.6; +} + +/* NAVBAR (Adapting to Theme) */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.25rem 2rem; +} + +.logo { + font-size: 1.5rem; + font-weight: 600; + color: var(--pst-color-text-base); +} + +.navbar nav a { + margin-left: 1.5rem; + text-decoration: none; + color: var(--pst-color-text-base); + transition: color 0.2s; +} + +.navbar nav a:hover { + color: var(--pst-color-primary); +} + +/* HERO SECTION */ +.hero { + text-align: center; + padding: 4rem 2rem; +} + +.hero h1 { + font-size: clamp(2.5rem, 5vw, 4rem); + color: var(--pst-color-text-base); +} + +.subtitle { + margin-top: 0.5rem; + font-size: 1.1rem; + color: var(--pst-color-text-muted); +} + +/* CONTENT LAYOUT */ +.content { + padding: 4rem 2rem; + max-width: 1100px; + margin: auto; +} + +.content h2 { + font-size: 2rem; + margin-bottom: 1.5rem; + color: var(--pst-color-text-base); +} + +.two-column { + display: grid; + gap: 2rem; +} + +@media (min-width: 768px) { + .two-column { + grid-template-columns: 1fr 1fr; + align-items: center; + } +} + +/* IMAGE/VISUAL PLACEHOLDERS */ +.image-placeholder { + background: var(--pst-color-surface); + border: 1px solid var(--pst-color-border); + border-radius: 12px; + height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + +/* INSTALLATION TABS */ +.tabs-container { + margin: 20px 0; +} + +.tabs { + display: flex; + list-style: none; + padding: 0; + margin: 0; + border-bottom: 2px solid var(--pst-color-border); +} + +.tab-item { + padding: 12px 24px; + cursor: pointer; + color: var(--pst-color-text-muted); + font-weight: 600; + transition: all 0.2s; + margin-bottom: -2px; +} + +.tab-item.active { + color: var(--pst-color-primary); + border-bottom: 3px solid var(--pst-color-primary); +} + +/* CODE BLOCKS (Unified Styling) */ +pre, .command-block pre { + background-color: var(--pst-color-inline-code-background) !important; + color: var(--pst-color-text-base) !important; + border: 1px solid var(--pst-color-border) !important; + padding: 16px !important; + border-radius: 8px !important; + font-family: var(--pst-font-family-monospace); + font-size: 0.9rem; + overflow-x: auto; + margin: 1rem 0; +} + +code { + font-family: var(--pst-font-family-monospace); + color: var(--pst-color-text-base); +} + +/* LEARN MORE GRID */ +.card-grid { + display: grid; + gap: 1.5rem; + margin-top: 2rem; +} + +@media (min-width: 600px) { .card-grid { grid-template-columns: repeat(2, 1fr); } } +@media (min-width: 900px) { .card-grid { grid-template-columns: repeat(4, 1fr); } } + +.card, .custom-card { + background-color: var(--pst-color-surface); + border: 1px solid var(--pst-color-border); + padding: 2rem; + border-radius: 16px; + font-weight: 600; + text-align: center; + text-decoration: none; + color: var(--pst-color-text-base); + transition: transform 0.2s, border-color 0.2s; + display: block; +} + +.card:hover, .custom-card:hover { + transform: translateY(-3px); + border-color: var(--pst-color-primary); + color: var(--pst-color-primary); +} + +/* REFERENCES / CITATION BOX */ +.citation-text { + background-color: var(--pst-color-surface); + border-left: 5px solid var(--pst-color-primary); + padding: 20px; + border-radius: 0 8px 8px 0; + color: var(--pst-color-text-base); + line-height: 1.6; + margin: 1.5rem 0; +} + +/* FOOTER */ +footer { + text-align: center; + padding: 2rem; + font-size: 0.9rem; + color: var(--pst-color-text-muted); +} + +/* SPHINX LAYOUT OVERRIDES (Keep these for full-width landing) */ +body.pagename-index .bd-sidebar { display: none !important; } +body.pagename-index .bd-toc { display: none !important; } +body.pagename-index .bd-main { grid-template-columns: 1fr !important; } +body.pagename-index .bd-content { max-width: 100% !important; } diff --git a/docs/source/_static/javascript/javascript.js b/docs/source/_static/javascript/javascript.js new file mode 100644 index 00000000..783bfb00 --- /dev/null +++ b/docs/source/_static/javascript/javascript.js @@ -0,0 +1,44 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll('.tab-item').forEach(tab => { + tab.addEventListener('click', () => { + const container = tab.closest('.tabs-container'); + const targetTab = tab.getAttribute('data-tab'); + + // Remove active class from all tabs and panels + container.querySelectorAll('.tab-item') + .forEach(t => t.classList.remove('active')); + container.querySelectorAll('.tab-panel') + .forEach(p => p.classList.remove('active')); + + // Activate selected tab + tab.classList.add('active'); + document.getElementById(targetTab).classList.add('active'); + }); + }); +}); + +function copyCode(button) { + // Find the code text relative to the button + const container = button.parentElement; + const codeText = container.querySelector('code').innerText; + + // Use the Clipboard API + navigator.clipboard.writeText(codeText).then(() => { + // Visual feedback + const originalText = button.innerText; + button.innerText = 'Copied!'; + button.classList.add('copied'); + + // Reset button after 2 seconds + setTimeout(() => { + button.innerText = originalText; + button.classList.remove('copied'); + }, 2000); + }).catch(err => { + console.error('Failed to copy: ', err); + }); +} + +document.querySelectorAll('.copy-btn').forEach(button => { + button.addEventListener('click', () => copyCode(button)); +}); diff --git a/docs/source/_templates/index.html b/docs/source/_templates/index.html new file mode 100644 index 00000000..0ec9439e --- /dev/null +++ b/docs/source/_templates/index.html @@ -0,0 +1,108 @@ +{% extends "pydata_sphinx_theme/layout.html" %} + +{% block sidebar %}{% endblock %} + +{% block content %} +
+ +
+

Blop

+

+ a BeamLine Optimization Package +

+
+ +
+
+

What is Blop?

+

+ Blop is a Python library for performing optimization for beamline + experiments. It is designed to integrate nicely with the Bluesky + ecosystem and primarily targets rapid beamline data acquisition + and control. +

+

+ Our goal is to provide a simple and practical data-driven + optimization interface for beamline experiments. +

+
+ +
+
+ [Visualization Placeholder] +
+
+
+ +
+ +
+

Learn More!

+
+ + {# We define the list outside the loop for cleaner syntax #} + {% set nav_items = [ + ("Tutorials", "tutorials.html"), + ("How-to", "howto.html"), + ("References", "references.html"), + ("Release History", "release_history.html") + ] %} + + {% for title, link in nav_items %} + + {{ title }} + + {% endfor %} +
+
+ +
+ +
+

References

+

If you use this package in your work, please cite the following paper:

+ +
+ Morris, T. W., Rakitin, M., Du, Y., Fedurin, M., Giles, A. C., Leshchev, D., Li, W. H., Romasky, B., Stavitski, E., Walter, A. L., Moeller, P., Nash, B., & Islegen-Wojdyla, A. (2024). A general Bayesian algorithm for the autonomous alignment of beamlines. Journal of Synchrotron Radiation, 31(6), 1446–1456. + + https://doi.org/10.1107/S1600577524008993 + +
+ +

BibTeX:

+
+@Article{Morris2024,
+  author   = {Morris, Thomas W. and others},
+  journal  = {Journal of Synchrotron Radiation},
+  title    = {A general Bayesian algorithm for the autonomous alignment of beamlines},
+  year     = {2024},
+  doi      = {10.1107/S1600577524008993},
+  url      = {https://doi.org/10.1107/S1600577524008993},
+}
+    
+
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/docs/source/conf.py b/docs/source/conf.py index 1756eed1..7fd91048 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -144,7 +144,11 @@ # further. For a list of options available for each theme, see the # documentation. # -# html_theme_options = {} +html_theme_options = { + "navbar_start": ["navbar-logo"], + "navbar_center": ["navbar-nav"], + "navbar_end": [], +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -169,8 +173,13 @@ # Add custom CSS to fix .content height constraint for plotly plots html_css_files = [ "fix-content-height.css", + "css/styles.css", ] +html_additional_pages = { + "index": "index.html", +} + # -- Options for LaTeX output --------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index f00b53a4..e69de29b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,51 +0,0 @@ -What is Blop? -------------- - -Blop is a Python library for performing optimization for beamline experiments. It is designed to integrate nicely with the Bluesky ecosystem and primarily acts as a bridge between optimization routines and fine-grained data acquisition and control. Our goal is to provide a simple and practical data-driven optimization interface for beamline experiments. - - -Documentation structure ------------------------ - -- :doc:`installation` - Installation instructions -- :doc:`how-to-guides` - How-to guides for common tasks -- :doc:`explanation` - Explanation of the underlying concepts -- :doc:`tutorials` - Tutorials for learning -- :doc:`reference` - Reference documentation for the API -- :doc:`release-history` - Release history - -.. toctree:: - :maxdepth: 2 - :hidden: - - installation - how-to-guides - explanation - tutorials - reference - release-history - -Citation --------- - -If you use this package in your work, please cite the following paper: - - Morris, T. W., Rakitin, M., Du, Y., Fedurin, M., Giles, A. C., Leshchev, D., Li, W. H., Romasky, B., Stavitski, E., Walter, A. L., Moeller, P., Nash, B., & Islegen-Wojdyla, A. (2024). A general Bayesian algorithm for the autonomous alignment of beamlines. Journal of Synchrotron Radiation, 31(6), 1446–1456. https://doi.org/10.1107/S1600577524008993 - -BibTeX: - -.. code-block:: bibtex - - @Article{Morris2024, - author = {Morris, Thomas W. and Rakitin, Max and Du, Yonghua and Fedurin, Mikhail and Giles, Abigail C. and Leshchev, Denis and Li, William H. and Romasky, Brianna and Stavitski, Eli and Walter, Andrew L. and Moeller, Paul and Nash, Boaz and Islegen-Wojdyla, Antoine}, - journal = {Journal of Synchrotron Radiation}, - title = {{A general Bayesian algorithm for the autonomous alignment of beamlines}}, - year = {2024}, - month = {Nov}, - number = {6}, - pages = {1446--1456}, - volume = {31}, - doi = {10.1107/S1600577524008993}, - keywords = {Bayesian optimization, automated alignment, synchrotron radiation, digital twins, machine learning}, - url = {https://doi.org/10.1107/S1600577524008993}, - } From 9a40e125d4b331bcca4886fabbe2af264ace040e Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Mon, 9 Feb 2026 16:26:28 -0500 Subject: [PATCH 2/6] keep original index.rst for now --- docs/source/index.rst | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index e69de29b..f00b53a4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -0,0 +1,51 @@ +What is Blop? +------------- + +Blop is a Python library for performing optimization for beamline experiments. It is designed to integrate nicely with the Bluesky ecosystem and primarily acts as a bridge between optimization routines and fine-grained data acquisition and control. Our goal is to provide a simple and practical data-driven optimization interface for beamline experiments. + + +Documentation structure +----------------------- + +- :doc:`installation` - Installation instructions +- :doc:`how-to-guides` - How-to guides for common tasks +- :doc:`explanation` - Explanation of the underlying concepts +- :doc:`tutorials` - Tutorials for learning +- :doc:`reference` - Reference documentation for the API +- :doc:`release-history` - Release history + +.. toctree:: + :maxdepth: 2 + :hidden: + + installation + how-to-guides + explanation + tutorials + reference + release-history + +Citation +-------- + +If you use this package in your work, please cite the following paper: + + Morris, T. W., Rakitin, M., Du, Y., Fedurin, M., Giles, A. C., Leshchev, D., Li, W. H., Romasky, B., Stavitski, E., Walter, A. L., Moeller, P., Nash, B., & Islegen-Wojdyla, A. (2024). A general Bayesian algorithm for the autonomous alignment of beamlines. Journal of Synchrotron Radiation, 31(6), 1446–1456. https://doi.org/10.1107/S1600577524008993 + +BibTeX: + +.. code-block:: bibtex + + @Article{Morris2024, + author = {Morris, Thomas W. and Rakitin, Max and Du, Yonghua and Fedurin, Mikhail and Giles, Abigail C. and Leshchev, Denis and Li, William H. and Romasky, Brianna and Stavitski, Eli and Walter, Andrew L. and Moeller, Paul and Nash, Boaz and Islegen-Wojdyla, Antoine}, + journal = {Journal of Synchrotron Radiation}, + title = {{A general Bayesian algorithm for the autonomous alignment of beamlines}}, + year = {2024}, + month = {Nov}, + number = {6}, + pages = {1446--1456}, + volume = {31}, + doi = {10.1107/S1600577524008993}, + keywords = {Bayesian optimization, automated alignment, synchrotron radiation, digital twins, machine learning}, + url = {https://doi.org/10.1107/S1600577524008993}, + } From 2706a2ff17466137b93b4a3ee4b290f684df6bad Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Wed, 18 Feb 2026 11:25:06 -0500 Subject: [PATCH 3/6] fix index.html still need to add installation instructions --- docs/source/_static/javascript/javascript.js | 59 +++++ docs/source/_templates/index.html | 231 ++++++++++++++++--- 2 files changed, 259 insertions(+), 31 deletions(-) diff --git a/docs/source/_static/javascript/javascript.js b/docs/source/_static/javascript/javascript.js index 783bfb00..f48bb936 100644 --- a/docs/source/_static/javascript/javascript.js +++ b/docs/source/_static/javascript/javascript.js @@ -1,4 +1,5 @@ document.addEventListener("DOMContentLoaded", () => { + // Existing tab functionality document.querySelectorAll('.tab-item').forEach(tab => { tab.addEventListener('click', () => { const container = tab.closest('.tabs-container'); @@ -15,6 +16,64 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById(targetTab).classList.add('active'); }); }); + + // Header search functionality + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + const query = this.value.trim(); + if (query) { + // Navigate to search page with query + const searchUrl = new URL('search.html', window.location.origin + window.location.pathname); + searchUrl.searchParams.set('q', query); + window.location.href = searchUrl.toString(); + } + } + }); + } + + // Header theme toggle functionality + const themeButton = document.querySelector('.theme-switch-button'); + const themeIcon = document.getElementById('theme-icon'); + + if (themeButton && themeIcon) { + + // Function to update icon based on current theme + function updateThemeIcon() { + const currentTheme = document.documentElement.dataset.theme || 'auto'; + const isDark = currentTheme === 'dark' || + (currentTheme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); + + themeIcon.className = isDark ? 'fa fa-moon' : 'fa fa-sun'; + } + + // Initial icon update + updateThemeIcon(); + + // Theme toggle click handler + themeButton.addEventListener('click', function() { + const currentTheme = document.documentElement.dataset.theme || 'auto'; + let newTheme; + + if (currentTheme === 'auto' || currentTheme === 'light') { + newTheme = 'dark'; + } else { + newTheme = 'light'; + } + + // Update theme + document.documentElement.dataset.theme = newTheme; + localStorage.setItem('theme', newTheme); + + // Update icon + updateThemeIcon(); + }); + + // Listen for system theme changes when in auto mode + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeIcon); + } }); function copyCode(button) { diff --git a/docs/source/_templates/index.html b/docs/source/_templates/index.html index 0ec9439e..bd3047dd 100644 --- a/docs/source/_templates/index.html +++ b/docs/source/_templates/index.html @@ -3,7 +3,151 @@ {% block sidebar %}{% endblock %} {% block content %} -
+
+
+ + {# Left side: Logo and Navigation #} +
+ {# Logo placeholder - you can replace this with an actual logo #} +
+
+ B +
+ + Blop + +
+ + {# Navigation Links #} + +
+ + {# Right side: Search and Theme Toggle #} +
+ {# Search Bar #} + + + {# Theme Toggle Button #} + +
+
+
+ +

Blop

@@ -38,36 +182,51 @@

What is Blop?

Learn More!

-
+
- {# We define the list outside the loop for cleaner syntax #} + {# We define the list so additional items can be easily added #} {% set nav_items = [ - ("Tutorials", "tutorials.html"), - ("How-to", "howto.html"), - ("References", "references.html"), - ("Release History", "release_history.html") + ("Tutorials", "tutorials.html", "Step-by-step guides to get started with Blop fundamentals and basic workflows"), + ("How-to", "howto.html", "Practical recipes and solutions for specific beamline optimization tasks"), + ("References", "references.html", "Complete API documentation, class references, and technical specifications"), + ("Release History", "release-history.html", "Version updates, new features, bug fixes, and changelog for v" + release) ] %} - {% for title, link in nav_items %} - - {{ title }} - + {% for title, link, description in nav_items %} +
+ + {{ title }} + +
+ {{ description }} +
+
{% endfor %}

-
+

References

If you use this package in your work, please cite the following paper:

@@ -88,15 +247,25 @@

References

BibTeX:

-
-@Article{Morris2024,
-  author   = {Morris, Thomas W. and others},
-  journal  = {Journal of Synchrotron Radiation},
-  title    = {A general Bayesian algorithm for the autonomous alignment of beamlines},
-  year     = {2024},
-  doi      = {10.1107/S1600577524008993},
-  url      = {https://doi.org/10.1107/S1600577524008993},
-}
+    
+      @Article{Morris2024,
+          author   = {Morris, Thomas W. and Rakitin, Max and Du, Yonghua and Fedurin, Mikhail and Giles, Abigail C. and Leshchev, Denis and Li, William H. and Romasky, Brianna and Stavitski, Eli and Walter, Andrew L. and Moeller, Paul and Nash, Boaz and Islegen-Wojdyla, Antoine},
+          journal  = {Journal of Synchrotron Radiation},
+          title    = {A general Bayesian algorithm for the autonomous alignment of beamlines},
+          year     = {2024},
+          month    = {Nov},
+          number   = {6},
+          pages    = {1446--1456},
+          volume   = {31},
+          doi      = {10.1107/S1600577524008993},
+          keywords = {Bayesian optimization, automated alignment, synchrotron radiation, digital twins, machine learning},
+          url      = {https://doi.org/10.1107/S1600577524008993},
+        }
     
@@ -104,5 +273,5 @@

References

{% block scripts %} {{ super() }} - + {% endblock %} From 0548dfc556713a2985bc1f4dd56bbd39dd1d75b0 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Mon, 23 Feb 2026 09:56:03 -0500 Subject: [PATCH 4/6] added warnings --- docs/source/_static/css/styles.css | 161 ++++++++++++++++++- docs/source/_static/javascript/javascript.js | 47 ++++++ docs/source/_templates/index.html | 44 ++++- 3 files changed, 247 insertions(+), 5 deletions(-) diff --git a/docs/source/_static/css/styles.css b/docs/source/_static/css/styles.css index c79eb665..689696b6 100644 --- a/docs/source/_static/css/styles.css +++ b/docs/source/_static/css/styles.css @@ -1,7 +1,3 @@ -/* ========================================================================== - BLOP CUSTOM THEME STYLES (PyData Theme Compatible) - ========================================================================== */ - /* RESET & BASE */ * { box-sizing: border-box; @@ -16,6 +12,143 @@ body { line-height: 1.6; } +/* Warning Banner and*/ +/* Light mode colors */ +:root { + --warning-bg: #ff3300; + --warning-text: white; + --warning-details-bg: #fef9f8; + --warning-details-text: #2d2d2d; + --warning-btn-bg: white; + --warning-btn-text: #ff3300; + --warning-btn-hover-bg: #f0f0f0; +} + +/* Dark mode colors */ +html[data-theme="dark"] { + --warning-bg: #74210d; + --warning-text: white; + --warning-details-bg: #1a1a1a; + --warning-details-text: #e0e0e0; + --warning-btn-bg: #2d2d2d; + --warning-btn-text: #ff6633; + --warning-btn-hover-bg: #3d3d3d; +} + +.warning-banner { + position: fixed; + top: 73px; + left: 0; + width: 100%; + background-color: var(--warning-bg); + color: var(--warning-text); + font-family: sans-serif; + z-index: 999; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + transition: all 0.01s ease; +} + +.warning-content { + max-width: 1200px; + margin: 0 auto; + padding: 15px 20px; +} + +.warning-main { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; +} + +.warning-text { + font-size: 1rem; + font-weight: 500; +} + +.warning-expand-btn { + background-color: rgba(255, 255, 255, 0.2); + color: var(--warning-text); + border: 1px solid rgba(255, 255, 255, 0.4); + padding: 5px 15px; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 1s ease; +} + +.warning-expand-btn:hover { + background-color: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.6); +} + +.warning-details { + display: flex; + max-height: 0; + overflow: hidden; + opacity: 0; + transition: all 0.01s ease; + margin-top: 0; +} + +.warning-details.expanded { + max-height: 100%; + opacity: 1; + margin-top: 15px; +} + +.warning-details-box { + display: flex; + flex-direction: column; + align-items: center; + border: 2px solid var(--warning-bg); + background-color: var(--warning-details-bg); + color: var(--warning-details-text); + border-radius: 8px; + padding: 20px; + text-align: left; + margin: 0 auto; + width: 100%; +} + +.warning-details-box p { + margin: 10px 0 5px 0; + font-size: 0.95rem; + color: var(--warning-details-text); + width: 100%; +} + +.warning-details-box p:last-of-type { + margin: 5px 0; + font-size: 0.9rem; + opacity: 0.85; +} + +.warning-close-btn { + background-color: var(--warning-btn-bg); + color: var(--warning-btn-text); + border: none; + padding: 8px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + margin-top: 10px; + transition: all 0.3s ease; +} + +.warning-close-btn:hover { + background-color: var(--warning-btn-hover-bg); + transform: translateY(-1px); +} + +.warning-banner.hidden { + transform: translateY(-100%); + opacity: 0; + pointer-events: none; +} + /* NAVBAR (Adapting to Theme) */ .navbar { display: flex; @@ -187,6 +320,26 @@ footer { color: var(--pst-color-text-muted); } +/* SEARCH INPUT STYLING */ +#search-input::placeholder { + color: var(--pst-color-text-muted); + opacity: 0.8; +} + +#search-input:focus::placeholder { + opacity: 0.6; +} + +/* Dark mode specific placeholder styling */ +html[data-theme="dark"] #search-input::placeholder { + color: var(--pst-color-text-muted); + opacity: 0.9; +} + +html[data-theme="dark"] #search-input:focus::placeholder { + opacity: 0.7; +} + /* SPHINX LAYOUT OVERRIDES (Keep these for full-width landing) */ body.pagename-index .bd-sidebar { display: none !important; } body.pagename-index .bd-toc { display: none !important; } diff --git a/docs/source/_static/javascript/javascript.js b/docs/source/_static/javascript/javascript.js index f48bb936..1ffdb66b 100644 --- a/docs/source/_static/javascript/javascript.js +++ b/docs/source/_static/javascript/javascript.js @@ -76,6 +76,53 @@ document.addEventListener("DOMContentLoaded", () => { } }); +function toggleWarningDetails() { + const details = document.getElementById('warningDetails'); + const btn = document.getElementById('expandBtn'); + const banner = document.getElementById('warningBanner'); + const mainContent = document.querySelector('.landing'); + + if (details.classList.contains('expanded')) { + details.classList.remove('expanded'); + btn.textContent = 'Learn More'; + + // Reset padding to default when collapsed + if (mainContent) { + mainContent.style.transition = 'padding-top 0.3s ease'; + mainContent.style.paddingTop = '9rem'; + } + } else { + details.classList.add('expanded'); + btn.textContent = 'Show Less'; + + // Wait for expansion animation, then adjust padding based on banner height + setTimeout(() => { + if (banner && mainContent) { + const bannerHeight = banner.offsetHeight; + const headerHeight = 73; + const totalOffset = bannerHeight + headerHeight; + mainContent.style.transition = 'padding-top 0.2s ease'; + mainContent.style.paddingTop = `${totalOffset + 24}px`; + } + }, 50); + } +} + +function closeWarningBanner() { + const banner = document.getElementById('warningBanner'); + const mainContent = document.querySelector('.landing'); + + banner.classList.add('hidden'); + + // Adjust main content padding when banner is closed + setTimeout(() => { + if (mainContent) { + mainContent.style.transition = 'padding-top 0.3s ease'; + mainContent.style.paddingTop = '6rem'; + } + }, 300); +} + function copyCode(button) { // Find the code text relative to the button const container = button.parentElement; diff --git a/docs/source/_templates/index.html b/docs/source/_templates/index.html index bd3047dd..958a4a42 100644 --- a/docs/source/_templates/index.html +++ b/docs/source/_templates/index.html @@ -118,6 +118,27 @@ "> + @@ -147,7 +168,28 @@ -
+
+
+
+ ⚠️ Warning: This is a sample warning. + +
+
+
+

+ This is a sample warning +

+ +
+
+
+
+ +

Blop

From 425a018e7c81866357f85ced272162292779c78e Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Mon, 23 Feb 2026 09:58:39 -0500 Subject: [PATCH 5/6] Merged from main --- .github/workflows/_docs.yml | 24 +- .github/workflows/_testing.yml | 22 +- README.md | 3 +- docs/source/how-to-guides.rst | 1 + .../how-to-guides/manual-suggestions.rst | 253 ++++++++++++++++++ pyproject.toml | 1 + src/blop/ax/agent.py | 36 ++- src/blop/ax/optimizer.py | 35 ++- src/blop/plans/__init__.py | 2 + src/blop/plans/plans.py | 145 +++++++--- src/blop/plans/utils.py | 23 +- src/blop/protocols.py | 25 ++ 12 files changed, 510 insertions(+), 60 deletions(-) create mode 100644 docs/source/how-to-guides/manual-suggestions.rst diff --git a/.github/workflows/_docs.yml b/.github/workflows/_docs.yml index 094bd1cf..de08e19c 100644 --- a/.github/workflows/_docs.yml +++ b/.github/workflows/_docs.yml @@ -6,12 +6,9 @@ on: deploy_key: required: false - jobs: build_docs: runs-on: ubuntu-latest - strategy: - fail-fast: false defaults: run: @@ -28,19 +25,10 @@ jobs: with: fetch-depth: 0 - - name: Install documentation-building requirements with apt/dpkg - run: | - set -vxeuo pipefail - wget --progress=dot:giga "https://github.com/jgm/pandoc/releases/download/3.1.6.1/pandoc-3.1.6.1-1-amd64.deb" -O /tmp/pandoc.deb - sudo dpkg -i /tmp/pandoc.deb - # conda install -c conda-forge -y pandoc - which pandoc - pandoc --version - - name: Install pixi and activate environment - uses: prefix-dev/setup-pixi@v0.8.14 + uses: prefix-dev/setup-pixi@82d477f15f3a381dbcc8adc1206ce643fe110fb7 # v0.9.3 with: - pixi-version: v0.46.0 + pixi-version: v0.60.0 environments: docs cache: false activate-environment: docs @@ -51,13 +39,6 @@ jobs: - name: Build Docs run: pixi run build-docs - - name: Upload JupyterLite docs artifact - uses: actions/upload-artifact@v4 - with: - name: jupyterlite-docs - path: docs/build/jupyter_execute - if-no-files-found: error - - name: Upload HTML docs artifact uses: actions/upload-artifact@v4 with: @@ -65,7 +46,6 @@ jobs: path: docs/build/html/ - name: Deploy documentation to nsls-ii.github.io - # if: github.repository_owner == 'NSLS-II' && github.ref_name == 'main' if: github.event_name == 'release' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions diff --git a/.github/workflows/_testing.yml b/.github/workflows/_testing.yml index ba3f2aac..89ed58dd 100644 --- a/.github/workflows/_testing.yml +++ b/.github/workflows/_testing.yml @@ -35,7 +35,25 @@ jobs: activate-environment: ${{ matrix.python-version }} - name: Run unit tests - run: pixi run unit-tests + run: | + if [ "${{ matrix.python-version }}" = "py313-cpu" ]; then + pixi run -e ${{ matrix.python-version }} pytest --cov=blop --cov-report=xml --cov-report=term src/blop/tests/unit + else + pixi run unit-tests + fi - name: Run integration tests - run: pixi run integration-tests + run: | + if [ "${{ matrix.python-version }}" = "py313-cpu" ]; then + pixi run -e ${{ matrix.python-version }} pytest --cov=blop --cov-append --cov-report=xml --cov-report=term src/blop/tests/integration + else + pixi run integration-tests + fi + + - name: Upload coverage to Codecov + if: matrix.python-version == 'py313-cpu' + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true diff --git a/README.md b/README.md index ec93945d..00bf66fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Blop - Beamline Optimization Package -[![Testing](https://github.com/NSLS-II/blop/actions/workflows/ci.yml/badge.svg)](https://github.com/NSLS-II/blop/actions/workflows/ci.yml) +[![Testing](https://github.com/bluesky/blop/actions/workflows/ci.yml/badge.svg)](https://github.com/bluesky/blop/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/bluesky/blop/branch/main/graph/badge.svg)](https://codecov.io/gh/bluesky/blop) [![PyPI](https://img.shields.io/pypi/v/blop.svg)](https://pypi.python.org/pypi/blop) [![Conda](https://img.shields.io/conda/vn/conda-forge/blop.svg)](https://anaconda.org/conda-forge/blop) diff --git a/docs/source/how-to-guides.rst b/docs/source/how-to-guides.rst index dae99aa4..da738adb 100644 --- a/docs/source/how-to-guides.rst +++ b/docs/source/how-to-guides.rst @@ -7,6 +7,7 @@ How-to Guides how-to-guides/use-ophyd-devices.rst how-to-guides/attach-data-to-experiments.rst how-to-guides/custom-generation-strategies.rst + how-to-guides/manual-suggestions.rst how-to-guides/set-dof-constraints.rst how-to-guides/set-outcome-constraints.rst how-to-guides/acquire-baseline.rst diff --git a/docs/source/how-to-guides/manual-suggestions.rst b/docs/source/how-to-guides/manual-suggestions.rst new file mode 100644 index 00000000..33db0b86 --- /dev/null +++ b/docs/source/how-to-guides/manual-suggestions.rst @@ -0,0 +1,253 @@ +.. testsetup:: + + from unittest.mock import MagicMock + from typing import Any + import time + + from bluesky.protocols import NamedMovable, Readable, Status, Hints, HasHints, HasParent + from bluesky.run_engine import RunEngine + from tiled.client.container import Container + + class AlwaysSuccessfulStatus(Status): + def add_callback(self, callback) -> None: + callback(self) + + def exception(self, timeout = 0.0): + return None + + @property + def done(self) -> bool: + return True + + @property + def success(self) -> bool: + return True + + class ReadableSignal(Readable, HasHints, HasParent): + def __init__(self, name: str) -> None: + self._name = name + self._value = 0.0 + + @property + def name(self) -> str: + return self._name + + @property + def hints(self) -> Hints: + return { + "fields": [self._name], + "dimensions": [], + "gridding": "rectilinear", + } + + @property + def parent(self) -> Any | None: + return None + + def read(self): + return { + self._name: { "value": self._value, "timestamp": time.time() } + } + + def describe(self): + return { + self._name: { "source": self._name, "dtype": "number", "shape": [] } + } + + class MovableSignal(ReadableSignal, NamedMovable): + def __init__(self, name: str, initial_value: float = 0.0) -> None: + super().__init__(name) + self._value: float = initial_value + + def set(self, value: float) -> Status: + self._value = value + return AlwaysSuccessfulStatus() + + db = MagicMock(spec=Container) + RE = RunEngine({}) + + sensor = ReadableSignal("signal") + motor_x = MovableSignal("motor_x") + motor_y = MovableSignal("motor_y") + + # Mock evaluation function for examples + def evaluation_function(uid: str, suggestions: list[dict]) -> list[dict]: + """Mock evaluation function that returns constant outcomes.""" + outcomes = [] + for suggestion in suggestions: + outcome = { + "_id": suggestion["_id"], + "signal": 0.5, + } + outcomes.append(outcome) + return outcomes + +Manual Point Injection +====================== + +This guide shows how to inject custom parameter combinations based on domain knowledge or external sources, alongside optimizer-driven suggestions. + +Basic Usage +----------- + +To evaluate manually-specified points, use the ``sample_suggestions`` method with parameter combinations (without ``"_id"`` keys). The optimizer will automatically register these trials and incorporate the results into the Bayesian model. + +.. testcode:: + + from blop.ax import Agent, RangeDOF, Objective + + # Configure agent + agent = Agent( + sensors=[sensor], + dofs=[ + RangeDOF(actuator=motor_x, bounds=(-10, 10), parameter_type="float"), + RangeDOF(actuator=motor_y, bounds=(-10, 10), parameter_type="float"), + ], + objectives=[Objective(name="signal", minimize=False)], + evaluation_function=evaluation_function, + ) + + # Define points of interest + manual_points = [ + {'motor_x': 0.5, 'motor_y': 1.0}, # Center region + {'motor_x': 0.0, 'motor_y': 0.0}, # Origin + ] + + # Evaluate them + RE(agent.sample_suggestions(manual_points)) + +.. testoutput:: + :hide: + + ... + +The manual points will be treated just like optimizer suggestions - they'll be tracked, evaluated, and used to improve the model. + +Mixed Workflows +--------------- + +You can combine optimizer suggestions with manual points throughout your optimization: + +.. testcode:: + + from blop.ax import Agent, RangeDOF, Objective + + agent = Agent( + sensors=[sensor], + dofs=[ + RangeDOF(actuator=motor_x, bounds=(-10, 10), parameter_type="float"), + RangeDOF(actuator=motor_y, bounds=(-10, 10), parameter_type="float"), + ], + objectives=[Objective(name="signal", minimize=False)], + evaluation_function=evaluation_function, + ) + + # Run optimizer for initial exploration + RE(agent.optimize(iterations=3)) + + # Try a manual point based on domain insight + manual_point = [{'motor_x': 0.75, 'motor_y': 0.25}] + RE(agent.sample_suggestions(manual_point)) + + # Continue optimization + RE(agent.optimize(iterations=3)) + +.. testoutput:: + :hide: + + ... + +The optimizer will incorporate your manual point into its model and use it to inform future suggestions. + +Manual Approval Workflow +------------------------- + +You can review optimizer suggestions before running them by using ``suggest()`` to get suggestions without acquiring data: + +.. testcode:: + + from blop.ax import Agent, RangeDOF, Objective + + agent = Agent( + sensors=[sensor], + dofs=[ + RangeDOF(actuator=motor_x, bounds=(-10, 10), parameter_type="float"), + RangeDOF(actuator=motor_y, bounds=(-10, 10), parameter_type="float"), + ], + objectives=[Objective(name="signal", minimize=False)], + evaluation_function=evaluation_function, + ) + + # Get suggestions without running + suggestions = agent.suggest(num_points=5) + + # Review and filter + print("Reviewing suggestions:") + for s in suggestions: + trial_id = s['_id'] + x = s['motor_x'] + y = s['motor_y'] + print(f" Trial {trial_id}: x={x:.2f}, y={y:.2f}") + + # Only run approved suggestions + approved = [s for s in suggestions if s['motor_x'] > -5.0] + + if approved: + RE(agent.sample_suggestions(approved)) + else: + print("No suggestions approved") + +.. testoutput:: + + Reviewing suggestions: + ... + +This workflow allows you to apply safety checks, domain constraints, or other validation before running trials. + +Iterative Refinement +-------------------- + +A common pattern is to alternate between automated optimization and targeted manual exploration: + +.. testcode:: + + from blop.ax import Agent, RangeDOF, Objective + + agent = Agent( + sensors=[sensor], + dofs=[ + RangeDOF(actuator=motor_x, bounds=(-10, 10), parameter_type="float"), + RangeDOF(actuator=motor_y, bounds=(-10, 10), parameter_type="float"), + ], + objectives=[Objective(name="signal", minimize=False)], + evaluation_function=evaluation_function, + ) + + for cycle in range(3): + # Automated exploration + RE(agent.optimize(iterations=2, n_points=2)) + + # Review results and manually probe interesting regions + # (Look at plots, current best, etc.) + + # Try edge cases or special points + if cycle == 1: + # After first cycle, check boundaries + boundary_points = [ + {'motor_x': -10.0, 'motor_y': 0.0}, + {'motor_x': 10.0, 'motor_y': 0.0}, + ] + RE(agent.sample_suggestions(boundary_points)) + +.. testoutput:: + :hide: + + ... + +See Also +-------- + +- :meth:`blop.ax.Agent.suggest` - Get optimizer suggestions without running +- :meth:`blop.ax.Agent.sample_suggestions` - Evaluate specific suggestions +- :meth:`blop.ax.Agent.optimize` - Run full optimization loop +- :class:`blop.protocols.CanRegisterSuggestions` - Protocol for manual trial support diff --git a/pyproject.toml b/pyproject.toml index fae747b5..ec1a9413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dynamic = ["version"] [project.optional-dependencies] dev = [ "pytest", + "pytest-cov", "ipykernel", "ruff", "nbstripout", diff --git a/src/blop/ax/agent.py b/src/blop/ax/agent.py index 8e99e2b9..306fbbbe 100644 --- a/src/blop/ax/agent.py +++ b/src/blop/ax/agent.py @@ -15,7 +15,8 @@ # =============================== from bluesky.utils import MsgGenerator -from ..plans import acquire_baseline, optimize +from ..plans import acquire_baseline, optimize, sample_suggestions +from ..plans.utils import InferredReadable from ..protocols import AcquisitionPlan, Actuator, EvaluationFunction, OptimizationProblem, Sensor from .dof import DOF, DOFConstraint from .objective import Objective, OutcomeConstraint, to_ax_objective_str @@ -98,6 +99,7 @@ def __init__( checkpoint_path=checkpoint_path, **kwargs, ) + self._readable_cache: dict[str, InferredReadable] = {} @classmethod def from_checkpoint( @@ -293,7 +295,37 @@ def optimize(self, iterations: int = 1, n_points: int = 1) -> MsgGenerator[None] suggest : Get point suggestions without running acquisition. ingest : Manually ingest evaluation results. """ - yield from optimize(self.to_optimization_problem(), iterations=iterations, n_points=n_points) + yield from optimize( + self.to_optimization_problem(), iterations=iterations, n_points=n_points, readable_cache=self._readable_cache + ) + + def sample_suggestions(self, suggestions: list[dict]) -> MsgGenerator[tuple[str, list[dict], list[dict]]]: + """ + Evaluate specific parameter combinations. + + Acquires data for given suggestions and ingests results. Supports both + optimizer suggestions and manual points. + + Parameters + ---------- + suggestions : list[dict] + Either optimizer suggestions (with "_id") or manual points (without "_id"). + + Returns + ------- + tuple[str, list[dict], list[dict]] + Bluesky run UID, suggestions with "_id", and outcomes. + + See Also + -------- + suggest : Get optimizer suggestions. + optimize : Run full optimization loop. + """ + return ( + yield from sample_suggestions( + self.to_optimization_problem(), suggestions=suggestions, readable_cache=self._readable_cache + ) + ) def plot_objective( self, x_dof_name: str, y_dof_name: str, objective_name: str, *args: Any, **kwargs: Any diff --git a/src/blop/ax/optimizer.py b/src/blop/ax/optimizer.py index 9b473246..aafa93a8 100644 --- a/src/blop/ax/optimizer.py +++ b/src/blop/ax/optimizer.py @@ -3,10 +3,10 @@ from ax import ChoiceParameterConfig, Client, RangeParameterConfig -from ..protocols import ID_KEY, Checkpointable, Optimizer +from ..protocols import ID_KEY, CanRegisterSuggestions, Checkpointable, Optimizer -class AxOptimizer(Optimizer, Checkpointable): +class AxOptimizer(Optimizer, Checkpointable, CanRegisterSuggestions): """ An optimizer that uses Ax as the backend for optimization and experiment tracking. @@ -158,6 +158,37 @@ def ingest(self, points: list[dict]) -> None: trial_idx = self._client.attach_baseline(parameters=parameters) self._client.complete_trial(trial_index=trial_idx, raw_data=outcomes) + def register_suggestions(self, suggestions: list[dict]) -> list[dict]: + """ + Register manual suggestions with the Ax experiment. + + Attaches trials to the experiment and returns the suggestions with "_id" keys + added for tracking. This enables manual point injection alongside optimizer-driven + suggestions. + + Parameters + ---------- + suggestions : list[dict] + Parameter combinations to register. The "_id" key will be overwritten if present. + + Returns + ------- + list[dict] + The same suggestions with "_id" keys added. + """ + registered = [] + for suggestion in suggestions: + # Extract parameters (ignore _id if present) + parameters = {k: v for k, v in suggestion.items() if k != ID_KEY} + + # Attach trial to Ax experiment + trial_idx = self._client.attach_trial(parameters=parameters) + + # Return with trial ID + registered.append({ID_KEY: trial_idx, **parameters}) + + return registered + def checkpoint(self) -> None: """ Save the optimizer's state to JSON file. diff --git a/src/blop/plans/__init__.py b/src/blop/plans/__init__.py index fc196814..efc9d6cd 100644 --- a/src/blop/plans/__init__.py +++ b/src/blop/plans/__init__.py @@ -6,6 +6,7 @@ optimize_step, per_step_background_read, read, + sample_suggestions, take_reading_with_background, ) from .utils import get_route_index, route_suggestions @@ -18,6 +19,7 @@ "optimize", "optimize_step", "per_step_background_read", + "sample_suggestions", "read", "route_suggestions", "take_reading_with_background", diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index 3918ecd2..5b5ba6a6 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -11,14 +11,15 @@ from bluesky.protocols import Readable, Reading from bluesky.utils import MsgGenerator, plan -from ..protocols import ID_KEY, Actuator, Checkpointable, OptimizationProblem, Optimizer, Sensor -from .utils import InferredReadable, route_suggestions +from ..protocols import ID_KEY, Actuator, CanRegisterSuggestions, Checkpointable, OptimizationProblem, Optimizer, Sensor +from .utils import InferredReadable, collect_optimization_metadata, route_suggestions logger = logging.getLogger(__name__) _BLUESKY_UID_KEY: Literal["bluesky_uid"] = "bluesky_uid" _SUGGESTION_IDS_KEY: Literal["suggestion_ids"] = "suggestion_ids" _DEFAULT_ACQUIRE_RUN_KEY: Literal["default_acquire"] = "default_acquire" +_SAMPLE_SUGGESTIONS_RUN_KEY: Literal["sample_suggestions"] = "sample_suggestions" _OPTIMIZE_RUN_KEY: Literal["optimize"] = "optimize" @@ -253,7 +254,7 @@ def optimize( iterations: int = 1, n_points: int = 1, checkpoint_interval: int | None = None, - *args: Any, + readable_cache: dict[str, InferredReadable] | None = None, **kwargs: Any, ) -> MsgGenerator[None]: """ @@ -271,8 +272,9 @@ def optimize( The number of iterations between optimizer checkpoints. If None, checkpoints will not be saved. Optimizer must implement the :class:`blop.protocols.Checkpointable` protocol. - *args : Any - Additional positional arguments to pass to the :func:`optimize_step` plan. + readable_cache: dict[str, InferredReadable] | None = None + Cache of readable objects to store the suggestions and outcomes as events. + If None, a new cache will be created. **kwargs : Any Additional keyword arguments to pass to the :func:`optimize_step` plan. @@ -284,37 +286,26 @@ def optimize( """ # Cache to track readables created from suggestions and outcomes - readable_cache: dict[str, InferredReadable] = {} - - # Collect metadata for this optimization run - if hasattr(optimization_problem.evaluation_function, "__name__"): - evaluation_function_name = optimization_problem.evaluation_function.__name__ # type: ignore[attr-defined] - else: - evaluation_function_name = optimization_problem.evaluation_function.__class__.__name__ - if hasattr(optimization_problem.acquisition_plan, "__name__"): - acquisition_plan_name = optimization_problem.acquisition_plan.__name__ # type: ignore[attr-defined] - else: - acquisition_plan_name = optimization_problem.acquisition_plan.__class__.__name__ - _md = { - "plan_name": "optimize", - "sensors": [sensor.name for sensor in optimization_problem.sensors], - "actuators": [actuator.name for actuator in optimization_problem.actuators], - "evaluation_function": evaluation_function_name, - "acquisition_plan": acquisition_plan_name, - "optimizer": optimization_problem.optimizer.__class__.__name__, - "iterations": iterations, - "n_points": n_points, - "checkpoint_interval": checkpoint_interval, - "run_key": _OPTIMIZE_RUN_KEY, - } + readable_cache = readable_cache or {} + + _md = collect_optimization_metadata(optimization_problem) + _md.update( + { + "plan_name": "optimize", + "iterations": iterations, + "n_points": n_points, + "checkpoint_interval": checkpoint_interval, + "run_key": _OPTIMIZE_RUN_KEY, + } + ) # Encapsulate the optimization plan in a run decorator @bpp.set_run_key_decorator(_OPTIMIZE_RUN_KEY) @bpp.run_decorator(md=_md) - def _optimize(): + def _optimize() -> MsgGenerator[None]: for i in range(iterations): # Perform a single step of the optimization - uid, suggestions, outcomes = yield from optimize_step(optimization_problem, n_points, *args, **kwargs) + uid, suggestions, outcomes = yield from optimize_step(optimization_problem, n_points, **kwargs) # Read the optimization step into the Bluesky and emit events for each suggestion and outcome yield from _read_step(uid, suggestions, outcomes, n_points, readable_cache) @@ -326,6 +317,100 @@ def _optimize(): return (yield from _optimize()) +@plan +def sample_suggestions( + optimization_problem: OptimizationProblem, + suggestions: list[dict], + readable_cache: dict[str, InferredReadable] | None = None, + **kwargs: Any, +) -> MsgGenerator[tuple[str, list[dict], list[dict]]]: + """ + Evaluate specific parameter combinations. + + This plan acquires data for given suggestions and ingests results into the optimizer. + Supports both optimizer-generated suggestions (with "_id") and manual points + (without "_id", if optimizer implements CanRegisterSuggestions). + + Parameters + ---------- + optimization_problem : OptimizationProblem + The optimization problem. + suggestions : list[dict] + Parameter combinations to evaluate. Can be: + + - Optimizer suggestions (with "_id" keys from suggest()) + - Manual points (without "_id", requires CanRegisterSuggestions protocol) + + readable_cache : dict[str, InferredReadable] | None + Cache for storing suggestions/outcomes as events. + **kwargs : Any + Additional arguments for acquisition plan. + + Returns + ------- + uid : str + Bluesky run UID. + suggestions : list[dict] + Suggestions with "_id" keys. + outcomes : list[dict] + Evaluated outcomes. + + Raises + ------ + ValueError + If suggestions lack "_id" and optimizer doesn't implement CanRegisterSuggestions. + + See Also + -------- + optimize_step : Standard optimizer-driven step. + blop.protocols.CanRegisterSuggestions : Protocol for manual suggestions. + """ + + # Ensure the suggestions have an ID_KEY or register them with the optimizer + if not isinstance(optimization_problem.optimizer, CanRegisterSuggestions) and any( + ID_KEY not in suggestion for suggestion in suggestions + ): + raise ValueError( + f"All suggestions must contain an '{ID_KEY}' key to later match with the outcomes or your optimizer must " + "implement the `blop.protocols.CanRegisterSuggestions` protocol. Please review your optimizer " + f"implementation. Got suggestions: {suggestions}" + ) + elif isinstance(optimization_problem.optimizer, CanRegisterSuggestions): + suggestions = optimization_problem.optimizer.register_suggestions(suggestions) + + # Collect the metadata for the run + _md = collect_optimization_metadata(optimization_problem) + _md.update( + { + "plan_name": "sample_suggestions", + "suggestions": suggestions, + "run_key": _SAMPLE_SUGGESTIONS_RUN_KEY, + } + ) + + @bpp.set_run_key_decorator(_SAMPLE_SUGGESTIONS_RUN_KEY) + @bpp.run_decorator(md=_md) + def _inner_sample_suggestions() -> MsgGenerator[tuple[str, list[dict], list[dict]]]: + + # Acquire data, evaluate, and ingest outcomes + if optimization_problem.acquisition_plan is None: + acquisition_plan = default_acquire + else: + acquisition_plan = optimization_problem.acquisition_plan + uid = yield from acquisition_plan( + suggestions, optimization_problem.actuators, optimization_problem.sensors, **kwargs + ) + outcomes = optimization_problem.evaluation_function(uid, suggestions) + optimization_problem.optimizer.ingest(outcomes) + + # Emit a Bluesky event + yield from _read_step(uid, suggestions, outcomes, len(suggestions), readable_cache or {}) + + return uid, suggestions, outcomes + + return (yield from _inner_sample_suggestions()) + + @plan def read(readables: Sequence[Readable], **kwargs: Any) -> MsgGenerator[dict[str, Any]]: """ diff --git a/src/blop/plans/utils.py b/src/blop/plans/utils.py index 05a95aef..815bf4fa 100644 --- a/src/blop/plans/utils.py +++ b/src/blop/plans/utils.py @@ -8,7 +8,7 @@ from event_model import DataKey from numpy.typing import ArrayLike -from ..protocols import ID_KEY +from ..protocols import ID_KEY, OptimizationProblem def _infer_data_key(value: ArrayLike) -> DataKey: @@ -133,3 +133,24 @@ def route_suggestions(suggestions: list[dict], starting_position: dict | None = starting_point = np.array([starting_position[dim] for dim in dims_to_route]) if starting_position else None return [suggestions[i] for i in get_route_index(points=points, starting_point=starting_point)] + + +def collect_optimization_metadata(optimization_problem: OptimizationProblem) -> dict[str, Any]: + """ + Collect the metadata for the optimization problem. + """ + if hasattr(optimization_problem.evaluation_function, "__name__"): + evaluation_function_name = optimization_problem.evaluation_function.__name__ # type: ignore[attr-defined] + else: + evaluation_function_name = optimization_problem.evaluation_function.__class__.__name__ + if hasattr(optimization_problem.acquisition_plan, "__name__"): + acquisition_plan_name = optimization_problem.acquisition_plan.__name__ # type: ignore[attr-defined] + else: + acquisition_plan_name = optimization_problem.acquisition_plan.__class__.__name__ + return { + "evaluation_function": evaluation_function_name, + "acquisition_plan": acquisition_plan_name, + "optimizer": optimization_problem.optimizer.__class__.__name__, + "sensors": [sensor.name for sensor in optimization_problem.sensors], + "actuators": [actuator.name for actuator in optimization_problem.actuators], + } diff --git a/src/blop/protocols.py b/src/blop/protocols.py index 66938492..dc31f0c6 100644 --- a/src/blop/protocols.py +++ b/src/blop/protocols.py @@ -10,6 +10,31 @@ Sensor = Readable | EventCollectable | EventPageCollectable +@runtime_checkable +class CanRegisterSuggestions(Protocol): + """ + A protocol for optimizers that can register suggestions. This + allows them to add an "_id" key to the suggestions dynamically and ensure + that the suggestions are unique. + """ + + def register_suggestions(self, suggestions: list[dict]) -> list[dict]: + """ + Register the suggestions with the optimizer. + + Parameters + ---------- + suggestions: list[dict] + The suggestions to register. The "_id" key is optional and will be overwritten if present. + + Returns + ------- + list[dict] + The original suggestions with an "_id" key added. + """ + ... + + @runtime_checkable class Checkpointable(Protocol): """ From 0983bda08870dde45d422617ec98fdf0f59f6434 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Mon, 23 Feb 2026 13:37:40 -0500 Subject: [PATCH 6/6] add installation instructions copy doesn't work --- docs/source/_static/css/styles.css | 55 +++++++++++ docs/source/_templates/index.html | 143 ++++++++++++++++++++++++----- 2 files changed, 175 insertions(+), 23 deletions(-) diff --git a/docs/source/_static/css/styles.css b/docs/source/_static/css/styles.css index 689696b6..504485d8 100644 --- a/docs/source/_static/css/styles.css +++ b/docs/source/_static/css/styles.css @@ -267,6 +267,17 @@ pre, .command-block pre { margin: 1rem 0; } +.installation-box{ + background: #1e1e1e; + border: 2px solid #333; + border-radius: 8px; + padding: 1rem; + position: relative; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 0.9rem; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.1) +} + code { font-family: var(--pst-font-family-monospace); color: var(--pst-color-text-base); @@ -340,6 +351,50 @@ html[data-theme="dark"] #search-input:focus::placeholder { opacity: 0.7; } +/* Learn More grid layout */ +.learn-more-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +@media (max-width: 768px) { + .learn-more-grid { + grid-template-columns: 1fr; + grid-template-rows: repeat(4, 1fr); + } +} + +/* Copy button feedback styles */ +.copy-btn { + position: absolute; + right: 0.5rem; + top: 0.5rem; + background: #4CAF50; + color: white; + border: none; + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; + font-weight: 500; +} + +.copy-btn.copied { + background: #2e7d32 !important; + transform: scale(0.95); +} + +.copy-btn { + transition: all 0.2s ease; +} + +.copy-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + /* SPHINX LAYOUT OVERRIDES (Keep these for full-width landing) */ body.pagename-index .bd-sidebar { display: none !important; } body.pagename-index .bd-toc { display: none !important; } diff --git a/docs/source/_templates/index.html b/docs/source/_templates/index.html index 958a4a42..d2b45816 100644 --- a/docs/source/_templates/index.html +++ b/docs/source/_templates/index.html @@ -118,27 +118,6 @@ "> - @@ -168,6 +147,7 @@ +
@@ -222,10 +202,122 @@

What is Blop?


+
+

Installation

+ + +
+

PyTorch Installation Options

+

+ By default, blop installs PyTorch with GPU support (~7GB). + For environments without GPU support, or to reduce installation size, you can install a CPU-only version (~900MB) using uv. + This is particularly useful for containerized environments with GPU access, CI/CD pipelines, development environments on laptops without NVIDIA GPUs, or edge computing scenarios. +

+

+ Note: CPU-only installation requires uv, a fast Python package installer. +

+
+ +
+ +
+

Via PyPI

+ + +

Standard (GPU support):

+
+
+ + $ pip install blop + + +
+
+ + +

CPU-only (containers, CI/CD, laptops):

+
+
+ + $ pip install uv
+ $ uv pip install blop[cpu] +
+ +
+
+ +
+ + +
+

Via Conda-forge

+ + +

Standard:

+
+
+ + $ conda install -c conda-forge blop + + +
+
+ + +

CPU-only:

+
+
+ + $ conda install -c conda-forge blop pytorch cpuonly -c pytorch + + +
+
+ +
+ +
+ +
+
+
+ +
+ +

Learn More!

-
- + +
{# We define the list so additional items can be easily added #} {% set nav_items = [ ("Tutorials", "tutorials.html", "Step-by-step guides to get started with Blop fundamentals and basic workflows"), @@ -313,6 +405,11 @@

References

{% endblock %} +{% block stylesheets %} + {{ super() }} + +{% endblock %} + {% block scripts %} {{ super() }}