feat: #1141 - [E5-F3-P3] Implement step() for particle-resolved with energy tracking and tests#1148
Conversation
Mirror CondensationIsothermal.step for latent-heat strategies, update particle and gas states, and record per-step latent-heat energy with last_latent_heat_energy. Forward dynamic_viscosity to mass_transfer_rate and add overloads for legacy and data inputs. Add latent-heat step tests covering mass conservation, energy tracking, sign conventions, isothermal parity, gas updates, dynamic viscosity pass-through, and MassCondensation compatibility. Closes uncscode#1141 ADW-ID: 2260fbb3
Update feature docs, plans, and README to reflect the implemented CondensationLatentHeat step, energy diagnostics, and viscosity override notes. Expand the strategy system guide with latent heat details and example usage. Closes uncscode#1141 ADW-ID: 2260fbb3
There was a problem hiding this comment.
Pull request overview
This PR completes the particle-resolved CondensationLatentHeat.step() workflow by mirroring the existing isothermal step() logic while adding a per-step latent heat energy diagnostic and threading an optional dynamic_viscosity override through the step path. It also expands unit tests and updates user/dev documentation to reflect the new capability.
Changes:
- Implement
CondensationLatentHeat.step()(legacy + data-only) with latent heat energy tracking anddynamic_viscositypass-through. - Add/extend tests validating mass conservation, energy tracking/sign conventions, isothermal parity, type behavior, gas updates, and runnable compatibility.
- Update feature docs/readme and dev-plan docs to reflect that latent-heat step support is now implemented.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
particula/dynamics/condensation/condensation_strategies.py |
Implements CondensationLatentHeat.step(), adds _update_latent_heat_energy, overloads, and forwards dynamic_viscosity. |
particula/dynamics/condensation/tests/condensation_strategies_test.py |
Adds comprehensive CondensationLatentHeat.step() coverage (mass/energy/parity/types/gas update/viscosity/runnable). |
docs/Features/condensation_strategy_system.md |
Documents latent-heat step behavior and the new energy diagnostic. |
docs/index.md |
Updates top-level feature list to reflect implemented latent-heat step(). |
readme.md |
Updates feature bullets to reflect implemented latent-heat step() and energy diagnostic. |
adw-docs/dev-plans/features/index.md |
Updates plan status text for E5-F3. |
adw-docs/dev-plans/features/E5-F3-condensation-latent-heat-strategy.md |
Marks E5-F3-P3 complete and links to issue #1141; adds changelog entry. |
adw-docs/dev-plans/epics/E5-non-isothermal-condensation.md |
Marks E5-F3-P3 complete and updates epic changelog. |
adw-docs/dev-plans/README.md |
Updates dev-plan README to reference P3/#1141. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### CondensationLatentHeat (energy diagnostics) | ||
|
|
||
| `CondensationLatentHeat` mirrors the isothermal step but applies a latent-heat | ||
| correction when a latent heat strategy (or scalar fallback) is provided. It | ||
| records `last_latent_heat_energy` each step (positive for condensation, | ||
| negative for evaporation) and accepts a `dynamic_viscosity` override for | ||
| particle-resolved workflows. | ||
|
|
||
| ```python | ||
| latent = par.dynamics.CondensationLatentHeat( | ||
| molar_mass=0.018, | ||
| diffusion_coefficient=2e-5, | ||
| accommodation_coefficient=1.0, | ||
| latent_heat=2.4e6, # J/kg fallback | ||
| ) | ||
| particle, gas = latent.step( | ||
| particle=particle, | ||
| gas_species=gas, | ||
| temperature=298.15, | ||
| pressure=101325.0, | ||
| time_step=1.0, | ||
| dynamic_viscosity=1.8e-5, | ||
| ) | ||
| energy_released = latent.last_latent_heat_energy | ||
| ``` |
There was a problem hiding this comment.
The docs/example use last_latent_heat_energy without stating units. Given the condensation workflow uses mass concentrations (kg/m^3), this diagnostic is currently an energy density (J/m^3) unless multiplied by the parcel/box volume. Please clarify the units in this section so users don’t interpret it as total energy [J].
|
|
||
| self._update_latent_heat_energy(mass_transfer, temperature) | ||
|
|
||
| species_mass = particle_data.masses[0] | ||
| if species_mass.ndim == 1: |
There was a problem hiding this comment.
_update_latent_heat_energy() is called before mass_transfer is reshaped/clipped/broadcast to match particle_data.masses[0]. In cases where get_mass_transfer() returns a different species width than the particle inventory (handled a few lines later), the diagnostic can include mass-transfer components that are never applied to the particle/gas updates. Consider moving the energy update to after the mass-transfer shape normalization (or passing the normalized mass_transfer).
| """Store latent heat energy released for the current step. | ||
|
|
||
| Args: | ||
| mass_transfer: Per-particle mass transfer in kg for this step. | ||
| temperature: System temperature in Kelvin. | ||
| """ | ||
| if self._latent_heat_strategy is None: | ||
| self.last_latent_heat_energy = 0.0 | ||
| return | ||
|
|
||
| latent_heat = self._latent_heat_strategy.latent_heat(temperature) | ||
| self.last_latent_heat_energy = float( | ||
| np.sum( | ||
| get_latent_heat_energy_released( | ||
| mass_transfer, |
There was a problem hiding this comment.
The docstrings describe mass_transfer as kg and last_latent_heat_energy as [J], but in step() the mass transfer returned by get_mass_transfer() is scaled by particle concentration (#/m^3), matching the existing isothermal step’s kg/m^3 semantics. That makes last_latent_heat_energy an energy density (J/m^3) unless it’s multiplied by the box volume. Please either update the docs/attribute text to reflect J/m^3, or multiply by particle_data.volume[0] if the intended diagnostic is total energy [J].
ADW Code ReviewPR: #1148 - feat: #1141 - [E5-F3-P3] Implement step() for particle-resolved with energy tracking and tests Summary
Warnings (Should Fix)
Suggestions (Overview)
Positive Observations
Inline CommentsDetailed feedback has been posted as inline comments on the following locations:
This review was generated by ADW Multi-Agent Code Review System. |
|
WARNING: Latent heat energy computed before normalization
Suggested fix: # normalize mass_transfer first, then compute latent heat energy
mass_transfer = _normalize_mass_transfer(...)
self._update_latent_heat_energy(mass_transfer, ...)This keeps energy tracking consistent with the normalized mass transfer actually applied. |
|
WARNING: Sanitize non-finite The isothermal path handles non-finite Suggested fix: pressure_delta = np.where(np.isfinite(pressure_delta), pressure_delta, 0.0)Aligns behavior across pathways and prevents NaNs from propagating. |
|
WARNING: Validate volume and concentration in
Suggested fix: if not np.all(np.isfinite(volume)) or np.any(volume <= 0):
raise ValueError("volume must be positive and finite")
if not np.all(np.isfinite(concentration)):
raise ValueError("concentration must be finite")Prevents invalid normalization and downstream NaNs. |
|
WARNING: Clamp gas concentration to nonnegative Gas concentration updates can drive values below zero when mass transfer is large. Suggested fix: gas_concentration = np.maximum(gas_concentration, 0.0)Avoids negative concentrations and keeps physical invariants intact. |
|
SUGGESTION: Reduce allocations in mass clamp This section allocates multiple temporary arrays during clamp. Suggested fix: # in-place add + maximum where possible
np.add(mass_rate, 0.0, out=mass_rate)
np.maximum(mass_rate, 0.0, out=mass_rate)This reduces temporary allocations in a hot path. |
🤖 Agent Developer Workflow
Current Status
Workflow Plan
📋 Recent Activity🔍 Archived UpdatesThis comment is automatically updated as the workflow progresses |
Successfully fixed: - Refactored CondensationLatentHeat.step into helpers to reduce complexity and keep behavior intact. - Sanitized non-finite pressure_delta values to mirror isothermal behavior. - Validated volume/concentration inputs for norm_conc and raise clear errors. - Normalized mass_transfer shape before latent heat energy accounting. - Clamped gas concentration updates to nonnegative values. - Documented latent heat energy units and expanded latent heat step tests. Remaining issues: none. Closes uncscode#1148 ADW-ID: 31535132
Fix SummaryImplemented the requested latent-heat step fixes and documentation updates, with expanded test coverage for the new behavior. Key changes include normalized mass-transfer energy tracking, non-finite pressure-delta handling parity with the isothermal path, input validation for normalization, gas concentration clamping, and allocation-reducing in-place updates. Changes Made
Review Comments Addressed
(Comment IDs were not recorded; summary is based on the workflow spec.) Files Changed
Automated fix by ADW workflow |
Target Branch:
mainFixes #1141 | Workflow:
2260fbb3Summary
Implements the latent-heat-aware
step()forCondensationLatentHeat, mirroring the isothermal workflow while recording per-step latent heat release. Adds a dedicated energy update hook, forwards optionaldynamic_viscositythrough the step, and keeps API compatibility withMassCondensation. Documentation now describes the latent-heat step behavior and energy diagnostic.What Changed
New Components
Modified Components
particula/dynamics/condensation/condensation_strategies.py- ImplementedCondensationLatentHeat.step()with latent-heat energy tracking, added overloads includingdynamic_viscosity, and shared helper_update_latent_heat_energyplus proper skip-partitioning/shape handling.particula/dynamics/condensation/tests/condensation_strategies_test.py- ExpandedTestCondensationLatentHeatwith step mass/energy conservation, sign, type, gas-update, viscosity pass-through, andMassCondensationcompatibility coverage.docs/Features/condensation_strategy_system.mdand indexes/readme - Documented latent-heat step support and energy diagnostic.adw-docs/dev-plans/*anddocs/index.md/readme.md- Synced plan references and feature links for this phase.Tests Added/Updated
condensation_strategies_test.py– New cases for mass conservation (legacy & data paths), energy matchesdm × L, sign conventions, isothermal parity & zero-energy fallback, return types, gas update behavior, dynamic viscosity forwarding, andMassCondensationrunnable integration.How It Works
The step unwraps particle/gas data, enforces single-box, computes per-particle mass transfer with skip-partitioning, records latent heat via
get_latent_heat_energy_released, then updates particle masses and optionally gas concentration. Energy is overwritten each call and reflects condensation (>0) or evaporation (<0).Implementation Notes
dynamic_viscositynow threads throughstep()tomass_transfer_ratefor transport overrides._update_latent_heat_energycentralizes the diagnostic and zeros it when no latent heat strategy is configured (isothermal parity).MassCondensationcontinues to work without API changes.Testing
TestCondensationLatentHeatcoverage for mass/energy conservation, sign conventions, parity with isothermal, type returns, gas updates, viscosity forwarding, and runnable compatibility.