예제 #1
0
    def __init__(self, wn, mode='DD'):

        super(WNTRSimulator, self).__init__(wn, mode)
        self._internal_graph = None
        self._node_pairs_with_multiple_links = None
        self._presolve_controls = ControlManager()
        self._rules = ControlManager()
        self._postsolve_controls = ControlManager()
        self._time_per_step = []
        self._solver = None
        self._model = None
예제 #2
0
class WNTRSimulator(WaterNetworkSimulator):
    """
    WNTR simulator class.
    The WNTR simulator uses a custom newton solver and linear solvers from scipy.sparse.

    Parameters
    ----------
    wn : WaterNetworkModel object
        Water network model

    mode: string (optional)
        Specifies whether the simulation will be demand-driven (DD) or
        pressure dependent demand (PDD), default = DD
    """

    def __init__(self, wn, mode='DD'):

        super(WNTRSimulator, self).__init__(wn, mode)
        self._internal_graph = None
        self._node_pairs_with_multiple_links = None
        self._presolve_controls = ControlManager()
        self._rules = ControlManager()
        self._postsolve_controls = ControlManager()
        self._time_per_step = []
        self._solver = None
        self._model = None

    def _get_time(self):
        s = int(self._wn.sim_time)
        h = int(s/3600)
        s -= h*3600
        m = int(s/60)
        s -= m*60
        s = int(s)
        return str(h)+':'+str(m)+':'+str(s)

    def run_sim(self, solver_options={}, convergence_error=True):
        """
        Run an extended period simulation (hydraulics only).

        Parameters
        ----------
        solver_options: dict
            Solver options are specified using the following dictionary keys:

            * MAXITER: the maximum number of iterations for each hydraulic solve (each timestep and trial) (default = 100)
            * TOL: tolerance for the hydraulic equations (default = 1e-6)
            * BT_RHO: the fraction by which the step length is reduced at each iteration of the line search (default = 0.5)
            * BT_MAXITER: the maximum number of iterations for each line search (default = 20)
            * BACKTRACKING: whether or not to use a line search (default = True)
            * BT_START_ITER: the newton iteration at which a line search should start being used (default = 2)

        convergence_error: bool (optional)
            If convergence_error is True, an error will be raised if the
            simulation does not converge. If convergence_error is False,
            a warning will be issued and results.error_code will be set to 2
            if the simulation does not converge.  Default = True.
        """
        logger_level = logger.getEffectiveLevel()

        if logger_level <= 1:
            logger.log(1, 'beginning of run_sim')

        self._time_per_step = []

        self._presolve_controls = ControlManager()
        self._postsolve_controls = ControlManager()
        self._rules = ControlManager()

        def categorize_control(control):
            if control.epanet_control_type in {_ControlType.presolve, _ControlType.pre_and_postsolve}:
                self._presolve_controls.register_control(control)
            if control.epanet_control_type in {_ControlType.postsolve, _ControlType.pre_and_postsolve}:
                self._postsolve_controls.register_control(control)
            if control.epanet_control_type == _ControlType.rule:
                self._rules.register_control(control)

        for c_name, c in self._wn.controls():
            categorize_control(c)
        for c in (self._wn._get_all_tank_controls() + self._wn._get_cv_controls() + self._wn._get_pump_controls() +
                  self._wn._get_valve_controls()):
            categorize_control(c)

        if logger_level <= 1:
            logger.log(1, 'collected presolve controls:')
            for c in self._presolve_controls:
                logger.log(1, '\t' + str(c))
            logger.log(1, 'collected rules:')
            for c in self._rules:
                logger.log(1, '\t' + str(c))
            logger.log(1, 'collected postsolve controls:')
            for c in self._postsolve_controls:
                logger.log(1, '\t' + str(c))

            logger.log(1, 'initializing hydraulic model')

        model = HydraulicModel(self._wn, self.mode)
        self._model = model
        model.initialize_results_dict()

        self._solver = NewtonSolver(model.num_nodes, model.num_links, model.num_leaks, model, options=solver_options)

        results = NetResults()
        results.error_code = 0
        results.time = []

        # Initialize X
        # Vars will be ordered:
        #    1.) head
        #    2.) demand
        #    3.) flow
        #    4.) leak_demand
        model.set_network_inputs_by_id()
        head0 = model.initialize_head()
        demand0 = model.initialize_demand()
        flow0 = model.initialize_flow()
        leak_demand0 = model.initialize_leak_demand()

        X_init = np.concatenate((head0, demand0, flow0, leak_demand0))

        self._initialize_internal_graph()

        if self._wn.sim_time == 0:
            first_step = True
        else:
            first_step = False
        trial = -1
        max_trials = self._wn.options.solver.trials
        resolve = False
        rule_iter = 0  # this is used to determine the rule timestep

        if first_step:
            self._model.update_network_previous_values()
            self._wn._prev_sim_time = -1

        if logger_level <= 1:
            logger.log(1, 'starting simulation')

        while True:
            if logger_level <= logging.DEBUG:
                logger.debug('\n\n')

            if not resolve:
                """
                Within this if statement:
                    1) Determine the next time step. This depends on both presolve controls and rules. Note that 
                       (unless this is the first time step) the current value of wn.sim_time is the next hydraulic 
                       timestep. If there are presolve controls or rules that need activated before the next hydraulic
                       timestep, then the wn.sim_time will be adjusted within this if statement.
                       
                        a) check the presolve controls to see which ones need activated.
                        b) if there is a presolve control(s) that need activated and it needs activated at a time
                           that is earlier than the next rule timestep, then the next simulation time is determined
                           by that presolve controls
                        c) if there are any rules that need activated before the next hydraulic timestep, then 
                           wn.sim_time will be adjusted to the appropriate rule timestep.
                    2) Activate the appropriate controls
                """
                start_step_time = time.time()  # this is just for timing

                if not first_step:
                    """
                    The tank levels/heads must be done before checking the controls because the TankLevelControls
                    depend on the tank levels. These will be updated again after we determine the next actual timestep.
                    """
                    self._model.update_tank_heads()
                trial = 0

                # check which presolve controls need to be activated before the next hydraulic timestep
                presolve_controls_to_run = self._presolve_controls.check()
                presolve_controls_to_run.sort(key=lambda i: i[0]._priority)  # sort them by priority
                # now sort them from largest to smallest "backtrack"; this way they are in the time-order
                # in which they need to be activated
                presolve_controls_to_run.sort(key=lambda i: i[1], reverse=True)
                if first_step:  # we don't want to backtrack if the sim time is 0
                    presolve_controls_to_run = [(c, 0) for c, b in presolve_controls_to_run]
                if logger_level <= 1:
                    logger.log(1, 'presolve_controls that need activated before the next hydraulic timestep:')
                    for pctr in presolve_controls_to_run:
                        logger.log(1, '\tcontrol: {0} \tbacktrack: {1}'.format(pctr[0], pctr[1]))
                cnt = 0

                # loop until we have checked all of the presolve_controls_to_run and all of the rules prior to the next
                # hydraulic timestep
                while cnt < len(presolve_controls_to_run) or rule_iter * self._wn.options.time.rule_timestep <= self._wn.sim_time:
                    if cnt >= len(presolve_controls_to_run):
                        # We have already checked all of the presolve_controls_to_run, and nothing changed
                        # Now we just need to check the rules
                        if logger_level <= 1:
                            logger.log(1, 'no presolve controls need activated; checking rules at rule timestep {0}'.format(rule_iter * self._wn.options.time.rule_timestep))
                        old_time = self._wn.sim_time
                        self._wn.sim_time = rule_iter * self._wn.options.time.rule_timestep
                        if not first_step:
                            self._model.update_tank_heads()
                        rule_iter += 1
                        rules_to_run = self._rules.check()
                        rules_to_run.sort(key=lambda i: i[0]._priority)
                        for rule, rule_back in rules_to_run:  # rule_back is the "backtrack" which is not actually used for rules
                            if logger_level <= 1:
                                logger.log(1, '\tactivating rule {0}'.format(rule))
                            rule.run_control_action()
                        if self._rules.changes_made():
                            # If changes were made, then we found the next timestep; break
                            break
                        # if no changes were made, then set the wn.sim_time back
                        if logger_level <= 1:
                            logger.log(1, 'no changes made by rules at rule timestep {0}'.format((rule_iter - 1) * self._wn.options.time.rule_timestep))
                        self._wn.sim_time = old_time
                    else:
                        # check the next presolve control in presolve_controls_to_run
                        control, backtrack = presolve_controls_to_run[cnt]
                        if logger_level <= 1:
                            logger.log(1, 'checking control {0}; backtrack: {1}'.format(control, backtrack))
                        if self._wn.sim_time - backtrack < rule_iter * self._wn.options.time.rule_timestep:
                            # The control needs activated before the next rule timestep; Activate the control and
                            # any controls with the samve value for backtrack
                            if logger_level <= 1:
                                logger.log(1, 'control {0} needs run before the next rule timestep.'.format(control))
                            control.run_control_action()
                            cnt += 1
                            while cnt < len(presolve_controls_to_run) and presolve_controls_to_run[cnt][1] == backtrack:
                                # Also activate all of the controls that have the same value for backtrack
                                if logger_level <= 1:
                                    logger.log(1, '\talso activating control {0}; backtrack: {1}'.format(presolve_controls_to_run[cnt][0],
                                                                                                   presolve_controls_to_run[cnt][1]))
                                presolve_controls_to_run[cnt][0].run_control_action()
                                cnt += 1
                            if self._presolve_controls.changes_made():
                                # changes were actually made; we found the next timestep; update wn.sim_time and break
                                self._wn.sim_time -= backtrack
                                break
                            if logger_level <= 1:
                                logger.log(1, 'controls with backtrack {0} did not make any changes'.format(backtrack))
                        elif self._wn.sim_time - backtrack == rule_iter * self._wn.options.time.rule_timestep:
                            # the control needs activated at the same time as the next rule timestep;
                            # activate the control, any controls with the same value for backtrack, and any rules at
                            # this rule timestep
                            # the rules need run first (I think to match epanet)
                            if logger_level <= 1:
                                logger.log(1, 'control has backtrack equivalent to next rule timestep')
                            rule_iter += 1
                            self._wn.sim_time -= backtrack
                            if not first_step:
                                self._model.update_tank_heads()
                            rules_to_run = self._rules.check()
                            rules_to_run.sort(key=lambda i: i[0]._priority)
                            for rule, rule_back in rules_to_run:
                                if logger_level <= 1:
                                    logger.log(1, '\tactivating rule {0}'.format(rule))
                                rule.run_control_action()
                            if logger_level <= 1:
                                logger.log(1, '\tactivating control {0}; backtrack: {1}'.format(control, backtrack))
                            control.run_control_action()
                            cnt += 1
                            while cnt < len(presolve_controls_to_run) and presolve_controls_to_run[cnt][1] == backtrack:
                                if logger_level <= 1:
                                    logger.log(1, '\talso activating control {0}; backtrack: {1}'.format(presolve_controls_to_run[cnt][0], presolve_controls_to_run[cnt][1]))
                                presolve_controls_to_run[cnt][0].run_control_action()
                                cnt += 1
                            if self._presolve_controls.changes_made() or self._rules.changes_made():
                                break
                            if logger_level <= 1:
                                logger.log(1, 'no changes made by presolve controls or rules at backtrack {0}'.format(backtrack))
                            self._wn.sim_time += backtrack
                        else:
                            if logger_level <= 1:
                                logger.log(1, 'The next rule timestep is before this control needs activated; checking rules')
                            old_time = self._wn.sim_time
                            self._wn.sim_time = rule_iter * self._wn.options.time.rule_timestep
                            rule_iter += 1
                            if not first_step:
                                self._model.update_tank_heads()
                            rules_to_run = self._rules.check()
                            rules_to_run.sort(key=lambda i: i[0]._priority)
                            for rule, rule_back in rules_to_run:
                                if logger_level <= 1:
                                    logger.log(1, '\tactivating rule {0}'.format(rule))
                                rule.run_control_action()
                            if self._rules.changes_made():
                                break
                            if logger_level <= 1:
                                logger.log(1, 'no changes made by rules at rule timestep {0}'.format((rule_iter - 1) * self._wn.options.time.rule_timestep))
                            self._wn.sim_time = old_time
                self._update_internal_graph()
                if logger_level <= logging.DEBUG:
                    logger.debug('changes made by rules: ')
                    for obj, attr in self._rules.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                    logger.debug('changes made by presolve controls:')
                    for obj, attr in self._presolve_controls.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                self._presolve_controls.reset()
                self._rules.reset()

            logger.info('simulation time = %s, trial = %d', self._get_time(), trial)

            # Prepare for solve
            if logger_level <= logging.DEBUG:
                logger.debug('checking for isolated junctions and links')
            isolated_junctions, isolated_links = self._get_isolated_junctions_and_links()
            if logger_level <= logging.DEBUG:
                if len(isolated_junctions) > 0 or len(isolated_links) > 0:
                    logger.debug('isolated junctions: {0}'.format(isolated_junctions))
                    logger.debug('isolated links: {0}'.format(isolated_links))
                else:
                    logger.debug('no isolated junctions or links found')
            model.set_isolated_junctions_and_links(isolated_junctions, isolated_links)
            if not first_step and not resolve:
                model.update_tank_heads()
            model.set_network_inputs_by_id()
            model.set_jacobian_constants()

            # Solve
            if logger_level <= logging.DEBUG:
                logger.debug('solving')
            [self._X, num_iters, solver_status, message] = self._solver.solve(model.get_hydraulic_equations, model.get_jacobian, X_init)
            if solver_status == 0:
                if convergence_error:
                    logger.error('Simulation did not converge. ' + message)
                    raise RuntimeError('Simulation did not converge. ' + message)
                warnings.warn('Simulation did not converge. ' + message)
                logger.warning('Simulation did not converge at time ' + str(self._get_time()) + '. ' + message)
                model.get_results(results)
                results.error_code = 2
                return results
            X_init = np.array(self._X)

            # Enter results in network and update previous inputs
            if logger_level <= logging.DEBUG:
                logger.debug('storing results in network')
            model.store_results_in_network(self._X)

            if logger_level <= logging.DEBUG:
                logger.debug('checking postsolve controls')
            self._postsolve_controls.reset()
            postsolve_controls_to_run = self._postsolve_controls.check()
            postsolve_controls_to_run.sort(key=lambda i: i[0]._priority)
            for control, unused in postsolve_controls_to_run:
                if logger_level <= 1:
                    logger.log(1, '\tactivating control {0}'.format(control))
                control.run_control_action()
            if self._postsolve_controls.changes_made():
                if logger_level <= logging.DEBUG:
                    logger.debug('postsolve controls made changes:')
                    for obj, attr in self._postsolve_controls.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                resolve = True
                self._update_internal_graph()
                self._postsolve_controls.reset()
                trial += 1
                if trial > max_trials:
                    if convergence_error:
                        logger.error('Exceeded maximum number of trials.')
                        raise RuntimeError('Exceeded maximum number of trials.')
                    results.error_code = 2
                    warnings.warn('Exceeded maximum number of trials.')
                    logger.warning('Exceeded maximum number of trials at time %s', self._get_time())
                    model.get_results(results)
                    return results
                continue

            logger.debug('no changes made by postsolve controls; moving to next timestep')

            resolve = False
            if type(self._wn.options.time.report_timestep) == float or type(self._wn.options.time.report_timestep) == int:
                if self._wn.sim_time % self._wn.options.time.report_timestep == 0:
                    model.save_results(self._X, results)
                    if len(results.time) > 0 and int(self._wn.sim_time) == results.time[-1]:
                        raise RuntimeError('Simulation already solved this timestep')
                    results.time.append(int(self._wn.sim_time))
            elif self._wn.options.time.report_timestep.upper() == 'ALL':
                model.save_results(self._X, results)
                if len(results.time) > 0 and int(self._wn.sim_time) == results.time[-1]:
                    raise RuntimeError('Simulation already solved this timestep')
                results.time.append(int(self._wn.sim_time))
            model.update_network_previous_values()
            first_step = False
            self._wn.sim_time += self._wn.options.time.hydraulic_timestep
            overstep = float(self._wn.sim_time) % self._wn.options.time.hydraulic_timestep
            self._wn.sim_time -= overstep

            if self._wn.sim_time > self._wn.options.time.duration:
                break

            self._time_per_step.append(time.time()-start_step_time)

        model.get_results(results)
        return results

    def _initialize_internal_graph(self):
        n_links = {}
        rows = []
        cols = []
        vals = []
        for link_name, link in itertools.chain(self._wn.pipes(), self._wn.pumps(), self._wn.valves()):
            from_node_name = link.start_node_name
            to_node_name = link.end_node_name
            from_node_id = self._model._node_name_to_id[from_node_name]
            to_node_id = self._model._node_name_to_id[to_node_name]
            if (from_node_id, to_node_id) not in n_links:
                n_links[(from_node_id, to_node_id)] = 0
                n_links[(to_node_id, from_node_id)] = 0
            n_links[(from_node_id, to_node_id)] += 1
            n_links[(to_node_id, from_node_id)] += 1
            rows.append(from_node_id)
            cols.append(to_node_id)
            rows.append(to_node_id)
            cols.append(from_node_id)
            if link.status == wntr.network.LinkStatus.closed:
                vals.append(0)
                vals.append(0)
            else:
                vals.append(1)
                vals.append(1)

        self._internal_graph = scipy.sparse.csr_matrix((vals, (rows, cols)))

        ndx_map = {}
        for link_name, link in self._wn.links():
            ndx1 = None
            ndx2 = None
            from_node_name = link.start_node_name
            to_node_name = link.end_node_name
            from_node_id = self._model._node_name_to_id[from_node_name]
            to_node_id = self._model._node_name_to_id[to_node_name]
            ndx1 = _get_csr_data_index(self._internal_graph, from_node_id, to_node_id)
            ndx2 = _get_csr_data_index(self._internal_graph, to_node_id, from_node_id)
            ndx_map[link] = (ndx1, ndx2)
        self._map_link_to_internal_graph_data_ndx = ndx_map

        self._number_of_connections = [0 for i in range(self._model.num_nodes)]
        for node_id in self._model._node_ids:
            self._number_of_connections[node_id] = self._internal_graph.indptr[node_id+1] - self._internal_graph.indptr[node_id]

        self._node_pairs_with_multiple_links = {}
        for from_node_id, to_node_id in n_links.keys():
            if n_links[(from_node_id, to_node_id)] > 1:
                if (to_node_id, from_node_id) in self._node_pairs_with_multiple_links:
                    continue
                self._internal_graph[from_node_id, to_node_id] = 0
                self._internal_graph[to_node_id, from_node_id] = 0
                from_node_name = self._model._node_id_to_name[from_node_id]
                to_node_name = self._model._node_id_to_name[to_node_id]
                tmp_list = self._node_pairs_with_multiple_links[(from_node_id, to_node_id)] = []
                for link_name in self._wn.get_links_for_node(from_node_name):
                    link = self._wn.get_link(link_name)
                    if link.start_node_name == to_node_name or link.end_node_name == to_node_name:
                        tmp_list.append(link)
                        if link.status != wntr.network.LinkStatus.closed:
                            ndx1, ndx2 = ndx_map[link]
                            self._internal_graph.data[ndx1] = 1
                            self._internal_graph.data[ndx2] = 1

    def _update_internal_graph(self):
        data = self._internal_graph.data
        ndx_map = self._map_link_to_internal_graph_data_ndx
        for mgr in [self._presolve_controls, self._rules, self._postsolve_controls]:
            for obj, attr in mgr.get_changes():
                if 'status' == attr:
                    if obj.status == wntr.network.LinkStatus.closed:
                        ndx1, ndx2 = ndx_map[obj]
                        data[ndx1] = 0
                        data[ndx2] = 0
                    else:
                        ndx1, ndx2 = ndx_map[obj]
                        data[ndx1] = 1
                        data[ndx2] = 1

        for key, link_list in self._node_pairs_with_multiple_links.items():
            from_node_id = key[0]
            to_node_id = key[1]
            first_link = link_list[0]
            ndx1, ndx2 = ndx_map[first_link]
            data[ndx1] = 0
            data[ndx2] = 0
            for link in link_list:
                if link.status != wntr.network.LinkStatus.closed:
                    ndx1, ndx2 = ndx_map[link]
                    data[ndx1] = 1
                    data[ndx2] = 1

    def _get_isolated_junctions_and_links(self):

        node_set = [1 for i in range(self._model.num_nodes)]

        def grab_group(node_id):
            node_set[node_id] = 0
            nodes_to_explore = set()
            nodes_to_explore.add(node_id)
            indptr = self._internal_graph.indptr
            indices = self._internal_graph.indices
            data = self._internal_graph.data
            num_connections = self._number_of_connections

            while len(nodes_to_explore) != 0:
                node_being_explored = nodes_to_explore.pop()
                ndx = indptr[node_being_explored]
                number_of_connections = num_connections[node_being_explored]
                vals = data[ndx:ndx+number_of_connections]
                cols = indices[ndx:ndx+number_of_connections]
                for i, val in enumerate(vals):
                    if val == 1:
                        col = cols[i]
                        if node_set[col] ==1:
                            node_set[col] = 0
                            nodes_to_explore.add(col)

        for tank_name, tank in self._wn.nodes(wntr.network.Tank):
            tank_id = self._model._node_name_to_id[tank_name]
            if node_set[tank_id] == 1:
                grab_group(tank_id)
            else:
                continue

        for reservoir_name, reservoir in self._wn.nodes(wntr.network.Reservoir):
            reservoir_id = self._model._node_name_to_id[reservoir_name]
            if node_set[reservoir_id] == 1:
                grab_group(reservoir_id)
            else:
                continue

        isolated_junction_ids = [i for i in range(len(node_set)) if node_set[i] == 1]
        isolated_junctions = set()
        isolated_links = set()
        for j_id in isolated_junction_ids:
            j = self._model._node_id_to_name[j_id]
            isolated_junctions.add(j)
            connected_links = self._wn.get_links_for_node(j)
            for l in connected_links:
                isolated_links.add(l)
        isolated_junctions = list(isolated_junctions)
        isolated_links = list(isolated_links)

        return isolated_junctions, isolated_links
예제 #3
0
    def run_sim(self, solver_options={}, convergence_error=True):
        """
        Run an extended period simulation (hydraulics only).

        Parameters
        ----------
        solver_options: dict
            Solver options are specified using the following dictionary keys:

            * MAXITER: the maximum number of iterations for each hydraulic solve (each timestep and trial) (default = 100)
            * TOL: tolerance for the hydraulic equations (default = 1e-6)
            * BT_RHO: the fraction by which the step length is reduced at each iteration of the line search (default = 0.5)
            * BT_MAXITER: the maximum number of iterations for each line search (default = 20)
            * BACKTRACKING: whether or not to use a line search (default = True)
            * BT_START_ITER: the newton iteration at which a line search should start being used (default = 2)

        convergence_error: bool (optional)
            If convergence_error is True, an error will be raised if the
            simulation does not converge. If convergence_error is False,
            a warning will be issued and results.error_code will be set to 2
            if the simulation does not converge.  Default = True.
        """
        logger_level = logger.getEffectiveLevel()

        if logger_level <= 1:
            logger.log(1, 'beginning of run_sim')

        self._time_per_step = []

        self._presolve_controls = ControlManager()
        self._postsolve_controls = ControlManager()
        self._rules = ControlManager()

        def categorize_control(control):
            if control.epanet_control_type in {_ControlType.presolve, _ControlType.pre_and_postsolve}:
                self._presolve_controls.register_control(control)
            if control.epanet_control_type in {_ControlType.postsolve, _ControlType.pre_and_postsolve}:
                self._postsolve_controls.register_control(control)
            if control.epanet_control_type == _ControlType.rule:
                self._rules.register_control(control)

        for c_name, c in self._wn.controls():
            categorize_control(c)
        for c in (self._wn._get_all_tank_controls() + self._wn._get_cv_controls() + self._wn._get_pump_controls() +
                  self._wn._get_valve_controls()):
            categorize_control(c)

        if logger_level <= 1:
            logger.log(1, 'collected presolve controls:')
            for c in self._presolve_controls:
                logger.log(1, '\t' + str(c))
            logger.log(1, 'collected rules:')
            for c in self._rules:
                logger.log(1, '\t' + str(c))
            logger.log(1, 'collected postsolve controls:')
            for c in self._postsolve_controls:
                logger.log(1, '\t' + str(c))

            logger.log(1, 'initializing hydraulic model')

        model = HydraulicModel(self._wn, self.mode)
        self._model = model
        model.initialize_results_dict()

        self._solver = NewtonSolver(model.num_nodes, model.num_links, model.num_leaks, model, options=solver_options)

        results = NetResults()
        results.error_code = 0
        results.time = []

        # Initialize X
        # Vars will be ordered:
        #    1.) head
        #    2.) demand
        #    3.) flow
        #    4.) leak_demand
        model.set_network_inputs_by_id()
        head0 = model.initialize_head()
        demand0 = model.initialize_demand()
        flow0 = model.initialize_flow()
        leak_demand0 = model.initialize_leak_demand()

        X_init = np.concatenate((head0, demand0, flow0, leak_demand0))

        self._initialize_internal_graph()

        if self._wn.sim_time == 0:
            first_step = True
        else:
            first_step = False
        trial = -1
        max_trials = self._wn.options.solver.trials
        resolve = False
        rule_iter = 0  # this is used to determine the rule timestep

        if first_step:
            self._model.update_network_previous_values()
            self._wn._prev_sim_time = -1

        if logger_level <= 1:
            logger.log(1, 'starting simulation')

        while True:
            if logger_level <= logging.DEBUG:
                logger.debug('\n\n')

            if not resolve:
                """
                Within this if statement:
                    1) Determine the next time step. This depends on both presolve controls and rules. Note that 
                       (unless this is the first time step) the current value of wn.sim_time is the next hydraulic 
                       timestep. If there are presolve controls or rules that need activated before the next hydraulic
                       timestep, then the wn.sim_time will be adjusted within this if statement.
                       
                        a) check the presolve controls to see which ones need activated.
                        b) if there is a presolve control(s) that need activated and it needs activated at a time
                           that is earlier than the next rule timestep, then the next simulation time is determined
                           by that presolve controls
                        c) if there are any rules that need activated before the next hydraulic timestep, then 
                           wn.sim_time will be adjusted to the appropriate rule timestep.
                    2) Activate the appropriate controls
                """
                start_step_time = time.time()  # this is just for timing

                if not first_step:
                    """
                    The tank levels/heads must be done before checking the controls because the TankLevelControls
                    depend on the tank levels. These will be updated again after we determine the next actual timestep.
                    """
                    self._model.update_tank_heads()
                trial = 0

                # check which presolve controls need to be activated before the next hydraulic timestep
                presolve_controls_to_run = self._presolve_controls.check()
                presolve_controls_to_run.sort(key=lambda i: i[0]._priority)  # sort them by priority
                # now sort them from largest to smallest "backtrack"; this way they are in the time-order
                # in which they need to be activated
                presolve_controls_to_run.sort(key=lambda i: i[1], reverse=True)
                if first_step:  # we don't want to backtrack if the sim time is 0
                    presolve_controls_to_run = [(c, 0) for c, b in presolve_controls_to_run]
                if logger_level <= 1:
                    logger.log(1, 'presolve_controls that need activated before the next hydraulic timestep:')
                    for pctr in presolve_controls_to_run:
                        logger.log(1, '\tcontrol: {0} \tbacktrack: {1}'.format(pctr[0], pctr[1]))
                cnt = 0

                # loop until we have checked all of the presolve_controls_to_run and all of the rules prior to the next
                # hydraulic timestep
                while cnt < len(presolve_controls_to_run) or rule_iter * self._wn.options.time.rule_timestep <= self._wn.sim_time:
                    if cnt >= len(presolve_controls_to_run):
                        # We have already checked all of the presolve_controls_to_run, and nothing changed
                        # Now we just need to check the rules
                        if logger_level <= 1:
                            logger.log(1, 'no presolve controls need activated; checking rules at rule timestep {0}'.format(rule_iter * self._wn.options.time.rule_timestep))
                        old_time = self._wn.sim_time
                        self._wn.sim_time = rule_iter * self._wn.options.time.rule_timestep
                        if not first_step:
                            self._model.update_tank_heads()
                        rule_iter += 1
                        rules_to_run = self._rules.check()
                        rules_to_run.sort(key=lambda i: i[0]._priority)
                        for rule, rule_back in rules_to_run:  # rule_back is the "backtrack" which is not actually used for rules
                            if logger_level <= 1:
                                logger.log(1, '\tactivating rule {0}'.format(rule))
                            rule.run_control_action()
                        if self._rules.changes_made():
                            # If changes were made, then we found the next timestep; break
                            break
                        # if no changes were made, then set the wn.sim_time back
                        if logger_level <= 1:
                            logger.log(1, 'no changes made by rules at rule timestep {0}'.format((rule_iter - 1) * self._wn.options.time.rule_timestep))
                        self._wn.sim_time = old_time
                    else:
                        # check the next presolve control in presolve_controls_to_run
                        control, backtrack = presolve_controls_to_run[cnt]
                        if logger_level <= 1:
                            logger.log(1, 'checking control {0}; backtrack: {1}'.format(control, backtrack))
                        if self._wn.sim_time - backtrack < rule_iter * self._wn.options.time.rule_timestep:
                            # The control needs activated before the next rule timestep; Activate the control and
                            # any controls with the samve value for backtrack
                            if logger_level <= 1:
                                logger.log(1, 'control {0} needs run before the next rule timestep.'.format(control))
                            control.run_control_action()
                            cnt += 1
                            while cnt < len(presolve_controls_to_run) and presolve_controls_to_run[cnt][1] == backtrack:
                                # Also activate all of the controls that have the same value for backtrack
                                if logger_level <= 1:
                                    logger.log(1, '\talso activating control {0}; backtrack: {1}'.format(presolve_controls_to_run[cnt][0],
                                                                                                   presolve_controls_to_run[cnt][1]))
                                presolve_controls_to_run[cnt][0].run_control_action()
                                cnt += 1
                            if self._presolve_controls.changes_made():
                                # changes were actually made; we found the next timestep; update wn.sim_time and break
                                self._wn.sim_time -= backtrack
                                break
                            if logger_level <= 1:
                                logger.log(1, 'controls with backtrack {0} did not make any changes'.format(backtrack))
                        elif self._wn.sim_time - backtrack == rule_iter * self._wn.options.time.rule_timestep:
                            # the control needs activated at the same time as the next rule timestep;
                            # activate the control, any controls with the same value for backtrack, and any rules at
                            # this rule timestep
                            # the rules need run first (I think to match epanet)
                            if logger_level <= 1:
                                logger.log(1, 'control has backtrack equivalent to next rule timestep')
                            rule_iter += 1
                            self._wn.sim_time -= backtrack
                            if not first_step:
                                self._model.update_tank_heads()
                            rules_to_run = self._rules.check()
                            rules_to_run.sort(key=lambda i: i[0]._priority)
                            for rule, rule_back in rules_to_run:
                                if logger_level <= 1:
                                    logger.log(1, '\tactivating rule {0}'.format(rule))
                                rule.run_control_action()
                            if logger_level <= 1:
                                logger.log(1, '\tactivating control {0}; backtrack: {1}'.format(control, backtrack))
                            control.run_control_action()
                            cnt += 1
                            while cnt < len(presolve_controls_to_run) and presolve_controls_to_run[cnt][1] == backtrack:
                                if logger_level <= 1:
                                    logger.log(1, '\talso activating control {0}; backtrack: {1}'.format(presolve_controls_to_run[cnt][0], presolve_controls_to_run[cnt][1]))
                                presolve_controls_to_run[cnt][0].run_control_action()
                                cnt += 1
                            if self._presolve_controls.changes_made() or self._rules.changes_made():
                                break
                            if logger_level <= 1:
                                logger.log(1, 'no changes made by presolve controls or rules at backtrack {0}'.format(backtrack))
                            self._wn.sim_time += backtrack
                        else:
                            if logger_level <= 1:
                                logger.log(1, 'The next rule timestep is before this control needs activated; checking rules')
                            old_time = self._wn.sim_time
                            self._wn.sim_time = rule_iter * self._wn.options.time.rule_timestep
                            rule_iter += 1
                            if not first_step:
                                self._model.update_tank_heads()
                            rules_to_run = self._rules.check()
                            rules_to_run.sort(key=lambda i: i[0]._priority)
                            for rule, rule_back in rules_to_run:
                                if logger_level <= 1:
                                    logger.log(1, '\tactivating rule {0}'.format(rule))
                                rule.run_control_action()
                            if self._rules.changes_made():
                                break
                            if logger_level <= 1:
                                logger.log(1, 'no changes made by rules at rule timestep {0}'.format((rule_iter - 1) * self._wn.options.time.rule_timestep))
                            self._wn.sim_time = old_time
                self._update_internal_graph()
                if logger_level <= logging.DEBUG:
                    logger.debug('changes made by rules: ')
                    for obj, attr in self._rules.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                    logger.debug('changes made by presolve controls:')
                    for obj, attr in self._presolve_controls.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                self._presolve_controls.reset()
                self._rules.reset()

            logger.info('simulation time = %s, trial = %d', self._get_time(), trial)

            # Prepare for solve
            if logger_level <= logging.DEBUG:
                logger.debug('checking for isolated junctions and links')
            isolated_junctions, isolated_links = self._get_isolated_junctions_and_links()
            if logger_level <= logging.DEBUG:
                if len(isolated_junctions) > 0 or len(isolated_links) > 0:
                    logger.debug('isolated junctions: {0}'.format(isolated_junctions))
                    logger.debug('isolated links: {0}'.format(isolated_links))
                else:
                    logger.debug('no isolated junctions or links found')
            model.set_isolated_junctions_and_links(isolated_junctions, isolated_links)
            if not first_step and not resolve:
                model.update_tank_heads()
            model.set_network_inputs_by_id()
            model.set_jacobian_constants()

            # Solve
            if logger_level <= logging.DEBUG:
                logger.debug('solving')
            [self._X, num_iters, solver_status, message] = self._solver.solve(model.get_hydraulic_equations, model.get_jacobian, X_init)
            if solver_status == 0:
                if convergence_error:
                    logger.error('Simulation did not converge. ' + message)
                    raise RuntimeError('Simulation did not converge. ' + message)
                warnings.warn('Simulation did not converge. ' + message)
                logger.warning('Simulation did not converge at time ' + str(self._get_time()) + '. ' + message)
                model.get_results(results)
                results.error_code = 2
                return results
            X_init = np.array(self._X)

            # Enter results in network and update previous inputs
            if logger_level <= logging.DEBUG:
                logger.debug('storing results in network')
            model.store_results_in_network(self._X)

            if logger_level <= logging.DEBUG:
                logger.debug('checking postsolve controls')
            self._postsolve_controls.reset()
            postsolve_controls_to_run = self._postsolve_controls.check()
            postsolve_controls_to_run.sort(key=lambda i: i[0]._priority)
            for control, unused in postsolve_controls_to_run:
                if logger_level <= 1:
                    logger.log(1, '\tactivating control {0}'.format(control))
                control.run_control_action()
            if self._postsolve_controls.changes_made():
                if logger_level <= logging.DEBUG:
                    logger.debug('postsolve controls made changes:')
                    for obj, attr in self._postsolve_controls.get_changes():
                        logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
                resolve = True
                self._update_internal_graph()
                self._postsolve_controls.reset()
                trial += 1
                if trial > max_trials:
                    if convergence_error:
                        logger.error('Exceeded maximum number of trials.')
                        raise RuntimeError('Exceeded maximum number of trials.')
                    results.error_code = 2
                    warnings.warn('Exceeded maximum number of trials.')
                    logger.warning('Exceeded maximum number of trials at time %s', self._get_time())
                    model.get_results(results)
                    return results
                continue

            logger.debug('no changes made by postsolve controls; moving to next timestep')

            resolve = False
            if type(self._wn.options.time.report_timestep) == float or type(self._wn.options.time.report_timestep) == int:
                if self._wn.sim_time % self._wn.options.time.report_timestep == 0:
                    model.save_results(self._X, results)
                    if len(results.time) > 0 and int(self._wn.sim_time) == results.time[-1]:
                        raise RuntimeError('Simulation already solved this timestep')
                    results.time.append(int(self._wn.sim_time))
            elif self._wn.options.time.report_timestep.upper() == 'ALL':
                model.save_results(self._X, results)
                if len(results.time) > 0 and int(self._wn.sim_time) == results.time[-1]:
                    raise RuntimeError('Simulation already solved this timestep')
                results.time.append(int(self._wn.sim_time))
            model.update_network_previous_values()
            first_step = False
            self._wn.sim_time += self._wn.options.time.hydraulic_timestep
            overstep = float(self._wn.sim_time) % self._wn.options.time.hydraulic_timestep
            self._wn.sim_time -= overstep

            if self._wn.sim_time > self._wn.options.time.duration:
                break

            self._time_per_step.append(time.time()-start_step_time)

        model.get_results(results)
        return results