コード例 #1
0
def main():
    args = parse_args()
    metrics.reset()
    start_time = time.time()
    logging.basicConfig(level=logging.INFO)

    # Create a SimPy environment
    env = simpy.Environment()

    # Seed the random generator
    random.seed(args.seed)
    numpy.random.seed(args.seed)

    # Parse network and get NetworkX object and ingress network list
    network, ing_nodes = reader.read_network(args.network,
                                             node_cap=10,
                                             link_cap=10)

    # Getting current SFC list, and the SF list of each SFC, and config

    # use dummy placement and schedule for running simulator without algorithm
    # TODO: make configurable via CLI
    sf_placement = dummy_data.triangle_placement
    schedule = dummy_data.triangle_schedule

    # Getting current SFC list, and the SF list of each SFC, and config
    sfc_list = reader.get_sfc(args.sf)
    sf_list = reader.get_sf(args.sf, args.sfr)
    config = reader.get_config(args.config)

    # Create the simulator parameters object with the provided args
    params = SimulatorParams(network,
                             ing_nodes,
                             sfc_list,
                             sf_list,
                             config,
                             args.seed,
                             sf_placement=sf_placement,
                             schedule=schedule)
    log.info(params)

    if args.trace:
        trace = reader.get_trace(args.trace)
        TraceProcessor(params, env, trace)

    # Create a FlowSimulator object, pass the SimPy environment and params objects
    simulator = FlowSimulator(env, params)

    # Start the simulation
    simulator.start()

    # Run the simpy environment for the specified duration
    env.run(until=args.duration)

    # Record endtime and running_time metrics
    end_time = time.time()
    metrics.running_time(start_time, end_time)

    # dump all metrics
    log.info(metrics.metrics)
コード例 #2
0
class TestFlowSimulator(TestCase):
    flow_simulator = None
    simulator_params = None

    def setUp(self):
        """
        Setup test environment
        """
        logging.basicConfig(level=logging.ERROR)

        self.env = simpy.Environment()
        # Configure simulator parameters
        network, ing_nodes, eg_nodes = reader.read_network(NETWORK_FILE,
                                                           node_cap=10,
                                                           link_cap=10)
        sfc_list = reader.get_sfc(SERVICE_FUNCTIONS_FILE)
        sf_list = reader.get_sf(SERVICE_FUNCTIONS_FILE, RESOURCE_FUNCTION_PATH)
        config = reader.get_config(CONFIG_FILE)

        self.metrics = Metrics(network, sf_list)

        sf_placement = dummy_data.triangle_placement
        schedule = dummy_data.triangle_schedule

        # Initialize Simulator and SimulatoParams objects
        self.simulator_params = SimulatorParams(log,
                                                network,
                                                ing_nodes,
                                                eg_nodes,
                                                sfc_list,
                                                sf_list,
                                                config,
                                                self.metrics,
                                                sf_placement=sf_placement,
                                                schedule=schedule)
        self.flow_simulator = FlowSimulator(self.env, self.simulator_params)
        self.flow_simulator.start()
        self.env.run(until=SIMULATION_DURATION)

    def test_simulator(self):
        """
        Test the simulator
        """
        # Collect metrics
        self.metric_collection = self.metrics.get_metrics()
        # Check if Simulator is initiated correctly
        self.assertIsInstance(self.flow_simulator, FlowSimulator)
        # Check if Params are set correctly
        self.assertIsInstance(self.simulator_params, SimulatorParams)
        # Check if generated flows are equal to processed flow + dropped + active flows
        gen_flow_check = self.metric_collection['generated_flows'] == (
            self.metric_collection['processed_flows'] +
            self.metric_collection['dropped_flows'] +
            self.metric_collection['total_active_flows'])
        self.assertIs(gen_flow_check, True)
コード例 #3
0
class Simulator(SimulatorInterface):
    def __init__(self, test_mode=False, test_dir=None):
        # Number of time the simulator has run. Necessary to correctly calculate env run time of apply function
        self.run_times = int(1)
        self.test_mode = test_mode
        self.test_dir = test_dir
        # Create CSV writer
        self.writer = ResultWriter(self.test_mode, self.test_dir)

    def init(self,
             network_file,
             service_functions_file,
             config_file,
             seed,
             trace=None,
             resource_functions_path=""):

        # Initialize metrics, record start time
        metrics.reset()
        self.run_times = int(1)
        self.start_time = time.time()

        # Parse network and SFC + SF file
        self.network, self.ing_nodes = reader.read_network(network_file,
                                                           node_cap=10,
                                                           link_cap=10)
        self.sfc_list = reader.get_sfc(service_functions_file)
        self.sf_list = reader.get_sf(service_functions_file,
                                     resource_functions_path)
        self.config = reader.get_config(config_file)

        # Generate SimPy simulation environment
        self.env = simpy.Environment()

        # Instantiate the parameter object for the simulator.
        self.params = SimulatorParams(self.network, self.ing_nodes,
                                      self.sfc_list, self.sf_list, self.config,
                                      seed)

        # Trace handling
        if trace:
            trace = reader.get_trace(trace)
            TraceProcessor(self.params, self.env, trace)

        self.duration = self.params.run_duration
        # Get and plant random seed
        self.seed = seed
        random.seed(self.seed)
        numpy.random.seed(self.seed)

        # Instantiate a simulator object, pass the environment and params
        self.simulator = FlowSimulator(self.env, self.params)

        # Start the simulator
        self.simulator.start()

        # Run the environment for one step to get initial stats.
        self.env.step()

        # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # for changing node remaining capacities.
        # Also, parse the network stats and prepare it in SimulatorState format.
        self.parse_network()
        self.network_metrics()

        # Record end time and running time metrics
        self.end_time = time.time()
        metrics.running_time(self.start_time, self.end_time)
        simulator_state = SimulatorState(self.network_dict,
                                         self.simulator.params.sf_placement,
                                         self.sfc_list, self.sf_list,
                                         self.traffic, self.network_stats)
        # self.writer.write_state_results(self.env, simulator_state)
        return simulator_state

    def apply(self, actions: SimulatorAction):

        self.writer.write_action_result(self.env, actions)
        # increase performance when debug logging is disabled
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f"SimulatorAction: %s", repr(actions))

        # Get the new placement from the action passed by the RL agent
        # Modify and set the placement parameter of the instantiated simulator object.
        self.simulator.params.sf_placement = actions.placement
        # Update which sf is available at which node
        for node_id, placed_sf_list in actions.placement.items():
            available = {}
            # Keep only SFs which still process
            for sf, sf_data in self.simulator.params.network.nodes[node_id][
                    'available_sf'].items():
                if sf_data['load'] != 0:
                    available[sf] = sf_data
            # Add all SFs which are in the placement
            for sf in placed_sf_list:
                available[sf] = available.get(sf, {'load': 0.0})
            self.simulator.params.network.nodes[node_id][
                'available_sf'] = available

        # Get the new schedule from the SimulatorAction
        # Set it in the params of the instantiated simulator object.
        self.simulator.params.schedule = actions.scheduling

        # reset metrics for steps
        metrics.reset_run()

        # Run the simulation again with the new params for the set duration.
        # Due to SimPy restraints, we multiply the duration by the run times because SimPy does not reset when run()
        # stops and we must increase the value of "until=" to accomodate for this. e.g.: 1st run call runs for 100 time
        # uniits (1 run time), 2nd run call will also run for 100 more time units but value of "until=" is now 200.
        runtime_steps = self.duration * self.run_times
        logger.debug("Running simulator until time step %s", runtime_steps)
        self.env.run(until=runtime_steps)

        # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # for changing node remaining capacities.
        # Also, parse the network stats and prepare it in SimulatorState format.
        self.parse_network()
        self.network_metrics()

        # Increment the run times variable
        self.run_times += 1

        # Record end time of the apply round, doesn't change start time to show the running time of the entire
        # simulation at the end of the simulation.
        self.end_time = time.time()
        metrics.running_time(self.start_time, self.end_time)

        # Create a new SimulatorState object to pass to the RL Agent
        simulator_state = SimulatorState(self.network_dict,
                                         self.simulator.params.sf_placement,
                                         self.sfc_list, self.sf_list,
                                         self.traffic, self.network_stats)
        self.writer.write_state_results(self.env, simulator_state)
        return simulator_state

    def parse_network(self) -> dict:
        """
        Converts the NetworkX network in the simulator to a dict in a format specified in the SimulatorState class.
        """
        self.network_dict = {'nodes': [], 'edges': []}
        for node in self.params.network.nodes(data=True):
            node_cap = node[1]['cap']
            used_node_cap = node[1]['cap'] - node[1]['remaining_cap']
            self.network_dict['nodes'].append({
                'id': node[0],
                'resource': node_cap,
                'used_resources': used_node_cap
            })
        for edge in self.network.edges(data=True):
            edge_src = edge[0]
            edge_dest = edge[1]
            edge_delay = edge[2]['delay']
            edge_dr = edge[2]['cap']
            # We use a fixed user data rate for the edges here as the functionality is not yet incorporated in the
            # simulator.
            # TODO: Implement used edge data rates in the simulator.
            edge_used_dr = 0
            self.network_dict['edges'].append({
                'src': edge_src,
                'dst': edge_dest,
                'delay': edge_delay,
                'data_rate': edge_dr,
                'used_data_rate': edge_used_dr
            })

    def network_metrics(self):
        """
        Processes the metrics and parses them in a format specified in the SimulatorState class.
        """
        stats = metrics.get_metrics()
        self.traffic = stats['run_total_requested_traffic']
        self.network_stats = {
            'total_flows': stats['generated_flows'],
            'successful_flows': stats['processed_flows'],
            'dropped_flows': stats['dropped_flows'],
            'in_network_flows': stats['total_active_flows'],
            'avg_end2end_delay': stats['avg_end2end_delay'],
            'run_avg_end2end_delay': stats['run_avg_end2end_delay'],
            'run_max_end2end_delay': stats['run_max_end2end_delay'],
            'run_total_processed_traffic': stats['run_total_processed_traffic']
        }
コード例 #4
0
class Simulator(SimulatorInterface):
    def __init__(self, network_file, service_functions_file, config_file, resource_functions_path="", test_mode=False,
                 test_dir=None):
        super().__init__(test_mode)
        # Number of time the simulator has run. Necessary to correctly calculate env run time of apply function
        self.network_file = network_file
        self.test_dir = test_dir
        # init network, sfc, sf, and config files
        self.network, self.ing_nodes, self.eg_nodes = reader.read_network(self.network_file)
        self.sfc_list = reader.get_sfc(service_functions_file)
        self.sf_list = reader.get_sf(service_functions_file, resource_functions_path)
        self.config = reader.get_config(config_file)
        self.metrics = Metrics(self.network, self.sf_list)
        # Assume result path is the path where network file is in.
        self.result_base_path = os.path.dirname(self.network_file)
        if 'trace_path' in self.config:
            # Quick solution to copy trace file to same path for network file as provided by calling algo.
            trace_path = os.path.join(os.getcwd(), self.config['trace_path'])
            copyfile(trace_path, os.path.join(self.result_base_path, os.path.basename(trace_path)))

        self.prediction = False
        # Check if future ingress traffic setting is enabled
        if 'future_traffic' in self.config and self.config['future_traffic']:
            self.prediction = True
        self.params = SimulatorParams(logger, self.network, self.ing_nodes, self.eg_nodes, self.sfc_list, self.sf_list,
                                      self.config, self.metrics, prediction=self.prediction)
        write_schedule = False
        if 'write_schedule' in self.config and self.config['write_schedule']:
            write_schedule = True
        write_flow_actions = False
        if 'write_flow_actions' in self.config and self.config['write_flow_actions']:
            write_flow_actions = True
        # Create CSV writer
        self.writer = ResultWriter(self.test_mode, self.test_dir, write_schedule, write_flow_actions)
        self.params.writer = self.writer
        self.episode = 0
        self.params.episode = 0
        self.last_apply_time = None
        # Load trace file
        if 'trace_path' in self.config:
            trace_path = os.path.join(os.getcwd(), self.config['trace_path'])
            self.trace = reader.get_trace(trace_path)

        self.lstm_predictor = None
        if 'lstm_prediction' in self.config and self.config['lstm_prediction']:
            self.lstm_predictor = LSTM_Predictor(self.trace, params=self.params,
                                                 weights_dir=self.config['lstm_weights'])

    def __del__(self):
        # write dropped flow locs to yaml
        self.writer.write_dropped_flow_locs(self.metrics.metrics['dropped_flows_locs'])

    def init(self, seed):
        # Reset predictor class at beginning of every init
        if self.prediction:
            self.predictor = TrafficPredictor(self.params, self.lstm_predictor)
        # increment episode count
        self.episode += 1
        self.params.episode += 1
        # reset network caps and available SFs:
        reader.reset_cap(self.network)
        # Initialize metrics, record start time
        self.params.run_times = int(1)
        self.start_time = time.time()

        # Generate SimPy simulation environment
        self.env = simpy.Environment()
        self.env.process(self.writer.begin_writing(self.env, self.params))

        self.params.metrics.reset_metrics()

        # Instantiate the parameter object for the simulator.
        if self.params.use_states and 'trace_path' in self.config:
            logger.warning('Two state model and traces are both activated, thi will cause unexpected behaviour!')

        if self.params.use_states:
            if self.params.in_init_state:
                self.params.in_init_state = False
            # else:
            self.env.process(self.params.start_mmpp(self.env))

        self.duration = self.params.run_duration
        # Get and plant random seed
        self.seed = seed
        random.seed(self.seed)
        numpy.random.seed(self.seed)

        self.params.reset_flow_lists()
        # generate flow lists 1x here since we are in `init()`
        self.params.generate_flow_lists()

        # Instantiate a simulator object, pass the environment and params
        self.simulator = FlowSimulator(self.env, self.params)

        # Trace handling
        if 'trace_path' in self.config:
            TraceProcessor(self.params, self.env, self.trace, self.simulator)

        # Start the simulator
        self.simulator.start()

        # TODO: Create runner here
        controller_cls = eval(self.params.controller_class)
        self.controller = controller_cls(self.env, self.params, self.simulator)
        # # Run the environment for one step to get initial stats.
        # self.env.step()

        # # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # # for changing node remaining capacities.
        # # Also, parse the network stats and prepare it in SimulatorState format.
        # self.parse_network()
        # self.network_metrics()

        # Record end time and running time metrics
        self.end_time = time.time()
        self.params.metrics.running_time(self.start_time, self.end_time)
        simulator_state = self.controller.get_init_state()
        # Check to see if traffic prediction is enabled to provide future traffic not current traffic
        # if self.prediction:
        #     requested_traffic = self.get_current_ingress_traffic()
        #     self.predictor.predict_traffic(self.env.now, current_traffic=requested_traffic)
        #     stats = self.params.metrics.get_metrics()
        #     self.traffic = stats['run_total_requested_traffic']
        # simulator_state = SimulatorState(self.network_dict, self.simulator.params.sf_placement, self.sfc_list,
        #                                  self.sf_list, self.traffic, self.network_stats)
        # logger.debug(f"t={self.env.now}: {simulator_state}")
        # set time stamp to calculate runtime of next apply call
        self.last_apply_time = time.time()
        # Check to see if init called in warmup, if so, set warmup to false
        # This is to allow for better prediction and better overall control
        # in the future
        return simulator_state

    def apply(self, actions):

        logger.debug(f"t={self.env.now}: {actions}")

        # calc runtime since last apply (or init): that's the algorithm's runtime without simulation
        alg_runtime = time.time() - self.last_apply_time
        self.writer.write_runtime(alg_runtime)
        simulator_state = self.controller.get_next_state(actions)

        # # Get the new placement from the action passed by the RL agent
        # # Modify and set the placement parameter of the instantiated simulator object.
        # self.simulator.params.sf_placement = actions.placement
        # Update which sf is available at which node
        # for node_id, placed_sf_list in actions.placement.items():
        #     available = {}
        #     # Keep only SFs which still process
        #     for sf, sf_data in self.simulator.params.network.nodes[node_id]['available_sf'].items():
        #         if sf_data['load'] != 0:
        #             available[sf] = sf_data
        #     # Add all SFs which are in the placement
        #     for sf in placed_sf_list:
        #         if sf not in available.keys():
        #             available[sf] = available.get(sf, {
        #                 'load': 0.0,
        #                 'last_active': self.env.now,
        #                 'startup_time': self.env.now
        #             })
        #     self.simulator.params.network.nodes[node_id]['available_sf'] = available

        # Get the new schedule from the SimulatorAction
        # Set it in the params of the instantiated simulator object.
        # self.simulator.params.schedule = actions.scheduling

        # reset metrics for steps
        self.params.metrics.reset_run_metrics()

        # Run the simulation again with the new params for the set duration.
        # Due to SimPy restraints, we multiply the duration by the run times because SimPy does not reset when run()
        # stops and we must increase the value of "until=" to accomodate for this. e.g.: 1st run call runs for 100 time
        # uniits (1 run time), 2nd run call will also run for 100 more time units but value of "until=" is now 200.
        # runtime_steps = self.duration * self.params.run_times
        # logger.debug("Running simulator until time step %s", runtime_steps)
        # self.env.run(until=runtime_steps)

        # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # for changing node remaining capacities.
        # Also, parse the network stats and prepare it in SimulatorState format.
        # self.parse_network()
        # self.network_metrics()

        # Increment the run times variable
        self.params.run_times += 1

        # Record end time of the apply round, doesn't change start time to show the running time of the entire
        # simulation at the end of the simulation.
        self.end_time = time.time()
        self.params.metrics.running_time(self.start_time, self.end_time)

        # if self.params.use_states:
        #     self.params.update_state()
        # generate flow data for next run (used for prediction)
        self.params.generate_flow_lists(now=self.env.now)

        # Check to see if traffic prediction is enabled to provide future traffic not current traffic
        # if self.prediction:
        #     requested_traffic = self.get_current_ingress_traffic()
        #     self.predictor.predict_traffic(self.env.now, current_traffic=requested_traffic)
        #     stats = self.params.metrics.get_metrics()
        #     self.traffic = stats['run_total_requested_traffic']
        # # Create a new SimulatorState object to pass to the RL Agent
        # simulator_state = SimulatorState(self.network_dict, self.simulator.params.sf_placement, self.sfc_list,
        #                                  self.sf_list, self.traffic, self.network_stats)
        # self.writer.write_state_results(self.episode, self.env.now, simulator_state,
        # self.params.metrics.get_metrics())
        logger.debug(f"t={self.env.now}: {simulator_state}")
        # set time stamp to calculate runtime of next apply call
        self.last_apply_time = time.time()
        return simulator_state

    def get_current_ingress_traffic(self) -> float:
        """
        Get current ingress traffic for the LSTM module
        Current limitation: works for 1 SFC and 1 ingress node
        """
        # Get name of ingress SF from first SFC
        first_sfc = list(self.sfc_list.keys())[0]
        ingress_sf = self.params.sfc_list[first_sfc][0]
        ingress_node = self.params.ing_nodes[0][0]
        ingress_traffic = self.metrics.metrics['run_total_requested_traffic'][ingress_node][first_sfc][ingress_sf]
        return ingress_traffic

    def get_active_ingress_nodes(self):
        """Return names of all ingress nodes that are currently active, ie, produce flows."""
        return [ing[0] for ing in self.ing_nodes if self.params.inter_arr_mean[ing[0]] is not None]
コード例 #5
0
ファイル: simulator.py プロジェクト: SoonyangZhang/coord-sim
class Simulator(SimulatorInterface):
    def __init__(self,
                 network_file,
                 service_functions_file,
                 config_file,
                 resource_functions_path="",
                 test_mode=False,
                 test_dir=None):
        super().__init__(test_mode)
        # Number of time the simulator has run. Necessary to correctly calculate env run time of apply function
        self.run_times = int(1)
        self.network_file = network_file
        self.test_dir = test_dir
        # Create CSV writer
        self.writer = ResultWriter(self.test_mode, self.test_dir)
        # init network, sfc, sf, and config files
        self.network, self.ing_nodes, self.eg_nodes = reader.read_network(
            self.network_file)
        self.sfc_list = reader.get_sfc(service_functions_file)
        self.sf_list = reader.get_sf(service_functions_file,
                                     resource_functions_path)
        self.config = reader.get_config(config_file)
        self.metrics = Metrics(self.network, self.sf_list)
        # Assume result path is the path where network file is in.
        self.result_base_path = os.path.dirname(self.network_file)
        if 'trace_path' in self.config:
            # Quick solution to copy trace file to same path for network file as provided by calling algo.
            trace_path = os.path.join(os.getcwd(), self.config['trace_path'])
            copyfile(
                trace_path,
                os.path.join(self.result_base_path,
                             os.path.basename(trace_path)))

        self.prediction = False
        # Check if future ingress traffic setting is enabled
        if 'future_traffic' in self.config and self.config['future_traffic']:
            self.prediction = True
        self.params = SimulatorParams(self.network,
                                      self.ing_nodes,
                                      self.eg_nodes,
                                      self.sfc_list,
                                      self.sf_list,
                                      self.config,
                                      self.metrics,
                                      prediction=self.prediction)
        self.episode = 0

    def __del__(self):
        # write dropped flow locs to yaml
        self.writer.write_dropped_flow_locs(
            self.metrics.metrics['dropped_flows_locs'])

    def init(self, seed):
        # Reset predictor class at beginning of every init
        if self.prediction:
            self.predictor = TrafficPredictor(self.params)
        # increment episode count
        self.episode += 1
        # reset network caps and available SFs:
        reader.reset_cap(self.network)
        # Initialize metrics, record start time
        self.run_times = int(1)
        self.start_time = time.time()

        # Generate SimPy simulation environment
        self.env = simpy.Environment()

        self.params.metrics.reset_metrics()

        # Instantiate the parameter object for the simulator.
        if self.params.use_states and 'trace_path' in self.config:
            logger.warning(
                'Two state model and traces are both activated, thi will cause unexpected behaviour!'
            )

        if self.params.use_states:
            if self.params.in_init_state:
                self.params.in_init_state = False
            else:
                self.params.update_state()

        self.duration = self.params.run_duration
        # Get and plant random seed
        self.seed = seed
        random.seed(self.seed)
        numpy.random.seed(self.seed)

        self.params.reset_flow_lists()
        # generate flow lists 1x here since we are in `init()`
        self.params.generate_flow_lists()

        # Instantiate a simulator object, pass the environment and params
        self.simulator = FlowSimulator(self.env, self.params)

        # Trace handling
        if 'trace_path' in self.config:
            trace_path = os.path.join(os.getcwd(), self.config['trace_path'])
            trace = reader.get_trace(trace_path)
            TraceProcessor(self.params, self.env, trace, self.simulator)

        # Start the simulator
        self.simulator.start()

        # Run the environment for one step to get initial stats.
        self.env.step()

        # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # for changing node remaining capacities.
        # Also, parse the network stats and prepare it in SimulatorState format.
        self.parse_network()
        self.network_metrics()

        # Record end time and running time metrics
        self.end_time = time.time()
        self.params.metrics.running_time(self.start_time, self.end_time)
        # Check to see if traffic prediction is enabled to provide future traffic not current traffic
        if self.prediction:
            self.predictor.predict_traffic(self.env.now)
            stats = self.params.metrics.get_metrics()
            self.traffic = stats['run_total_requested_traffic']
        simulator_state = SimulatorState(self.network_dict,
                                         self.simulator.params.sf_placement,
                                         self.sfc_list, self.sf_list,
                                         self.traffic, self.network_stats)
        logger.debug(f"t={self.env.now}: {simulator_state}")
        # Check to see if init called in warmup, if so, set warmup to false
        # This is to allow for better prediction and better overall control
        # in the future
        return simulator_state

    def apply(self, actions: SimulatorAction):

        self.writer.write_action_result(self.episode, self.env.now, actions)
        logger.debug(f"t={self.env.now}: {actions}")

        # Get the new placement from the action passed by the RL agent
        # Modify and set the placement parameter of the instantiated simulator object.
        self.simulator.params.sf_placement = actions.placement
        # Update which sf is available at which node
        for node_id, placed_sf_list in actions.placement.items():
            available = {}
            # Keep only SFs which still process
            for sf, sf_data in self.simulator.params.network.nodes[node_id][
                    'available_sf'].items():
                if sf_data['load'] != 0:
                    available[sf] = sf_data
            # Add all SFs which are in the placement
            for sf in placed_sf_list:
                available[sf] = available.get(sf, {'load': 0.0})
            self.simulator.params.network.nodes[node_id][
                'available_sf'] = available

        # Get the new schedule from the SimulatorAction
        # Set it in the params of the instantiated simulator object.
        self.simulator.params.schedule = actions.scheduling

        # reset metrics for steps
        self.params.metrics.reset_run_metrics()

        # Run the simulation again with the new params for the set duration.
        # Due to SimPy restraints, we multiply the duration by the run times because SimPy does not reset when run()
        # stops and we must increase the value of "until=" to accomodate for this. e.g.: 1st run call runs for 100 time
        # uniits (1 run time), 2nd run call will also run for 100 more time units but value of "until=" is now 200.
        runtime_steps = self.duration * self.run_times
        logger.debug("Running simulator until time step %s", runtime_steps)
        self.env.run(until=runtime_steps)

        # Parse the NetworkX object into a dict format specified in SimulatorState. This is done to account
        # for changing node remaining capacities.
        # Also, parse the network stats and prepare it in SimulatorState format.
        self.parse_network()
        self.network_metrics()

        # Increment the run times variable
        self.run_times += 1

        # Record end time of the apply round, doesn't change start time to show the running time of the entire
        # simulation at the end of the simulation.
        self.end_time = time.time()
        self.params.metrics.running_time(self.start_time, self.end_time)

        if self.params.use_states:
            self.params.update_state()
        # generate flow data for next run (used for prediction)
        self.params.generate_flow_lists(now=self.env.now)

        # Check to see if traffic prediction is enabled to provide future traffic not current traffic
        if self.prediction:
            self.predictor.predict_traffic(self.env.now)
            stats = self.params.metrics.get_metrics()
            self.traffic = stats['run_total_requested_traffic']
        # Create a new SimulatorState object to pass to the RL Agent
        simulator_state = SimulatorState(self.network_dict,
                                         self.simulator.params.sf_placement,
                                         self.sfc_list, self.sf_list,
                                         self.traffic, self.network_stats)
        self.writer.write_state_results(self.episode, self.env.now,
                                        simulator_state)
        logger.debug(f"t={self.env.now}: {simulator_state}")

        return simulator_state

    def parse_network(self) -> dict:
        """
        Converts the NetworkX network in the simulator to a dict in a format specified in the SimulatorState class.
        """
        max_node_usage = self.params.metrics.get_metrics(
        )['run_max_node_usage']
        self.network_dict = {'nodes': [], 'edges': []}
        for node in self.params.network.nodes(data=True):
            node_cap = node[1]['cap']
            run_max_node_usage = max_node_usage[node[0]]
            # 'used_resources' here is the max usage for the run.
            self.network_dict['nodes'].append({
                'id':
                node[0],
                'resource':
                node_cap,
                'used_resources':
                run_max_node_usage
            })
        for edge in self.network.edges(data=True):
            edge_src = edge[0]
            edge_dest = edge[1]
            edge_delay = edge[2]['delay']
            edge_dr = edge[2]['cap']
            # We use a fixed user data rate for the edges here as the functionality is not yet incorporated in the
            # simulator.
            # TODO: Implement used edge data rates in the simulator.
            edge_used_dr = 0
            self.network_dict['edges'].append({
                'src': edge_src,
                'dst': edge_dest,
                'delay': edge_delay,
                'data_rate': edge_dr,
                'used_data_rate': edge_used_dr
            })

    def network_metrics(self):
        """
        Processes the metrics and parses them in a format specified in the SimulatorState class.
        """
        stats = self.params.metrics.get_metrics()
        self.traffic = stats['run_total_requested_traffic']
        self.network_stats = {
            'processed_traffic': stats['run_total_processed_traffic'],
            'total_flows': stats['generated_flows'],
            'successful_flows': stats['processed_flows'],
            'dropped_flows': stats['dropped_flows'],
            'in_network_flows': stats['total_active_flows'],
            'avg_end2end_delay': stats['avg_end2end_delay'],
            'run_avg_end2end_delay': stats['run_avg_end2end_delay'],
            'run_max_end2end_delay': stats['run_max_end2end_delay'],
            'run_avg_path_delay': stats['run_avg_path_delay'],
            'run_total_processed_traffic':
            stats['run_total_processed_traffic'],
            'run_dropped_flows_per_node': stats['run_dropped_flows_per_node']
        }

    def get_active_ingress_nodes(self):
        """Return names of all ingress nodes that are currently active, ie, produce flows."""
        return [
            ing[0] for ing in self.ing_nodes
            if self.params.inter_arr_mean[ing[0]] is not None
        ]