Skip to content

feat: #1143 - [E5-F3-P5] Add data-only path support and parity tests#1150

Open
Gorkowski wants to merge 3 commits intouncscode:mainfrom
Gorkowski:issue-1143-adw-cec4a093
Open

feat: #1143 - [E5-F3-P5] Add data-only path support and parity tests#1150
Gorkowski wants to merge 3 commits intouncscode:mainfrom
Gorkowski:issue-1143-adw-cec4a093

Conversation

@Gorkowski
Copy link
Copy Markdown
Collaborator

Target Branch: main

Fixes #1143 | Workflow: cec4a093

Summary

Add parity coverage for CondensationLatentHeat, ensuring the data-only path (ParticleData + GasData) matches the legacy path for step(), rate(), and mass_transfer_rate(). Tighten edge-case guards (empty radii, zero concentration, variable time steps) and document the phase status in the E5 plan. This raises confidence that latent-heat condensation behaves identically across both input styles.

What Changed

New Components

  • None.

Modified Components

  • particula/dynamics/condensation/mass_transfer_utils.pycalc_mass_to_change now broadcasts per-particle or per-species time steps correctly for 2D rates, avoiding shape/broadcasting mismatches and supporting variable time-step inputs.
  • particula/dynamics/condensation/condensation_strategies.py_fill_zero_radius short-circuits on empty arrays to avoid max-of-empty warnings while preserving the existing zero-radius clipping behavior.
  • particula/dynamics/condensation/tests/condensation_strategies_test.py – Adds latent-heat parity suite (legacy vs data-only) plus edge-case coverage for zero particles, zero concentration, very small radii, single-species shapes, missing strategies, mixed input types, and helper functions (_normalize_mass_transfer_shape, _apply_mass_transfer_to_particles, _apply_mass_transfer_to_gas, _get_mass_transfer_variable_time_step, Kelvin term finiteness).
  • adw-docs/dev-plans/* – Marks E5-F3-P5 ([E5-F3-P5] Add data-only path support and parity tests #1143) as in progress and updates timestamps/issue linkage.

Tests Added/Updated

  • Latent-heat parity and edge cases: test_latent_heat_step_with_particle_data_gas_data, test_latent_heat_step_numerical_parity, test_latent_heat_rate_numerical_parity, test_latent_heat_mass_transfer_rate_numerical_parity, test_latent_heat_energy_parity, test_latent_heat_zero_particles_no_crash, test_latent_heat_single_species_data_only, test_latent_heat_very_small_particles_no_nan, test_latent_heat_zero_concentration_particles, plus configuration error checks for missing strategies and mixed input types.
  • Helper coverage: _get_mass_transfer_variable_time_step per-particle dt broadcast, Kelvin term finiteness, _normalize_mass_transfer_shape, _apply_mass_transfer_to_particles zero-conc guard, _apply_mass_transfer_to_gas non-negative clamp.

How It Works

  • Parity tests construct identical latent-heat strategies for legacy and data-only inputs, run step(), rate(), and mass_transfer_rate(), and assert mass/concentration/energy match to tight tolerances (rtol 1e-10 for mass/rate, 1e-14 for energy).
  • Zero-concentration and very-small-radius cases exercise the data-only path’s guarded division (np.divide(..., where=...)) and Kelvin term handling to ensure finite outputs and non-negative mass updates. An early return in _fill_zero_radius prevents warnings for empty particle arrays.
  • calc_mass_to_change now converts time_step to an array and broadcasts per-particle or per-species durations when rates are 2D, aligning variable-Δt mass-transfer calculations with helper expectations.

Implementation Notes

  • No API or public signature changes; behavior adjustments are limited to guards and broadcasting. Documentation updates only reflect status/issue linkage.
  • The latent-heat helper coverage mirrors the isothermal pattern to ensure both paths stay aligned.

Testing

  • pytest particula/dynamics/condensation/tests/condensation_strategies_test.py -v -k "LatentHeat or data or Kelvin"

Handle empty radius arrays by returning early to avoid max on empty
inputs. Add parity and edge-case coverage for CondensationLatentHeat
across step, rate, mass_transfer_rate, and energy, including checks for
variable time step, Kelvin term finiteness, zero-concentration bins,
shape normalization, and gas clamp behavior.

Closes uncscode#1143

ADW-ID: cec4a093
Handle scalar, per-particle, and per-species timesteps when computing
mass changes. Use numpy arrays for broadcasting so mass_rate multiplies
align with particle_concentration without shape errors. Keeps backward
compatibility with per-species timestep inputs and fixes related tests.
Update dev plan summaries to mark the E5-F3-P5 parity tests
as issue uncscode#1143 in progress and refresh last-updated dates.

Closes uncscode#1143

ADW-ID: cec4a093
Copilot AI review requested due to automatic review settings March 5, 2026 22:49
@Gorkowski Gorkowski added agent Created or managed by ADW automation blocked Blocked - review required before ADW can process labels Mar 5, 2026
@Gorkowski
Copy link
Copy Markdown
Collaborator Author

ADW Code Review

PR: #1150 - feat: #1143 - [E5-F3-P5] Add data-only path support and parity tests
Review Date: 2026-03-05
Reviewers: Code Quality, Correctness, Performance (C++/Python), Security


Summary

Severity Count
Critical 0
Warning 8
Suggestion 5

Critical Issues (Must Fix)

None.


Warnings (Should Fix)

  • particula/dynamics/condensation/condensation_strategies.py:1561-1568, 1668-1671, 1986-1997 - Isothermal staggered paths use raw counts vs concentration/volume in _calculate_single_particle_transfer/_calculate_batch_particle_transfer (outside diff; included here only).
  • condensation_strategies.py:1439-1455 - CondensationIsothermalStaggered.mass_transfer_rate missing _normalize_first_order_mass_transport for 2D pressure_delta (outside diff).
  • particula/dynamics/condensation/mass_transfer_utils.py:52-65 - calc_mass_to_change time_step shape ambiguity for 1D mass_rate (in diff; inline comment posted).
  • condensation_strategies.py:1104-1113, 1986-1997 - Data-only norm_conc computed without validating volume/concentration (outside diff).
  • condensation_strategies.py:2239-2257 - Latent heat strategy should reject negative latent heat (outside diff).
  • condensation_strategies.py:2185-2224 - mass_transfer_rate docstring out of sync with non-finite pressure delta handling (outside diff).
  • Architecture: behavior divergence across strategies re normalization/clamping (overview only).
  • particula/dynamics/condensation/mass_transfer_utils.py:52 - time_step_arr forced to float64 may upcast (in diff; inline comment posted).

Suggestions (Overview)

  • Guard _normalize_mass_transfer_shape for 1D + multi-species (condensation_strategies.py:2362-2384) (outside diff).
  • Cache np.max(radius) in _fill_zero_radius (condensation_strategies.py:524) (outside diff).
  • Prefer in-place multiply in calc_mass_to_change (mass_transfer_utils.py:64) (in diff; inline note posted).
  • Tests: per-species time_step broadcast (mass_transfer_utils.py), mixed legacy/data input error paths, non-finite time_step guard, min radius clamp (overview only).
  • Dev plan status alignment for E5-F3-P5 (adw-docs/dev-plans/README.md:~79) (outside diff).

Positive Observations

  • Strong parity coverage for latent-heat data-only vs legacy paths.
  • Added edge-case tests for empty radii/zero concentration and finiteness guards.

Inline Comments

Detailed feedback has been posted as inline comments on the following locations:

  • particula/dynamics/condensation/mass_transfer_utils.py:52-65 - time_step broadcasting + dtype/in-place considerations

This review was generated by ADW Multi-Agent Code Review System.
Reviewers: Quality, Correctness, C++ Performance, Python Performance, Security

@Gorkowski
Copy link
Copy Markdown
Collaborator Author

WARNING: time_step shape ambiguity for 1D rates

calc_mass_to_change converts time_step to an array, but when mass_rate is 1D the broadcast expectations are unclear and may accept mismatched shapes silently.

Suggested fix:

# Example guard
if mass_rate.ndim == 1 and time_step_arr.ndim > 1:
    raise ValueError("time_step must be scalar or 1D for 1D mass_rate")

This makes the broadcast contract explicit and avoids accidental shape bugs.

@Gorkowski
Copy link
Copy Markdown
Collaborator Author

SUGGESTION: Avoid forced float64 + use in-place multiply

time_step_arr = np.asarray(time_step, dtype=np.float64) can upcast inputs; and mass_rate * time_step_arr could be in-place if safe.

Suggested fix:

time_step_arr = np.asarray(time_step)
np.multiply(mass_rate, time_step_arr, out=mass_rate)

This preserves dtype when possible and reduces temporary allocations.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds parity testing and edge-case guards to ensure CondensationLatentHeat behaves identically for legacy (ParticleRepresentation/GasSpecies) and data-only (ParticleData/GasData) inputs, including variable time-step handling.

Changes:

  • Add latent-heat parity + edge-case test suite for step(), rate(), mass_transfer_rate(), and energy tracking.
  • Update calc_mass_to_change() to broadcast scalar/per-particle/per-species time_step for 2D rates.
  • Guard _fill_zero_radius() against empty arrays and update E5 plan docs with #1143 status.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
particula/dynamics/condensation/tests/condensation_strategies_test.py Adds latent-heat parity/edge-case tests and helper-method coverage (including variable-Δt).
particula/dynamics/condensation/mass_transfer_utils.py Improves time_step broadcasting rules for 2D mass_rate calculations.
particula/dynamics/condensation/condensation_strategies.py Avoids np.max on empty radius arrays via early return.
adw-docs/dev-plans/features/E5-F3-condensation-latent-heat-strategy.md Marks P5 as in progress with issue #1143 and updates timestamps/changelog.
adw-docs/dev-plans/epics/E5-non-isothermal-condensation.md / adw-docs/dev-plans/README.md Links #1143 and updates “Last Updated” metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

particle_concentration=particle_concentration,
)

np.testing.assert_allclose(result, expected, rtol=1e-15)
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The test uses an extremely tight tolerance (rtol=1e-15) on values around ~1e-9, which is likely to be flaky across platforms/BLAS/NumPy versions. Consider relaxing the tolerance (e.g., rtol ~1e-12) and/or adding a small atol to keep the test stable while still validating correctness.

Suggested change
np.testing.assert_allclose(result, expected, rtol=1e-15)
np.testing.assert_allclose(result, expected, rtol=1e-12, atol=1e-18)

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +63
if time_step_arr.ndim == 0:
time_step_broadcast = time_step_arr
elif time_step_arr.shape == (mass_rate.shape[0],):
# Per-particle timestep: align with the first axis
time_step_broadcast = time_step_arr[:, None]
elif time_step_arr.shape == (mass_rate.shape[1],):
# Per-species timestep fallback for backward compatibility
time_step_broadcast = time_step_arr[None, :]
else:
time_step_broadcast = time_step_arr
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

For 2D mass_rate, unsupported time_step shapes fall through to time_step_broadcast = time_step_arr, which can silently trigger unintended NumPy broadcasting (or produce hard-to-debug shape errors downstream). Consider explicitly validating allowed shapes (scalar, (n_particles,), (n_species,), or (n_particles, n_species) / broadcastable equivalents) and raising a clear ValueError when the shape is not compatible with mass_rate.

Suggested change
if time_step_arr.ndim == 0:
time_step_broadcast = time_step_arr
elif time_step_arr.shape == (mass_rate.shape[0],):
# Per-particle timestep: align with the first axis
time_step_broadcast = time_step_arr[:, None]
elif time_step_arr.shape == (mass_rate.shape[1],):
# Per-species timestep fallback for backward compatibility
time_step_broadcast = time_step_arr[None, :]
else:
time_step_broadcast = time_step_arr
try:
# Allow scalar, per-particle, per-species or full (particle, species)
# time steps, along with any NumPy-broadcastable equivalents.
time_step_broadcast = np.broadcast_to(time_step_arr, mass_rate.shape)
except ValueError as exc:
raise ValueError(
"time_step shape is not compatible with 2D mass_rate. "
f"Got time_step.shape={time_step_arr.shape}, expected a scalar "
f"or an array broadcastable to {mass_rate.shape} (e.g. "
"(n_particles,), (n_species,), (n_particles, n_species))."
) from exc

Copilot uses AI. Check for mistakes.
Comment on lines +3328 to +3345
legacy_strategy = CondensationLatentHeat(
molar_mass=self.molar_mass,
diffusion_coefficient=self.diffusion_coefficient,
accommodation_coefficient=self.accommodation_coefficient,
activity_strategy=self.activity_strategy,
surface_strategy=self.surface_strategy,
vapor_pressure_strategy=self.vapor_pressure_strategy,
latent_heat_strategy=ConstantLatentHeat(latent_heat_ref=2.26e6),
)
data_strategy = CondensationLatentHeat(
molar_mass=self.molar_mass,
diffusion_coefficient=self.diffusion_coefficient,
accommodation_coefficient=self.accommodation_coefficient,
activity_strategy=self.activity_strategy,
surface_strategy=self.surface_strategy,
vapor_pressure_strategy=self.vapor_pressure_strategy,
latent_heat_strategy=ConstantLatentHeat(latent_heat_ref=2.26e6),
)
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The test file repeats the same CondensationLatentHeat(...) construction in multiple tests, increasing maintenance cost if defaults/required args change. Consider adding a small helper (e.g., _make_latent_heat_strategy(...)) and reusing it across the parity/edge-case tests.

Copilot uses AI. Check for mistakes.
@Gorkowski Gorkowski added request:fix Request AI to implement review suggestions and removed blocked Blocked - review required before ADW can process request:fix Request AI to implement review suggestions labels Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Created or managed by ADW automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[E5-F3-P5] Add data-only path support and parity tests

2 participants