def sorting_algorithm(self, active_sessions: List[SessionInfo], infrastructure: InfrastructureInfo) -> np.ndarray: """ Schedule EVs by first sorting them by sort_fn, then allocating them their maximum feasible rate. See class documentation for description of the algorithm. Args: active_sessions (List[SessionInfo]): see BaseAlgorithm infrastructure (InfrastructureInfo): Description of the electrical infrastructure. Returns: np.array[Union[float, int]]: Array of charging rates, where each row is the charging rates for a specific session. """ queue: List[SessionInfo] = self._sort_fn(active_sessions, self.interface) schedule: np.ndarray = np.zeros(infrastructure.num_stations) # Start each EV at its lower bound for session in queue: station_index: int = infrastructure.get_station_index( session.station_id) lb: float = max(0, session.min_rates[0]) schedule[station_index] = lb if not infrastructure_constraints_feasible(schedule, infrastructure): raise ValueError( "Charging all sessions at their lower bound is not feasible.") for session in queue: station_index = infrastructure.get_station_index( session.station_id) ub: float = min(session.max_rates[0], self.interface.remaining_amp_periods(session)) lb: float = max(0, session.min_rates[0]) if infrastructure.is_continuous[station_index]: charging_rate: float = self.max_feasible_rate(station_index, ub, schedule, infrastructure, eps=0.01, lb=lb) else: allowable = [ a for a in infrastructure.allowable_pilots[station_index] if lb <= a <= ub ] if len(allowable) == 0: charging_rate = 0 else: charging_rate = self.discrete_max_feasible_rate( station_index, allowable, schedule, infrastructure) schedule[station_index] = charging_rate return schedule
def test_inputs_consistent(self) -> None: m, n = 6, 5 # Here, the last two arguments are kwargs, left unspecified to test that the # order of overflow is unchanged. infra = InfrastructureInfo( np.ones((m, n)), np.ones((m, )), np.ones((n, )), np.ones((n, )), [f"C-{i}" for i in range(m)], [f"S-{i}" for i in range(n)], np.ones((n, )), np.zeros((n, )), [np.array([1, 2, 3, 4])] * n, # allowable_pilots np.zeros((n, )), # is_continuous ) self.assertEqual(infra.constraint_matrix.shape, (m, n)) self.assertEqual(infra.constraint_limits.shape, (m, )) self.assertEqual(infra.phases.shape, (n, )) self.assertEqual(infra.voltages.shape, (n, )) self.assertEqual(len(infra.constraint_ids), m) self.assertEqual(len(infra.station_ids), n) self.assertEqual(infra.max_pilot.shape, (n, )) self.assertEqual(infra.min_pilot.shape, (n, )) self.assertEqual(len(infra.allowable_pilots), n) self.assertEqual(infra.is_continuous.shape, (n, )) self.assertEqual(infra.is_continuous.dtype, "bool")
def remove_finished_sessions( active_sessions: List[SessionInfo], infrastructure: InfrastructureInfo, period: float, ) -> List[SessionInfo]: """ Remove any sessions where the remaining demand is less than threshold. Here, the threshold is defined as the amount of energy delivered by charging at the min_pilot of a session's station, at the station's voltage, for one simulation period. Args: active_sessions (List[SessionInfo]): List of SessionInfo objects for all active charging sessions. infrastructure (InfrastructureInfo): Description of the charging infrastructure. period (float): Length of each time period in minutes. Returns: List[SessionInfo]: Active sessions without any sessions that are finished. """ modified_sessions = [] for s in active_sessions: station_index = infrastructure.get_station_index(s.station_id) threshold = ( infrastructure.min_pilot[station_index] * infrastructure.voltages[station_index] / (60 / period) / 1000 ) # kWh if s.remaining_demand > threshold: modified_sessions.append(s) return modified_sessions
def test_num_stations_num_constraints_mismatch(self) -> None: m, n = 5, 6 with self.assertRaises(ValueError): InfrastructureInfo( np.ones((m + 1, n)), np.ones((m, )), np.ones((n, )), np.ones((n, )), [f"C-{i}" for i in range(m - 1)], [f"S-{i}" for i in range(n)], np.ones((n, )), np.zeros((n + 1, )), allowable_pilots=[np.array([1, 2, 3, 4])] * n, is_continuous=np.zeros((n - 1, )), )
def test_inputs_allowable_pilot_defaults(self) -> None: m, n = 6, 5 infra = InfrastructureInfo( np.ones((m, n)), np.ones((m, )), np.ones((n, )), np.ones((n, )), [f"C-{i}" for i in range(m)], [f"S-{i}" for i in range(n)], np.ones((n, )), np.zeros((n, )), is_continuous=np.zeros((n, )), ) self.assertEqual(len(infra.allowable_pilots), n) self.assertEqual(infra.allowable_pilots, [None] * n)
def test_pilot_less_than_existing_max(self) -> None: # pylint: disable=no-self-use sessions = session_generator( num_sessions=N, arrivals=[ARRIVAL_TIME] * N, departures=[ARRIVAL_TIME + SESSION_DUR] * N, requested_energy=[3.3] * N, remaining_energy=[3.3] * N, max_rates=[np.repeat(40, SESSION_DUR)] * N, ) sessions = [SessionInfo(**s) for s in sessions] infrastructure = InfrastructureInfo( **single_phase_single_constraint(num_evses=N, limit=32)) modified_sessions = enforce_pilot_limit(sessions, infrastructure) for session in modified_sessions: nptest.assert_almost_equal(session.max_rates, 32)
def test_inputs_is_continuous_default(self) -> None: m, n = 6, 5 infra = InfrastructureInfo( np.ones((m, n)), np.ones((m, )), np.ones((n, )), np.ones((n, )), [f"C-{i}" for i in range(m)], [f"S-{i}" for i in range(n)], np.ones((n, )), np.zeros((n, )), allowable_pilots=[np.array([1, 2, 3, 4])] * n, ) nptest.assert_array_equal(infra.is_continuous, True) self.assertEqual(infra.is_continuous.shape, (n, )) self.assertEqual(infra.is_continuous.dtype, "bool")
def _test_remove_sessions_within_threshold( self, remaining_energies: List[float]) -> None: sessions = session_generator( num_sessions=N, arrivals=[1, 2, 3], departures=[1 + SESSION_DUR, 2 + SESSION_DUR, 3 + SESSION_DUR], requested_energy=[3.3] * N, remaining_energy=remaining_energies, max_rates=[np.repeat(32, SESSION_DUR)] * N, ) infrastructure = InfrastructureInfo( **three_phase_balanced_network(1, limit=100)) sessions = [SessionInfo(**s) for s in sessions] modified_sessions = remove_finished_sessions(sessions, infrastructure, 5) self.assertEqual(len(modified_sessions), 2)
def remaining_amp_periods(session: SessionInfo, infrastructure: InfrastructureInfo, period: float) -> float: """ Return the session's remaining demand in A*periods. This function is a static version of acnsim.Interface.remaining_amp_periods. Args: session (SessionInfo): The SessionInfo object for which to get remaining demand. infrastructure (InfrastructureInfo): The InfrastructureInfo object that contains voltage information about the network. period (float): Period of the simulation in minutes. Returns: float: the EV's remaining demand in A*periods. """ i = infrastructure.get_station_index(session.station_id) amp_hours = session.remaining_demand * 1000 / infrastructure.voltages[i] return amp_hours * 60 / period
def test_num_constraints_mismatch(self) -> None: m, n = 5, 6 for i in range(3): for error in [-1, 1]: errors = [0] * 3 errors[i] = error with self.assertRaises(ValueError): InfrastructureInfo( np.ones((m + errors[0], n)), np.ones((m + errors[1], )), np.ones((n, )), np.ones((n, )), [f"C-{i}" for i in range(m + errors[2])], [f"S-{i}" for i in range(n)], np.ones((n, )), np.zeros((n, )), allowable_pilots=[np.array([1, 2, 3, 4])] * n, is_continuous=np.zeros((n, )), )
def infrastructure_info(self) -> InfrastructureInfo: """ Returns an InfrastructureInfo object generated from interface. Returns: InfrastructureInfo: A description of the charging infrastructure. """ infrastructure = self._get_or_error("infrastructure_info") return InfrastructureInfo( np.array(infrastructure["constraint_matrix"]), np.array(infrastructure["constraint_limits"]), np.array(infrastructure["phases"]), np.array(infrastructure["voltages"]), infrastructure["constraint_ids"], infrastructure["station_ids"], np.array(infrastructure["max_pilot"]), np.array(infrastructure["min_pilot"]), [np.array(allowable) for allowable in infrastructure["allowable_pilots"]], np.array(infrastructure["is_continuous"]), )
def enforce_pilot_limit( active_sessions: List[SessionInfo], infrastructure: InfrastructureInfo ) -> List[SessionInfo]: """ Update the max_rates vector for each session to be less than the max pilot supported by its EVSE. Args: active_sessions (List[SessionInfo]): List of SessionInfo objects for all active charging sessions. infrastructure (InfrastructureInfo): Description of the charging infrastructure. Returns: List[SessionInfo]: Active sessions with max_rates updated to be at most the max_pilot of the corresponding EVSE. """ for session in active_sessions: i = infrastructure.get_station_index(session.station_id) session.max_rates = np.minimum(session.max_rates, infrastructure.max_pilot[i]) return active_sessions
def test_num_stations_mismatch(self) -> None: m, n = 5, 6 for i in range(8): for error in [-1, 1]: errors = [0] * 8 errors[i] = error with self.assertRaises(ValueError): InfrastructureInfo( np.ones((m, n + errors[0])), np.ones((m, )), np.ones((n + errors[1], )), np.ones((n + errors[2], )), [f"C-{i}" for i in range(m)], [f"S-{i}" for i in range(n + errors[3])], np.ones((n + errors[4], )), np.zeros((n + errors[5], )), allowable_pilots=[np.array([1, 2, 3, 4])] * (n + errors[6]), is_continuous=np.zeros((n + errors[7], )), )
def test_apply_min_infeasible(self) -> None: # pylint: disable=no-self-use sessions = session_generator( num_sessions=N, arrivals=[1, 2, 3], departures=[1 + SESSION_DUR, 2 + SESSION_DUR, 3 + SESSION_DUR], requested_energy=[3.3] * N, remaining_energy=[3.3] * N, max_rates=[np.repeat(32, SESSION_DUR)] * N, ) sessions = [SessionInfo(**s) for s in sessions] infrastructure = InfrastructureInfo( **single_phase_single_constraint(N, 16)) modified_sessions = apply_minimum_charging_rate( sessions, infrastructure, PERIOD) for i in range(2): nptest.assert_almost_equal(modified_sessions[i].min_rates[0], 8) nptest.assert_almost_equal(modified_sessions[i].min_rates[1:], 0) # It is not feasible to deliver 8 A to session '2', so max and # min should be 0 at time t=0. nptest.assert_almost_equal(modified_sessions[2].min_rates, 0) nptest.assert_almost_equal(modified_sessions[2].max_rates[0], 0)
def _session_generation_helper( max_rate_list: List[Union[float, np.ndarray]], min_rate_list: Optional[List[Union[float, np.ndarray]]] = None, remaining_energy: float = 3.3, ) -> List[SessionInfo]: if min_rate_list is not None: min_rate_list *= N sessions: List[SessionDict] = session_generator( num_sessions=N, arrivals=[ARRIVAL_TIME] * N, departures=[ARRIVAL_TIME + SESSION_DUR] * N, requested_energy=[3.3] * N, remaining_energy=[remaining_energy] * N, max_rates=max_rate_list * N, min_rates=min_rate_list, ) sessions: List[SessionInfo] = [SessionInfo(**s) for s in sessions] infrastructure = InfrastructureInfo( **single_phase_single_constraint(N, 32)) modified_sessions = apply_minimum_charging_rate( sessions, infrastructure, PERIOD) return modified_sessions
def energy_constraints( rates: cp.Variable, active_sessions: List[SessionInfo], infrastructure: InfrastructureInfo, period, enforce_energy_equality=False, ): """Get constraints on the energy delivered for each session. Args: rates (cp.Variable): cvxpy variable representing all charging rates. Shape should be (N, T) where N is the total number of EVSEs in the system and T is the length of the optimization horizon. active_sessions (List[SessionInfo]): List of SessionInfo objects for all active charging sessions. infrastructure (InfrastructureInfo): InfrastructureInfo object describing the electrical infrastructure at a site. period (int): Length of each discrete time period. (min) enforce_energy_equality (bool): If True, energy delivered must be equal to energy requested for each EV. If False, energy delivered must be less than or equal to request. Returns: List[cp.Constraint]: List of energy delivered constraints for each session. """ constraints = {} for session in active_sessions: i = infrastructure.get_station_index(session.station_id) planned_energy = cp.sum( rates[i, session.arrival_offset:session.arrival_offset + session.remaining_time, ]) planned_energy *= infrastructure.voltages[i] * period / 1e3 / 60 constraint_name = f"energy_constraints.{session.session_id}" if enforce_energy_equality: constraints[constraint_name] = ( planned_energy == session.remaining_demand) else: constraints[constraint_name] = (planned_energy <= session.remaining_demand) return constraints
def round_robin(self, active_sessions: List[SessionInfo], infrastructure: InfrastructureInfo) -> np.ndarray: """ Schedule EVs using a round robin based equal sharing scheme. Implements abstract method schedule from BaseAlgorithm. See class documentation for description of the algorithm. Args: active_sessions (List[SessionInfo]): see BaseAlgorithm infrastructure (InfrastructureInfo): Description of electrical infrastructure. Returns: np.array[Union[float, int]]: Array of charging rates, where each row is the charging rates for a specific session. """ queue = deque(self._sort_fn(active_sessions, self.interface)) schedule = np.zeros(infrastructure.num_stations) rate_idx = np.zeros(infrastructure.num_stations, dtype=int) allowable_pilots = infrastructure.allowable_pilots.copy() for session in queue: i = infrastructure.get_station_index(session.station_id) # If pilot signal is continuous discretize it with increments of # continuous_inc. if infrastructure.is_continuous[i]: allowable_pilots[i] = np.arange( session.min_rates[0], session.max_rates[0] + self.continuous_inc / 2, self.continuous_inc, ) ub = min( session.max_rates[0], infrastructure.max_pilot[i], self.interface.remaining_amp_periods(session), ) lb = max(0, session.min_rates[0]) # Remove any charging rates which are not feasible. allowable_pilots[i] = allowable_pilots[i][ lb <= allowable_pilots[i]] allowable_pilots[i] = allowable_pilots[i][ allowable_pilots[i] <= ub] # All charging rates should start at their lower bound schedule[i] = allowable_pilots[i][0] if len( allowable_pilots[i]) > 0 else 0 if not infrastructure_constraints_feasible(schedule, infrastructure): raise ValueError( "Charging all sessions at their lower bound is not feasible.") while len(queue) > 0: session = queue.popleft() i = infrastructure.get_station_index(session.station_id) if rate_idx[i] < len(allowable_pilots[i]) - 1: schedule[i] = allowable_pilots[i][rate_idx[i] + 1] if infrastructure_constraints_feasible(schedule, infrastructure): rate_idx[i] += 1 queue.append(session) else: schedule[i] = allowable_pilots[i][rate_idx[i]] return schedule