From caa49d603def4e66f810d3c0c5a758815402772a Mon Sep 17 00:00:00 2001 From: DanOlds Date: Thu, 20 Aug 2026 17:54:22 -0400 Subject: [PATCH 1/2] fix: run() normalization crashed on null tables from server failure responses Early server-side failures serialize unit_cell_data/peak_list_data as explicit nulls; .get(key, {}) passes None through and None.items() raised 'NoneType' object has no attribute 'items' client-side, masking the real refinement error. Found debugging a live PowderLLM session (server request completed in 0.3s; client crashed normalizing the failure payload). Co-Authored-By: Claude Fable 5 --- src/powderline/_code_hash.json | 2 +- src/powderline/kicker.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/powderline/_code_hash.json b/src/powderline/_code_hash.json index 1110dcc..0b5fdca 100644 --- a/src/powderline/_code_hash.json +++ b/src/powderline/_code_hash.json @@ -1,4 +1,4 @@ { "schema_version": "0.26.0", - "kicker_hash": "ec29f60ae1d7a0ad3433d9ab67418050" + "kicker_hash": "786bb51cf296c35d55327db299b7420c" } diff --git a/src/powderline/kicker.py b/src/powderline/kicker.py index 0cf6d6e..5160972 100644 --- a/src/powderline/kicker.py +++ b/src/powderline/kicker.py @@ -3468,13 +3468,15 @@ def run( result['fit_profile'] = ( pd.DataFrame(fit_profile_raw) if fit_profile_raw else pd.DataFrame() ) + # `or {}` (not a .get default): early-failure results serialize these + # tables as an explicit null, which .get(key, {}) passes through as None. result['unit_cell_data'] = { phase: pd.DataFrame(records) - for phase, records in result.get('unit_cell_data', {}).items() + for phase, records in (result.get('unit_cell_data') or {}).items() } result['peak_list_data'] = { phase: pd.DataFrame(records) - for phase, records in result.get('peak_list_data', {}).items() + for phase, records in (result.get('peak_list_data') or {}).items() } refined_params_raw = result.get('refined_parameters') result['refined_parameters'] = ( From 2d8b26cac1d9b16a7975db9c3e5e8f0a5d3aa2df Mon Sep 17 00:00:00 2001 From: DanOlds Date: Thu, 20 Aug 2026 18:50:25 -0400 Subject: [PATCH 2/2] fix: surface GSAS-II's refinement failure message instead of a silent no-Rwp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G2Project.refine() discards G2strMain.Refine's (OK, Rvals) return, so errors like 'Invalid metric tensor for phase #0 — check for refinement of conflicting variables' reached only the console; callers (and any agent driving run()) saw only 'produced no Rwp'. New _refine_with_message mirrors the scriptable wrapper's non-sequential steps to keep Rvals['msg'], with a fallback to the plain proj.refine() if GSAS-II internals change. Verified against a live failure case (CeO2, cell+scale fit diverging on a restricted range). Co-Authored-By: Claude Fable 5 --- src/powderline/_code_hash.json | 2 +- src/powderline/kicker.py | 49 +++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/powderline/_code_hash.json b/src/powderline/_code_hash.json index 0b5fdca..8de559c 100644 --- a/src/powderline/_code_hash.json +++ b/src/powderline/_code_hash.json @@ -1,4 +1,4 @@ { "schema_version": "0.26.0", - "kicker_hash": "786bb51cf296c35d55327db299b7420c" + "kicker_hash": "53db8708bbdfa12496f974e89a1befbe" } diff --git a/src/powderline/kicker.py b/src/powderline/kicker.py index 5160972..c2fd134 100644 --- a/src/powderline/kicker.py +++ b/src/powderline/kicker.py @@ -2837,6 +2837,41 @@ def set_refinement_cycles(proj: Any, num_cycles: int, print_info: bool = False) print(f"Set number of refinement cycles to {num_cycles}") +def _refine_with_message(proj) -> tuple[bool, str]: + """Run the project refinement, returning (ok, GSAS-II failure message). + + ``G2Project.refine()`` calls ``G2strMain.Refine`` and DISCARDS its + ``(OK, Rvals)`` return, so failure text like "Invalid metric tensor for + phase #0" reaches only the console — callers see a silent no-Rwp failure. + Mirror the non-sequential branch of ``refine()`` (index, constraint + check, Refine, reload) to keep ``Rvals['msg']``. Any surprise from + GSAS-II internals falls back to the plain ``proj.refine()`` so behavior + is never worse than before. + """ + try: + from GSASII import GSASIIstrIO as G2stIO + from GSASII import GSASIIstrMain as G2strMain + + seq_setting = proj.data['Controls']['data'].get('Seq Data', []) + if not seq_setting: + proj.index_ids() # saves the project, as refine() does + errmsg, _warnmsg = G2stIO.ReadCheckConstraints(proj.filename) + if errmsg: + return False, f"Constraint error: {errmsg}" + ret = G2strMain.Refine(proj.filename, makeBack=False) + proj.reload() + ok, rvals = (ret if isinstance(ret, tuple) and len(ret) == 2 + else (True, {})) + msg = rvals.get('msg', '') if isinstance(rvals, dict) else '' + msg = msg.replace('**** ERROR: Refinement failed ****', '').strip() + return bool(ok), msg + except Exception: + pass # GSAS-II internals changed: fall back to the plain call below + + proj.refine() + return True, '' + + def execute_rietveld_refinement( proj: Any, hist: Any, @@ -2865,17 +2900,23 @@ def execute_rietveld_refinement( print(f" Cycles: {controls.refinement_cycles}") print(f"{'='*60}\n") - # Execute refinement - proj.refine() + # Execute refinement (keeping GSAS-II's failure message, which + # G2Project.refine() would otherwise discard) + refine_ok, g2_msg = _refine_with_message(proj) # Extract Rwp rwp_final = hist.residuals.get("wR") - if rwp_final is None: + if not refine_ok or rwp_final is None: + if g2_msg: + error = f"Rietveld refinement failed: {' '.join(g2_msg.split())}" + else: + error = ("Rietveld refinement produced no Rwp — proj.refine() " + "may have failed silently") return { 'success': False, 'rwp': None, - 'error': "Rietveld refinement produced no Rwp — proj.refine() may have failed silently", + 'error': error, } if verbose: