Repository: USEPA/WNTR

Issue:
Setting/overriding Link initial_status/initial_setting does not affect simulation behavior
# Summary
Setting the `initial_status` to `Closed` and `initial_setting` to `0.0` for a valve link does not change the valve's status or flow in the simulation results. The valve remains `Active` with a non-zero flow.

The expected behavior is that the simulation should respect the `initial_status` and `initial_setting` provided, resulting in the valve being reported as `Closed` with a flow of `0.0` throughout the simulation.

# Example
The following code reproduces the issue using the `Net6.inp` file from the WNTR examples folder.

```python
import wntr
from wntr.network.base import LinkStatus

inp_file = 'networks/Net6.inp' 

# Standard Run with the valve being open (for comparison)
print("Running standard simulation...")
wn_standard = wntr.network.WaterNetworkModel(inp_file)
sim_standard = wntr.sim.WNTRSimulator(wn_standard)
sim_standard.run_sim()
valve_standard = wn_standard.get_link('VALVE-3891')
results_standard = sim_standard.results
# Get status and flow at the first time step (or average/specific time)
status_standard = results_standard.link['status'].loc[wn_standard.options.time.duration, 'VALVE-3891']
flow_standard = results_standard.link['flowrate'].loc[wn_standard.options.time.duration, 'VALVE-3891']
print(f"Standard - Valve Status: {LinkStatus(status_standard)}, Flow: {flow_standard:.6f}")
print("Expected Standard - Valve Status: Active, Flow:  0.009864278387532802")


# Run with the valve set to Closed initially
print("\nRunning simulation with valve initially closed...")
wn_closed = wntr.network.WaterNetworkModel(inp_file)
valve_closed = wn_closed.get_link('VALVE-3891')

# Set initial status and setting
valve_closed.initial_status = LinkStatus.Closed
valve_closed.initial_setting = 0.0

# Verify the initial settings were applied to the model object
print(f"Model Check - Valve initial_status: {valve_closed.initial_status}, initial_setting: {valve_closed.initial_setting}")

sim_closed = wntr.sim.WNTRSimulator(wn_closed)
sim_closed.run_sim()
results_closed = sim_closed.results

# Get status and flow at the first time step (or average/specific time)
status_closed = results_closed.link['status'].loc[wn_closed.options.time.duration, 'VALVE-3891']
flow_closed = results_closed.link['flowrate'].loc[wn_closed.options.time.duration, 'VALVE-3891']

print(f"Closed - Valve Status: {LinkStatus(status_closed)}, Flow: {flow_closed:.6f}")
print("Expected Closed - Valve Status: Closed, Flow: 0.0")
```
**Environment**
Provide information on your computing environment.
 - Operating system: 
 - Python version:
 - WNTR version:

# Additional context
The issue appears to stem from how the `initial_status` and `initial_setting` properties are handled within the `wntr.network.base.Link` class

Setters Don't Update Simulation State: The property setters for `initial_status` and `initial_setting` update internal attributes (`_initial_status, _initial_setting`) but do not seem to modify the attributes (`_user_status, _setting`) that the simulator actually uses to determine the link's behavior at the start of the simulation.

    Code Snippet (wntr/network/base.py):
```python
    @property
    def initial_status(self):
        """:class:`~wntr.network.base.LinkStatus`: The initial status (`Opened`, `Closed`, `Active`) of the Link"""
        return self._initial_status
    @initial_status.setter
    def initial_status(self, status):
        if not isinstance(status, LinkStatus):
            if isinstance(status, int): status = LinkStatus(status)
            elif isinstance(status, str): status = LinkStatus[status]
            else: status = LinkStatus(int(status))
        self._initial_status = status
        self._user_status = status
        
    @property
    def initial_setting(self):
        """float: The initial setting for the link (if `Active`)"""
        return self._initial_setting
    @initial_setting.setter
    def initial_setting(self, setting):
        # TODO: typechecking
        self._initial_setting = setting
        self._setting =  setting
```
status and setting Setters Deprecated: The setters for the main status and setting properties now explicitly raise a RuntimeError, directing the user to use initial_status and initial_setting instead. This confirms the intention is for the initial_* properties to control the simulation behavior.

    Code Snippet (wntr/network/base.py):
```python
 @property
    @abc.abstractmethod
    def status(self):
        """:class:`~wntr.network.base.LinkStatus`: (**abstract**) current status of the link"""
        pass
    @status.setter
    # @abc.abstractmethod
    def status(self, status):
        raise RuntimeError("The status attribute is an output (result) property. Setting status by"
                            " the user has been deprecated to avoid confusion. To change the simulation"
                            " behavior, use initial_status.")
        # self._user_status = status

    @property
    def setting(self):
        """float: (read-only) current simulated setting of the link"""
        return self._setting
    @setting.setter
    def setting(self, setting):
        raise RuntimeError("The setting attribute is an output (result) property. Setting the setting by"
                            " the user has been deprecated to avoid confusion. To change the simulation"
                            " behavior, use initial_setting.")
        # self._setting = setting
```
It appears that the necessary logic to connect the `_initial_status` (and potentially `_initial_setting`) to the simulator's internal representation of link status/settings (perhaps via `_user_status` or modifying `_status/_setting` before the simulation) is missing or commented out in the current version. Re-linking these, for example by setting `self._user_status = status` within the `initial_status` setter (or equivalent logic), seems to resolve the issue and makes the simulation respect the initial values.

Discussion:
@kaklise: Thanks for catching this.  This is an issue with the WNTRSimulator only, the EpanetSimulator closes the valve and there is no flow.  

When I looked at [base.py#L420](https://github.com/USEPA/WNTR/blob/684cdbccc828250099141c2b3a2051e1291730e7/wntr/network/base.py#L420C9-L420C23), I don't see the line `self._user_status = status`.  This should be there to set the link status used by the WNTRSimulator, using [elements.py#L1643](https://github.com/USEPA/WNTR/blob/684cdbccc828250099141c2b3a2051e1291730e7/wntr/network/elements.py#L1643)

We can use the example you provided as a test.  

The use of `_initial_status`, `_user_status`, `_internal_status`, and `status` needs to be documented or cleaned up.@kaklise: Closed by #506. 

Solution patch:
diff --git a/wntr/network/base.py b/wntr/network/base.py
index 11139772..347b5ef9 100644
--- a/wntr/network/base.py
+++ b/wntr/network/base.py
@@ -355,22 +355,26 @@ class Link(six.with_metaclass(abc.ABCMeta, object)):
         # Set and register the ending node
         self._end_node = self._node_reg[end_node_name]
         self._node_reg.add_usage(end_node_name, (link_name, self.link_type))
-        # Set up other metadata fields
-        self._initial_status = LinkStatus.Opened
-        self._initial_setting = None
-        self._vertices = []
-        self._tag = None
-        # Model state variables
-        self._user_status = LinkStatus.Opened
-        self._internal_status = LinkStatus.Active
+        # Set status variables
+        self._user_status = LinkStatus.Opened # Control status, can be changed by the user
+        self._initial_status = LinkStatus.Opened # Model initial status, can be changed by the user
+        self._internal_status = LinkStatus.Active # Intermediate simulation status, read only
+        self._status= None # Current simulation status, read only
+        # Set setting variables
+        self._initial_setting = None # Model initial setting, can be changed by the user
+        self._prev_setting = None # Previous simulation setting, read only
+        self._setting = None # Current simulation setting, read only
+        # Other model state variables
         self._initial_quality = None
-        self._prev_setting = None
-        self._setting = None
         self._flow = None
         self._velocity = None
         self._is_isolated = False
         self._quality = None
         self._headloss = None
+        # Other
+        self._vertices = []
+        self._tag = None
+        
 
     def _compare(self, other):
         """
@@ -423,6 +427,7 @@ class Link(six.with_metaclass(abc.ABCMeta, object)):
             elif isinstance(status, str): status = LinkStatus[status]
             else: status = LinkStatus(int(status))
         self._initial_status = status
+        self._user_status = status
         
     @property
     def initial_setting(self):
@@ -432,6 +437,7 @@ class Link(six.with_metaclass(abc.ABCMeta, object)):
     def initial_setting(self, setting):
         # TODO: typechecking
         self._initial_setting = setting
+        self._setting =  setting
 
     @property
     def start_node(self):


Test patch:
diff --git a/wntr/tests/test_network_controls.py b/wntr/tests/test_network_controls.py
index 9b6222a6..94f8ebf4 100644
--- a/wntr/tests/test_network_controls.py
+++ b/wntr/tests/test_network_controls.py
@@ -2,6 +2,7 @@
 import unittest
 import warnings
 from os.path import abspath, dirname, join
+import copy
 
 import wntr
 
@@ -11,15 +12,24 @@ ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestValveSettingControls(unittest.TestCase):
-    def test_status_open_when_setting_changes(self):
+    @classmethod
+    def setUpClass(self):
         wn = wntr.network.WaterNetworkModel()
         wn.add_reservoir("r1", base_head=10)
         wn.add_junction("j1", base_demand=0)
         wn.add_junction("j2", base_demand=0.05)
         wn.add_pipe("p1", "r1", "j1")
-        wn.add_valve("v1", "j1", "j2", valve_type="PRV", initial_setting=2)
+        wn.add_valve("v1", "j1", "j2", valve_type="PRV", 
+                     initial_setting=2, initial_status='Active')
         wn.options.time.duration = 3600 * 5
-
+        self.wn = wn
+    
+    @classmethod
+    def tearDownClass(self):
+        pass
+    
+    def test_status_open_when_setting_changes(self):
+        wn = copy.deepcopy(self.wn)
         action = wntr.network.ControlAction(
             wn.get_link("v1"), "status", wntr.network.LinkStatus.Closed
         )
@@ -37,8 +47,64 @@ class TestValveSettingControls(unittest.TestCase):
         self.assertEqual(
             results.link["status"].at[7200, "v1"], wntr.network.LinkStatus.Active
         )
-
-
+    
+    def test_initial_status(self):
+        # Run simulations with open valve
+        wn = copy.deepcopy(self.wn)
+        valve_name = 'v1'
+        valve = wn.get_link(valve_name)
+        valve.initial_status = 'Open'
+        
+        sim = wntr.sim.EpanetSimulator(wn)
+        results_epanet_open = sim.run_sim()
+        
+        sim = wntr.sim.WNTRSimulator(wn)
+        results_wntr_open = sim.run_sim()
+        
+        # Check that valve is open (1) and flow is not 0
+        assert (results_epanet_open.link['status'].loc[:,'v1'] == 1).all()
+        assert (results_wntr_open.link['status'].loc[:,'v1'] == 1).all()
+        assert (results_epanet_open.link['flowrate'].loc[:,'v1'].abs() > 0).all()
+        assert (results_wntr_open.link['flowrate'].loc[:,'v1'].abs() > 0).all()
+        
+        # Run simulations with closed valve
+        wn = copy.deepcopy(self.wn)
+        valve_name = 'v1'
+        valve = wn.get_link(valve_name)
+        valve.initial_status = 'Closed'
+        
+        sim = wntr.sim.EpanetSimulator(wn)
+        results_epanet_closed = sim.run_sim()
+        
+        sim = wntr.sim.WNTRSimulator(wn)
+        results_wntr_closed = sim.run_sim()
+        
+        # Check that valve is closed (0) and flow is 0
+        assert (results_epanet_closed.link['status'].loc[:,'v1'] == 0).all()
+        assert (results_wntr_closed.link['status'].loc[:,'v1'] == 0).all()
+        assert (results_epanet_closed.link['flowrate'].loc[:,'v1'] == 0).all()
+        assert (results_wntr_closed.link['flowrate'].loc[:,'v1'] == 0).all()
+
+    def test_initial_setting(self):
+        # Run simulations with valve setting of 4
+        wn = copy.deepcopy(self.wn)
+        valve_name = 'v1'
+        valve = wn.get_link(valve_name)
+        # pressure setting on its downstream side when the upstream pressure is above the setting
+        valve.initial_setting = 4
+        
+        sim = wntr.sim.EpanetSimulator(wn)
+        results_epanet_open = sim.run_sim()
+        
+        sim = wntr.sim.WNTRSimulator(wn)
+        results_wntr_open = sim.run_sim()
+        
+        # Check that valve is active (2) and the downstream pressure is 4
+        assert (results_epanet_open.link['status'].loc[:,'v1'] == 2).all()
+        assert (results_wntr_open.link['status'].loc[:,'v1'] == 2).all()
+        assert (results_epanet_open.node['pressure'].loc[:,'j2'] == 4).all()
+        assert (results_wntr_open.node['pressure'].loc[:,'j2'] == 4).all()
+        
 class TestPumpSettingControls(unittest.TestCase):
     def test_status_open_when_setting_changes(self):
         wn = wntr.network.WaterNetworkModel()