Skip to content

[Phase E3-F4-P2] Create GasSpecies facade over GasData with deprecation warnings and tests #1069

@Gorkowski

Description

@Gorkowski

Dependencies:

#1017 [E3-F1] ──┐
#1018 [E3-F2] ──┤
                ├──► #THIS [E3-F4-P2]
#1019 [E3-F3] ──┤
#1068 [E3-F4-P1]┘

Parent Issue: #1020

Dependencies:


Description

Refactor GasSpecies to become a facade that wraps GasData internally while maintaining the exact same public API. All existing code that uses GasSpecies must continue to work without modification. Add deprecation warnings to the constructor to guide users toward GasData.

Context

This is the second phase of the E3-F4 Facade and Migration feature. The new GasData container (from E3-F2 #1018) provides a clean data-only container with batch dimensions for multi-box CFD simulations. The GasSpecies class (506 lines) currently combines data storage with vapor pressure strategy behavior. This phase separates them by making GasSpecies a facade over GasData.

Parent Issue: #1020 (E3-F4: Facade and Migration)
Blocked by: #1017, #1018, #1019 (E3 data containers), #1068 (E3-F4-P1 ParticleRepresentation facade, for pattern consistency)

Value:

  • Enables gradual migration from GasSpecies to GasData
  • Separates data from behavior (vapor pressure strategies)
  • Deprecation warnings guide users toward the preferred GasData API

Scope

Estimated Lines of Code: ~150 lines (excluding tests)
Complexity: Medium

Files to Modify:

  • particula/gas/species.py (~150 LOC changes) - Refactor class to facade wrapping GasData

Files to Create:

  • particula/gas/tests/species_facade_test.py (~120 LOC) - New facade-specific tests

Acceptance Criteria

Core Implementation

  • Refactor GasSpecies.__init__() to create an internal GasData instance (self._data) from name, molar_mass, concentration, and partitioning
  • Add DeprecationWarning to __init__() with message directing users to GasData and migration guide
  • Delegate get_name() to self._data.name
  • Delegate get_molar_mass() to self._data.molar_mass
  • Delegate get_concentration() to self._data.concentration[0] (single box, adapting 2D to scalar/1D)
  • Delegate get_partitioning() to self._data.partitioning
  • Keep pure_vapor_pressure_strategy as an attribute on the facade (behavior, not data)
  • Ensure get_pure_vapor_pressure(), get_partial_pressure(), get_saturation_ratio(), and get_saturation_concentration() continue to work using the strategy + delegated data
  • Ensure add_concentration() updates self._data.concentration internally
  • Ensure set_concentration() updates self._data.concentration internally
  • Ensure append() updates self._data by creating a new merged GasData
  • Ensure __iadd__() and __add__() work correctly with the facade
  • Ensure __len__() returns correct count via self._data.n_species
  • Ensure __str__() returns unchanged output
  • Add data property that returns the underlying GasData instance
  • Add from_data() classmethod that creates a facade from an existing GasData + strategies without deprecation warning
  • Maintain backward compatibility for the self.molar_mass, self.concentration, self.name direct attribute access patterns used throughout the codebase (these are accessed directly in condensation strategies)

Conversion Helpers

  • The existing from_species() function in gas_data.py must continue to work with the facade
  • The existing to_species() function in gas_data.py must continue to work

Testing (REQUIRED - Co-located with implementation)

  • All existing tests in particula/gas/tests/species_test.py pass WITHOUT modification (critical backward compatibility)
  • New test: test_init_deprecation_warning - verify DeprecationWarning on constructor
  • New test: test_data_property_returns_gas_data - verify .data returns GasData instance
  • New test: test_from_data_no_deprecation_warning - verify from_data() does NOT trigger warning
  • New test: test_facade_delegation_get_name - verify name delegation
  • New test: test_facade_delegation_get_molar_mass - verify molar mass delegation
  • New test: test_facade_delegation_get_concentration - verify concentration delegation
  • New test: test_add_concentration_updates_internal_data - verify add_concentration() updates self._data
  • New test: test_set_concentration_updates_internal_data - verify set_concentration() updates self._data
  • New test: test_append_updates_internal_data - verify append() creates merged GasData
  • New test: test_multi_species_facade - verify facade works with multiple appended species
  • All tests pass before merge
  • Achieve 95%+ coverage on modified code

Technical Notes

Implementation Approach

The facade keeps the existing constructor signature but internally creates a GasData instance. Vapor pressure strategies stay on the facade as behavior:

import warnings
from particula.gas.gas_data import GasData

class GasSpecies:
    @validate_inputs({"molar_mass": "positive"})
    def __init__(self, name, molar_mass, vapor_pressure_strategy=...,
                 partitioning=True, concentration=0.0):
        warnings.warn(
            "GasSpecies is deprecated. Use GasData instead. "
            "See migration guide: docs/migration/particle-data.md",
            DeprecationWarning,
            stacklevel=2,
        )
        # Keep strategy (behavior stays on facade)
        self.pure_vapor_pressure_strategy = vapor_pressure_strategy
        
        # Normalize to lists/arrays for GasData
        names = [name] if isinstance(name, str) else list(name)
        mm = np.atleast_1d(np.asarray(molar_mass, dtype=np.float64))
        conc = np.atleast_1d(np.asarray(concentration, dtype=np.float64))
        part = np.array([partitioning] * len(names), dtype=np.bool_)
        
        self._data = GasData(
            name=names,
            molar_mass=mm,
            concentration=conc.reshape(1, -1),  # (1, n_species)
            partitioning=part,
        )

    @property
    def data(self) -> GasData:
        return self._data

    # Backward-compat attribute access (used by condensation)
    @property
    def molar_mass(self):
        if self._data.n_species == 1:
            return float(self._data.molar_mass[0])
        return self._data.molar_mass

    @property 
    def concentration(self):
        conc = self._data.concentration[0]  # box_index=0
        if self._data.n_species == 1:
            return float(conc[0])
        return conc

Key Design Decisions

  1. Single-box assumption: The facade always uses box_index=0, since the old API has no batch dimension.
  2. Attribute access compatibility: self.molar_mass and self.concentration are widely accessed as direct attributes in condensation strategies (e.g., gas_species.molar_mass on line 1168 of condensation_strategies.py). These MUST remain accessible as properties.
  3. Strategy preservation: Vapor pressure strategies remain on the facade, not in GasData (data-only container).

Integration Points

  • Uses particula/gas/gas_data.py:GasData (from E3-F2 [E3-F2] Gas Data Container #1018)
  • from_species() and to_species() conversion functions in gas_data.py must continue working
  • Condensation strategies directly access gas_species.molar_mass, gas_species.concentration, gas_species.pure_vapor_pressure_strategy - these MUST remain accessible
  • CondensationIsothermalStaggered._calculate_single_particle_transfer() accesses gas_species.pure_vapor_pressure_strategy as both single strategy and list

Edge Cases and Considerations

  • Scalar vs array molar_mass/concentration: GasSpecies supports both float and NDArray for molar_mass and concentration. The facade must handle seamless conversion.
  • Negative concentration clamping: _check_if_negative_concentration() must still clamp to 0 and update _data
  • Strategy as list: When species are appended, pure_vapor_pressure_strategy becomes a list. This must remain compatible.
  • Direct attribute writes: Some code may write gas_species.concentration = new_value directly. The property setter must update _data.

Example Usage

import warnings
import numpy as np
from particula.gas.species import GasSpecies
from particula.gas.vapor_pressure_strategies import ConstantVaporPressureStrategy

# Old code still works (with deprecation warning)
with warnings.catch_warnings():
    warnings.simplefilter("always")
    species = GasSpecies(
        name="Water",
        molar_mass=0.018,
        vapor_pressure_strategy=ConstantVaporPressureStrategy(2330),
        partitioning=True,
        concentration=1e-3,
    )

# Access new data container
gas_data = species.data  # Returns GasData
print(gas_data.n_boxes)  # 1
print(gas_data.n_species)  # 1
print(gas_data.concentration.shape)  # (1, 1)

References

Feature Plan:

  • adw-docs/dev-plans/features/E3-F4-facade-migration.md

Related Issues:

Related Code:

  • particula/gas/species.py (506 lines) - Current implementation to refactor
  • particula/gas/gas_data.py (300 lines) - New data container to wrap
  • particula/gas/tests/species_test.py - Existing tests that MUST pass
  • particula/gas/tests/gas_data_test.py - GasData tests for reference
  • particula/dynamics/condensation/condensation_strategies.py - Accesses gas_species attributes directly

Coding Standards:

  • adw-docs/code_style.md - Python standards (snake_case, 80-char lines)
  • adw-docs/testing_guide.md - Testing patterns (*_test.py suffix)

Metadata

Metadata

Assignees

No one assigned

    Labels

    adw:in-progressADW workflow actively processing - remove to re-triggeragentCreated or managed by ADW automationfeatureNew feature or significant enhancementmodel:defaultUse base/sonnet tier (workflow default)type:patchQuick patch workflow (plan → build → ship)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions