@@ -1762,7 +1762,7 @@ class CondensationLatentHeat(CondensationStrategy):
17621762 Attributes:
17631763 latent_heat_strategy_input: Strategy input provided at initialization.
17641764 latent_heat_input: Raw latent heat value provided at initialization.
1765- last_latent_heat_energy: Net latent heat energy released in the most
1765+ last_latent_heat_energy: Total latent heat energy released in the most
17661766 recent step [J]. Positive values indicate condensation and
17671767 negative values indicate evaporation. Overwritten each call.
17681768 """
@@ -1955,11 +1955,9 @@ def mass_transfer_rate(
19551955 pressure_delta = self .calculate_pressure_delta (
19561956 particle , gas_species , temperature , radius_with_fill
19571957 )
1958- if not np .all (np .isfinite (pressure_delta )):
1959- raise ValueError (
1960- "Non-finite pressure_delta computed for latent-heat "
1961- "condensation."
1962- )
1958+ pressure_delta = np .nan_to_num (
1959+ pressure_delta , posinf = 0.0 , neginf = 0.0 , nan = 0.0
1960+ )
19631961
19641962 if self ._latent_heat_strategy is None :
19651963 return get_mass_transfer_rate (
@@ -2062,6 +2060,98 @@ def _update_latent_heat_energy(
20622060 )
20632061 )
20642062
2063+ def _calculate_norm_conc (
2064+ self , volume : float , concentration : NDArray [np .float64 ]
2065+ ) -> NDArray [np .float64 ]:
2066+ """Validate inputs and compute normalized concentration.
2067+
2068+ Args:
2069+ volume: Particle volume for the single box [m^3].
2070+ concentration: Particle concentration array.
2071+
2072+ Returns:
2073+ Normalized concentration (concentration / volume).
2074+
2075+ Raises:
2076+ ValueError: If volume or concentration inputs are invalid.
2077+ """
2078+ if not np .isfinite (volume ) or volume <= 0.0 :
2079+ raise ValueError ("volume must be finite and positive." )
2080+ if not np .all (np .isfinite (concentration )) or np .any (
2081+ concentration < 0.0
2082+ ):
2083+ raise ValueError (
2084+ "concentration must be finite and nonnegative for norm_conc."
2085+ )
2086+ return concentration / volume
2087+
2088+ def _normalize_mass_transfer_shape (
2089+ self ,
2090+ mass_transfer : NDArray [np .float64 ],
2091+ species_mass : NDArray [np .float64 ],
2092+ ) -> NDArray [np .float64 ]:
2093+ """Normalize mass transfer shape to match species count.
2094+
2095+ Args:
2096+ mass_transfer: Per-particle mass transfer array.
2097+ species_mass: Particle mass array used to infer species count.
2098+
2099+ Returns:
2100+ Mass transfer array with consistent species dimension.
2101+ """
2102+ species_count = 1 if species_mass .ndim == 1 else species_mass .shape [1 ]
2103+ if mass_transfer .ndim == 1 :
2104+ return mass_transfer .reshape (- 1 , species_count )
2105+ if mass_transfer .shape [1 ] > species_count :
2106+ return mass_transfer [:, :species_count ]
2107+ if mass_transfer .shape [1 ] < species_count :
2108+ return np .broadcast_to (
2109+ mass_transfer , (mass_transfer .shape [0 ], species_count )
2110+ )
2111+ return mass_transfer
2112+
2113+ def _apply_mass_transfer_to_particles (
2114+ self ,
2115+ particle_data : ParticleData ,
2116+ mass_transfer : NDArray [np .float64 ],
2117+ norm_conc : NDArray [np .float64 ],
2118+ ) -> None :
2119+ """Apply mass transfer to particle data in place."""
2120+ if mass_transfer .ndim == 2 :
2121+ nonzero_mask = norm_conc != 0
2122+ denom = norm_conc [:, np .newaxis ]
2123+ where_mask = nonzero_mask [:, np .newaxis ]
2124+ else :
2125+ denom = norm_conc
2126+ where_mask = norm_conc != 0
2127+ per_particle = np .divide (
2128+ mass_transfer ,
2129+ denom ,
2130+ out = np .zeros_like (mass_transfer ),
2131+ where = where_mask ,
2132+ )
2133+ np .add (
2134+ particle_data .masses [0 ],
2135+ per_particle ,
2136+ out = particle_data .masses [0 ],
2137+ )
2138+ np .maximum (particle_data .masses [0 ], 0.0 , out = particle_data .masses [0 ])
2139+
2140+ def _apply_mass_transfer_to_gas (
2141+ self , gas_data : GasData , mass_transfer : NDArray [np .float64 ]
2142+ ) -> None :
2143+ """Apply mass transfer to gas concentrations in place."""
2144+ np .subtract (
2145+ gas_data .concentration [0 ],
2146+ mass_transfer .sum (axis = 0 ),
2147+ out = gas_data .concentration [0 ],
2148+ )
2149+ np .maximum (
2150+ gas_data .concentration [0 ],
2151+ 0.0 ,
2152+ out = gas_data .concentration [0 ],
2153+ )
2154+
20652155 # pylint: disable=too-many-positional-arguments, too-many-arguments
20662156 @overload # type: ignore[override]
20672157 def step (
@@ -2101,8 +2191,8 @@ def step(
21012191 The mass transfer rate is computed, optional skip-partitioning applied,
21022192 and both the particle and gas states are updated while respecting
21032193 inventory limits. The per-step latent heat release is stored in
2104- ``last_latent_heat_energy`` (positive for condensation, negative for
2105- evaporation) and overwritten on every call.
2194+ ``last_latent_heat_energy`` [J] (positive for condensation, negative
2195+ for evaporation) and overwritten on every call.
21062196
21072197 Args:
21082198 particle: Particle representation to update.
@@ -2130,18 +2220,16 @@ def step(
21302220 dynamic_viscosity = dynamic_viscosity ,
21312221 )
21322222
2133- if isinstance (mass_rate , (int , float )):
2134- mass_rate_array = np .array ([mass_rate ])
2135- else :
2136- mass_rate_array = mass_rate
2223+ mass_rate_array = np .atleast_1d (np .asarray (mass_rate , dtype = np .float64 ))
21372224
21382225 mass_rate_array = self ._apply_skip_partitioning (mass_rate_array )
21392226
21402227 gas_mass_array : NDArray [np .float64 ] = np .atleast_1d (
21412228 np .asarray (gas_data .concentration [0 ], dtype = np .float64 )
21422229 )
21432230 volume = particle_data .volume [0 ]
2144- norm_conc = particle_data .concentration [0 ] / volume
2231+ concentration = particle_data .concentration [0 ]
2232+ norm_conc = self ._calculate_norm_conc (volume , concentration )
21452233 mass_transfer = get_mass_transfer (
21462234 mass_rate = mass_rate_array ,
21472235 time_step = time_step ,
@@ -2150,45 +2238,28 @@ def step(
21502238 particle_concentration = norm_conc ,
21512239 )
21522240
2153- self ._update_latent_heat_energy (mass_transfer , temperature )
2241+ mass_transfer = self ._normalize_mass_transfer_shape (
2242+ mass_transfer ,
2243+ particle_data .masses [0 ],
2244+ )
21542245
2155- species_mass = particle_data .masses [0 ]
2156- if species_mass .ndim == 1 :
2157- species_count = 1
2158- else :
2159- species_count = species_mass .shape [1 ]
2160- if mass_transfer .ndim == 1 :
2161- mass_transfer = mass_transfer .reshape (- 1 , species_count )
2162- elif mass_transfer .shape [1 ] > species_count :
2163- mass_transfer = mass_transfer [:, :species_count ]
2164- elif mass_transfer .shape [1 ] < species_count :
2165- mass_transfer = np .broadcast_to (
2166- mass_transfer , (mass_transfer .shape [0 ], species_count )
2167- )
2246+ self ._update_latent_heat_energy (mass_transfer , temperature )
21682247
21692248 if particle_is_legacy :
21702249 particle .add_mass (added_mass = mass_transfer ) # type: ignore[union-attr]
21712250 else :
2172- per_particle = np .divide (
2251+ self ._apply_mass_transfer_to_particles (
2252+ particle_data ,
21732253 mass_transfer ,
2174- norm_conc [:, np .newaxis ]
2175- if mass_transfer .ndim == 2
2176- else norm_conc ,
2177- out = np .zeros_like (mass_transfer ),
2178- where = norm_conc [:, np .newaxis ] != 0
2179- if mass_transfer .ndim == 2
2180- else norm_conc != 0 ,
2181- )
2182- particle_data .masses [0 ] = np .maximum (
2183- particle_data .masses [0 ] + per_particle , 0.0
2254+ norm_conc ,
21842255 )
21852256 if self .update_gases :
21862257 if gas_is_legacy :
21872258 gas_species .add_concentration ( # type: ignore[union-attr]
21882259 added_concentration = - mass_transfer .sum (axis = 0 )
21892260 )
21902261 else :
2191- gas_data . concentration [ 0 ] -= mass_transfer . sum ( axis = 0 )
2262+ self . _apply_mass_transfer_to_gas ( gas_data , mass_transfer )
21922263 if particle_is_legacy :
21932264 return cast (
21942265 Tuple [ParticleRepresentation , GasSpecies ],
0 commit comments