PowderLine Developer Guide
This guide provides comprehensive context for developers working on PowderLine (and for AI coding assistants used alongside them). Read this first to understand the project architecture, design decisions, and development workflows.
Project Overview
What is PowderLine? PowderLine automates crystallographic refinement from a declarative JSON “recipe” and drives interchangeable refinement engines — GSAS-II by default, with Bruker TOPAS v7 and open-source easydiffraction as optional alternatives. It takes a recipe describing refinement parameters and produces standardized output reports. The goal is to make powder diffraction refinements reproducible, automatable, and accessible.
Scope:
Command-line tool that runs refinements from JSON recipes, dispatching to interchangeable engines (GSAS-II by default; TOPAS v7 and easydiffraction optional)
Schema 0.26.0: Simplified two-workflow system (Rietveld refinement or single peak fitting)
Schema validation using Pydantic
Single-phase and multi-phase Rietveld refinements; phase-less single peak fitting
Standard output reports (fit profiles, unit cell parameters with ESDs, peak lists,
refined_parameters.csv)Programmatic Python API:
powderline.run()with DataFrame returnsExample-driven design: schema evolves based on real refinement cases
Design Philosophy:
Example-driven: Don’t build features speculatively - add them when real examples need them
Schema-first: Validate inputs before any GSAS-II operations
Fail fast: Detect errors early with helpful messages
Reproducible: Same JSON should produce identical refinement results
Schema Structure
Current schema: 0.26.0 (payload-based structure with fixed output filenames)
Top-Level Structure
All recipes use a three-key structure:
{
"schema_name": "GSASII_Rietveld", // or "GSASII_SPF"
"schema_version": "0.26.0",
"payload": {
// All refinement parameters here
}
}
Key field: schema_name - Selects the workflow executor:
"GSASII_Rietveld": Full Rietveld structural refinement"GSASII_SPF": Single peak fitting (peak-based analysis without crystal structure)
Payload Structure
The payload contains all refinement parameters organized by concern:
"payload": {
"xrd_data": {...}, // Required: Diffraction pattern
"instrument": {...}, // Required: Beamline/instrument config
"phases": {...}, // Required for Rietveld, null for SPF
"background": {...}, // Optional: Background model (defaults to empty if omitted)
"refinement_controls": {...}, // Optional: Cycles, convergence
"fit_range": [min, max], // Optional: 2θ range limits
"single_peaks": {...}, // Required for SPF, optional otherwise
"single_peak_fitting_mode": {...} // For SPF workflows
}
Required fields vary by schema_name:
Rietveld (
GSASII_Rietveld): Must havephasesdefinedSPF (
GSASII_SPF): Must havesingle_peaksandsingle_peak_fitting_mode
Output Files (Fixed Names)
PowderLine uses fixed output filenames independent of sample identity:
dummy.gpx: GSAS-II project file (reopenable in GUI)dummy.lst: Human-readable refinement log (Rwp, parameter tables)<phase>_unit_cell_report.csv: Refined unit cell (one per phase)<phase>_peak_list_report.csv: Reflection list (one per phase)fit_profile.txt: Observed/calculated/difference intensities
Rationale: Fixed filenames eliminate variability in automation/testing. Sample identity should be managed externally (directory names, databases, metadata systems).
Refined Parameters Export with ESDs
File: refined_parameters.csv (9 columns)
PowderLine exports all refined parameters with estimated standard deviations (ESDs) after each refinement. This provides a structured, machine-readable record of refinement results with uncertainty quantification.
Column structure:
parameter_name: GSAS-II internal parameter identifier (e.g.,::0:Scale,::A0,0::A0)descriptive_name: Human-readable description (e.g., “Scale factor for phase LaB6”, “Background coefficient A0”)phase_name,phase_idx: Phase associations for phase-specific parameters (null otherwise)atom_name,atom_idx: Atom associations for atom-specific parameters (null otherwise)value: Refined parameter valueesd: Estimated standard deviation from covariance matrixcategory: Parameter classification (instrument, background, cell, atom_position, atom_displacement, HAP, peak_broadening)
Implementation: export_refined_parameters_csv() in kicker.py
ESD calculation:
Primary source: GSAS-II covariance matrix (
proj.data['Covariance'])Fallback: A
.lst-parsing fallback exists inkicker.py(extract_refined_params_from_lst) but is currently disabled at its call site. It is preserved pending a test that confirms covariance matrix ESDs match what.lstparsing previously produced. Until that test exists, the covariance matrix is the sole ESD source.Note: The
.lstfallback was incomplete — it only covered instrument, background, and unit cell parameters, not atom or HAP parameters.
Unit cell ESDs: Uses
G2lat.getCellEsd()to convert reciprocal metric tensor ESDs to direct lattice parametersFunction:
calculate_cell_esds_from_A_matrix()in kicker.pyGSAS-II stores cell as reciprocal metric tensor (A-matrix); ESDs require conversion
Parameter name parsing:
parse_parameter_associations()in kicker.py: Extracts phase/atom indices from GSAS-II parameter namesget_descriptive_param_name()in kicker.py: Generates human-readable descriptionsbuild_phase_name_mapping()in kicker.py: Maps phase indices to namesbuild_atom_name_mapping()in kicker.py: Maps atom indices to labels
Phase-specific unit cell reports:
In addition to the comprehensive refined_parameters.csv, each phase generates <phase>_unit_cell_report.csv with 3 columns:
parameter: a, b, c, alpha, beta, gamma, volumevalue: Refined unit cell parameteresd: Estimated standard deviation
Note on simulations: Simulation mode (all refine_flag=false) does not generate refined_parameters.csv since no parameters are refined. Unit cell reports still generated with esd=0.0.
Schema Evolution
See SCHEMA_HISTORY.md for historical context.
IMPORTANT — Schema Version Discipline:
The GSAS-II server caches kicker.py at startup. If kicker.py changes (even without recipe format changes), a running server will use stale code. The schema_version in recipes is validated against EXPECTED_SCHEMA_VERSION in schema.py, so bumping the version forces a server restart. You MUST bump EXPECTED_SCHEMA_VERSION whenever kicker.py output behavior changes (new output files, format changes, column additions, etc.), even if the recipe input format is unchanged. Update all input.json files to match.
Domain Essentials
Rietveld Refinement Basics
What is Rietveld refinement? A method for refining crystal structures against powder diffraction data by fitting a calculated pattern to the observed pattern. The refinement adjusts parameters (unit cell, atom positions, peak shapes, background) to minimize the difference between calculated and observed intensities.
Key Quality Metric: Rwp (weighted residual) $\(R_{wp} = \sqrt{\frac{\sum w_i (y_{obs,i} - y_{calc,i})^2}{\sum w_i y_{obs,i}^2}} \times 100\%\)$
Lower Rwp indicates better fit. Typical values: 5-15% for good refinements.
Peak Profiles: GSAS-II uses Thompson-Cox-Hastings pseudo-Voigt profiles - a convolution of Gaussian and Lorentzian functions. Profile shape controlled by:
Instrument broadening (U, V, W parameters) - due to instrumental effects
Sample broadening (size, strain) - due to crystallite size and microstrain
Parameter Types
PowderLine organizes refinement parameters into logical groups:
Structural Parameters (Phase-specific):
Unit cell: a, b, c, α, β, γ define crystal lattice dimensions
Atom parameters: positions (x, y, z), occupancy, thermal parameters
Space group: symmetry operations (defined in CIF, not refined)
Intensity Parameters (Phase-specific):
Scale factor: Converts calculated intensities to absolute scale
Preferred orientation: Non-random crystallite orientations (not implemented yet)
Peak Shape Parameters (Phase-specific):
Sample broadening: Crystallite size, microstrain
Background Parameters:
Chebyshev polynomials: Smooth curved background
Single peaks: Individual pseudo-Voigt peaks for known impurities
Background Default Behavior
The background field is optional in the payload. If omitted (background: null):
No Chebyshev coefficients are initialized
No background single peaks are added
GSAS-II uses an empty background (effectively zero)
This is useful when background has been pre-subtracted from XRD data.
Instrument Parameters:
Instrument broadening: U, V, W (Gaussian), X, Y, Z (Lorentzian)
Profile asymmetry: Axial divergence, sample transparency
Wavelength: X-ray or neutron wavelength
Zero shift: 2θ offset correction
Polarization: For synchrotron data
The [value, refine_flag, min, max] Pattern
Throughout the JSON schema and code, you’ll see parameters represented as 4-element lists:
"wavelength": [0.45236, false, null, null]
value: Current parameter value
refine_flag:
true= refine this parameter,false= hold fixedmin/max: Parameter bounds (placeholders for future - GSAS-II may not support bounds)
This mirrors GSAS-II’s internal parameter representation.
Phase vs Histogram Parameters
Critical distinction (confusing in GSAS-II):
Phase parameters: Belong to the crystal structure (scale, unit cell, atoms, space group, sample broadening)
Histogram parameters: Belong to the measurement (instrument broadening, background)
In multi-phase refinements, each phase has its own structural parameters, but histogram parameters are shared across phases (except scale factors, which are phase-histogram pairs).
Atom Parameter Refinement
Refine Flags String Format: GSAS-II uses a single string at atom record index 2 to encode which parameters are refined:
‘F’: Refine occupancy (fraction)
‘X’: Refine fractional coordinates (x, y, z) - a single lumped flag for all three
‘U’: Refine displacement parameters (isotropic or anisotropic)
Combinations: ‘FXU’ (all), ‘XU’ (coordinates + displacement), ‘F’ (occupancy only), etc.
Per-Parameter Semantics (Schema 0.26): Although GSAS-II exposes only the lumped ‘X’ flag (and one ‘U’ flag for all six anisotropic components), PowderLine honors the recipe’s per-parameter flags: a coordinate or Uij component refines iff it is present with refine_flag=true; absent or false means fixed. This uses the same lumped-flag + “Hold”-constraint mechanism as unit-cell parameters — see Unit Cell Refinement Semantics (Schema 0.26) and src/powderline/constraints.py. For atoms, only site-symmetry-linked parameters remain coupled: e.g. x and y on an (x,x,z) site form one degree-of-freedom group that refines if either is requested; flags on symmetry-fixed parameters have no effect.
Displacement Parameter Types (ADP field):
Each atom has an atom-specific ADP field indicating which displacement type to use:
Structure:
phases[phase]['structure']['atoms'][label]['ADP']indicates which type is usedParameterization:
phases[phase]['parameterization']['atoms'][label]['ADP']indicates which type to refine
Values:
‘Uiso’: Isotropic displacement (single scalar U value)
‘Uaniso’: Anisotropic displacement (6 values: U11, U22, U33, U12, U13, U23)
The parameterization ADP controls atom_record[9] (‘I’ or ‘A’) in GSAS-II.
Dictionary Structure Examples:
Structure (embedded in payload):
"phases": {
"LaB6": {
"structure": {
"atoms": {
"La": {
"element": "La",
"x": 0.0, "y": 0.0, "z": 0.0,
"occupancy": 1.0,
"ADP": "Uiso",
"Uiso": 0.00858,
"Uaniso": null
}
}
}
}
}
Parameterization (refinement settings):
"phases": {
"LaB6": {
"parameterization": {
"atoms": {
"La": {
"x": [null, true, null, null],
"y": [null, false, null, null],
"z": [null, false, null, null],
"occupancy": [null, true, null, null],
"ADP": "Uiso",
"Uiso": [null, true, null, null],
"Uaniso": null
}
}
}
}
}
Anisotropic Displacement Parameters:
If using ADP="Uaniso", all 6 anisotropic U values (U11, U22, U33, U12, U13, U23) must be provided in both structure and parameterization:
"atoms": {
"Li": {
"ADP": "Uaniso",
"Uiso": 0.01, // Can provide both - ADP dictates which is used
"Uaniso": {
"U11": 0.012, "U22": 0.011, "U33": 0.010,
"U12": 0.001, "U13": 0.001, "U23": 0.001
}
}
}
Mixed ADP Types: It is common (and allowed) for one atom to have ADP="Uaniso" while another has ADP="Uiso" in the same phase.
Glossary
CIF: Crystallographic Information File - standard format for crystal structures (In PowderLine, structural information is provided as embedded dictionaries, not CIF files)
GPX: GSAS-II project file (binary format)
LST: GSAS-II refinement log file with Rwp and parameter tables
CHI: Two-column ASCII file (2θ, intensity) for powder diffraction data (Note: PowderLine uses embedded data arrays, not files. This format is for reference only)
INSTPRM: GSAS-II instrument parameter file (Note: PowderLine uses embedded instrument parameters, not files. This format is for reference only)
Recipe: PowderLine’s JSON input describing a refinement workflow
Multiplicity: Number of symmetry-equivalent reflections
d-spacing: Interplanar spacing for an hkl reflection
2θ (two-theta): Diffraction angle in degrees
hkl: Miller indices describing a crystallographic plane
GSAS-II Integration
Why Direct Dictionary Manipulation?
PowderLine manipulates the proj.data dictionary directly rather than using GSAS-II’s phase/histogram objects. This decision was made because:
Phase object ambiguity: Unclear how G2Phase initialization maps to parameter setting
Explicit control: Direct dictionary access makes parameter locations clear
Less abstraction: Easier to debug when you can see exact data structure
Trade-off: More brittle if GSAS-II internal structure changes (acceptable risk for Phase 1)
The proj.data Dictionary Structure
GSAS-II stores all project data in a nested dictionary. Key access patterns:
# Instrument parameters (list format: [current_value, value, refine_flag]). See kicker.py for setting functions
curr_wavelength = proj.data[hist.name]['Instrument Parameters'][0]['Lam'][1] # before setting, can get value from initialization (e.g., when xrd_data is added)
proj.data[hist.name]['Instrument Parameters'][0]['Lam'][1] = wavelength
proj.data[hist.name]['Instrument Parameters'][0]['Lam'][2] = refine_flag
# Background parameters
proj.data[hist.name]['Background'] = [
[chebyshev, no_peaks, background_type], # Background metadata
{'nDebye': 0, 'debyeTerms': [], ...}, # Debye-Scherrer params
{'nPeaks': n, 'peaksList': [...]}, # Single peaks
]
# Histogram-atom parameters (HAP)
# Change phase intensity scale given a value and refine_flag from payload for a phase's parameterization:
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Scale'][0] = value
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Scale'][1] = refine_flag
# Change isotropic size broadening for a given phase from the payload for a phase's parameterization:
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Size'][0] = model # assume model is "isotropic" in this example
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Size'][1][0] = value # set isotropic crystallite size value
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Size'][2][0] = refine_flag # set isotropic size value refinement flag (if not None)
# Change isotropic mustrain broadening for a given phase from the payload for a phase's parameterization:
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Mustrain'][0] = model # assume model is "isotropic" in this example
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Mustrain'][1][0] = value # set isotropic mustrain value
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Mustrain'][2][0] = refine_flag # set isotropic mustrain value refinement flag (if not None)
# Phase parameters
proj.data['Phases'][phase_name]['General']['Cell'] = [refine_cell, a, b, c, alpha, beta, gamma, volume]
Known GSAS-II Quirks
These inconsistencies exist in GSAS-II (as of 2025) and are documented in kicker.py comments:
Size/Strain Documentation Conflict:
GSAS-II API does not document structure of dicts / lists well
PowderLine uses correct extended format
Unit Cell Refinement:
GSAS-II natively exposes a single refine flag for all 6 unit cell parameters; since schema 0.26 PowderLine honors per-parameter flags instead.
See Unit Cell Refinement Semantics (Schema 0.26) for the full mechanism and symmetry-coupling rules.
Reflection List Headers:
GSAS-II provides 15-column reflection arrays without headers
PowderLine adds headers manually: h, k, l, multiplicity, d_spacing, 2theta, etc.
Peak Broadening Models
Schema Enhancement: Size and strain broadening support model branching with schema-level validation.
Size Broadening Models
PowderLine supports three size broadening models matching GSAS-II’s capabilities:
Model |
Parameters |
GSAS-II Structure |
Status |
|---|---|---|---|
isotropic |
|
|
✅ Implemented |
uniaxial |
|
|
⚠️ NotImplementedError |
ellipsoidal |
|
|
⚠️ NotImplementedError |
Usage (isotropic only):
"size_broadening": {
"model": "isotropic",
"isotropic_size": [100.0, true, null, null],
"LG_eta": [0.5, false, null, null]
}
Future models raise NotImplementedError at schema validation:
# This will fail during recipe validation:
"size_broadening": {
"model": "uniaxial", # Error: Not yet implemented
...
}
Strain Broadening Models
Strain broadening supports three models with increasing complexity:
Model |
Parameters |
GSAS-II Structure |
Status |
|---|---|---|---|
isotropic |
|
|
✅ Implemented |
uniaxial |
|
|
⚠️ NotImplementedError |
generalized (Stephens) |
Symmetry-dependent parameters |
Complex Laue class-specific |
⚠️ NotImplementedError |
Usage (isotropic only):
"strain_broadening": {
"model": "isotropic",
"isotropic_strain": [0.001, true, null, null],
"LG_eta": [0.8, false, null, null]
}
Note on Stephens Model: The generalized strain model uses Stephens formalism where the number and meaning of parameters depend on the crystal’s Laue class (cubic, hexagonal, etc.). Implementation deferred to Phase 2 due to complexity.
Implementation Details
Model Branching (kicker.py):
def set_phase_size_broadening(proj, hist, phase_name, size_dict, print_info=False):
"""Set size broadening with model-specific branching."""
model = size_dict.get('model', 'isotropic') # Default for backward compat
# Set model type in GSAS-II
proj.data['Phases'][phase_name]['Histograms'][hist.name]['Size'][0] = model
# Branch on model
if model == 'isotropic':
_set_isotropic_size_broadening(...) # Current implementation
elif model in ['uniaxial', 'ellipsoidal']:
raise NotImplementedError(f"{model} size broadening not yet supported")
Schema Validation (schema.py):
class SizeBroadening(BaseModel):
model: Literal["isotropic", "uniaxial", "ellipsoidal"] = "isotropic"
# ... parameters for all models ...
@model_validator(mode='after')
def validate_model_implementation(self) -> Self:
if self.model != 'isotropic':
raise NotImplementedError(
f"{self.model} size broadening not yet implemented. "
"Use 'isotropic' model instead."
)
return self
Design Rationale:
Schema documents complete API (all models) even if not implemented
Clear error messages guide users to supported features
Foundation for Phase 2 implementation without breaking changes
Validation happens early (recipe parsing), not during refinement
Architecture & Design Decisions
Module Responsibilities
src/powderline/kicker.py (the refinement engine):
CLI entry point via argparse
Recipe loading, template detection, validation
GSAS-II project initialization
All parameter setting functions (background, phases, instrument, unit cell, etc.)
Refinement execution
Output generation (CSV reports, fit profiles)
Role: Complete workflow orchestration + implementation
src/powderline/schema.py:
Pydantic models matching JSON recipe structure
Nested validation: RecipeModel → InstrumentModel, PhaseModel, BackgroundModel
Custom validators for coefficient counts, list lengths, fit range
Role: Input validation and type safety
src/powderline/gsas_server.py / gsas_client.py:
Persistent FastAPI server keeping GSAS-II loaded in memory, and the HTTP client that talks to it (with in-process subprocess fallback)
Role: Execution backends for
run()’sserver/automodes
src/powderline/topas/ + src/powderline/easydiff/ + src/powderline/engine.py:
Alternate translation engines — TOPAS v7 (recipe →
.inp/.xye,tc.exerunner, results round-trip; standalonetopas-kickerCLI) and easydiffraction (recipe → easydiffractionProject, lmfit fit) — plus the engine dispatcher behindpowderline.run(recipe, output_dir, engine="gsasii"|"topas"|"easydiffraction"). Both alternate paths import zero GSAS-II; thegsasiibranch is a transparent pass-through tokicker.run.Role: Alternate refinement engines + engine dispatch
tests/:
test_example_LaB6_regression.py: End-to-end regression test with exact output matchingconftest.py: Pytest fixtures for paths and test dataRole: Quality assurance - detect refinement result changes
Monolithic Design Rationale:
Phase 1 keeps all implementation in kicker.py intentionally:
Simplifies development during schema evolution
Easy to understand complete workflow in one file
Refactoring to modules planned for Phase 2 when API stabilizes
Comments saying “upstream” or “will be handled elsewhere” refer to future refactoring
Adding a Refinement Engine
PowderLine’s engine seam makes new backends cheap to add. The dispatcher
(src/powderline/engine.py) is a small _ENGINES tuple plus one lazy-import
branch per engine; every engine returns the same result-dict shape, locked
by tests/test_api.py. The TOPAS (src/powderline/topas/) and easydiffraction
(src/powderline/easydiff/) subpackages are the two reference implementations.
The established pattern:
New subpackage
src/powderline/<engine>/holding all engine logic: translation (builder/writer), execution (runner), result parsing, unit conversions, and arun_<engine>_recipe(recipe, output_dir, ...)adapter that returns the locked result dict.One lazy-import branch in
powderline/engine.py(add the name to_ENGINES). Import the engine module only inside that branch so machines without the engine’s dependencies can still use the others.No schema changes — engines translate the existing
GSASII_*recipes; they do not define their own recipe format.Reject unsupported features loudly — a recipe feature the engine cannot represent raises a translation error (or, if it’s a fixed unmappable value, is dropped with a recorded warning). Never silently ignore a refine flag.
Optional pixi feature/environment if the engine needs dependencies the default environment shouldn’t carry (e.g. easydiffraction needs Python ≥3.12, so it lives in the optional
easydiffenvironment).Keep it GSAS-II-free if it can be — the alternate engines import zero GSAS-II, a property enforced by subprocess import-block tests.
Simulation-mode semantics across engines. The GSAS-II engine treats
refinement_cycles == 1 as simulation and rejects any refine flag set in that
mode (validate_simulation_mode_parameters in kicker.py). The alternate
engines (TOPAS, easydiffraction) instead treat “no parameters flagged for
refinement” as simulation and do not enforce the cycles == 1 lock. For
well-formed recipes the behavior matches; the two definitions only diverge on
inconsistent input (cycles == 1 with a stray refine flag), which GSAS-II
refuses and the alternate engines would run. Uniform input-contract enforcement
across all engines is a tracked follow-up (see the PowderLine-devkit
easydiffraction dossier — a private maintainers’ repo, access on request).
Schema Design: Why extra='allow'?
Nearly all Pydantic models use ConfigDict(extra='allow') to permit fields not explicitly defined (the one exception is RefinementParameterModel, which uses extra='forbid'):
class RecipeModel(BaseModel):
model_config = ConfigDict(extra='allow')
Rationale:
Schema evolution: Example-driven design means new fields emerge as examples are added
Backwards compatibility: Old recipes continue working when new fields are added
Flexibility: Users can add custom metadata without validation errors
Trade-off: Typos in field names won’t be caught (acceptable for Phase 1 iteration speed)
Future phases may tighten validation to extra='forbid' once schema stabilizes.
The “Upstream” Pattern
You’ll see several TODO markers like # TODO: validation will be handled upstream throughout kicker.py. This means:
Currently: Validation happens in helper functions (e.g., check coefficient count in
set_chebyshev_background)Future: Validation moves to Pydantic schema (separation of concerns)
Not implemented yet: Still evolving what validation logic belongs where
The pattern reflects Phase 1’s iterative approach - implement in helper functions first, refactor to schema later.
File-less Schema Architecture
PowderLine uses file-less JSON payloads. All data (XRD patterns, structures, instrument parameters) is embedded directly in JSON. File handling (loading XRD data from .chi files, parsing CIF structures, reading instrument parameters) occurs upstream of PowderLine.
Embedded Data Format:
XRD data: “xrd_data” contains “tth”, “Itth”, “Itth_weights” arrays
Structure: “structure” contains “space_group”, “unit_cell”, “atoms” dictionaries
Instrument: “instrument” contains “initialization” and optional “parameterization” dictionaries
This design enables PowderLine to focus on refinement logic while delegating file I/O to upstream tools.
Phase Name Resolution
In PowderLine, phase names come from the phase_name field in the embedded structure dictionary:
# Phase name is explicitly specified in structure
"phases": {
"LaB6": { # This becomes the phase identifier
"structure": {
"phase_name": "LaB6", # Must match the key above
"space_group": "P m -3 m",
...
}
}
}
The phase name is used consistently throughout:
Phase dictionary keys
Structure
phase_namefield (must match key)Parameterization references
Output file naming
This ensures consistent phase identification throughout refinement and output generation.
Server Architecture (Three-Layer Execution Stack)
powderline.run() dispatches through three layers:
powderline.run() / CLI
│ validates recipe, normalises DataFrames
▼
GSASClient.submit_simulation()
│ routes: HTTP server ──or── in-process fallback
▼
run_refinement()
│ single execution engine, returns JSON-serializable primitives
▼
GSAS-II (GSASIIscriptable)
_submit_via_subprocess is in-process, not an OS subprocess. Despite its name,
this method calls run_refinement() directly in the current interpreter. The name
has been retained for API stability; if crash-isolation is needed in a future phase
it can be upgraded to multiprocessing without changing callers.
execution_mode vs method — two distinct concepts:
powderline.run()acceptsexecution_mode=('auto'|'server'|'subprocess') as its public input parameter.run_refinement()acceptsmethod=as its internal parameter (values:'server'|'subprocess'|'test').Both
run_refinement()andGSASClientincludemethodas a result dict output key (result['method']) recording which execution path was used.These are not a renamed pair — they serve different roles at different layers of the stack.
The FastAPI server (gsas_server.py) keeps GSAS-II imported in one persistent
process, amortising the ~2.6 s import cost across all requests. Requests are handled
synchronously (one-at-a-time) by design — concurrency is not needed for the current
single-user CLI and programmatic-API use cases.
After modifying any Python source file (kicker.py, schema.py, or any other
module under src/), restart the server so the running process loads the new code:
pixi run gsas-server restart
The server loads kicker.py at startup and caches it. Changes are invisible to the running server until it is restarted.
MP Pipeline Status
The Materials Project integration (mp_interface.py / mp_simulate.py /
simulation_builder.py) targets the current mp-api client (pinned
>=0.46,<1 in pixi.toml) and recipe schema 0.26.0.
Updated for mp-api 0.46.x (previously written against older client behavior):
Single summary fetch —
get_structure()usesclient.materials.summary.search(material_ids=[...], fields=[...])(one request for structure + symmetry + energetics) instead of the legacyget_structure_by_material_id()convenience method.Conventional-cell standardization — MP’s computed structure is often a primitive cell; it is now standardized via
SpacegroupAnalyzer(...).get_conventional_standard_structure()so the emitted space-group symbol matches the emitted cell. (Previously a centered-lattice primitive cell was paired with the conventional H-M symbol — wrong phase for anything non-primitive; LaB6 only worked because its primitive and conventional cells coincide.) The symbol is derived from the standardized structure itself (spglib condensed style with screw-axis underscores stripped, e.g.Pm-3m,I41/amd), whichkicker.pynormalizes viaG2spc.StandardizeSpcName.MPIDstringification — search results now returnmaterial_idasstr; previously rawMPIDobjects crashedmaterial_id.startswith('mp-')on the--formulapath.Polymorph selection —
search_by_formula()returns results sorted byenergy_above_hull(None last); the CLI lists all candidates and auto-selects the most stable one, printing the choice.Partial occupancy support — sites are extracted per species (
site.species.items()), carrying real occupancies into the recipe (AtomStructure.occupancy); disordered structures no longer crash onsite.specie.get_structure()reportsis_ordered.Error taxonomy —
MPInterfaceErrorbase withMPAuthError,MPNotFoundError,MPConnectionError; the CLI prints targeted hints instead of a generic wrapped exception.Metadata fixes —
crystal_systemnow holds the actual crystal system (previously the space-group number, now underspace_group_number);nelementsnow holds the element count (previously atom count, now undernum_atoms);energy_above_hullandtheoreticaladded.Key handling — API key resolution is config file first, then the
MP_API_KEYenvironment variable (ConfigLoader.get_mp_api_key());MPInterfaceis a context manager that closes the HTTP session.Server output-visibility guard — a GSAS-II server can be reachable over localhost yet have a divergent filesystem view (another node; a sandbox with a private
/tmp), reporting success while its output files are invisible to the client.GSASClientverifiesfit_profile.txtwas freshly written client-side after a “successful” server run (stat-compare before/after — a stale file from a previous run into the same output dir does not count) and falls back to in-process execution (or returns a structured error when fallback is disabled).mp-simulate --no-serverskips the server entirely, and its.chiexport is built from the in-bandfit_profileresult data rather than re-reading the file. Tests:tests/test_gsas_client_visibility.py,tests/test_mp_simulate_cli.py. KI-12 tracks the follow-up that removes the shared-filesystem assumption altogether (server returns output artifacts in-band).Strict config validation —
SimulationRecipeBuildervalidates thesimulation_defaultsblock against its allowed shape: unknown keys at any level (typos, the retired flat layout) and invalid values (null from an empty YAML key, strings where numbers belong) raise aValueErrornaming the offending key path; the CLI exits with that message. These previously passedRecipeModelvalidation (the instrument block isAny-typed passthrough) and failed opaquely inside GSAS-II.
Historical (schema 0.21 → 0.26 fixes, retained by the regression suite):
payload wrapper, schema_name, isotropic_size/isotropic_strain renames,
recipe= keyword for GSASClient.submit_simulation().
Regression guard: tests/test_mp_interface.py (mocked MPRester — real
pymatgen Structures, no live API) and tests/test_mp_pipeline.py run as part
of the standard test suite (pixi run test):
pixi run pytest tests/test_mp_interface.py tests/test_mp_pipeline.py -v
Limitation: mp_simulate.py requires a live Materials Project API key to
fetch structures. The test suite uses mocks and a synthetic LaB6 fixture
(mp_lab6_structure_data in conftest.py) so no API key is needed for testing.
Future: File-less Output Design
Currently run_refinement() always writes output files to output_dir. Two
options are under consideration for making file output optional:
Option A — save_files flag (default False)
result = powderline.run(recipe, output_dir=None, save_files=False)
# Returns DataFrames; no files written
Simplest API change; easy to add without breaking existing calls
Requires
output_dirto be optional throughout the stack
Option B — optional output_dir (default None)
result = powderline.run(recipe) # no files
result = powderline.run(recipe, output_dir=Path("/tmp/out")) # files + DataFrames
Slightly more discoverable (“no
output_dir= no files”)Requires
output_dir=Noneguard at every write site inrun_refinement()
Both options are backwards-compatible additions. This will be evaluated alongside the Phase 2 module refactoring work.
Development Workflow
Environment Setup
PowderLine uses pixi for reproducible conda environments:
# Install dependencies (first time)
pixi install
# Activate environment
pixi shell
# Run refinement
pixi run kicker examples/example_LaB6/input.json
# Run tests
pixi run test
# Validate recipe only (no refinement)
pixi run kicker --validate-only examples/example_LaB6/input.json
PYTHONPATH Configuration:
The kicker task in pixi.toml sets PYTHONPATH=$PWD/src:$PYTHONPATH to enable from powderline.schema import RecipeModel imports.
Execution Modes
PowderLine supports three execution modes for running refinements:
1. Auto-Detect Mode (Default)
pixi run kicker input.json
Behavior: Tries server → auto-starts if needed → falls back to subprocess
Performance: 0.1-0.3s per simulation (if server available), 1-2s (if fallback)
Use case: Normal development workflow - “just works”
Output: Shows “🚀 server mode” or “🐢 subprocess mode” at end
2. Force Server Mode
pixi run kicker input.json --use-server
Behavior: Uses server only, fails if unavailable, no fallback
Performance: 0.1s per simulation (10x+ faster than subprocess)
Use case: Batch processing, CI/CD where performance matters
Auto-start: Attempts to start server automatically if not running
Error handling: Exits with clear error if server can’t be started
3. Force Subprocess Mode
pixi run kicker input.json --no-server
Behavior: Direct in-process execution using loaded GSAS-II libraries, skip server entirely
Performance: 1-2s per simulation (slower; GSAS-II imported on first use)
Use case: Debugging, HPC/Slurm batch jobs, environments where the server is not suitable
Output: Returns the same full structured result (DataFrames) as server mode
Managing the Server
Start server manually:
pixi run gsas-server start # Starts in background
pixi run gsas-server status # Check if running
pixi run gsas-server stop # Shutdown server
Configure server port (if default port 19471 is in use):
# Set custom port before starting server
export POWDERLINE_SERVER_PORT=19472
pixi run gsas-server start
# Client will automatically detect the port from the powderline_gsas_server.port
# file in the platform temp directory (e.g. /tmp on Linux, %TEMP% on Windows)
pixi run kicker input.json # No additional configuration needed
View server logs (powderline_gsas_server.log in the platform temp directory):
tail -f /tmp/powderline_gsas_server.log # Linux; on Windows: %TEMP%\powderline_gsas_server.log
Server benefits:
GSAS-II libraries loaded once (2.6s startup amortized across requests)
Direct function calls using pre-loaded libraries (no subprocess overhead)
Persistent process (no repeated Python interpreter startup)
Ideal for batch simulations (10+ recipes)
Connection retry logic (3 attempts with exponential backoff) handles transient issues
Server caching behavior:
The server keeps Python modules loaded in memory for performance. This means:
✅ Production benefit: 4-6x speedup (first run ~12s, subsequent ~2-3s)
⚠️ Development caveat: Code changes require server restart to take effect
During active development:
# After making code changes to kicker.py or other modules:
pixi run gsas-server restart # gracefully restart with updated code
# Next run will use the freshly started server with current code
pixi run kicker input.json
For deterministic integration tests:
# Restart the server first so tests run against current code
pixi run gsas-server restart
pixi run pytest tests/test_schema.py -q
See TROUBLESHOOTING.md (“Stale Server (Old Code Running)”) for details on stale server issues.
When to use each mode:
Auto-detect: Default for interactive use
–use-server: Batch processing, automation, performance-critical
–no-server: Debugging kicker.py, isolated testing, server issues
Performance Comparison (Post-Fix ✅)
Mode |
LaB6 Simulation |
DRX_33 Multi-phase |
Typical Use |
|---|---|---|---|
Server (–use-server) |
0.1s |
0.3s |
Batch jobs |
Auto-detect (default) |
0.1-2s |
0.3-3s |
Interactive |
Subprocess (–no-server) |
1.1s |
3s |
Debugging |
Speedup: Server mode is 10.67x faster than subprocess for LaB6 simulations.
Times measured on NSLS-II workstation post-fix
Adding a New Refinement Parameter
Follow this pattern (using size broadening as example):
Add to schema (
schema.py):class PeakBroadening(BaseModel): size_broadening: dict[str, RefinementParameter | None] | None = None
Create setter function (
kicker.py):def set_phase_size_broadening(proj, hist, phase_name, size_dict, print_info=False): """Set crystallite size broadening for phase-histogram pair.""" # Extract from size_dict size_val = size_dict['size'] # Access proj.data dictionary hap = proj.data[hist.name]['Histograms'][phase_name] hap['Size'] = [size_val[0], size_val[1], 'isotropic', [0, 0], 1.0, size_val[1]]
Integrate into parameterization (in main execution):
if phase_param.peak_broadening and phase_param.peak_broadening.size_broadening: set_phase_size_broadening(proj, hist, phase_name, phase_param.peak_broadening.size_broadening)
Add example (create directory under
examples/):input.jsonwith new parameterDESCRIPTION.mdexplaining use caseRun refinement and commit output files
Add test (if example becomes reference):
def test_example_with_size_broadening(tmp_output_dir): # Run refinement, compare outputs
Adding a New Example
Examples drive schema evolution. To contribute:
Create directory:
examples/example_N/Add required files:
input.json- complete recipeDESCRIPTION.md- what refinement demonstrates, data source, expected results
Run refinement:
pixi run kicker examples/example_N/input.jsonCommit outputs:
output/directory with .gpx, .lst, .csv filesUpdate schema if new fields needed
Add regression test if example becomes quality reference
See examples/CONTRIBUTING_EXAMPLES.md for detailed guidelines.
Running Tests
# All tests
pixi run test
# Specific test file
pixi run pytest tests/test_example_LaB6_regression.py
# With verbose output
pixi run pytest -v
# See stdout/stderr
pixi run pytest -s
Regression test strategy:
Exact matching of Rwp, unit cell parameters, peak counts
Detects any changes in GSAS-II refinement algorithm
Reference outputs committed to git for comparison
If test fails: inspect .lst file to determine if change is expected or bug
Note on test speed: All tests currently run as a single suite (pixi run test). Introducing @pytest.mark.slow / @pytest.mark.integration markers to allow skipping GSAS-II-dependent tests in CI is planned.
Template File Detection
PowderLine prevents users from accidentally running template files with three-level detection:
Path check: Filename/directory contains “template”
Required fields check: Missing
payload.xrd_data,payload.instrument, orpayload.phasesPlaceholder text check: Fields contain
"XXXX"or"TODO"or"REPLACE"
If detected, shows error with helpful message directing to real examples.
Key Gotchas & Implementation Notes
Output File Formats
After refinement, PowderLine generates (fixed filenames):
dummy.gpx: GSAS-II project file (can reopen in GUI)dummy.bak0.gpx: Backup before refinement starteddummy.lst: Human-readable refinement log with Rwp, parameter tables, correlationsfit_profile.txt: 2θ, observed, calculated, background, difference intensities<phase>_unit_cell_report.csv: Refined unit cell parameters per phase<phase>_peak_list_report.csv: Reflection list (hkl, d-spacing, 2θ, intensities) per phase
For single peak fitting (SPF) mode:
single_peaks_report.txt: Peak fit results (position, width, intensity per peak)peak_convergence_diagnostics.txt: Convergence warnings for aphysical parameters
Note: Output filenames are now fixed (dummy.*) regardless of sample name. This simplifies automation and testing by eliminating filename variability.
Parameter Correlation Warnings
GSAS-II may print warnings about highly correlated parameters (e.g., background coefficients):
** Warning: parameters 0 and 1 are 99.8% correlated **
This is normal for over-parameterized models. Common causes:
Too many Chebyshev background terms
Refining both instrument and sample broadening simultaneously
Limited 2θ range
Not necessarily a problem if Rwp is good and parameters are physically reasonable.
Unit Cell Refinement Semantics (Schema 0.26)
GSAS-II natively refines all unit cell parameters together with a single lumped flag, but since schema 0.26 PowderLine honors per-parameter flags: a cell parameter refines iff it is present with refine_flag=true; absent or false means fixed. Internally, PowderLine sets the lumped GSAS-II flag when any parameter is requested and emits “Hold” constraints for the unrefined degrees of freedom (src/powderline/constraints.py):
"unit_cell": {
"a": [11.777, true, null, null], // refines
"c": [3.986, false, null, null] // held fixed (e.g. a tetragonal phase)
}
Only symmetry-linked parameters refine together: parameters coupled by the phase’s Laue class form one degree-of-freedom group with logical OR within the group. For example, cubic a=b=c is a single group (any true flag refines all three), and the oblique monoclinic parameters a/c/beta form one coupled group (see KI-02 in docs/known_issues.md). Flags on symmetry-fixed parameters (e.g. cubic angles) have no effect. PowderLine does not validate flag/symmetry consistency — producing symmetry-consistent flags is the recipe author’s responsibility.
Chebyshev Background Coefficients
Indexing starts from 0th order (constant term):
"coefficients": [
[100.0, true, null, null], // 0th order (constant)
[-50.0, true, null, null], // 1st order (linear)
[10.0, true, null, null] // 2nd order (quadratic)
]
Number of coefficients must match num_coefficients field (validated in schema).
Single Peak Background Format
Each single peak is a pseudo-Voigt with 4 parameters:
"background": {
"single_peaks": {
"positions": [[35.5, false, null, null]], // 2θ position
"intensities": [[50.0, true, null, null]], // Peak height
"pv_gaussian_sigma": [[0.1, false, null, null]], // Gaussian width (σ)
"pv_lorentzian_gamma": [[0.05, false, null, null]] // Lorentzian width (γ, HWHM)
}
}
All lists must have same length (validated in schema). Useful for known impurity peaks that are part of the background.
Note: pv_mixing_eta parameter not currently implemented in schema.
Single Peak Fitting (Peak List Mode)
Earlier schemas introduced dedicated single peak fitting capabilities via the top-level single_peaks field (distinct from background.single_peaks), maintained in the current schema.
This enables refining individual peaks with direct control over position, intensity, and pseudo-Voigt width parameters—useful for:
Materials without crystal structure (amorphous, liquids, polymers)
Unknown phases requiring peak characterization before structure solution
Detailed peak shape analysis independent of structure
Le Bail fitting (fit peak intensities without structural model)
Key Features:
Independent width refinement: With
use_instrument_profile: false, peak widths (σ, γ) refine directly rather than being derived from instrument parameters (U, V, W, X, Y, Z)Coexistence: Both
background.single_peaksandsingle_peakscan be used in same recipe—they populate different GSAS-II structuresExtended output: Generates
single_peaks_report.txtwith refined parameters plus calculated FWHM and integral breadths
JSON Structure:
// use_instrument_profile lives in refinement_controls.single_peak_fitting_mode
// (false = direct width refinement), not inside single_peaks:
"single_peaks": {
"positions": [[21.3, true, null, null], [...]], // 2θ positions
"intensities": [[100.0, true, null, null], [...]], // Peak intensities
"pv_gaussian_sigma_sq": [[0.15, true, null, null], [...]], // Gaussian width variances (σ²)
"pv_lorentzian_gamma": [[0.08, true, null, null], [...]] // Lorentzian widths (γ, HWHM)
}
Implementation Details:
GSAS-II Location: Peaks stored in
proj.data[hist.name]['Peak List']['peaks'](not Background)Format: Each peak is
[pos, pos_flag, intensity, int_flag, σ², σ²_flag, γ, γ_flag]Note: GSAS-II uses variance (σ²) internally, not σ
Instrument mode: Set via
G2pwd.setPeakInstPrmMode(use_instrument_profile)False(default): Peak widths refined independentlyTrue: Widths constrained by instrument parameters (U, V, W, X, Y, Z)
Output File: single_peaks_report.txt
Tab-separated file with 10 columns per peak:
position_2theta intensity sigma gamma fwhm_gaussian fwhm_lorentzian fwhm_pseudovoigt integral_breadth_gaussian integral_breadth_lorentzian integral_breadth_pseudovoigt
Calculated Values:
FWHM values:
Gaussian: FWHM_G = 2√(2ln2) × σ ≈ 2.355σ
Lorentzian: FWHM_L = 2γ
Pseudo-Voigt: Thompson et al. (1987) approximation
Integral Breadths: Area under peak / peak height
Gaussian: IB_G = FWHM_G × √(π/(4ln2)) ≈ 1.065 × FWHM_G
Lorentzian: IB_L = π × γ = (π/2) × FWHM_L
Pseudo-Voigt: Weighted combination using mixing parameter η
Verification: FWHM values are cross-checked against GSAS-II’s G2pwd.getgamFW(γ, σ) with warning if >1% difference.
Example: See examples/example_LaB6_singlepeakfit/ for complete demonstration.
Schema Classes:
SinglePeaksBackground: For background-associated peaks (background.single_peaks)SinglePeaks: For Peak List fitting (single_peaksat top level) - includesuse_instrument_profile
Error Handling Strategy
PowderLine fails fast with actionable error messages:
File loading errors: Check paths relative to JSON, file permissions
Template detection: Redirect to real examples
Validation errors: Show which field failed Pydantic validation
Histogram creation errors: Check XRD data format, instrument file compatibility
Refinement errors: Check parameter ranges, correlation warnings in .lst
Output errors: Check directory permissions, disk space
All errors exit with code 1 for automation compatibility.
Future Roadmap
Phase 2 (Next): Recipe generation API
Python functions to build JSON programmatically
Template system for common refinement types
Possible FastAPI wrapper for web service
Phase 3: Output parsing
Structured extraction of all refined parameters to JSON/CSV/HDF5
Quality metrics beyond Rwp (χ², GOF, R-Bragg)
Phase 4: Dash GUI for recipe building
Visual parameter selection
Live preview of refinement setup
Phase 5: Dash GUI for results visualization
Interactive plots of fit profiles
Parameter evolution across refinement cycles
Comparison between refinements
Contributing
Key principles for future work:
Add features when real examples need them (not speculatively)
Schema evolves based on examples
Maintain backwards compatibility when possible
Document GSAS-II quirks as you discover them
Add regression tests for new examples
Questions? Open an issue or discussion on GitHub.