From 6ba6156ece91da9d57fa060bb73eda08e4a2a20e Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Sat, 3 Oct 2026 11:14:56 -0400 Subject: [PATCH 1/3] Loop over table rows with seamm.Table; commit after each body step Also fixes the row selection failing for numeric columns when the second value is empty (it is only needed for between). Co-Authored-By: Claude Opus 5.5 --- loop_step/loop.py | 106 +++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 73 deletions(-) diff --git a/loop_step/loop.py b/loop_step/loop.py index ba7c01b..96915d0 100644 --- a/loop_step/loop.py +++ b/loop_step/loop.py @@ -80,7 +80,6 @@ def __init__(self, flowchart=None, extension=None): """ logger.debug("Creating Loop {}".format(self)) - self.table_handle = None self.table = None self._loop_count = None self._loop_value = None @@ -317,17 +316,16 @@ def run(self): self.set_variable("_loop_indices", (None,)) elif P["type"] == "For rows in table": if self._loop_value is None: - self.table_handle = self.get_variable(P["table"]) - self.table = self.table_handle["table"] - self.table_handle["loop index"] = True + self.table = self.get_table(P["table"], create=False) + self.table["loop index"] = True self.logger.info( "Initialize loop over {} rows in table {}".format( - self.table.shape[0], P["table"] + self.table.n_rows, P["table"] ) ) self._loop_value = 0 - self._loop_length = self.table.shape[0] + self._loop_length = self.table.n_rows if self.variable_exists("_loop_indices"): tmp = self.get_variable("_loop_indices") self.set_variable( @@ -341,77 +339,33 @@ def run(self): self.set_variable("_loop_indices", (None,)) where = P["where"] if where == "Use all rows": - table_indices = [*self.table.index] - n_table_indices = len(table_indices) + table_rows = [row for row, _ in self.table.rows()] elif where == "Select rows where column": - tmp_col = P["query-column"].lower() - column = None op = P["query-op"] - - for col in self.table: - if col.lower() == tmp_col: - column = col - if column is None: + if op not in seamm.table.operators: + raise NotImplementedError(f"Loop query '{op}' not implemented") + try: + selection = ( + P["query-column"], + op, + P["query-value"], + P["query-value2"], + ) + table_rows = [ + row for row, _ in self.table.rows(where=selection) + ] + except ValueError as e: + if "has no column" not in str(e): + raise column = P["query-column"] raise ValueError( f"Looping over table with criterion on column '{column}': " "that column does not exist." ) - - dtype = self.table.dtypes[column] - value = dtype.type(P["query-value"]) - value2 = dtype.type(P["query-value2"]) - - # Find the indices - table_indices = [] - for i, row_value in zip(self.table.index, self.table[column]): - if op == "==": - if row_value == value: - table_indices.append(i) - elif op == "!=": - if row_value != value: - table_indices.append(i) - elif op == ">": - if row_value > value: - table_indices.append(i) - elif op == ">=": - if row_value >= value: - table_indices.append(i) - elif op == "<": - if row_value < value: - table_indices.append(i) - elif op == "<=": - if row_value <= value: - table_indices.append(i) - elif op == "between": - if row_value >= value and row_value <= value2: - table_indices.append(i) - elif op == "contains": - if value in row_value: - table_indices.append(i) - elif op == "does not contain": - if value not in row_value: - table_indices.append(i) - elif op == "contains regexp": - if re.search(value, row_value) is not None: - table_indices.append(i) - elif op == "does not contain regexp": - if re.search(value, row_value) is None: - table_indices.append(i) - elif op == "is empty": - # Might be numpy.nan, and NaN != NaN hence odd test. - if row_value == "" or row_value != row_value: - table_indices.append(i) - elif op == "is not empty": - if row_value != "" and row_value == row_value: - table_indices.append(i) - else: - raise NotImplementedError( - f"Loop query '{op}' not implemented" - ) - n_table_indices = len(table_indices) else: raise NotImplementedError(f"Loop cannot handle '{where}'") + table_indices = [self.table.label(row) for row in table_rows] + n_table_indices = len(table_indices) printer.important( __( f"The loop will have {n_table_indices} iterations.\n\n", @@ -579,11 +533,10 @@ def run(self): self.set_variable("_loop_indices", tmp[0:-1]) self.set_variable("_loop_index", tmp[-2]) - # and the other info in the table handle - self.table_handle["loop index"] = False + # and the other info in the table + self.table["loop index"] = False self.table = None - self.table_handle = None self.logger.info( "The loop over table " @@ -604,7 +557,7 @@ def run(self): " --> {}".format(self.get_variable("_loop_indices")) ) self.set_variable("_loop_index", index) - self.table_handle["current index"] = index + self.table.current_row = table_rows[self._loop_value - 1] # Name of directory is the index (+1 since tends to be 0 based) if index_is_int: @@ -612,7 +565,11 @@ def run(self): else: self._custom_directory_name = self.safe_filename(str(index)) - row = {k: self.table.at[index, k] for k in self.table} + row = { + k: v + for k, v in self.table.get_row().items() + if k != self.table.index_column + } self.set_variable("_row", row) if P["as variables"]: for key, value in row.items(): @@ -696,6 +653,9 @@ def run(self): # Run through the steps in the loop body try: next_node = next_node.run() + # Commit the step's table and structure changes, as the flowchart + # evaluator does for steps outside loops. + seamm.flowchart_variables.get_variable("_system_db").db.commit() except DeprecationWarning as e: printer.normal("\nDeprecation warning: " + str(e)) traceback.print_exc(file=sys.stderr) From 8327ae60687124b38e8aac868f63260283a61ef5 Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Sat, 3 Oct 2026 11:39:22 -0400 Subject: [PATCH 2/3] Use seamm.step_completed after each body step Co-Authored-By: Claude Opus 5.5 --- loop_step/loop.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/loop_step/loop.py b/loop_step/loop.py index 96915d0..7ba40ff 100644 --- a/loop_step/loop.py +++ b/loop_step/loop.py @@ -652,10 +652,9 @@ def run(self): # Run through the steps in the loop body try: + node = next_node next_node = next_node.run() - # Commit the step's table and structure changes, as the flowchart - # evaluator does for steps outside loops. - seamm.flowchart_variables.get_variable("_system_db").db.commit() + seamm.step_completed(node) except DeprecationWarning as e: printer.normal("\nDeprecation warning: " + str(e)) traceback.print_exc(file=sys.stderr) From 4fdb79981d9c38c92a93b79a9f2bedf12b9dd2cf Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Sat, 3 Oct 2026 14:17:46 -0400 Subject: [PATCH 3/3] Release 2026.10.3: pin seamm>=2026.10.3; HISTORY; CI on uv (no test_env.yaml) Co-Authored-By: Claude Opus 5.5 --- HISTORY.rst | 7 ++++++ devtools/conda-envs/test_env.yaml | 36 ------------------------------- requirements.txt | 2 +- 3 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 devtools/conda-envs/test_env.yaml diff --git a/HISTORY.rst b/HISTORY.rst index f5d17f8..88d72c7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,13 @@ ======= History ======= +2026.10.3 -- Loops over tables in the job's database + * Loops over the rows of a table work with tables kept in the job's database (seamm + 2026.10.3); the job's database is committed after each step of the loop body. + * Bugfix: selecting rows with a test on a numeric column failed when the second + value was empty, although only "between" uses it. + * Rows can be selected on the table's index column too. + * The values of ``_row`` and of the row variables are plain Python values. 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 diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml deleted file mode 100644 index d02a912..0000000 --- a/devtools/conda-envs/test_env.yaml +++ /dev/null @@ -1,36 +0,0 @@ -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 - - # Compiled packages: from conda-forge so the binaries are consistent - - numpy - - openbabel - - psutil - - rdkit - - # Testing - - black - - codecov - - flake8 - - pytest - - pytest-cov - - # Documentation - - pydata-sphinx-theme - - sphinx-design - - # Pure-Python packages: from PyPI - - pip: - - seamm>=2026.9.18.1 - # Documentation - - sphinx-copybutton diff --git a/requirements.txt b/requirements.txt index f04a8bd..80fa3e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ psutil -seamm>=2026.9.18.1 +seamm>=2026.10.3