Skip to content

Port main features to staging + additional minor improvements - #1609

Open
jsnel wants to merge 19 commits into
stagingfrom
feature/port-main-features-to-staging
Open

jsnel wants to merge 19 commits into
stagingfrom
feature/port-main-features-to-staging

Conversation

@jsnel

@jsnel jsnel commented Sep 12, 2026

Copy link
Copy Markdown
Member

Changes here have been validated on @jsnel 's 0_8-pygta-validation-workspace and discussed on Slack.

Change summary

  • Add native v0.8 PFID element support, including plugin registration, matrix generation, simulation, optimization, and tests.
  • Improve optimization robustness: handle models with no free parameters, skip unresolved coefficient relations, and preserve consistent result metadata.
  • Correct signed and xarray-aware equal-area penalty handling, including expanded matrices in result metadata.
  • Fix kinetic activation normalization across all activation compartments.
  • Prevent shared optimization matrices from being mutated during scaling and reduce repeated coordinate alignment overhead.
  • Add compatibility and correctness fixes for pandas 3, NumPy scalar coordinates, dimension ordering, CLP ordering, and near-zero standard-error rendering.
  • Add regression tests for the new and corrected behavior.

Checklist

  • ✔️ Passing the tests
  • 🚧 Added changes to changelog
  • 👌 Closes issue
  • 🧪 Adds new tests for the feature
  • 📚 Adds documentation of the feature

Closes issues

closes #XXXX

jsnel added 15 commits July 12, 2026 17:18
Ignore relations whose endpoints are absent from the active axes, preventing lookup errors while preserving existing values. Add coverage for missing labels.
Skip the solver when nothing can vary and return a successful result with consistent optimization metadata
To avoid some of the performance regressions.

- Avoid slow coordinate comparisons
- Use plain coordinate values during label lookup to avoid repeated alignment overhead.
Return a scaled copy so index-dependent matrices keep their original values.
Add coverage for preserving the unscaled matrix.

This avoids us having to having to mutate the object before plotting
Copilot AI lite review requested due to automatic review settings September 12, 2026 18:27
@jsnel
jsnel requested review from a team and joernweissenborn as code owners September 12, 2026 18:27
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bc7b580d-9615-4af9-96c6-f027698c3c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.81319% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.9%. Comparing base (7efc9d1) to head (87c4306).

Files with missing lines Patch % Lines
glotaran/builtin/items/activation/gaussian.py 0.0% 15 Missing ⚠️
glotaran/optimization/optimization.py 66.6% 5 Missing ⚠️
glotaran/optimization/penalty.py 77.7% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           staging   #1609     +/-   ##
=========================================
+ Coverage     87.3%   87.9%   +0.6%     
=========================================
  Files           92      95      +3     
  Lines         4302    4455    +153     
  Branches       487     504     +17     
=========================================
+ Hits          3758    3920    +162     
+ Misses         422     420      -2     
+ Partials       122     115      -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a native PFID element and broad optimization, activation, data-preparation, compatibility, and metadata corrections.

  • Registers PFID model support and adds its matrix, simulation, optimization, and result-decomposition paths.
  • Revises equal-area penalties, no-free-parameter optimization, CLP relation restoration, and matrix handling.
  • Adds pandas 3, dimension-order, explicit-ASCII, seeded-noise, and parameter-rendering compatibility fixes.
  • Adds regression coverage across the new and corrected behavior.

Confidence Score: 2/5

The PR is not yet safe to merge because descending-axis penalties, shifted dispersion metadata, and nonnegative PFID rates can produce materially incorrect fitting or result behavior.

Equal-area penalties can disappear on descending axes, activation results can report centers different from those used by the model, and validly constructed PFID rates can silently generate zero basis columns; overlapping intervals and zero-sum activation scales add narrower correctness risks.

Files Needing Attention: glotaran/optimization/penalty.py, glotaran/builtin/items/activation/gaussian.py, glotaran/builtin/elements/pfid/matrix.py

Important Files Changed

Filename Overview
glotaran/builtin/elements/pfid/element.py Defines and registers PFID configuration, matrix construction, and labeled result decomposition.
glotaran/builtin/elements/pfid/matrix.py Implements the PFID Gaussian convolution, but silently drops nonnegative rates and permits zero-sum scale normalization.
glotaran/optimization/penalty.py Preserves signed CLPs but its interval selection assumes ascending, disjoint coordinate ranges.
glotaran/builtin/items/activation/gaussian.py Optimizes dispersion-center calculation but omits configured shifts from reported centers.
glotaran/optimization/optimization.py Adds a coherent successful-result path for models with no free parameters.
glotaran/optimization/matrix.py Preserves CLP insertion order and prevents scaling from mutating shared matrices.
glotaran/io/prepare_dataset.py Computes SVDs in explicitly requested dimension order.
glotaran/simulation/simulation.py Canonicalizes dimension order while generating seeded noise and restores the original layout afterward.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Config[PFID and activation configuration] --> Matrix[PFID matrix generation]
  Matrix --> Simulation[Simulation]
  Matrix --> Estimate[Conditional CLP estimation]
  Estimate --> Penalties[Equal-area penalties]
  Penalties --> Optimizer[Outer optimization]
  Optimizer --> Results[Result assembly]
  Results --> Decomposition[PFID amplitudes, phases, and concentrations]
Loading

Reviews (1): Last reviewed commit: "Avoid mutating shared optimization matri..." | Re-trigger Greptile

Comment on lines +32 to +34
start = int(np.argmin(np.abs(global_axis - lower)))
stop = int(np.argmin(np.abs(global_axis - upper))) + 1
for matrix, estimation in zip(matrices[start:stop], estimations[start:stop], strict=True):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Descending Axes Drop Penalties

On a descending global axis, the interval endpoints produce reversed positional indices. For example, the default interval on [20, 10, 0] yields start=2 and stop=1, so _get_area returns an empty area and every equal-area penalty silently becomes zero. Select points by their coordinate values rather than assuming ascending positional order.

Knowledge Base Used: Optimization workflow

Comment on lines +25 to +36
for interval_lower, interval_upper in intervals:
if interval_lower > global_axis[-1]:
continue
lower = max(interval_lower, np.min(global_axis))
upper = min(interval_upper, np.max(global_axis))
if lower > upper:
lower, upper = upper, lower
start = int(np.argmin(np.abs(global_axis - lower)))
stop = int(np.argmin(np.abs(global_axis - upper))) + 1
for matrix, estimation in zip(matrices[start:stop], estimations[start:stop], strict=True):
if label in matrix.clp_axis:
area.append(estimation.clp[matrix.clp_axis.index(label)])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Overlapping Intervals Double Count

Values are appended independently for every interval, so a coordinate is counted multiple times when intervals overlap. EqualAreaPenalty accepts overlapping intervals, while its existing applies() behavior uses any(...) and counts each coordinate once. This inflates the calculated area and changes the optimization objective for valid configurations.

Knowledge Base Used: Optimization workflow

Comment on lines +156 to +165
axis_values = np.asarray(axis, dtype=np.float64)
dispersion_center = float(self.dispersion_center)
distance = (
(1e3 / axis_values - 1e3 / dispersion_center)
if self.reciproke_global_axis
else (axis_values - dispersion_center) / 100
)
center_columns = center[:, np.newaxis] * np.ones((nr_gaussians, axis_values.size))
for i, coefficient in enumerate(self.center_dispersion_coefficients):
center_columns += float(coefficient) * np.power(distance, i + 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reported Centers Omit Shifts

When an activation has both wavelength-dependent dispersion and explicit per-index shifts, this calculation starts from the base centers and applies only the dispersion coefficients. Matrix generation applies each shift before dispersion, so result datasets report centers that differ from those actually used during fitting.

Knowledge Base Used: Optimization workflow

shifted_axis = model_axis - center
left_shifted_axis_indices = np.where(shifted_axis < 5 * width)[0]
left_shifted_axis = shifted_axis[left_shifted_axis_indices]
negative_rate_indices = np.where(rates < 0)[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nonnegative Rates Remove Oscillations

PFID rates are unconstrained, but this matrix calculation populates only entries whose rate is negative. A configured nonnegative rate, or an unconstrained fit trial that crosses zero, therefore produces all-zero cosine and sine basis columns without an error. The affected oscillation silently disappears from simulation and fitting.

Knowledge Base Used:

parameter.scale,
global_axis[index],
)
matrix[index] /= sum(parameter.scale for parameter in parameters)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Zero Scale Sum Produces NaNs

The sum of Gaussian scales is used as a divisor without validation, even though configured scales can be zero or cancel each other. A zero normalization sum propagates NaNs through the PFID matrix and into simulation or optimization. Reject this configuration with a clear error before dividing.

Knowledge Base Used:

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved PFID rate, Gaussian shift, penalty interval, overlap, and parameter-rendering issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Ports native PFID support to staging and adds optimization, compatibility, and numerical robustness improvements.

Changes:

  • Adds PFID modeling, simulation, optimization, registration, and tests.
  • Improves penalties, parameter handling, matrix ordering, and zero-parameter optimization.
  • Updates compatibility, kinetic normalization, SVD preparation, ASCII output, changelog, and linting.
File summaries
File Summary
tests/simulation/test_simulation.py Tests dimension-independent seeded noise.
tests/parameter/test_parameters.py Tests missing-expression handling.
tests/parameter/test_parameters_rendering.py Tests near-zero error rendering.
tests/parameter/test_parameter.py Tests expression normalization.
tests/optimization/test_relations_and_constraints.py Tests unresolved relations.
tests/optimization/test_optimization.py Tests optimization without used free parameters.
tests/optimization/test_objective.py Tests equal-area penalties.
tests/optimization/test_matrix.py Tests matrix ordering and scaling.
tests/io/test_prepare_dataset.py Tests dimension-aware SVD preparation.
tests/builtin/io/ascii/test_explicit_file_reader.py Tests ASCII formats.
tests/builtin/elements/pfid/test_pfid_element.py Tests PFID functionality.
tests/builtin/elements/pfid/__init__.py Initializes PFID tests.
tests/builtin/elements/kinetic/test_kinetic_element.py Tests activation normalization.
requirements_pinned.txt Adds pandas version markers.
pyproject.toml Registers the PFID plugin.
glotaran/simulation/simulation.py Canonicalizes seeded noise dimensions.
glotaran/parameter/parameters.py Handles near-zero standard errors.
glotaran/parameter/parameter.py Normalizes expression values.
glotaran/optimization/penalty.py Reworks equal-area penalty calculation.
glotaran/optimization/optimization.py Supports no-free-parameter runs.
glotaran/optimization/objective.py Preserves penalty metadata and improves amplitude gathering.
glotaran/optimization/matrix.py Preserves CLP order and avoids scale mutation.
glotaran/optimization/estimation.py Skips invalid CLP relations.
glotaran/io/prepare_dataset.py Makes SVD dimension-aware.
glotaran/builtin/items/activation/gaussian.py Updates Gaussian dispersion calculation.
glotaran/builtin/io/ascii/wavelength_time_explicit_file.py Improves NumPy coordinate output.
glotaran/builtin/elements/pfid/matrix.py Implements PFID matrix generation.
glotaran/builtin/elements/pfid/element.py Implements PFID model behavior and results.
glotaran/builtin/elements/pfid/__init__.py Exports PFID classes.
glotaran/builtin/elements/kinetic/element.py Fixes activation normalization.
changelog.md Documents the changes.
.pre-commit-config.yaml Adjusts Taplo linting.
Review details

Suppressed comments (2)

glotaran/optimization/penalty.py:36

  • Overlapping intervals are accumulated independently here, so a global-axis point covered by two intervals contributes twice. This differs from EqualAreaPenalty.applies, which treats the interval list as an any/union; for example, [(0, 2), (1, 3)] should not double-count the shared points.
        for matrix, estimation in zip(matrices[start:stop], estimations[start:stop], strict=True):
            if label in matrix.clp_axis:
                area.append(estimation.clp[matrix.clp_axis.index(label)])

glotaran/parameter/parameters.py:562

  • The new threshold is applied only to the table renderer here. Parameter.markdown() still divides by standard_error whenever it is not NaN, so a zero/near-zero error can still raise or render an infinite t-value when parameters are rendered directly or through nested expressions. Reuse the same threshold in that renderer as well.
            standard_error = (
                np.nan
                if abs(parameter.standard_error) < MINIMUM_STANDARD_ERROR
                else parameter.standard_error
            )
  • Files reviewed: 32/32 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +49 to +50
negative_rate_indices = np.where(rates < 0)[0]

Comment on lines +153 to +154
if self.dispersion_center is None:
return np.array([np.full(axis.size, c) for c in center])
Comment on lines +26 to +27
if interval_lower > global_axis[-1]:
continue
Always persist scale and weighted error metadata, using the unweighted fit when no weights are present. Pin semantic validation to the reviewed reference and retain its report for troubleshooting.
jsnel and others added 2 commits September 14, 2026 00:37
Advance the dependency revision to bring the latest feature set into staging.
pyyaml isn't a dependency of us and thus not guarantee to be installed.
See CI failure: https://github.com/glotaran/pyglotaran/actions/runs/34787513732/job/103805630377?pr=1609

In a full dev env it is installed because of doc and validation dependencies.
Run: uv tree | rg pyyaml --passthru
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants