From 0c44c6d5eced4591a89924053d5aaf4798811590 Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Sat, 22 Nov 2025 14:22:41 -0500 Subject: [PATCH 1/3] Fixed wrong month on release date --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index fe16706..eb60403 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,7 @@ ======= History ======= -2025.9.22: Bugfix: fixed floating-point precision issues in For loops +2025.11.22: Bugfix: fixed floating-point precision issues in For loops * This fix addresses floating-point precision issues in For loops that caused early termination and generated malformed directory names (e.g., "3.00000000002" or "2.999999999998"). The fix calculates the number of decimal places from loop From 58007c3594ee7509ab65e35c76501010cfebf6c0 Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Thu, 25 Jun 2026 19:01:26 -0400 Subject: [PATCH 2/3] Add 'update' make target for post-release main/dev sync Self-documented target that checks out and pulls main, fast-forwards dev onto it (git merge --ff-only as a guardrail), runs lint/install/test, then pushes dev -- codifying the manual post-release workflow. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index 4b1a1f1..4357e1f 100644 --- a/Makefile +++ b/Makefile @@ -104,3 +104,12 @@ install: uninstall ## install the package to the active Python's site-packages uninstall: clean ## uninstall the package pip uninstall --yes $(MODULE) + +.PHONY: update +update: ## post-release: sync main and dev, reinstall, run checks, push dev + git checkout main + git pull + git checkout dev + git merge --ff-only main + $(MAKE) lint install test + git push From eda4ba0097c0ed092ce65569d953cb0aff505998 Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Fri, 18 Sep 2026 12:55:33 -0400 Subject: [PATCH 3/3] Standard structure selection for loops over systems The "For systems in the database" loop uses seamm's standard structure selection (defaults unchanged: every system, its last configuration), with the old option names and values translated when an older flowchart is loaded. Fixes "name is" never matching (identity comparison) and "name matches" / "name regexp" falling through to no selection. CI environment moved to the pip-based layout. Co-Authored-By: Claude Fable 5.1 --- HISTORY.rst | 13 +++++ devtools/conda-envs/test_env.yaml | 13 +++-- loop_step/loop.py | 68 +++-------------------- loop_step/loop_parameters.py | 90 ++++++++++++++++--------------- loop_step/tk_loop.py | 29 ++++------ requirements.txt | 2 +- tests/test_structure_selection.py | 69 ++++++++++++++++++++++++ 7 files changed, 156 insertions(+), 128 deletions(-) create mode 100644 tests/test_structure_selection.py diff --git a/HISTORY.rst b/HISTORY.rst index eb60403..f5d17f8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,19 @@ ======= History ======= +2026.9.18 -- Standard structure selection for loops over systems + * The "For systems in the database" loop now uses SEAMM's standard structure + selection: the systems to loop over (all, the current one, or those whose name + is / matches / matches a regular expression) and which configurations of each + (last, first, all, the current one, or by name), or a variable holding a list + of configurations or systems. The defaults -- every system, its last + configuration -- are unchanged, and flowcharts saved with the old options are + translated when loaded. + * Bugfix: selecting configurations by "name is" never matched (the name was + compared by identity, not equality), and "name matches" / "name regexp" fell + through to no selection at all. + * Requires seamm 2026.9.18.1 or later. + 2025.11.22: Bugfix: fixed floating-point precision issues in For loops * This fix addresses floating-point precision issues in For loops that caused early termination and generated malformed directory names (e.g., "3.00000000002" or diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 8775277..d02a912 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -3,14 +3,20 @@ name: test channels: - conda-forge +# The CI environment. The package under test is installed on top of it with +# `pip install . --no-deps`, so everything the tests need must be listed here. +# Pure-Python dependencies (the SEAMM packages) come from PyPI, which has a new +# release within minutes; only compiled packages come from conda-forge. dependencies: # Base depends - python - pip - # Dependencies for this module + # Compiled packages: from conda-forge so the binaries are consistent + - numpy + - openbabel - psutil - - seamm + - rdkit # Testing - black @@ -23,7 +29,8 @@ dependencies: - pydata-sphinx-theme - sphinx-design - # Pip-only installs + # Pure-Python packages: from PyPI - pip: + - seamm>=2026.9.18.1 # Documentation - sphinx-copybutton diff --git a/loop_step/loop.py b/loop_step/loop.py index 29164bd..ba7c01b 100644 --- a/loop_step/loop.py +++ b/loop_step/loop.py @@ -2,7 +2,6 @@ """Non-graphical part of the Loop step in a SEAMM flowchart""" -import fnmatch import logging from pathlib import Path import re @@ -187,7 +186,10 @@ def description_text(self, P=None): elif P["type"] == "For rows in table": subtext = "For rows in table {table}\n" elif P["type"] == "For systems in the database": - subtext = "For system in the database\n" + subtext = seamm.standard_parameters.structure_selection_description( + P + ).replace("will be used", "will be looped over") + subtext += "\n" else: subtext = "Loop type defined by {type}\n" @@ -423,65 +425,9 @@ def run(self): if index_is_int: fmt = f"0{len(str(max(table_indices) + 1))}d" elif P["type"] == "For systems in the database": - # Get a list of all the matching systems and configurations - system_db = self.get_variable("_system_db") - systems = system_db.systems - - # Filter on system names - choice = P["where system name"] - if choice == "is anything": - pass - elif choice == "is": - name = P["system name"] - systems = [s for s in systems if s.name == name] - elif choice == "matches": - pattern = P["system name"] - systems = [s for s in systems if fnmatch.fnmatch(s.name, pattern)] - elif choice == "regexp": - pattern = P["system name"] - systems = [s for s in systems if re.search(pattern, s.name) is not None] - else: - raise RuntimeError( - f"Matching system names by '{choice}' is not supported" - ) - - # Finally, allow only systems that contain the requested configuration - choice = P["default configuration"] - if choice == "last" or choice == "-1": - systems = [s for s in systems if s.n_configurations > 0] - configurations = [s.configurations[-1] for s in systems] - elif choice == "first" or choice == "1": - systems = [s for s in systems if s.n_configurations > 0] - configurations = [s.configurations[0] for s in systems] - elif choice == "name is": - name = P["configuration name"] - systems = [s for s in systems if s.n_configurations > 0] - configurations = [] - for s in systems: - for c in s.configurations: - if c.name is name: - configurations.append(c) - elif choice == "matches": - pattern = P["configuration name"] - systems = [s for s in systems if s.n_configurations > 0] - configurations = [] - for s in systems: - for c in s.configurations: - if fnmatch.fnmatch(c.name, pattern): - configurations.append(c) - elif choice == "regexp": - pattern = P["configuration name"] - configurations = [] - for s in systems: - for c in s.configurations: - if re.search(pattern, c.name) is not None: - configurations.append(c) - elif choice == "all": - name = P["configuration name"] - systems = [s for s in systems if s.n_configurations > 0] - configurations = [] - for s in systems: - configurations.extend(s.configurations) + # The configurations to loop over: the standard SEAMM selection. An + # empty selection is simply a loop with no iterations. + configurations = self.select_configurations(P, errors=False) if self._loop_value is None: self._loop_value = 0 diff --git a/loop_step/loop_parameters.py b/loop_step/loop_parameters.py index eab0393..5363e5d 100644 --- a/loop_step/loop_parameters.py +++ b/loop_step/loop_parameters.py @@ -7,6 +7,34 @@ logger = logging.getLogger(__name__) +# Legacy (pre-2026.9.18) names and values for the system selection, translated on +# loading an old flowchart to the standard SEAMM structure-selection parameters. +_legacy_keys = { + "where system name": "source systems", + "system name": "source system name", + "default configuration": "source configurations", + "configuration name": "source configuration name", +} +_legacy_values = { + "source systems": { + "is anything": "all", + "is": "name is", + "matches": "name matches", + "regexp": "name regexp", + }, + "source configurations": {"-1": "last", "1": "first"}, +} + +_selection_parameters = { + key: dict(value) + for key, value in seamm.standard_parameters.structure_selection_parameters.items() +} +_selection_parameters["source systems"]["default"] = "all" +_selection_parameters["source systems"]["description"] = "For systems:" +_selection_parameters["source configurations"]["default"] = "last" +_selection_parameters["source configurations"]["description"] = "using configuration:" + + class LoopParameters(seamm.Parameters): """The control parameters for loops""" @@ -147,49 +175,9 @@ class LoopParameters(seamm.Parameters): "description": "Values as variables:", "help_text": "Whether to put the values for the row as seperate variables.", }, - "where system name": { - "default": "is anything", - "kind": "string", - "default_units": "", - "enumeration": ("is anything", "is", "matches", "regexp"), - "format_string": "s", - "description": "where name:", - "help_text": "The filter for the system name, defaults to all systems.", - }, - "system name": { - "default": "", - "kind": "string", - "default_units": "", - "enumeration": tuple(), - "format_string": "s", - "description": "", - "help_text": "The filter for the system name", - }, - "default configuration": { - "default": "last", - "kind": "string", - "default_units": "", - "enumeration": ( - "all", - "last", - "first", - "name is", - "name matches", - "name regexp", - ), - "format_string": "s", - "description": "Select configuration:", - "help_text": "The configuration to select as the default.", - }, - "configuration name": { - "default": "", - "kind": "string", - "default_units": "", - "enumeration": tuple(), - "format_string": "s", - "description": "", - "help_text": "The filter for the configuration name", - }, + # The standard SEAMM structure selection, but looping over every system + # and its last configuration by default, as this step always has. + **_selection_parameters, "directory name": { "default": "loop iteration", "kind": "string", @@ -223,3 +211,19 @@ def __init__(self, defaults={}, data=None): parameters given in the class""" super().__init__(defaults={**LoopParameters.parameters, **defaults}, data=data) + + def update(self, data): + """Update from a dictionary, translating the legacy system-selection keys + ('where system name', 'system name', 'default configuration', + 'configuration name') and their values from flowcharts saved before + 2026.9.18 to the standard structure-selection parameters.""" + translated = {} + for key, value in data.items(): + new_key = _legacy_keys.get(key, key) + if new_key in _legacy_values and isinstance(value, dict): + value = dict(value) + value["value"] = _legacy_values[new_key].get( + str(value.get("value", "")).strip(), value.get("value") + ) + translated[new_key] = value + super().update(translated) diff --git a/loop_step/tk_loop.py b/loop_step/tk_loop.py index 8e4dda1..ca135ff 100644 --- a/loop_step/tk_loop.py +++ b/loop_step/tk_loop.py @@ -135,11 +135,15 @@ def create_dialog(self): "type", "where", "query-op", - "where system name", - "default configuration", ): self[widget].bind("<>", self.reset_dialog) self[widget].combobox.config(state="readonly") + # The structure selection: re-lay out when the choices change (they stay + # editable so a $variable can be typed). + for widget in ("source systems", "source configurations"): + self[widget].combobox.bind("<>", self.reset_dialog) + self[widget].combobox.bind("", self.reset_dialog) + self[widget].combobox.bind("", self.reset_dialog) self["errors"].combobox.config(state="readonly") @@ -224,24 +228,9 @@ def reset_dialog(self, widget=None): # frame.rowconfigure(row, weight=1, minsize=100) # row += 1 - self["where system name"].grid( - row=row, column=0, columnspan=2, sticky=tk.EW - ) - choice = self["where system name"].get() - if choice != "is anything": - self["system name"].grid(row=row, column=2, columnspan=2, sticky=tk.EW) - row += 1 - - self["default configuration"].grid( - row=row, column=0, columnspan=2, sticky=tk.EW - ) - op = self["default configuration"].get() - if "name" in op: - self["configuration name"].grid( - row=row, column=2, columnspan=2, sticky=tk.EW - ) - row += 1 - frame.columnconfigure(3, weight=1) + row, widgets = self.layout_structure_selection(row=row, column=0) + sw.align_labels(widgets, sticky=tk.E) + frame.columnconfigure(1, weight=1) self["directory name"].grid(row=row, column=0, columnspan=3, sticky=tk.W) row += 1 else: diff --git a/requirements.txt b/requirements.txt index c706f72..f04a8bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ psutil -seamm +seamm>=2026.9.18.1 diff --git a/tests/test_structure_selection.py b/tests/test_structure_selection.py new file mode 100644 index 0000000..bdd02bd --- /dev/null +++ b/tests/test_structure_selection.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +"""The loop over systems uses SEAMM's standard structure selection, and reads +flowcharts saved with the old option names.""" + +import loop_step +import seamm + + +def _legacy(where="is anything", system_name="", default="last", conf_name=""): + return { + "type": {"value": "For systems in the database", "units": None}, + "where system name": {"value": where, "units": None}, + "system name": {"value": system_name, "units": None}, + "default configuration": {"value": default, "units": None}, + "configuration name": {"value": conf_name, "units": None}, + } + + +def test_defaults_loop_over_every_system_last_configuration(): + P = loop_step.LoopParameters() + assert P["source systems"].value == "all" + assert P["source configurations"].value == "last" + for key in seamm.standard_parameters.structure_selection_parameters: + assert key in P + for key in ("where system name", "default configuration"): + assert key not in P + + +def test_legacy_keys_and_values_are_translated(): + P = loop_step.LoopParameters() + P.from_dict(_legacy()) + assert P["source systems"].value == "all" + assert P["source configurations"].value == "last" + + P.from_dict( + _legacy(where="is", system_name="water", default="name is", conf_name="x") + ) + assert P["source systems"].value == "name is" + assert P["source system name"].value == "water" + assert P["source configurations"].value == "name is" + assert P["source configuration name"].value == "x" + + P.from_dict(_legacy(where="matches", system_name="H2O*", default="-1")) + assert P["source systems"].value == "name matches" + assert P["source configurations"].value == "last" + + P.from_dict(_legacy(where="regexp", system_name="^w", default="1")) + assert P["source systems"].value == "name regexp" + assert P["source configurations"].value == "first" + + P.from_dict(_legacy(default="name matches", conf_name="frame*")) + assert P["source configurations"].value == "name matches" + + +def test_new_keys_pass_through(): + P = loop_step.LoopParameters() + P.from_dict( + { + "source systems": {"value": "current", "units": None}, + "source configurations": {"value": "all", "units": None}, + } + ) + assert P["source systems"].value == "current" + assert P["source configurations"].value == "all" + text = seamm.standard_parameters.structure_selection_description( + P.current_values_to_dict() + ) + assert text == "All configurations of the current system will be used."