├── .gitattributes ├── Pipfile ├── Pipfile.lock ├── README.md ├── configuration.py ├── icarus_simulator ├── __init__.py ├── data │ ├── .gitattributes │ ├── GDP_PPP_1990_2015_5arcmin_v2.nc │ ├── WUP2018-F22-Cities_Over_300K_Annual.csv │ ├── mer2005sum.asc │ └── natural_earth_world_small.geo.json ├── default_properties.py ├── icarus_simulator.py ├── multiprocessor.py ├── phases │ ├── __init__.py │ ├── base_phase.py │ ├── coverage_phase.py │ ├── edge_phase.py │ ├── grid_phase.py │ ├── link_attack_phase.py │ ├── lsn_phase.py │ ├── routing_phase.py │ ├── traffic_phase.py │ └── zone_attack_phase.py ├── sat_core │ ├── __init__.py │ ├── constellation.py │ ├── constellation_network.py │ ├── coordinate_util.py │ ├── coverage.py │ ├── isl_util.py │ ├── orbit_shift_algo.py │ ├── orbit_util.py │ ├── planetary_const.py │ └── satellite.py ├── strategies │ ├── __init__.py │ ├── atk_detect_optimisation │ │ ├── __init__.py │ │ ├── base_optim_strat.py │ │ └── bin_search_optim_strat.py │ ├── atk_feasibility_check │ │ ├── __init__.py │ │ ├── base_feas_strat.py │ │ ├── lp_feas_strat.py │ │ └── prob_feas_strat.py │ ├── atk_geo_constraint │ │ ├── __init__.py │ │ ├── base_geo_constraint_strat.py │ │ ├── geo_constr_strat.py │ │ ├── grid_constr_strat.py │ │ └── no_constr_strat.py │ ├── atk_path_filtering │ │ ├── __init__.py │ │ ├── base_path_filtering_strat.py │ │ └── directional_filtering_strat.py │ ├── base_strat.py │ ├── bw_assignment │ │ ├── __init__.py │ │ ├── base_bw_assig_strat.py │ │ └── bidir_bw_assign_strat.py │ ├── bw_selection │ │ ├── __init__.py │ │ ├── base_bw_select_strat.py │ │ └── sampled_bw_select_strat.py │ ├── coverage │ │ ├── __init__.py │ │ ├── angle_cov_strat.py │ │ └── base_coverage_strat.py │ ├── edge │ │ ├── __init__.py │ │ ├── base_edge_strat.py │ │ └── bidir_edge_strat.py │ ├── grid │ │ ├── __init__.py │ │ ├── base_grid_strat.py │ │ └── geodesic_grid_strat.py │ ├── grid_weight │ │ ├── __init__.py │ │ ├── base_weight_strat.py │ │ ├── gdp_weight_strat.py │ │ └── uniform_weight_strat.py │ ├── lsn │ │ ├── __init__.py │ │ ├── base_lsn_strat.py │ │ └── manh_lsn_strat.py │ ├── routing │ │ ├── __init__.py │ │ ├── base_routing_strat.py │ │ ├── kdg_rout_strat.py │ │ ├── kds_rout_strat.py │ │ ├── klo_rout_strat.py │ │ ├── ksp_rout_strat.py │ │ └── ssp_rout_strat.py │ ├── zone_bneck │ │ ├── __init__.py │ │ ├── base_zone_bneck_strat.py │ │ └── detect_bneck_strat.py │ ├── zone_build │ │ ├── __init__.py │ │ ├── base_zone_build_strat.py │ │ └── k_closest_zone_strat.py │ ├── zone_edges │ │ ├── __init__.py │ │ ├── base_zone_edges_strat.py │ │ ├── dwl_zone_strat.py │ │ └── isl_zone_strat.py │ └── zone_select │ │ ├── __init__.py │ │ ├── base_zone_select_strat.py │ │ ├── list_zone_strat.py │ │ └── rand_zone_strat.py ├── structure_definitions.py └── utils.py ├── license.txt ├── main.py ├── sat_plotter ├── __init__.py ├── amazingplots.mpstyle ├── geo_plot_builder.py └── stat_plot_builder.py └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | icarus_simulator/data/ filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pyephem = "*" 10 | numpy = "*" 11 | scipy = "*" 12 | networkx = "*" 13 | plotly = "*" 14 | pandas = "*" 15 | scikit-learn = "*" 16 | matplotlib = "*" 17 | antiprism-python = "*" 18 | geopy = "*" 19 | netcdf4 = "*" 20 | compress-pickle = "*" 21 | icarus_simulator = {path = "."} 22 | sat_plotter = {path = "."} 23 | requests = "*" 24 | shapely = "*" 25 | 26 | [requires] 27 | python_version = "3.6" 28 | 29 | [pipenv] 30 | allow_prereleases = true 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The ICARUS Attack Simulator 2 | 3 | This repository cointains the code for a new, extensible and customizable simulator for the ICARUS attack on satellite networks. 4 | More specifically, two libraries are provided: 5 | * `icarus_simulator`: code for the ICARUS simulator; 6 | * `sat_plotter`: a visualization utility for geographical and statistical plots. 7 | 8 | ## Usage instructions 9 | The simulator is based on a few key classes. 10 | 11 | **`IcarusSimulator`:** 12 | Main interface of the library, it receives phases and executes them sequentially. It also manages the middle results, storing them in a key-value fashion, and the dependencies between phases. By passing different phases, the simulation algorithm can be fully adapted to the user's needs. 13 | 14 | **`BasePhase`:** 15 | Phases are classes that manage the execution of a macrotask, and their execution always yields a milestone in the computation (e.g. the attack result for all ISLs in the network). They accept the keys for the inputs and outputs and get and save values directly into the simulator instance. 16 | Custom phases can be created by extending this class. The phase code is supposed to be a template, that gets its full behaviour through the use of strategies. 17 | 18 | **`BaseStrategy`:** 19 | Strategies are classes that manage the execution of a microtask, following a specific signature. They specify the behaviour when many alternatives would be possible, e.g. the chosen routing algorithm. They allow for a runtime decision of the detailed algorithm, allowing for easy prototyping and experimentation. 20 | Custom strategies can be created by extending this class. 21 | 22 | The library already includes several predefined phases, used in the ICARUS attack, found in the directory `phases`. 23 | Predefined strategies can also be found in `strategies`, with a different subdirectory for each microtask. 24 | 25 | Each base class features a detailed explanation. Please read the initial comment of `main.py` for more details and further instructions. 26 | 27 | 28 | ## Installation instructions 29 | 30 | This project uses `pipenv` for dependency management. 31 | To install the two libraries, checkout the project and navigate to the project directory, then run: 32 | ```bash 33 | pipenv install . 34 | ``` 35 | 36 | After activating the virtual environment with ```pipenv shell```, run: 37 | ```bash 38 | python setup.py install 39 | ``` 40 | 41 | If using the strategy `strategies/atk_feasibility_check/lp_feas_strat.py`, an installation of Gurobi is necessary, which requires additional steps. After registering for a free academic license, activate the license. To install, run the following command: 42 | ```bash 43 | pipenv install -i https://pypi.gurobi.com gurobipy 44 | ``` 45 | Refer to https://www.gurobi.com/documentation/9.1/quickstart_windows/cs_using_pip_to_install_gr.html for more info. 46 | 47 | The two libraries are now available and can be imported as any other python packages. You can run the following as a first test: 48 | ```bash 49 | python main.py 50 | ``` 51 | 52 | 53 | Important note: 54 | The simulations are very computation- and memory-heavy. Therefore, we recommend to run the simulator on a multicore cluster. 55 | 56 | 57 | -------------------------------------------------------------------------------- /configuration.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | """ 4 | This file introduces the special mechanism of the configuration file. 5 | The dictionary CONFIG includes entries with all the strategies used in the current simulation definition. 6 | Each entry contains the parameters passed, by name, to the strategy constructors, in a list format. 7 | If you wish to run multiple simulations, you can add the needed parameters to the corresponding list, and the script 8 | will automatically create new instances of the configuration by extending the last elements of all other lists. 9 | For example, setting CONFIG['lsn']['orbits'] = [72, 50] will run two identical simulations, except for this parameter. 10 | If you change a strategy class across simulations, just put the union of all parameters needed. At runtime, unneeded 11 | parameters will be automatically discarded by the different strategy classes. 12 | 13 | Running simulations with this method is NOT mandatory, phases can be configured manually every time. 14 | """ 15 | from typing import List, Dict 16 | from icarus_simulator.strategies import * 17 | 18 | CONFIG = { 19 | "lsn": { 20 | "strat": [ManhLSNStrat], 21 | "inclination": [53], 22 | "sats_per_orbit": [22], 23 | "orbits": [72], 24 | "f": [11], 25 | "elevation": [550000], 26 | "hrs": [0], 27 | "mins": [2], 28 | "secs": [17], # list(range(0, 130)) 29 | "millis": [0], 30 | "epoch": ["2020/01/01 00:00:00"], 31 | }, 32 | "grid": {"strat": [GeodesicGridStrat], "repeats": [22]}, 33 | "gweight": {"strat": [GDPWeightStrat], "dataset_file": [None]}, 34 | "cover": {"strat": [AngleCovStrat], "min_elev_angle": [40]}, 35 | "rout": { 36 | "strat": [SSPRoutStrat], 37 | "desirability_stretch": [2.3], 38 | "k": [5], 39 | "esx_theta": [0.5], 40 | }, 41 | "edges": {"strat": [BidirEdgeStrat]}, 42 | "bw_sel": {"strat": [SampledBwSelectStrat], "sampled_quanta": [250000]}, 43 | "bw_asg": { 44 | "strat": [BidirBwAssignStrat], 45 | "isl_bw": [2000], 46 | "udl_bw": [400], 47 | "utilisation": [0.9], 48 | }, 49 | "atk_constr": { 50 | "strat": [NoConstrStrat], 51 | "geo_names": [["USA", "RUS"]], 52 | "grid_points": [[1549, 1530]], 53 | }, 54 | "atk_filt": {"strat": [DirectionalFilteringStrat]}, 55 | "atk_feas": { 56 | "strat": [LPFeasStrat], 57 | }, 58 | "atk_optim": {"strat": [BinSearchOptimStrat], "rate": [1.0]}, 59 | "zone_select": {"strat": [RandZoneStrat], "samples": [5000]}, 60 | "zone_build": {"strat": [KclosestZoneStrat], "size": [6]}, 61 | "zone_edges": { 62 | "strat": [ISLZoneStrat], 63 | }, 64 | "zone_bneck": { 65 | "strat": [DetectBneckStrat], 66 | }, 67 | } 68 | 69 | 70 | # Here follow methods used for the parsing. 71 | def parse_config(config_lists) -> List[Dict]: 72 | """ Parse the configuration """ 73 | # Parse the base elements 74 | # Get a list of all the lists and determine the longest 75 | full_config = [] 76 | keys = config_lists.keys() 77 | base_list = [] 78 | for key in keys: 79 | base_list.extend(list(config_lists[key].values())) 80 | num_runs = len(max(base_list, key=lambda k: len(k))) 81 | 82 | # Extend all the lists 83 | for key in keys: 84 | for inner_key, val in config_lists[key].items(): 85 | val.extend([val[-1]] * (num_runs - (len(val)))) 86 | 87 | # Turn lists in dict of dicts 88 | for idx in range(num_runs): 89 | run = {} 90 | for key in keys: 91 | run[key] = [ 92 | dict(zip(config_lists[key], t)) 93 | for t in zip(*config_lists[key].values()) 94 | ][idx] 95 | full_config.append(run) 96 | return full_config 97 | 98 | 99 | def get_strat(strat_id: str, conf: Dict): 100 | return conf[strat_id]["strat"](**conf[strat_id]) 101 | -------------------------------------------------------------------------------- /icarus_simulator/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2020 Tommaso Ciussani 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | -------------------------------------------------------------------------------- /icarus_simulator/data/.gitattributes: -------------------------------------------------------------------------------- 1 | WUP2018-F22-Cities_Over_300K_Annual.csv filter=lfs diff=lfs merge=lfs -text 2 | mer2005sum.asc filter=lfs diff=lfs merge=lfs -text 3 | natural_earth_world_small.geo.json filter=lfs diff=lfs merge=lfs -text 4 | GDP_PPP_1990_2015_5arcmin_v2.nc filter=lfs diff=lfs merge=lfs -text 5 | -------------------------------------------------------------------------------- /icarus_simulator/data/GDP_PPP_1990_2015_5arcmin_v2.nc: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:975ddfad5d2ee71aa227c269b530a4917f8305299b77d09ee0f9cfabc83e5e5b 3 | size 134025184 4 | -------------------------------------------------------------------------------- /icarus_simulator/data/WUP2018-F22-Cities_Over_300K_Annual.csv: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7e64dff818403e200df935f1c5303acbaa8b300143583c1b349274439441b9df 3 | size 113396 4 | -------------------------------------------------------------------------------- /icarus_simulator/data/mer2005sum.asc: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0d21b52fa6aca2a0666804b69e21b03fe37a2985587b7b3ba895e2ebf885dd6a 3 | size 382455 4 | -------------------------------------------------------------------------------- /icarus_simulator/data/natural_earth_world_small.geo.json: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:74251fe06e12be07ed67256e511fee6d95037dd7c5f31c91daf32f71eaef6ba3 3 | size 577300 4 | -------------------------------------------------------------------------------- /icarus_simulator/default_properties.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | File containing suggested property names (Pnames) that the user can adopt 4 | """ 5 | 6 | from icarus_simulator.structure_definitions import Pname 7 | 8 | SAT_POS: Pname = "sat_pos" 9 | SAT_NW: Pname = "sat_nw" 10 | SAT_ISLS: Pname = "sat_isls" 11 | FULL_GRID_POS: Pname = "full_grid_pos" 12 | GRID_POS: Pname = "grid_pos" 13 | COVERAGE: Pname = "coverage" 14 | GRID_FULL_SZ: Pname = "grid_sz" 15 | PATH_DATA: Pname = "path_data" 16 | EDGE_DATA: Pname = "edge_data" 17 | BW_DATA: Pname = "bw_data" 18 | ATK_DATA: Pname = "atk_data" 19 | ZONE_ATK_DATA: Pname = "zone_atk_data" 20 | -------------------------------------------------------------------------------- /icarus_simulator/icarus_simulator.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Main interface of the library. Every time an experiment is run, an icarusSimulator object must be created. 4 | The constructor takes a list of phases to run sequentially. This class manages the intermediate and final results, 5 | saving everything with passed property names, and correctly naming the file dumps based on phase dependencies. 6 | """ 7 | from typing import List, Any, Tuple, Set 8 | 9 | from icarus_simulator.phases.base_phase import BasePhase 10 | from icarus_simulator.structure_definitions import PropertyDict, Pname, DependencyDict 11 | 12 | 13 | class IcarusSimulator: 14 | def __init__(self, phases: List[BasePhase], results_directory: str): 15 | self.phases = phases 16 | self.basedir = results_directory 17 | self.properties: PropertyDict = {} 18 | self.dependencies: DependencyDict = {} 19 | 20 | def get_property(self, property_name: str): 21 | return self.properties[property_name] # Raises with wrong property name 22 | 23 | def compute_simulation(self): 24 | # Execute phases sequentially 25 | for phase in self.phases: 26 | # Get all necessary input data for the current phase 27 | input_properties, output_properties = ( 28 | phase.input_properties, 29 | phase.output_properties, 30 | ) 31 | phase_name, phase_descr = phase.name, phase.description 32 | input_values = self._get_input_values(input_properties) 33 | 34 | # Update the phase dependency dictionary and get the filename 35 | previous = self._update_dependencies( 36 | output_properties, input_properties, phase_descr 37 | ) 38 | phase_fname = self._get_phase_fname(phase_name, previous) 39 | 40 | # Execute the phase and update the results 41 | phase_result = phase.execute_phase(input_values, phase_fname) 42 | self._update_properties(phase_result, output_properties) 43 | 44 | def _get_input_values(self, input_properties: List[Pname]) -> List[Any]: 45 | inputs = [] 46 | for inp in input_properties: 47 | inputs.append(self.properties[inp]) 48 | assert len(inputs) == len(input_properties) 49 | return inputs 50 | 51 | def _update_dependencies( 52 | self, 53 | output_properties: List[Pname], 54 | input_properties: List[Pname], 55 | phase_descr: str, 56 | ) -> Set[str]: 57 | new_deps = set() 58 | new_deps.add(phase_descr) 59 | for inp in input_properties: 60 | new_deps.update(self.dependencies[inp]) 61 | for outp in output_properties: 62 | self.dependencies[outp] = new_deps 63 | return new_deps 64 | 65 | def _get_phase_fname(self, phase_name: str, previous: Set[str]) -> str: 66 | previous = sorted(list(previous)) 67 | return self.basedir + "/" + phase_name + "||" + "_".join(previous) + ".pkl.bz2" 68 | 69 | def _update_properties( 70 | self, phase_result: Tuple, output_properties: List[Pname] 71 | ) -> None: 72 | for idx, outp in enumerate(output_properties): 73 | if outp not in self.properties: 74 | self.properties[outp] = None 75 | self.properties[outp] = phase_result[idx] 76 | -------------------------------------------------------------------------------- /icarus_simulator/multiprocessor.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Utils for a standardised batched multithreaded computing, in order to best accommodate python's shortcomings. 4 | The class spawns the desired number of processes after dividing the sample list in a desired number of batches. 5 | To reduce biases and thus computation tail times, samples are shuffled before execution. 6 | """ 7 | 8 | import math 9 | import pickle 10 | import random 11 | import time 12 | import multiprocessing as mp 13 | 14 | from abc import abstractmethod 15 | from typing import Tuple, List, Dict 16 | from icarus_simulator.utils import compute_intervals_uniform 17 | 18 | 19 | class Multiprocessor: 20 | def __init__( 21 | self, 22 | num_procs: int, 23 | num_batches: int, 24 | samples: List, 25 | process_params: Tuple, 26 | verbose: bool = False, 27 | ): 28 | assert num_procs > 0 and num_batches > 0 29 | random.seed("DINFK") 30 | self.num_procs: int = min(mp.cpu_count(), num_procs) 31 | self.num_batches: int = num_batches 32 | self.samples: List = samples 33 | random.shuffle(self.samples) 34 | self.verbose: bool = verbose 35 | self.process_params: Tuple = process_params 36 | samples_len = len(samples) 37 | per_proc = int(math.ceil((samples_len / num_batches) / self.num_procs)) 38 | self.batch_size: int = per_proc * self.num_procs 39 | 40 | # Override this method only 41 | @abstractmethod 42 | def _single_sample_process( 43 | self, sample, process_result: Dict, params: Tuple 44 | ) -> None: 45 | raise NotImplementedError 46 | 47 | def process_batches(self) -> Dict: 48 | batch_start = 0 49 | samples_len = len(self.samples) 50 | idx = 0 51 | # Run one batch at a time 52 | result_total = {} 53 | while batch_start < samples_len: 54 | print(f"Batch {idx}") 55 | batch_end = min(batch_start + self.batch_size, samples_len) 56 | samples_batch = self.samples[batch_start:batch_end] 57 | batch_start = batch_end 58 | if self.num_procs == 1: 59 | dummy_dict = {} 60 | self._proc_worker(0, dummy_dict, samples_batch) 61 | dummy_dict[0] = pickle.loads(dummy_dict[0]) 62 | result_batch = self._assemble({}, dummy_dict) 63 | else: 64 | result_batch = self._spawn_procs(samples_batch) 65 | result_total[idx] = result_batch 66 | idx += 1 67 | return self._assemble({}, result_total) 68 | 69 | def _spawn_procs(self, samples_batch) -> Dict: 70 | shared_dict, jobs = mp.Manager().dict(), [] 71 | intervals = compute_intervals_uniform(len(samples_batch), self.num_procs) 72 | self._verbprint(f"Spawning {len(intervals)} threads") 73 | for i in range(len(intervals)): 74 | samples_proc = [s for s in samples_batch[intervals[i][0] : intervals[i][1]]] 75 | p = mp.Process( 76 | target=self._proc_worker, args=(i, shared_dict, samples_proc) 77 | ) 78 | jobs.append(p) 79 | p.start() 80 | for proc in jobs: 81 | proc.join() 82 | 83 | # Unpickle results 84 | unpickle_dict = {} 85 | for key in shared_dict: 86 | unpickle_dict[key] = pickle.loads(shared_dict[key]) 87 | return self._assemble({}, unpickle_dict) 88 | 89 | def _proc_worker(self, proc_id: int, return_dict, samples_proc: List) -> None: 90 | st = time.time() 91 | random.seed(f"{proc_id}-{proc_id}-{proc_id}") 92 | samples_len = len(samples_proc) 93 | last_min = 0 94 | thread_result = {} 95 | for s_id, sample in enumerate(samples_proc): 96 | minute = int((time.time() - st) / 60) 97 | if minute != last_min: 98 | self._verbprint( 99 | f"Proc {proc_id}, {(s_id / samples_len) * 100}%, {minute}m" 100 | ) 101 | last_min = minute 102 | self._single_sample_process(sample, thread_result, self.process_params) 103 | 104 | return_dict[proc_id] = pickle.dumps(thread_result) 105 | self._verbprint( 106 | f"Process {proc_id}, {samples_len} samples, finished in: {time.time() - st}" 107 | ) 108 | 109 | def _verbprint(self, text: str): 110 | if self.verbose: 111 | print(text) 112 | 113 | @staticmethod 114 | def _assemble(final_result: Dict, part_result: Dict) -> Dict: 115 | for key in part_result: 116 | final_result.update(part_result[key]) 117 | return final_result 118 | -------------------------------------------------------------------------------- /icarus_simulator/phases/__init__.py: -------------------------------------------------------------------------------- 1 | from .lsn_phase import LSNPhase 2 | from .grid_phase import GridPhase 3 | from .coverage_phase import CoveragePhase 4 | from .routing_phase import RoutingPhase 5 | from .edge_phase import EdgePhase 6 | from .traffic_phase import TrafficPhase 7 | from .link_attack_phase import LinkAttackPhase 8 | from .zone_attack_phase import ZoneAttackPhase 9 | -------------------------------------------------------------------------------- /icarus_simulator/phases/base_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | This class defines the abstraction for a computation phase, passed to the IcarusSimulator class to be executed. 4 | This class is open for custom extension, in order to create different phases. 5 | 6 | The input and output parameters are identified in IcarusSimulator by string identifiers, which the phase should provide. 7 | The _compute() method, which accepts any parameter in any number, contains the computation logic. Always returns tuple. 8 | _compute() is intended as a skeleton, where interchangeable steps are determined by BaseStrategy objects 9 | The methods name() and strategies() are used by IcarusSimulator to manage inter-phase dependencies and filenames. 10 | Moreover, this base class provides some basic logs and the resultfile dumping logic. 11 | 12 | For an extension example, see any provided phase class. All files in this directory are library-provided phases. 13 | """ 14 | import os 15 | import time 16 | 17 | from abc import abstractmethod 18 | from typing import List, Any, Tuple 19 | from compress_pickle import compress_pickle 20 | 21 | from icarus_simulator.strategies.base_strat import BaseStrat 22 | from icarus_simulator.structure_definitions import Pname 23 | 24 | 25 | class BasePhase: 26 | 27 | # Methods to override 28 | def __init__(self, read_persist: bool, persist: bool): 29 | self.read_persist: bool = read_persist 30 | self.persist: bool = persist 31 | 32 | @property 33 | def input_properties(self) -> List[Pname]: 34 | raise NotImplementedError 35 | 36 | @property 37 | def output_properties(self) -> List[Pname]: 38 | raise NotImplementedError 39 | 40 | @property 41 | def name(self) -> str: 42 | raise NotImplementedError 43 | 44 | @property 45 | def _strategies(self) -> List[BaseStrat]: 46 | raise NotImplementedError 47 | 48 | @abstractmethod 49 | def _compute(self, *args) -> Tuple: 50 | # Compute the result here, and always return a tuple, even if it has just one element in it! 51 | raise NotImplementedError 52 | 53 | @abstractmethod 54 | def _check_result(self, result) -> None: 55 | # Assertions go here 56 | raise NotImplementedError 57 | 58 | # Non-override methods 59 | @property 60 | def description(self) -> str: 61 | return ( 62 | self.name + "(" + "".join([st.description for st in self._strategies]) + ")" 63 | ) 64 | 65 | def execute_phase(self, input_values: List[Any], fname: str): 66 | print(f"{self.name} phase") 67 | start = time.time() 68 | read = True 69 | # If a results file is present, read it. Else, compute the result. 70 | if self.read_persist and os.path.isfile(fname): 71 | print(f"{self.name} reading") 72 | result = compress_pickle.load( 73 | fname, compression="bz2", set_default_extension=False 74 | ) 75 | print(f"{self.name} read in {time.time() - start}") 76 | else: 77 | read = False 78 | print(f"{self.name} computing") 79 | assert len(input_values) == len(self.input_properties) 80 | result = self._compute(*input_values) 81 | assert len(result) == len(self.output_properties) 82 | print(f"{self.name} computed in {time.time() - start}") 83 | 84 | # Check the consistency of the results 85 | self._check_result(result) 86 | 87 | # If the data has been computed and should be persisted, save it to file 88 | if self.persist and not read: 89 | st = time.time() 90 | compress_pickle.dump( 91 | result, fname, compression="bz2", set_default_extension=False 92 | ) 93 | print(f"{self.name} write: {time.time() - st}") 94 | print(f"{self.name} finished in {time.time() - start}") 95 | print("") 96 | return result 97 | -------------------------------------------------------------------------------- /icarus_simulator/phases/coverage_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import List, Tuple 4 | 5 | from icarus_simulator.strategies.coverage.base_coverage_strat import BaseCoverageStrat 6 | from icarus_simulator.strategies.base_strat import BaseStrat 7 | from icarus_simulator.phases.base_phase import BasePhase 8 | from icarus_simulator.structure_definitions import SatPos, GridPos, Coverage, Pname 9 | 10 | 11 | class CoveragePhase(BasePhase): 12 | def __init__( 13 | self, 14 | read_persist: bool, 15 | persist: bool, 16 | cov_strat: BaseCoverageStrat, 17 | grid_in: Pname, 18 | sat_in: Pname, 19 | grid_out: Pname, 20 | cov_out: Pname, 21 | ): 22 | super().__init__(read_persist, persist) 23 | self.cov_strat: BaseCoverageStrat = cov_strat 24 | self.ins: List[Pname] = [grid_in, sat_in] 25 | self.outs: List[Pname] = [grid_out, cov_out] 26 | 27 | @property 28 | def input_properties(self) -> List[Pname]: 29 | return self.ins 30 | 31 | @property 32 | def output_properties(self) -> List[Pname]: 33 | return self.outs 34 | 35 | @property 36 | def _strategies(self) -> List[BaseStrat]: 37 | return [self.cov_strat] 38 | 39 | @property 40 | def name(self) -> str: 41 | return "Cover" 42 | 43 | def _compute(self, grid_pos: GridPos, sat_pos: SatPos) -> Tuple[GridPos, Coverage]: 44 | # Compute the coverage 45 | coverage = self.cov_strat.compute(grid_pos, sat_pos) 46 | # Optimise coverage and grid by removing the uncovered points 47 | gplen = len(grid_pos) 48 | uncovered_gnds = [gnd for gnd in coverage if len(coverage[gnd].keys()) == 0] 49 | for gnd in uncovered_gnds: 50 | del coverage[gnd] 51 | del grid_pos[gnd] 52 | print(f"Earth grid size reduced from {gplen} to {len(grid_pos)}") 53 | return grid_pos, coverage 54 | 55 | def _check_result(self, result) -> None: # No check 56 | return 57 | -------------------------------------------------------------------------------- /icarus_simulator/phases/edge_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import List, Tuple, Dict 4 | 5 | import networkx as nx 6 | 7 | from icarus_simulator.strategies.edge.base_edge_strat import BaseEdgeStrat 8 | from icarus_simulator.strategies.base_strat import BaseStrat 9 | from icarus_simulator.phases.base_phase import BasePhase 10 | from icarus_simulator.multiprocessor import Multiprocessor 11 | from icarus_simulator.structure_definitions import ( 12 | PathData, 13 | GridPos, 14 | Pname, 15 | EdgeData, 16 | EdgeInfo, 17 | SatPos, 18 | Path, 19 | PathId, 20 | Edge, 21 | TempEdgeInfo, 22 | ) 23 | 24 | 25 | class EdgePhase(BasePhase): 26 | def __init__( 27 | self, 28 | read_persist: bool, 29 | persist: bool, 30 | num_procs: int, 31 | num_batches: int, 32 | ed_strat: BaseEdgeStrat, 33 | paths_in: Pname, 34 | nw_in: Pname, 35 | sats_in: Pname, 36 | grid_in: Pname, 37 | edges_out: Pname, 38 | ): 39 | super().__init__(read_persist, persist) 40 | self.num_procs = num_procs 41 | self.num_batches = num_batches 42 | self.ed_strat: BaseEdgeStrat = ed_strat 43 | self.ins: List[Pname] = [paths_in, nw_in, sats_in, grid_in] 44 | self.outs: List[Pname] = [edges_out] 45 | 46 | @property 47 | def input_properties(self) -> List[Pname]: 48 | return self.ins 49 | 50 | @property 51 | def output_properties(self) -> List[Pname]: 52 | return self.outs 53 | 54 | @property 55 | def _strategies(self) -> List[BaseStrat]: 56 | return [self.ed_strat] 57 | 58 | @property 59 | def name(self) -> str: 60 | return "Edges" 61 | 62 | def _compute( 63 | self, path_data: PathData, network: nx.Graph, sat_pos: SatPos, grid_pos: GridPos 64 | ) -> Tuple[EdgeData]: 65 | # Isolate all paths to be computed 66 | all_paths = [ 67 | (pd[0], (pair[0], pair[1], list_id)) 68 | for pair, pd_list in path_data.items() 69 | for list_id, pd in enumerate(pd_list) 70 | ] 71 | 72 | # Start a multithreaded computation 73 | multi = EdgeMultiproc( 74 | self.num_procs, self.num_batches, all_paths, process_params=(self.ed_strat,) 75 | ) 76 | edge_infos: Dict[Edge, TempEdgeInfo] = multi.process_batches() 77 | 78 | # Transform to EdgeInfo and add the missing edges 79 | all_edges = list(network.edges()) 80 | all_edges.extend([(ed[1], ed[0]) for ed in all_edges]) 81 | all_edges.extend([(-1, sat) for sat in sat_pos]) 82 | all_edges.extend([(sat, -1) for sat in sat_pos]) 83 | edge_data = {} 84 | for ed in all_edges: 85 | if ed in edge_infos: 86 | tup = edge_infos[ed] 87 | edge_data[ed] = EdgeInfo( 88 | tup.paths_through, 89 | tup.centrality / len(all_paths), 90 | sum(grid_pos[gnd].surface for gnd in tup.source_gridpoints), 91 | ) 92 | else: # Edge is never touched by the data, add default 93 | edge_data[ed] = EdgeInfo([], 0.0, 0.0) 94 | 95 | result_tup = (edge_data,) # Must be a tuple! 96 | return result_tup 97 | 98 | def _check_result(self, result: Tuple[EdgeData]) -> None: 99 | return 100 | 101 | 102 | class EdgeMultiproc(Multiprocessor): 103 | def _single_sample_process( 104 | self, 105 | sample: Tuple[Path, PathId], 106 | process_result: Dict[Edge, TempEdgeInfo], 107 | params: Tuple, 108 | ) -> None: 109 | path, path_id = sample 110 | ed_strat: BaseEdgeStrat = params[0] 111 | ed_strat.compute(process_result, path, path_id) 112 | 113 | @staticmethod 114 | def _assemble(final_result: Dict, part_result: Dict) -> Dict: 115 | for val in part_result.values(): 116 | for ed, tup in val.items(): 117 | if ed in final_result: 118 | final_result[ed].paths_through.extend(tup.paths_through) 119 | final_result[ed].centrality += tup.centrality 120 | final_result[ed].source_gridpoints.update(tup.source_gridpoints) 121 | else: 122 | final_result[ed] = tup 123 | return final_result 124 | -------------------------------------------------------------------------------- /icarus_simulator/phases/grid_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import Tuple, List 4 | 5 | from icarus_simulator.strategies.grid.base_grid_strat import BaseGridStrat 6 | from icarus_simulator.strategies.base_strat import BaseStrat 7 | from icarus_simulator.strategies.grid_weight.base_weight_strat import BaseWeightStrat 8 | from icarus_simulator.phases.base_phase import BasePhase 9 | from icarus_simulator.structure_definitions import GridPos, Pname 10 | 11 | 12 | class GridPhase(BasePhase): 13 | def __init__( 14 | self, 15 | read_persist: bool, 16 | persist: bool, 17 | grid_strat: BaseGridStrat, 18 | weight_strat: BaseWeightStrat, 19 | grid_out: Pname, 20 | size_out: Pname, 21 | ): 22 | super().__init__(read_persist, persist) 23 | self.grid_strat: BaseGridStrat = grid_strat 24 | self.weight_strat: BaseWeightStrat = weight_strat 25 | self.outs: List[Pname] = [grid_out, size_out] 26 | 27 | @property 28 | def input_properties(self) -> List[Pname]: 29 | return [] 30 | 31 | @property 32 | def output_properties(self) -> List[Pname]: 33 | return self.outs 34 | 35 | @property 36 | def _strategies(self) -> List[BaseStrat]: 37 | return [self.grid_strat, self.weight_strat] 38 | 39 | @property 40 | def name(self) -> str: 41 | return "Grid" 42 | 43 | def _compute(self) -> Tuple[GridPos, int]: 44 | grid_pos = self.grid_strat.compute() 45 | full_len = len(grid_pos) 46 | grid_pos = self.weight_strat.compute(grid_pos) 47 | return grid_pos, full_len 48 | 49 | def _check_result(self, result: Tuple[GridPos, int]) -> None: 50 | gp, full_length = result 51 | for idx in [0, 1]: 52 | if idx in gp: 53 | gp[full_length + idx] = gp[idx] 54 | del gp[idx] 55 | 56 | for idx in gp: 57 | assert idx > 1 58 | return 59 | -------------------------------------------------------------------------------- /icarus_simulator/phases/link_attack_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import List, Tuple 4 | 5 | from icarus_simulator.phases.base_phase import BasePhase 6 | from icarus_simulator.strategies.atk_detect_optimisation.base_optim_strat import ( 7 | BaseOptimStrat, 8 | ) 9 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 10 | BaseFeasStrat, 11 | ) 12 | from icarus_simulator.strategies.atk_geo_constraint.base_geo_constraint_strat import ( 13 | BaseGeoConstraintStrat, 14 | ) 15 | from icarus_simulator.strategies.atk_path_filtering.base_path_filtering_strat import ( 16 | BasePathFilteringStrat, 17 | ) 18 | from icarus_simulator.strategies.base_strat import BaseStrat 19 | from icarus_simulator.multiprocessor import Multiprocessor 20 | from icarus_simulator.structure_definitions import ( 21 | GridPos, 22 | Pname, 23 | BwData, 24 | PathData, 25 | EdgeData, 26 | Edge, 27 | AttackInfo, 28 | AttackData, 29 | ) 30 | 31 | 32 | class LinkAttackPhase(BasePhase): 33 | def __init__( 34 | self, 35 | read_persist: bool, 36 | persist: bool, 37 | num_procs: int, 38 | num_batches: int, 39 | geo_constr_strat: BaseGeoConstraintStrat, 40 | filter_strat: BasePathFilteringStrat, 41 | feas_strat: BaseFeasStrat, 42 | optim_strat: BaseOptimStrat, 43 | grid_in: Pname, 44 | paths_in: Pname, 45 | edges_in: Pname, 46 | bw_in: Pname, 47 | latk_out: Pname, 48 | ): 49 | super().__init__(read_persist, persist) 50 | self.num_procs = num_procs 51 | self.num_batches = num_batches 52 | self.geo_constr_strat: BaseGeoConstraintStrat = geo_constr_strat 53 | self.filter_strat: BasePathFilteringStrat = filter_strat 54 | self.feas_strat: BaseFeasStrat = feas_strat 55 | self.optim_strat: BaseOptimStrat = optim_strat 56 | self.ins: List[Pname] = [grid_in, paths_in, edges_in, bw_in] 57 | self.outs: List[Pname] = [latk_out] 58 | 59 | @property 60 | def input_properties(self) -> List[Pname]: 61 | return self.ins 62 | 63 | @property 64 | def output_properties(self) -> List[Pname]: 65 | return self.outs 66 | 67 | @property 68 | def _strategies(self) -> List[BaseStrat]: 69 | return [ 70 | self.geo_constr_strat, 71 | self.filter_strat, 72 | self.feas_strat, 73 | self.optim_strat, 74 | ] 75 | 76 | @property 77 | def name(self) -> str: 78 | return "LAtk" 79 | 80 | def _compute( 81 | self, 82 | grid_pos: GridPos, 83 | path_data: PathData, 84 | edge_data: EdgeData, 85 | bw_data: BwData, 86 | ) -> Tuple[AttackData]: 87 | # Elaborate a list of the edges to be attacked 88 | edges = list(bw_data.keys()) 89 | allowed_sources = self.geo_constr_strat.compute(grid_pos) 90 | # Start a multithreaded computation 91 | multi = AttackMultiproc( 92 | self.num_procs, 93 | self.num_batches, 94 | edges, 95 | process_params=( 96 | self.filter_strat, 97 | self.feas_strat, 98 | self.optim_strat, 99 | path_data, 100 | edge_data, 101 | bw_data, 102 | allowed_sources, 103 | ), 104 | ) 105 | ret_tuple = (multi.process_batches(),) # It must be a tuple! 106 | return ret_tuple 107 | 108 | def _check_result(self, result: Tuple[AttackData]) -> None: 109 | return 110 | 111 | 112 | class AttackMultiproc(Multiprocessor): 113 | def _single_sample_process( 114 | self, sample: Edge, process_result: AttackData, params: Tuple 115 | ) -> None: 116 | filter_strat: BasePathFilteringStrat 117 | feas_strat: BaseFeasStrat 118 | optim_strat: BaseOptimStrat 119 | ( 120 | filter_strat, 121 | feas_strat, 122 | optim_strat, 123 | path_data, 124 | edge_data, 125 | bw_data, 126 | allowed_sources, 127 | ) = params 128 | # This method computes the attack phases. 129 | # A1 is comprised of all the previously done work until here. 130 | # A2 is instead irrelevant as there is no bneck choice. 131 | # A3: path filtering 132 | direction_data = filter_strat.compute( 133 | [sample], edge_data, path_data, allowed_sources 134 | ) 135 | 136 | # A4: feasibility check 137 | uplink_size = max( 138 | bw_data[ed].capacity for ed in bw_data if ed[0] == -1 139 | ) # Max in case of weird bw assignments 140 | atk_flow_set, on_trg, detect = feas_strat.compute( 141 | [sample], path_data, bw_data, direction_data, uplink_size 142 | ) 143 | if atk_flow_set is None: 144 | process_result[sample] = None 145 | return 146 | 147 | # A5: iterative optimisation 148 | # We firstly need the maximum increase value possible, that is maximum capacity of all uplinks 149 | atk_flow_set, on_trg, detect = optim_strat.compute( 150 | [sample], path_data, bw_data, direction_data, uplink_size, feas_strat 151 | ) 152 | 153 | process_result[sample] = AttackInfo( 154 | cost=sum([el[1] for el in atk_flow_set]), 155 | detectability=detect, 156 | flows_on_trg=on_trg, 157 | atkflowset=atk_flow_set, 158 | ) 159 | -------------------------------------------------------------------------------- /icarus_simulator/phases/lsn_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | import networkx as nx 4 | from typing import Tuple, List 5 | 6 | from icarus_simulator.phases.base_phase import BasePhase 7 | from icarus_simulator.strategies.lsn.base_lsn_strat import BaseLSNStrat 8 | from icarus_simulator.strategies.base_strat import BaseStrat 9 | from icarus_simulator.structure_definitions import SatPos, Pname, IslInfo 10 | 11 | 12 | class LSNPhase(BasePhase): 13 | def __init__( 14 | self, 15 | read_persist: bool, 16 | persist: bool, 17 | lsn_strat: BaseLSNStrat, 18 | lsn_out: Pname, 19 | nw_out: Pname, 20 | isls_out: Pname, 21 | ): 22 | super().__init__(read_persist, persist) 23 | self.lsn_strat = lsn_strat 24 | self.outs: List[Pname] = [lsn_out, nw_out, isls_out] 25 | 26 | @property 27 | def input_properties(self) -> List[Pname]: 28 | return [] 29 | 30 | @property 31 | def output_properties(self) -> List[Pname]: 32 | return self.outs 33 | 34 | @property 35 | def _strategies(self) -> List[BaseStrat]: 36 | return [self.lsn_strat] 37 | 38 | @property 39 | def name(self) -> str: 40 | return "LSN" 41 | 42 | def _compute(self) -> Tuple[SatPos, nx.Graph, List[IslInfo]]: 43 | # Call the strategy to generate the network 44 | sat_pos, nw, isls = self.lsn_strat.compute() 45 | return sat_pos, nw, isls 46 | 47 | def _check_result(self, result) -> None: # No checks to be performed 48 | return 49 | -------------------------------------------------------------------------------- /icarus_simulator/phases/routing_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | import networkx as nx 4 | from typing import List, Tuple, Dict 5 | 6 | from icarus_simulator.phases.base_phase import BasePhase 7 | from icarus_simulator.strategies.base_strat import BaseStrat 8 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 9 | from icarus_simulator.multiprocessor import Multiprocessor 10 | from icarus_simulator.structure_definitions import ( 11 | PathData, 12 | GridPos, 13 | Coverage, 14 | SdPair, 15 | Pname, 16 | LbSet, 17 | ) 18 | 19 | 20 | class RoutingPhase(BasePhase): 21 | def __init__( 22 | self, 23 | read_persist: bool, 24 | persist: bool, 25 | num_procs: int, 26 | num_batches: int, 27 | rout_strat: BaseRoutingStrat, 28 | grid_in: Pname, 29 | nw_in: Pname, 30 | cov_in: Pname, 31 | paths_out: Pname, 32 | ): 33 | super().__init__(read_persist, persist) 34 | self.num_procs = num_procs 35 | self.num_batches = num_batches 36 | self.rout_strat: BaseRoutingStrat = rout_strat 37 | self.ins: List[Pname] = [grid_in, nw_in, cov_in] 38 | self.outs: List[Pname] = [paths_out] 39 | 40 | @property 41 | def input_properties(self) -> List[Pname]: 42 | return self.ins 43 | 44 | @property 45 | def output_properties(self) -> List[Pname]: 46 | return self.outs 47 | 48 | @property 49 | def _strategies(self) -> List[BaseStrat]: 50 | return [self.rout_strat] 51 | 52 | @property 53 | def name(self) -> str: 54 | return "Routes" 55 | 56 | def _compute( 57 | self, grid: GridPos, network: nx.Graph, coverage: Coverage 58 | ) -> Tuple[PathData]: 59 | # Elaborate a list of the sdpairs to be computed 60 | grid_ids = list(grid.keys()) 61 | pairs = [] 62 | for src_key_id in range(len(grid_ids) - 1): 63 | in_grid = grid_ids[src_key_id] 64 | for dst_key_id in range(src_key_id + 1, len(grid_ids)): 65 | out_grid = grid_ids[dst_key_id] 66 | pairs.append((in_grid, out_grid)) 67 | # Start a multithreaded computation 68 | multi = RoutingMultiproc( 69 | self.num_procs, 70 | self.num_batches, 71 | pairs, 72 | process_params=(grid, network, coverage, self.rout_strat), 73 | ) 74 | ret_tuple = (multi.process_batches(),) # It must be a tuple! 75 | return ret_tuple 76 | 77 | def _check_result(self, result: Tuple[PathData]) -> None: 78 | path_data = result[0] 79 | for sdpair in path_data: 80 | assert sdpair[0] < sdpair[1] 81 | return 82 | 83 | 84 | class RoutingMultiproc(Multiprocessor): 85 | def _single_sample_process( 86 | self, sample: SdPair, process_result: Dict[SdPair, LbSet], params: Tuple 87 | ) -> None: 88 | grid: GridPos 89 | network: nx.Graph 90 | coverage: Coverage 91 | rout_strat: BaseRoutingStrat 92 | grid, network, coverage, rout_strat = params 93 | process_result[sample] = rout_strat.compute(sample, grid, network, coverage) 94 | -------------------------------------------------------------------------------- /icarus_simulator/phases/traffic_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import List, Tuple 4 | 5 | from icarus_simulator.phases.base_phase import BasePhase 6 | from icarus_simulator.strategies.base_strat import BaseStrat 7 | from icarus_simulator.strategies.bw_selection.base_bw_select_strat import ( 8 | BaseBwSelectStrat, 9 | ) 10 | from icarus_simulator.strategies.bw_assignment.base_bw_assig_strat import ( 11 | BaseBwAssignStrat, 12 | ) 13 | from icarus_simulator.structure_definitions import ( 14 | GridPos, 15 | Pname, 16 | BwData, 17 | PathData, 18 | EdgeData, 19 | ) 20 | 21 | 22 | class TrafficPhase(BasePhase): 23 | def __init__( 24 | self, 25 | read_persist: bool, 26 | persist: bool, 27 | select_strat: BaseBwSelectStrat, 28 | assign_strat: BaseBwAssignStrat, 29 | grid_in: Pname, 30 | paths_in: Pname, 31 | edges_in: Pname, 32 | bw_out: Pname, 33 | ): 34 | super().__init__(read_persist, persist) 35 | self.select_strat: BaseBwSelectStrat = select_strat 36 | self.assign_strat: BaseBwAssignStrat = assign_strat 37 | self.ins: List[Pname] = [grid_in, paths_in, edges_in] 38 | self.outs: List[Pname] = [bw_out] 39 | 40 | @property 41 | def input_properties(self) -> List[Pname]: 42 | return self.ins 43 | 44 | @property 45 | def output_properties(self) -> List[Pname]: 46 | return self.outs 47 | 48 | @property 49 | def _strategies(self) -> List[BaseStrat]: 50 | return [self.select_strat, self.assign_strat] 51 | 52 | @property 53 | def name(self) -> str: 54 | return "Bw" 55 | 56 | def _compute( 57 | self, grid_pos: GridPos, path_data: PathData, edge_data: EdgeData 58 | ) -> Tuple[BwData]: 59 | # Let the selection strategy choose a list of paths to allocate in order 60 | # Each path in the list bears one single data quantum 61 | chosen_paths = self.select_strat.compute(grid_pos, path_data) 62 | 63 | # Assign the paths sequentially 64 | bw_data = self.assign_strat.compute(path_data, chosen_paths, edge_data) 65 | return (bw_data,) 66 | 67 | def _check_result(self, result: Tuple[BwData]) -> None: 68 | bw_data = result[0] 69 | for bd in bw_data.values(): 70 | assert bd.idle_bw <= bd.capacity 71 | return 72 | -------------------------------------------------------------------------------- /icarus_simulator/phases/zone_attack_phase.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | import itertools 4 | from typing import List, Tuple 5 | from geopy.distance import great_circle 6 | 7 | from icarus_simulator.phases.base_phase import BasePhase 8 | from icarus_simulator.strategies.base_strat import BaseStrat 9 | from icarus_simulator.strategies.atk_detect_optimisation.base_optim_strat import ( 10 | BaseOptimStrat, 11 | ) 12 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 13 | BaseFeasStrat, 14 | ) 15 | from icarus_simulator.strategies.atk_geo_constraint.base_geo_constraint_strat import ( 16 | BaseGeoConstraintStrat, 17 | ) 18 | from icarus_simulator.strategies.atk_path_filtering.base_path_filtering_strat import ( 19 | BasePathFilteringStrat, 20 | ) 21 | from icarus_simulator.strategies.zone_bneck.base_zone_bneck_strat import ( 22 | BaseZoneBneckStrat, 23 | ) 24 | from icarus_simulator.strategies.zone_build.base_zone_build_strat import ( 25 | BaseZoneBuildStrat, 26 | ) 27 | from icarus_simulator.strategies.zone_edges.base_zone_edges_strat import ( 28 | BaseZoneEdgesStrat, 29 | ) 30 | from icarus_simulator.strategies.zone_select.base_zone_select_strat import ( 31 | BaseZoneSelectStrat, 32 | ) 33 | from icarus_simulator.multiprocessor import Multiprocessor 34 | from icarus_simulator.structure_definitions import ( 35 | GridPos, 36 | Pname, 37 | BwData, 38 | PathData, 39 | EdgeData, 40 | AttackData, 41 | ZoneAttackData, 42 | PathEdgeData, 43 | ZoneAttackInfo, 44 | ) 45 | from icarus_simulator.utils import get_ordered_idx, get_edges 46 | 47 | 48 | class ZoneAttackPhase(BasePhase): 49 | def __init__( 50 | self, 51 | read_persist: bool, 52 | persist: bool, 53 | num_procs: int, 54 | num_batches: int, 55 | geo_constr_strat: BaseGeoConstraintStrat, 56 | zone_select_strat: BaseZoneSelectStrat, 57 | zone_build_strat: BaseZoneBuildStrat, 58 | zone_edges_strat: BaseZoneEdgesStrat, 59 | zone_bneck_strat: BaseZoneBneckStrat, 60 | atk_filter_strat: BasePathFilteringStrat, 61 | atk_feas_strat: BaseFeasStrat, 62 | atk_optim_strat: BaseOptimStrat, 63 | grid_in: Pname, 64 | paths_in: Pname, 65 | edges_in: Pname, 66 | bw_in: Pname, 67 | atk_in: Pname, 68 | zatk_out: Pname, 69 | ): 70 | super().__init__(read_persist, persist) 71 | self.num_procs = num_procs 72 | self.num_batches = num_batches 73 | self.geo_constr_strat: BaseGeoConstraintStrat = geo_constr_strat 74 | self.select_strat: BaseZoneSelectStrat = zone_select_strat 75 | self.build_strat: BaseZoneBuildStrat = zone_build_strat 76 | self.edges_strat: BaseZoneEdgesStrat = zone_edges_strat 77 | self.bneck_strat: BaseZoneBneckStrat = zone_bneck_strat 78 | self.filter_strat: BasePathFilteringStrat = atk_filter_strat 79 | self.feas_strat: BaseFeasStrat = atk_feas_strat 80 | self.optim_strat: BaseOptimStrat = atk_optim_strat 81 | self.ins: List[Pname] = [grid_in, paths_in, edges_in, bw_in, atk_in] 82 | self.outs: List[Pname] = [zatk_out] 83 | 84 | @property 85 | def input_properties(self) -> List[Pname]: 86 | return self.ins 87 | 88 | @property 89 | def output_properties(self) -> List[Pname]: 90 | return self.outs 91 | 92 | @property 93 | def _strategies(self) -> List[BaseStrat]: 94 | return [ 95 | self.geo_constr_strat, 96 | self.select_strat, 97 | self.build_strat, 98 | self.edges_strat, 99 | self.bneck_strat, 100 | self.filter_strat, 101 | self.feas_strat, 102 | self.optim_strat, 103 | ] 104 | 105 | @property 106 | def name(self) -> str: 107 | return "ZAtk" 108 | 109 | def _compute( 110 | self, 111 | grid_pos: GridPos, 112 | path_data: PathData, 113 | edge_data: EdgeData, 114 | bw_data: BwData, 115 | atk_data: AttackData, 116 | ) -> Tuple[AttackData]: 117 | allowed_sources = self.geo_constr_strat.compute(grid_pos) 118 | # Select the centres of the zones to be disconnected 119 | zone_pairs = self.select_strat.compute(grid_pos) 120 | # Start a multithreaded computation 121 | multi = ZoneAttackMultiproc( 122 | self.num_procs, 123 | self.num_batches, 124 | zone_pairs, 125 | process_params=( 126 | self.build_strat, 127 | self.edges_strat, 128 | self.bneck_strat, 129 | self.filter_strat, 130 | self.feas_strat, 131 | self.optim_strat, 132 | grid_pos, 133 | path_data, 134 | bw_data, 135 | edge_data, 136 | atk_data, 137 | allowed_sources, 138 | ), 139 | verbose=True, 140 | ) 141 | ret_tuple = (multi.process_batches(),) # It must be a tuple! 142 | return ret_tuple 143 | 144 | def _check_result(self, result: Tuple[AttackData]) -> None: 145 | return 146 | 147 | 148 | class ZoneAttackMultiproc(Multiprocessor): 149 | def _single_sample_process( 150 | self, sample: Tuple[int, int], process_result: ZoneAttackData, params: Tuple 151 | ) -> None: 152 | build_strat: BaseZoneBuildStrat 153 | edges_strat: BaseZoneEdgesStrat 154 | bneck_strat: BaseZoneBneckStrat 155 | filter_strat: BasePathFilteringStrat 156 | feas_strat: BaseFeasStrat 157 | optim_strat: BaseOptimStrat 158 | grid_pos: GridPos 159 | path_data: PathData 160 | bw_data: BwData 161 | edge_data: EdgeData 162 | atk_data: AttackData 163 | ( 164 | build_strat, 165 | edges_strat, 166 | bneck_strat, 167 | filter_strat, 168 | feas_strat, 169 | optim_strat, 170 | grid_pos, 171 | path_data, 172 | bw_data, 173 | edge_data, 174 | atk_data, 175 | allowed_sources, 176 | ) = params 177 | 178 | # Process the single sample 179 | zone1, zone2 = build_strat.compute(grid_pos, sample[0], sample[1]) 180 | 181 | sample_res_idx = tuple(zone1), tuple(zone2) 182 | # Firstly, if the zones overlap we say the disconnection fails, as an isl-only disconnection is not possible 183 | if len(set(zone1).intersection(set(zone2))) > 0: 184 | # The zones are not included in the results, as the sample pair is bad 185 | return 186 | 187 | # Find the minimum distance and all (desirable by routing phase) paths between zones 188 | min_dist = min( 189 | great_circle( 190 | (grid_pos[idx1].lat, grid_pos[idx1].lon), 191 | (grid_pos[idx2].lat, grid_pos[idx2].lon), 192 | ).meters 193 | for idx1 in zone1 194 | for idx2 in zone2 195 | ) 196 | cross_zone_paths = compute_zone_crossing_paths(zone1, zone2, path_data) 197 | 198 | # Find all edges in all desirable 199 | path_edges: PathEdgeData = {} 200 | covered_paths_potential = set() 201 | for d_idx, d in enumerate(cross_zone_paths): 202 | for ed in get_edges(d): 203 | # Filter out the edges not allowed by the edge strategy and the non-singularly-attackable 204 | if edges_strat.compute(ed) and atk_data[ed] is not None: 205 | covered_paths_potential.add(d_idx) 206 | if ed not in path_edges: 207 | path_edges[ed] = set() 208 | path_edges[ed].add(d_idx) 209 | 210 | # Check if a cut can be achieved with these edges only and exit early if not 211 | if len(path_edges) == 0 or len(covered_paths_potential) < len(cross_zone_paths): 212 | process_result[sample_res_idx] = None 213 | return 214 | 215 | # Compute the possible heuristically-determined bottlenecks and check which one is the best 216 | uplink_size = max( 217 | bw_data[ed].capacity for ed in bw_data if ed[0] == -1 218 | ) # Max in case of weird bw assignments 219 | possible_bnecks = bneck_strat.compute( 220 | bw_data, atk_data, path_edges, len(cross_zone_paths) 221 | ) 222 | best_atk_flow_set, best_on_trg, best_detect, best_bneck = ( 223 | None, 224 | 9000000000, 225 | 90000000000, 226 | None, 227 | ) 228 | for bneck in possible_bnecks: 229 | direction_data = filter_strat.compute( 230 | bneck, edge_data, path_data, allowed_sources 231 | ) 232 | 233 | # A4: feasibility check 234 | atk_flow_set, on_trg, detect = feas_strat.compute( 235 | bneck, path_data, bw_data, direction_data, uplink_size 236 | ) 237 | if atk_flow_set is None: 238 | continue 239 | 240 | # A5: iterative optimisation 241 | # We firstly need the maximum increase value possible, that is maximum capacity of all uplinks 242 | atk_flow_set, on_trg, detect = optim_strat.compute( 243 | bneck, path_data, bw_data, direction_data, uplink_size, feas_strat 244 | ) 245 | # Deterministic allocation, therefore cost = on_trg 246 | if detect < best_detect or (detect == best_detect and on_trg < best_on_trg): 247 | best_atk_flow_set = atk_flow_set 248 | best_on_trg = on_trg 249 | best_detect = detect 250 | best_bneck = bneck 251 | 252 | if best_atk_flow_set is None: 253 | process_result[sample_res_idx] = None 254 | return 255 | 256 | process_result[sample_res_idx] = ZoneAttackInfo( 257 | cost=best_on_trg, 258 | detectability=best_detect, 259 | flows_on_trg=best_on_trg, 260 | atkflowset=best_atk_flow_set, 261 | cross_zone_paths=cross_zone_paths, 262 | bottlenecks=best_bneck, 263 | distance=min_dist, 264 | ) 265 | return 266 | 267 | 268 | def compute_zone_crossing_paths(zone1: List[int], zone2: List[int], path_data): 269 | # Analyse all possible pairs that go across zones 270 | paths_across = [] 271 | for src in zone1: 272 | for trg in zone2: 273 | ord_pair, ordered = get_ordered_idx((src, trg)) 274 | path_part = [pd[0][1:-1] for pd in path_data[ord_pair]] 275 | for i in range(len(path_part)): 276 | p = path_part[i] 277 | if ordered: 278 | path_part[i] = [-1] + p + [-1] 279 | else: 280 | path_part[i] = [-1] + list(reversed(p)) + [-1] 281 | paths_across.extend(path_part) 282 | 283 | paths_across.sort() 284 | paths_across = [item for item, _ in itertools.groupby(paths_across)] 285 | return paths_across 286 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2020 Giacomo Giuliari 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .constellation import Constellation 24 | from .constellation_network import ConstellationNetwork, WalkerConstellationNetwork 25 | from .orbit_shift_algo import WalkerShift, OrbitShiftAlgo, SimpleShift, NoShift 26 | from .satellite import Satellite 27 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/constellation.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from typing import Dict, List 4 | 5 | import numpy as np 6 | 7 | from .orbit_shift_algo import OrbitShiftAlgo 8 | from .orbit_util import elevation_to_mean_motion, epoch_offset_to_date 9 | from .satellite import Satellite 10 | from icarus_simulator.sat_core.coordinate_util import GeodeticPosition 11 | 12 | 13 | class Constellation: 14 | """Class representation of a constellation. 15 | 16 | A constellation is a set of satellites on pre-defined orbits. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | num_sat_per_orbit: int, 22 | num_orbits: int, 23 | inclination: float, 24 | epoch: str, 25 | mean_motion: float = None, 26 | elevation: float = None, 27 | orbit_shift_algo: OrbitShiftAlgo = None, 28 | eccentricity: float = 1e-32, 29 | aug_perigee: float = 0.0, 30 | ) -> None: 31 | self.num_sat_per_orbit = num_sat_per_orbit 32 | self.num_orbits = num_orbits 33 | self.inclination = inclination 34 | self.epoch = epoch 35 | self.orbit_shift_algo = orbit_shift_algo 36 | self.eccentricity = eccentricity 37 | self.aug_perigee = aug_perigee 38 | # Check agreement between mean motion and elevation if specified 39 | if mean_motion is None and elevation is None: 40 | raise ValueError("Either mean_motion or elevation must be given.") 41 | if elevation is not None: 42 | self.elevation = elevation 43 | conv_mean_motion = elevation_to_mean_motion(elevation) 44 | if mean_motion is not None: 45 | assert np.isclose(mean_motion, conv_mean_motion) 46 | mean_motion = conv_mean_motion 47 | self.mean_motion = mean_motion 48 | # Satellite store 49 | self.satellites: Dict[int, Satellite] = {} 50 | 51 | def create_constellation(self): 52 | """Create the constellation, loading all the satellites. 53 | 54 | This has to be called once before calling other methods. 55 | """ 56 | for orbit_idx in range(self.num_orbits): 57 | for in_orbit_idx in range(self.num_sat_per_orbit): 58 | cur_orbit_shift = self.orbit_shift_algo.get_shift(orbit_idx) 59 | cur_sat = Satellite( 60 | sat_idx_in_orbit=in_orbit_idx, 61 | orbit_idx=orbit_idx, 62 | num_sat_per_orbit=self.num_sat_per_orbit, 63 | num_orbits=self.num_orbits, 64 | inclination=self.inclination, 65 | epoch=self.epoch, 66 | mean_motion=self.mean_motion, 67 | orbit_shift=cur_orbit_shift, 68 | eccentricity=self.eccentricity, 69 | aug_perigee=self.aug_perigee, 70 | ) 71 | self.satellites[cur_sat.sat_idx] = cur_sat 72 | 73 | def compute_positions_at_time(self, timestr: str) -> Dict[int, GeodeticPosition]: 74 | """ 75 | Compute satellite positions at a specific time 76 | Args: 77 | timestr: Time for which to compute the position. Has to be a string 78 | formatted as with `%Y/%m/%d %H:%M:%S`. 79 | 80 | Returns: 81 | Dict[int, SatPosition]: A dictionary of satellite positions, keyed 82 | by satellite index. 83 | """ 84 | positions = {} 85 | for idx, satellite in self.satellites.items(): 86 | positions[idx] = satellite.position_at_time(timestr) 87 | return positions 88 | 89 | def compute_positions_at_epoch_offset( 90 | self, hours: int = 0, minutes: int = 0, seconds: int = 0, millisecs: int = 0 91 | ) -> Dict[int, GeodeticPosition]: 92 | """ 93 | Compute satellite positions at a particular epoch offset 94 | Args: 95 | hours: Offset hours. 96 | minutes: Offset minutes. 97 | seconds: Offset seconds. 98 | millisecs: Offset milliseconds 99 | 100 | Returns: Dict[int, SatPosition]: A dictionary of satellite positions, keyed 101 | by satellite index. 102 | """ 103 | 104 | target_date_str = epoch_offset_to_date( 105 | self.epoch, hours, minutes, seconds, millisecs 106 | ) 107 | return self.compute_positions_at_time(target_date_str) 108 | 109 | def positions_tostring(self) -> List[str]: 110 | """ 111 | List of strings with index and position information. 112 | List is sorted by satellite idx. 113 | """ 114 | pos_str = [] 115 | for idx in range(self.num_sat_per_orbit * self.num_orbits): 116 | pos_str.append(self.satellites[idx].sat_position_tostring()) 117 | return pos_str 118 | 119 | 120 | if __name__ == "__main__": 121 | raise RuntimeError 122 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/constellation_network.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | 4 | from typing import List, Tuple, Dict 5 | from typing_extensions import TypedDict 6 | import networkx as nx 7 | 8 | from .constellation import Constellation 9 | from .isl_util import sat_idx_to_in_orbit_idx, compute_link_length, get_sat_by_offset 10 | from .orbit_shift_algo import WalkerShift 11 | from .coordinate_util import GeodeticPosition 12 | 13 | 14 | class Isl(TypedDict): 15 | sat1: int 16 | sat2: int 17 | length: float 18 | 19 | 20 | class ConstellationNetwork: 21 | """The network made by interconnecting satellites with a "motif".""" 22 | 23 | def __init__( 24 | self, 25 | sat_pos: Dict[int, GeodeticPosition], 26 | num_sat_per_orbit: int, 27 | num_orbits: int, 28 | max_shift: float = 0, 29 | ): 30 | self.sat_pos = sat_pos 31 | self.num_sat_per_orbit = num_sat_per_orbit 32 | self.num_orbits = num_orbits 33 | self.network: nx.Graph = nx.Graph() 34 | self.max_shift = max_shift 35 | 36 | def generate_network(self, motif): 37 | """ 38 | Args: 39 | motif: The description of a motif. A list of tuples. Each tuple 40 | contains two indices, representing the offset from the current 41 | satellite. 42 | """ 43 | for sat_idx in self.sat_pos: 44 | sat_idx_in_orbit, orbit_idx = sat_idx_to_in_orbit_idx( 45 | sat_idx, self.num_sat_per_orbit 46 | ) 47 | for sat_off, orbit_off in motif: 48 | neigh_idx, _, _ = get_sat_by_offset( 49 | sat_idx_in_orbit, 50 | orbit_idx, 51 | sat_off, 52 | orbit_off, 53 | self.num_sat_per_orbit, 54 | self.num_orbits, 55 | self.max_shift, 56 | ) 57 | length = compute_link_length( 58 | self.sat_pos[sat_idx], self.sat_pos[neigh_idx] 59 | ) 60 | self.network.add_edge(sat_idx, neigh_idx, length=length) 61 | 62 | def get_sats(self): 63 | return self.sat_pos.copy() 64 | 65 | def get_isls(self) -> List[Isl]: 66 | isls = [] 67 | for start, end in self.network.edges(): 68 | length = self.network[start][end]["length"] 69 | isl = {"sat1": start, "sat2": end, "length": length} 70 | isls.append(isl) 71 | return isls 72 | 73 | def isls_tostring(self) -> List[str]: 74 | """List of strings information on the ISLa. 75 | 76 | List is sorted by satellite idx. 77 | """ 78 | pos_str = [] 79 | for start, end in self.network.edges(): 80 | # slat, slong, selev = self.sat_pos[start] 81 | # elat, elong, eelev = self.sat_pos[end] 82 | length = self.network[start][end]["length"] 83 | cur = f"{start} {end} {length}" 84 | pos_str.append(cur) 85 | return pos_str 86 | 87 | 88 | class WalkerConstellationNetwork: 89 | """Helper class that combine Constellation and ConstellationNetwork. 90 | 91 | Helps create a constellation network with Walker topology fast. 92 | """ 93 | 94 | def __init__( 95 | self, 96 | num_sat_per_orbit: int, 97 | num_orbits: int, 98 | inclination: float, 99 | epoch: str, 100 | f_param: int, 101 | mean_motion: float = None, 102 | elevation: float = None, 103 | motif: List[Tuple[int, int]] = ((0, 1), (1, 0)), 104 | ) -> None: 105 | self.shiftalgo = WalkerShift( 106 | inclination, num_sat_per_orbit, num_orbits, f_param 107 | ) 108 | max_shift = self.shiftalgo.get_shift(num_orbits - 1) 109 | self.const = Constellation( 110 | num_sat_per_orbit, 111 | num_orbits, 112 | inclination, 113 | epoch, 114 | mean_motion=mean_motion, 115 | elevation=elevation, 116 | orbit_shift_algo=self.shiftalgo, 117 | ) 118 | self.const.create_constellation() 119 | sat_pos = self.const.compute_positions_at_epoch_offset() 120 | # Create the network 121 | self.cnet = ConstellationNetwork( 122 | sat_pos, num_sat_per_orbit, num_orbits, max_shift=max_shift 123 | ) 124 | self.cnet.generate_network(motif) 125 | self.f_param = f_param 126 | self.motif = motif 127 | self.num_sat_per_orbit = num_sat_per_orbit 128 | self.num_orbits = num_orbits 129 | self.max_shift = max_shift 130 | 131 | def compute_network_at_epoch_offset( 132 | self, hours=0, minutes=0, seconds=0, millisecs=0 133 | ): 134 | # TODO: Improve this integration 135 | sat_pos = self.const.compute_positions_at_epoch_offset( 136 | hours, minutes, seconds, millisecs 137 | ) 138 | self.cnet = ConstellationNetwork( 139 | sat_pos, self.num_sat_per_orbit, self.num_orbits, max_shift=self.max_shift 140 | ) 141 | self.cnet.generate_network(self.motif) 142 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/coordinate_util.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | 4 | """ 5 | This file contains the type definitions and conversions between coordinate schemes. 6 | All length values in m 7 | """ 8 | import numpy as np 9 | from typing import Tuple 10 | from typing_extensions import TypedDict 11 | 12 | from icarus_simulator.sat_core.planetary_const import EARTH_RADIUS 13 | 14 | 15 | class GeodeticPosition(TypedDict): 16 | lat: float 17 | lon: float 18 | elev: float # Elevation wrt Earth surface, NOT Earth center! 19 | 20 | 21 | class CartesianPosition(TypedDict): 22 | x: float 23 | y: float 24 | z: float 25 | 26 | 27 | CartCoords = Tuple[float, float, float] 28 | 29 | 30 | def geo2cart(geo_coord: GeodeticPosition) -> CartCoords: 31 | """ 32 | Converts a {lat, long, elevation} point to cartesian (x, y, z). 33 | Args: 34 | geo_coord: SatPosition. Coordinates of the point in geodesic format. 35 | 36 | Returns: 37 | Tuple[float, float, float]: Tuple of cartesian coordinates. 38 | """ 39 | theta = np.deg2rad(geo_coord["lon"]) 40 | phi = np.deg2rad(90 - geo_coord["lat"]) 41 | r = geo_coord["elev"] + EARTH_RADIUS 42 | x = r * np.sin(phi) * np.cos(theta) 43 | y = r * np.sin(phi) * np.sin(theta) 44 | z = r * np.cos(phi) 45 | cart = (x, y, z) 46 | rad = np.sqrt(np.sum(np.square(cart))) 47 | 48 | assert rad >= EARTH_RADIUS - 1000 # Allow for approximation error 49 | return cart 50 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/coverage.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | 4 | from typing import Dict, Tuple 5 | import pandas as pd 6 | import networkx as nx 7 | import numpy as np 8 | from sklearn.neighbors import KDTree 9 | 10 | from .coordinate_util import GeodeticPosition, geo2cart 11 | from .isl_util import max_ground_sat_dist, compute_link_length 12 | 13 | WUP_CITIES = "data/WUP2018-F22-Cities_Over_300K_Annual.csv" 14 | 15 | 16 | def in_reach( 17 | pos1: GeodeticPosition, pos2: GeodeticPosition, max_dist: float 18 | ) -> Tuple[bool, float]: 19 | """ 20 | Check if two objects are within reach. 21 | Args: 22 | pos1: The first position 23 | pos2: The second position 24 | max_dist: The maximum distance considered for being in reach 25 | Returns: 26 | bool: positions are in reach wrt each other, float: distance 27 | """ 28 | dist = compute_link_length(pos1, pos2) 29 | is_in_reach = dist < max_dist 30 | return is_in_reach, dist 31 | 32 | 33 | def load_big_cities(filename: str = WUP_CITIES) -> Dict[int, GeodeticPosition]: 34 | """Load the biggest cities from the WUP dataset.""" 35 | df = pd.read_csv(filename) 36 | df = df.rename( 37 | columns={ 38 | "Latitude": "lat", 39 | "Longitude": "lon", 40 | "Urban Agglomeration": "name", 41 | "2020": "population", 42 | } 43 | ) 44 | df = df[["lat", "lon", "name", "population"]] 45 | dfdict = df.to_dict("records") 46 | cities = {} 47 | idx = 0 48 | for value in dfdict: 49 | if value["lat"] < 57: 50 | value["elev"] = 0 51 | cities[idx] = value 52 | idx += 1 53 | return cities 54 | 55 | 56 | def add_cities_to_graph(G: nx.Graph, coverage: Dict[int, Dict[int, float]]): 57 | """ 58 | Adds the GSLs to a satellite network graph. 59 | City indices are prepended with `c `. E.g., city 24 is node 'c 24' in the graph. 60 | """ 61 | for city in coverage: 62 | for dst_sat in coverage[city]: 63 | G.add_edge(f"c {city}", dst_sat, length=coverage[city][dst_sat]) 64 | return G 65 | 66 | 67 | def positions_satellite_coverage( 68 | grid_pos: Dict[int, GeodeticPosition], 69 | sat_pos: Dict[int, GeodeticPosition], 70 | min_elev_angle: int, 71 | ) -> Dict[int, Dict[int, float]]: 72 | """ 73 | Check the satellite coverage for positions on Earth. 74 | Coverage is defined as all the satellites that are reachable with a 75 | line-of-sight path of length smaller than `max_dist`. 76 | Args: 77 | pos: Dict of indexed Positions. Positions of the points in the ground grid 78 | sat_pos: Dict of indexed Satpositions. Positions of the satellites 79 | elevation: Elevation of the satellites 80 | min_elev_angle: Minimum elevation angle of the satellites 81 | 82 | Returns: All the distances {ground_idx:{sat_idx: dist}} 83 | """ 84 | all_dist = {idx: {} for idx in grid_pos} 85 | # The dictionary format we use conflicts with this algorithm's format -> index map 86 | # Put grid points into a KD-tree 87 | index, id_map, grid_cart = 0, {}, np.zeros((len(grid_pos), 3)) 88 | for grid_id in grid_pos: 89 | id_map[index] = grid_id # map indices to sat indices 90 | grid_cart[index] = geo2cart(grid_pos[grid_id]) 91 | index += 1 92 | kd = KDTree(grid_cart) 93 | 94 | # Query all the satellites using the max_dist 95 | for sat_idx, sat in sat_pos.items(): 96 | max_dist = max_ground_sat_dist(sat["elev"], min_elev_angle) 97 | covered_grid_ids, distances = kd.query_radius( 98 | [geo2cart(sat)], r=max_dist, count_only=False, return_distance=True 99 | ) 100 | covered_grid_ids = covered_grid_ids[0] 101 | distances = distances[0] 102 | # Convert all the indices back 103 | for i in range(len(distances)): 104 | grid_idx = id_map[covered_grid_ids[i]] 105 | all_dist[grid_idx][sat_idx] = distances[i] 106 | return all_dist 107 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/isl_util.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | 4 | from typing import Tuple 5 | 6 | import numpy as np 7 | from scipy.spatial.distance import euclidean 8 | 9 | from .coordinate_util import GeodeticPosition, geo2cart 10 | from .planetary_const import * 11 | 12 | 13 | def compute_link_length(sat1: GeodeticPosition, sat2: GeodeticPosition) -> float: 14 | """ 15 | Compute the length of an Inter-Satellite Link 16 | Args: 17 | sat1: Position of the first satellite 18 | sat2:Position of the second satellite 19 | Returns: 20 | Euclidean distance between the points 21 | """ 22 | cart1 = geo2cart(sat1) 23 | cart2 = geo2cart(sat2) 24 | return euclidean(cart1, cart2) 25 | 26 | 27 | def get_sat_by_offset( 28 | sat_idx_in_orbit: int, 29 | orbit_idx: int, 30 | sat_idx_offset: int, 31 | orbit_offset: int, 32 | num_sat_per_orbit: int, 33 | num_orbits: int, 34 | max_shift: float = 0, 35 | ) -> Tuple[int, int, int]: 36 | """Compute the indexes of the neighbor satellite given by the offset. 37 | 38 | Args: 39 | sat_idx_in_orbit: Index of the satellite inside its orbit. 40 | orbit_idx: Index of the orbit of the satelliite. 41 | sat_idx_offset: In-orbit offset of the index of the neighboring 42 | satellite from `sat_idx_in_orbit`. 43 | orbit_offset: Orbit index offset of the neighboring satellite from the 44 | orbit of the current satellite `orbit_idx`. 45 | num_sat_per_orbit: Total number of satellites in each orbit of the 46 | constellation. 47 | num_orbits: Number of orbits in the constellation. 48 | max_shift: Maximum shift introduced by OrbitShiftAlgo or other means. 49 | This is needed to fix the problems with the shift at the seam. 50 | 51 | Returns: 52 | Tuple[int, int, int]: The indices of the neighboring satellite obtained 53 | that is offset from the current. The indices are 54 | `sat_idx, sat_idx_in_orbit, orbit_idx` 55 | """ 56 | assert not (sat_idx_offset == 0 and orbit_offset == 0) 57 | walker_shift_in_orbit = 0 58 | if orbit_idx == (num_orbits - 1) and orbit_offset > 0: 59 | # This satellite is west of the seam 60 | inter_sat = 360 / num_sat_per_orbit 61 | walker_shift_in_orbit = np.ceil(max_shift / inter_sat) 62 | # Get the index of the satellite in the orbit, eventually making up for the walker shift 63 | neigh_idx_in_orbit = ( 64 | sat_idx_in_orbit + sat_idx_offset + walker_shift_in_orbit 65 | ) % num_sat_per_orbit 66 | # Get the orbit index 67 | neigh_orbit_idx = (orbit_idx + orbit_offset) % num_orbits 68 | neigh_idx = in_orbit_idx_to_sat_idx( 69 | neigh_idx_in_orbit, neigh_orbit_idx, num_sat_per_orbit 70 | ) 71 | neigh_idx = int(neigh_idx) 72 | return neigh_idx, neigh_idx_in_orbit, neigh_orbit_idx 73 | 74 | 75 | def sat_idx_to_in_orbit_idx(sat_idx: int, num_sat_per_orbit: int) -> Tuple[int, int]: 76 | """ 77 | Compute the satellite index in orbit and orbit index. 78 | Starting from the satellite index in the constellation. 79 | Args: 80 | sat_idx: Index of the satellite in the constellation. 81 | num_sat_per_orbit: Total number of satellites in each orbit of the 82 | constellation. 83 | Returns: 84 | (int, int): Index of the satellite inside its orbit, index of the 85 | satellite's orbit. 86 | """ 87 | if num_sat_per_orbit < 1: 88 | raise ValueError 89 | sat_idx_in_orbit = sat_idx % num_sat_per_orbit 90 | orbit_idx = sat_idx // num_sat_per_orbit 91 | return sat_idx_in_orbit, orbit_idx 92 | 93 | 94 | def in_orbit_idx_to_sat_idx( 95 | sat_idx_in_orbit: int, orbit_idx: int, num_sat_per_orbit: int 96 | ) -> int: 97 | """Compute the satellite index in the constellation. 98 | Starting from from the satellite index in the orbit and orbit index. 99 | Args: 100 | sat_idx_in_orbit: Index of the satellite inside its orbit. 101 | orbit_idx: Index of the satellite's orbit. 102 | num_sat_per_orbit: Total number of satellites in each orbit of the 103 | constellation. 104 | Returns: 105 | int: Index of the satellite in the constellation. 106 | """ 107 | if sat_idx_in_orbit >= num_sat_per_orbit: 108 | raise ValueError( 109 | "Satellite index in orbit cannot be greater than " 110 | "the number of satellites per orbit" 111 | ) 112 | base_idx = orbit_idx * num_sat_per_orbit 113 | sat_idx = base_idx + sat_idx_in_orbit 114 | return sat_idx 115 | 116 | 117 | def max_ground_sat_dist(h: float, min_angle: float) -> float: 118 | """ 119 | Compute the maximum sat-ground distance given a minimum angle. 120 | Uses the law of sines. 121 | In the computation: 122 | alpha: angle at the GST, pointing SAT and CENTER. 123 | beta: angle at the SAT, pointing GST and CENTER. 124 | gamma: angle at GENTER, pointing at GST and SAT. 125 | (sides are relative). 126 | Args: 127 | h: Elevation of the satellite in meters. 128 | min_angle: Minimum elevation angle at the GST, from the horizon and 129 | pointing to the satellite. 130 | Returns: float: the maximum distance GST-SAT. 131 | """ 132 | alpha = np.deg2rad(min_angle + 90) 133 | a = h + EARTH_RADIUS 134 | b = EARTH_RADIUS 135 | sin_beta = np.sin(alpha) / a * b 136 | beta = np.arcsin(sin_beta) 137 | gamma = np.pi - alpha - beta 138 | c = a * np.sin(gamma) / np.sin(alpha) 139 | # arc = EARTH_RADIUS * gamma 140 | return c 141 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/orbit_shift_algo.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from abc import abstractmethod 4 | 5 | 6 | class OrbitShiftAlgo: 7 | """Base class to create orbit shift algorithms.""" 8 | 9 | def __init__(self): 10 | pass 11 | 12 | @abstractmethod 13 | def get_shift(self, orbit_idx): 14 | """Method to compute the orbit shift based on orbit index. 15 | 16 | Args: 17 | orbit_idx: the index of the orbit for which to compute the shift. 18 | 19 | Returns: 20 | float: The decimal representation of the degrees to shift. 21 | """ 22 | raise NotImplementedError 23 | 24 | 25 | class SimpleShift(OrbitShiftAlgo): 26 | """Shift each odd orbit by half the inter-satellite angular distance. 27 | 28 | This shift is a simplified version of a Walker shift. 29 | Credits: satnetwork.github.io 30 | """ 31 | 32 | def __init__(self, num_sat_per_orbit): 33 | super().__init__() 34 | self.num_sat_per_orbit = num_sat_per_orbit 35 | 36 | def get_shift(self, orbit_idx: int) -> float: 37 | """Method to compute the orbit shift based on orbit index. 38 | 39 | Args: 40 | orbit_idx: the index of the orbit for which to compute the shift. 41 | 42 | Returns: 43 | float: The decimal representation of the degrees to shift. 44 | """ 45 | if orbit_idx % 2 == 1: 46 | return 360 / (self.num_sat_per_orbit * 2) 47 | return 0 48 | 49 | 50 | class NoShift(OrbitShiftAlgo): 51 | """Do not shift.""" 52 | 53 | def __init__(self): 54 | super().__init__() 55 | 56 | def get_shift(self, orbit_idx: int) -> float: 57 | """Method to compute the orbit shift based on orbit index. 58 | 59 | Args: 60 | orbit_idx: the index of the orbit for which to compute the shift. 61 | 62 | Returns: 63 | float: The decimal representation of the degrees to shift. 64 | """ 65 | return 0 66 | 67 | 68 | class WalkerShift(OrbitShiftAlgo): 69 | """Compute the shift according to the Walker constellation design. 70 | 71 | See: 72 | https://en.wikipedia.org/wiki/Satellite_constellation 73 | """ 74 | 75 | def __init__(self, inclination, num_sat_per_orbit, num_orbits, f_param): 76 | super().__init__() 77 | assert 0 <= f_param < num_orbits 78 | self.inclination = inclination 79 | self.num_sat_per_orbit = num_sat_per_orbit 80 | self.num_orbits = num_orbits 81 | self.f_param = f_param 82 | # Compute the walker shift between adjacent planes 83 | t = num_sat_per_orbit * num_orbits 84 | self.plane_shift = f_param * 360 / t 85 | 86 | def get_shift(self, orbit_idx): 87 | # return (orbit_idx * self.plane_shift) % (360 / self.num_sat_per_orbit) 88 | return orbit_idx * self.plane_shift 89 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/orbit_util.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from datetime import datetime, timedelta 4 | 5 | import ephem 6 | import numpy as np 7 | 8 | from .planetary_const import * 9 | 10 | 11 | def right_ascension_ascending_node(orbit_idx: int, num_orbits: int) -> ephem.Angle: 12 | """Compute the right ascension of the ascending node (raan). 13 | 14 | Args: 15 | orbit_idx: Index of the orbit for which the raan is computed. 16 | num_orbits: Number of orbits in the constellation. 17 | 18 | Returns: 19 | ephem.Angle: The raan in degrees. 20 | """ 21 | if num_orbits < 1: 22 | raise ValueError 23 | raan = float(orbit_idx * 360 / num_orbits) 24 | raan = ephem.degrees(raan) 25 | return raan 26 | 27 | 28 | def mean_anomaly( 29 | sat_idx_in_orbit: int, num_sat_per_orbit: int, orbit_shift: float = 0.0 30 | ) -> ephem.Angle: 31 | """Compute the mean anomaly for the current satellite. 32 | 33 | Args: 34 | sat_idx_in_orbit: Index of the satellite inside its orbit. 35 | num_sat_per_orbit: Total number of satellites in each orbit of the 36 | constellation. 37 | orbit_shift: Shift of the orbit from the equatorial plane. This 38 | can be used to create more complex walker-type constellations. 39 | 40 | Returns: 41 | ephem.Angle: The mean anomaly for the current satellite. 42 | """ 43 | if num_sat_per_orbit < 1: 44 | raise ValueError 45 | ma = orbit_shift + sat_idx_in_orbit * 360 / num_sat_per_orbit 46 | ma = ephem.degrees(ma) 47 | return ma 48 | 49 | 50 | def elevation_to_period(elevation: float) -> float: 51 | """Compute the period of an orbit in seconds. 52 | 53 | Returns: 54 | float: The orbital period of a satellite at the given elevation in 55 | seconds. 56 | """ 57 | assert elevation > 0 58 | elevation = float(elevation) 59 | radius = elevation + EARTH_RADIUS 60 | period = 2 * np.pi * np.sqrt(np.power(radius, 3) / MU) 61 | return period 62 | 63 | 64 | def elevation_to_mean_motion(elevation: float, unit: str = "revs") -> float: 65 | """Compute the mean motion of the satellite given its elevation. 66 | 67 | Args: 68 | elevation: The elevation of the satellite, in meters from sea level. 69 | unit: The unit in which to compute the mean motion. Can be either 70 | "radians", "degrees", or "revs" for revolutions. 71 | Returns: 72 | float: The mean motion of a satellite orbiting at the given elevation. 73 | The measure is `unit / day`. 74 | """ 75 | # Get the period in days 76 | period = elevation_to_period(elevation) / SEC_IN_DAY 77 | if unit == "radians": 78 | return 2 * np.pi / period 79 | elif unit == "degrees": 80 | return 360 / period 81 | elif unit == "revs": 82 | return 1 / period 83 | else: 84 | raise ValueError("Specify a valid unit for mean motion") 85 | 86 | 87 | def epoch_offset_to_date( 88 | epoch: str, hours: int = 0, minutes: int = 0, seconds: int = 0, millisecs: int = 0 89 | ) -> ephem.Date: 90 | """Compute the date obtained by adding an offset to epoch.""" 91 | epoch_datetime = datetime.strptime(epoch, "%Y/%m/%d %H:%M:%S") 92 | delta = timedelta( 93 | hours=hours, minutes=minutes, seconds=seconds, milliseconds=millisecs 94 | ) 95 | target = epoch_datetime + delta 96 | targetstr = target.strftime("%Y/%m/%d %H:%M:%S.%f") 97 | return targetstr 98 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/planetary_const.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | from scipy.constants import G 4 | 5 | 6 | # Average great-circle radius in meters. 7 | EARTH_RADIUS = 6371 * 1000 8 | # Average duration of a day in seconds. 9 | SEC_IN_DAY = 86400 10 | # Earth mass 11 | EARTH_MASS = 5.9722e24 12 | # Atmosphere 13 | ATMOSPHERE_HEIGHT = 100 14 | # Standard Gravitational Parameter for earth 15 | MU = G * EARTH_MASS 16 | # Speed of light in m/s 17 | LIGHTSPEED = 299792458 18 | # Earth surface in km^2 19 | EARTH_SURFACE = 510100000 20 | -------------------------------------------------------------------------------- /icarus_simulator/sat_core/satellite.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | 4 | import ephem 5 | import math 6 | 7 | from icarus_simulator.sat_core.coordinate_util import GeodeticPosition 8 | 9 | from .isl_util import in_orbit_idx_to_sat_idx 10 | from .orbit_util import ( 11 | right_ascension_ascending_node as raan, 12 | mean_anomaly, 13 | epoch_offset_to_date, 14 | ) 15 | 16 | 17 | class Satellite: 18 | """Wrapper for `pyephem`'s `EarthSatellite`.""" 19 | 20 | def __init__( 21 | self, 22 | sat_idx_in_orbit: int, 23 | orbit_idx: int, 24 | num_sat_per_orbit: int, 25 | num_orbits: int, 26 | inclination: float, 27 | epoch: str, 28 | mean_motion: float, 29 | orbit_shift: float = 0.0, 30 | eccentricity: float = 1e-32, 31 | aug_perigee: float = 0.0, 32 | ) -> None: 33 | """ 34 | Args: 35 | sat_idx_in_orbit: Index of the satellite inside its orbit. 36 | orbit_idx: Index of the orbit of the satelliite. 37 | num_sat_per_orbit: Total number of satellites in each orbit of the 38 | constellation. 39 | num_orbits: Number of orbits in the constellation. 40 | inclination: Inclination of the orbits. 41 | epoch: Starting epoch. Has to be a string formatted as 42 | with `%Y/%m/%d %H:%M:%S`. 43 | mean_motion: The mean motion of the satellite. This can be 44 | computed as a function of the satellite's elevation. 45 | orbit_shift: Shift of the orbit from the equatorial plane. This 46 | can be used to create more complex walker-type constellations. 47 | Defaults to 0. 48 | eccentricity: Eccentricity of the orbit. 49 | Defaults to 0.001 (circular orbit). 50 | aug_perigee: Augmentation of the perigree. Defualts to 0.0 51 | (circular orbit). 52 | """ 53 | 54 | self.sat_idx_in_orbit = sat_idx_in_orbit 55 | self.orbit_idx = orbit_idx 56 | self.num_sat_per_orbit = num_sat_per_orbit 57 | self.num_orbits = num_orbits 58 | self.inclination = ephem.degrees(inclination) 59 | self.epoch = epoch 60 | self.mean_motion = mean_motion 61 | self.eccentricity = eccentricity 62 | self.aug_perigee = aug_perigee 63 | # Compute the derived parameters 64 | # TODO: maybe move the computation of these parameters outside the 65 | # Satellite class to make it more general. 66 | self.sat_idx = in_orbit_idx_to_sat_idx( 67 | self.sat_idx_in_orbit, self.orbit_idx, self.num_sat_per_orbit 68 | ) 69 | self.raan = raan(self.orbit_idx, num_orbits) 70 | self.mean_anomaly = mean_anomaly( 71 | self.sat_idx_in_orbit, num_sat_per_orbit, orbit_shift 72 | ) 73 | # Create the satellite object 74 | self.sat = ephem.EarthSatellite() 75 | self.sat._epoch = self.epoch 76 | self.sat._e = self.eccentricity 77 | self.sat._raan = self.raan 78 | self.sat._M = self.mean_anomaly 79 | self.sat._inc = self.inclination 80 | self.sat._ap = self.aug_perigee 81 | self.sat._n = mean_motion 82 | 83 | def position_at_time(self, timestr: str) -> GeodeticPosition: 84 | """ 85 | Compute the position of the satellite at a certain time. 86 | Args: 87 | timestr: Time for which to compute the position 88 | Returns: 89 | SatPosition: The {lat, long, elevation} of the satellite at the 90 | required time. 91 | """ 92 | self.sat.compute(timestr) 93 | return self.get_lat_long_elev() 94 | 95 | def position_at_epoch_offset( 96 | self, hours: int = 0, minutes: int = 0, seconds: int = 0, millisecs: int = 0 97 | ) -> GeodeticPosition: 98 | """ 99 | Compute the position of the satellite given an offset from epoch. 100 | Args: 101 | hours: Offset hours. 102 | minutes: Offset minutes. 103 | seconds: Offset seconds. 104 | millisecs: Offset milliseconds 105 | Returns: 106 | SatPosition: The {lat, long, elevation} of the satellite at the 107 | required time. 108 | """ 109 | target_date_str = epoch_offset_to_date( 110 | self.epoch, hours, minutes, seconds, millisecs 111 | ) 112 | return self.position_at_time(target_date_str) 113 | 114 | def get_lat_long_elev(self) -> GeodeticPosition: 115 | """ 116 | Get the lat, long and elev for the satellite. 117 | Returns: 118 | Position of the satellite 119 | The values are for the last position computed with `position_at_time` or 120 | `position_at_epoch_offset`. 121 | """ 122 | lat = math.degrees(self.sat.sublat) 123 | long = math.degrees(self.sat.sublong) 124 | elev = self.sat.elevation 125 | return {"lat": lat, "lon": long, "elev": elev} 126 | 127 | def sat_position_tostring(self) -> str: 128 | """Get a string with index and position information.""" 129 | lat, long, elev = self.get_lat_long_elev() 130 | return ( 131 | f"{self.sat_idx} {self.orbit_idx} {self.sat_idx_in_orbit} " 132 | f"{lat} {long} {elev}" 133 | ) 134 | 135 | 136 | if __name__ == "__main__": 137 | raise RuntimeError() 138 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | from .atk_detect_optimisation import * 2 | from .atk_feasibility_check import * 3 | from .atk_geo_constraint import * 4 | from .atk_path_filtering import * 5 | from .bw_assignment import * 6 | from .bw_selection import * 7 | from .coverage import * 8 | from .edge import * 9 | from .grid import * 10 | from .grid_weight import * 11 | from .lsn import * 12 | from .routing import * 13 | from .zone_bneck import * 14 | from .zone_build import * 15 | from .zone_edges import * 16 | from .zone_select import * 17 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_detect_optimisation/__init__.py: -------------------------------------------------------------------------------- 1 | from .bin_search_optim_strat import BinSearchOptimStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_detect_optimisation/base_optim_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List, Optional, Tuple 9 | 10 | from icarus_simulator.structure_definitions import ( 11 | PathData, 12 | Edge, 13 | DirectionData, 14 | BwData, 15 | AtkFlowSet, 16 | ) 17 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 18 | BaseFeasStrat, 19 | ) 20 | from icarus_simulator.strategies.base_strat import BaseStrat 21 | 22 | 23 | class BaseOptimStrat(BaseStrat): 24 | @abstractmethod 25 | def compute( 26 | self, 27 | congest_edges: List[Edge], 28 | path_data: PathData, 29 | bw_data: BwData, 30 | direction_data: DirectionData, 31 | uplink_max_val: int, 32 | feas_strat: BaseFeasStrat, 33 | ) -> Tuple[Optional[AtkFlowSet], int, int]: 34 | raise NotImplementedError 35 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_detect_optimisation/bin_search_optim_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from math import ceil 3 | from typing import List, Optional, Tuple 4 | 5 | from icarus_simulator.strategies.atk_detect_optimisation.base_optim_strat import ( 6 | BaseOptimStrat, 7 | ) 8 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 9 | BaseFeasStrat, 10 | ) 11 | from icarus_simulator.structure_definitions import ( 12 | Edge, 13 | PathData, 14 | DirectionData, 15 | BwData, 16 | AtkFlowSet, 17 | ) 18 | 19 | 20 | class BinSearchOptimStrat(BaseOptimStrat): 21 | def __init__(self, rate: float, **kwargs): 22 | super().__init__() 23 | if len(kwargs) > 0: 24 | pass # Appease the unused param inspection 25 | assert 0.0 <= rate <= 1.0 26 | self.rate = rate 27 | 28 | @property 29 | def name(self) -> str: 30 | return "bin" 31 | 32 | @property 33 | def param_description(self) -> str: 34 | return f"{self.rate}" 35 | 36 | def compute( 37 | self, 38 | congest_edges: List[Edge], 39 | path_data: PathData, 40 | bw_data: BwData, 41 | direction_data: DirectionData, 42 | uplink_max_val: int, 43 | feas_strat: BaseFeasStrat, 44 | ) -> Tuple[Optional[AtkFlowSet], int, int]: 45 | 46 | # If the rate is 0, no optimisation is required 47 | if self.rate == 0.0: 48 | return feas_strat.compute( 49 | congest_edges, path_data, bw_data, direction_data, uplink_max_val 50 | ) 51 | 52 | # Start a binary search algorithm to find the lowest increase constraint st the problem is feasible 53 | # Idea: left always infeasible, right always feasible, right is INCLUSIVE 54 | left, right = 0, uplink_max_val 55 | final_val = uplink_max_val 56 | while left != right - 1: 57 | half = left + int(ceil((right - left) / 2)) 58 | temp_atk_flow_set, _, _ = feas_strat.compute( 59 | congest_edges, path_data, bw_data, direction_data, half 60 | ) 61 | if temp_atk_flow_set is not None: # If optimal 62 | final_val = half 63 | right = half 64 | else: # If infeasible 65 | left = half 66 | 67 | # Based on the optimisation rate chosen, re-run for the correct value 68 | val_range = uplink_max_val - final_val 69 | val_incr = int( 70 | self.rate * val_range 71 | ) # Taking floor here ensures that ceil is taken in next line 72 | req_detect = uplink_max_val - val_incr 73 | return feas_strat.compute( 74 | congest_edges, path_data, bw_data, direction_data, req_detect 75 | ) 76 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_feasibility_check/__init__.py: -------------------------------------------------------------------------------- 1 | from .prob_feas_strat import ProbFeasStrat 2 | from .lp_feas_strat import LPFeasStrat 3 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_feasibility_check/base_feas_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List, Optional, Tuple 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import ( 12 | PathData, 13 | Edge, 14 | DirectionData, 15 | BwData, 16 | AtkFlowSet, 17 | ) 18 | 19 | 20 | class BaseFeasStrat(BaseStrat): 21 | @abstractmethod 22 | def compute( 23 | self, 24 | congest_edges: List[Edge], 25 | path_data: PathData, 26 | bw_data: BwData, 27 | direction_data: DirectionData, 28 | max_uplink_increase: int, 29 | ) -> Tuple[Optional[AtkFlowSet], int, int]: 30 | raise NotImplementedError 31 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_feasibility_check/lp_feas_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import numpy as np 3 | from math import ceil 4 | from typing import List, Optional, Tuple 5 | 6 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 7 | BaseFeasStrat, 8 | ) 9 | from icarus_simulator.structure_definitions import ( 10 | Edge, 11 | PathData, 12 | DirectionData, 13 | BwData, 14 | AtkFlowSet, 15 | PathEdgeData, 16 | ) 17 | from icarus_simulator.utils import get_edges 18 | 19 | 20 | class LPFeasStrat(BaseFeasStrat): 21 | def __init__(self, **kwargs): 22 | super().__init__() 23 | if len(kwargs) > 0: 24 | pass # Appease the unused param inspection 25 | 26 | @property 27 | def name(self) -> str: 28 | return "lp" 29 | 30 | @property 31 | def param_description(self) -> None: 32 | return None 33 | 34 | # Important note: this only works for single-target attacks! 35 | def compute( 36 | self, 37 | congest_edges: List[Edge], 38 | path_data: PathData, 39 | bw_data: BwData, 40 | direction_data: DirectionData, 41 | max_uplink_increase: int, 42 | ) -> Tuple[Optional[AtkFlowSet], int, int]: 43 | 44 | directions = list(direction_data.keys()) 45 | 46 | if len(directions) == 0: 47 | return None, -1, -1 48 | 49 | # Go through the edges and find out their coverage and their max bw 50 | # IMPORTANT: take the sum of variables as total bw, and as objective, to avoid having pass-through directions 51 | # counted two times 52 | direction_edges: PathEdgeData = {} 53 | tot_needed = 0 54 | for idx, p in enumerate(directions): 55 | for ed in get_edges(p): 56 | if ed not in direction_edges and ed not in congest_edges: 57 | direction_edges[ed] = set() 58 | elif ed not in direction_edges and ed in congest_edges: 59 | tot_needed += bw_data[ed].get_remaining_bw() 60 | direction_edges[ed] = set() 61 | direction_edges[ed].add(idx) 62 | 63 | # Formulate the linear program 64 | # The variables are the bw in flows assigned to each direction, the constraints are the bw limitations of edges 65 | # In each row of the constraint matrix, the index to the corresponding path will be set to 1 66 | 67 | # Matrix for leq inequalities 68 | # Num of columns is number of directions, so len(directions) 69 | # Num of rows is complicated: 70 | # - each path gets a constraint for bw >= 0 len(directions) 71 | # - each edge gets a constraint for bw <= cap len(direction_edges) 72 | # - each congest edge gets an additional constr to make equality len(congest_edges) 73 | # We need to track where each uplink edge is in the matrix in order to update the constraints 74 | num_rows = len(direction_edges) + len(congest_edges) 75 | numpy_g = np.zeros((num_rows, len(directions))) 76 | numpy_h = np.zeros(num_rows) 77 | numpy_c = np.ones(len(directions)) 78 | 79 | curr_row = 0 80 | # All edges need the less-than constraint 81 | for e in direction_edges: 82 | if e[0] == -1: 83 | numpy_h[curr_row] = min( 84 | bw_data[e].get_remaining_bw(), max_uplink_increase 85 | ) 86 | else: 87 | numpy_h[curr_row] = bw_data[e].get_remaining_bw() 88 | for j in direction_edges[e]: 89 | numpy_g[curr_row, j] = 1.0 90 | curr_row += 1 91 | 92 | # Congest edges also need the greater-than constraint -> invert sign! 93 | for e in congest_edges: 94 | if e[0] == -1: 95 | numpy_h[curr_row] = -min( 96 | bw_data[e].get_remaining_bw(), max_uplink_increase 97 | ) 98 | else: 99 | numpy_h[curr_row] = -bw_data[e].get_remaining_bw() 100 | for j in direction_edges[e]: 101 | numpy_g[curr_row, j] = -1.0 102 | curr_row += 1 103 | 104 | # Prepare Gurobi problem -> we import gurobi here as not everybody may habve it installed! 105 | import gurobipy as gp 106 | from gurobipy import GRB 107 | 108 | env = gp.Env(empty=True) 109 | env.setParam("OutputFlag", 0) 110 | env.start() 111 | m = gp.Model("attack", env=env) 112 | x = m.addMVar(shape=len(directions), lb=0.0, name="x") 113 | m.setObjective(numpy_c @ x, GRB.MINIMIZE) # @ is matrix product! 114 | # noinspection PyArgumentList 115 | m.addConstr(numpy_g @ x <= numpy_h, name="c") 116 | m.optimize() 117 | 118 | if m.status != GRB.OPTIMAL: # LP not feasible, attack not possible! 119 | return None, -1, -1 120 | 121 | # Gather info about the amount of flow each direction sends 122 | lp_vars, directions_bw, edges_bw = m.getVars(), {}, {} 123 | for i in range(len(directions)): 124 | # Get the value of the variable 125 | val = int(lp_vars[i].x) 126 | if val > 0: 127 | directions_bw[directions[i]] = val 128 | for ed in get_edges(directions[i]): 129 | if ed not in edges_bw: 130 | edges_bw[ed] = 0 131 | edges_bw[ed] += val 132 | 133 | # Find a conformant atkflowset, distribute each direction equally among the originating pairs 134 | atk_flow_set = set() 135 | for dire, bw in directions_bw.items(): 136 | tot_pairs = len(direction_data[dire]) 137 | flows_per_pair = max( 138 | 5, int(ceil(bw / tot_pairs)) 139 | ) # Enforce a min of 5 per pair for attack efficiency 140 | for pair in direction_data[dire]: 141 | if bw == 0: 142 | break 143 | flows = min(flows_per_pair, bw) 144 | atk_flow_set.add((pair, flows)) 145 | bw -= flows 146 | 147 | return ( 148 | atk_flow_set, 149 | tot_needed, 150 | max(edges_bw[ed] for ed in edges_bw if ed[0] == -1), 151 | ) 152 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_feasibility_check/prob_feas_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from math import sqrt, log, ceil 3 | from typing import List, Optional, Tuple 4 | 5 | from icarus_simulator.strategies.atk_feasibility_check.base_feas_strat import ( 6 | BaseFeasStrat, 7 | ) 8 | from icarus_simulator.structure_definitions import ( 9 | Edge, 10 | PathData, 11 | DirectionData, 12 | BwData, 13 | PairData, 14 | AtkFlowSet, 15 | PairInfo, 16 | ) 17 | from icarus_simulator.utils import get_ordered_idx, get_edges 18 | 19 | 20 | class ProbFeasStrat(BaseFeasStrat): 21 | def __init__(self, beta: float, **kwargs): 22 | super().__init__() 23 | self.beta = beta 24 | if len(kwargs) > 0: 25 | pass # Appease the unused param inspection 26 | 27 | @property 28 | def name(self) -> str: 29 | return "prb" 30 | 31 | @property 32 | def param_description(self) -> str: 33 | return f"{self.beta}" 34 | 35 | # Important note: this only works for single-target attacks! 36 | def compute( 37 | self, 38 | congest_edges: List[Edge], 39 | path_data: PathData, 40 | bw_data: BwData, 41 | direction_data: DirectionData, 42 | max_uplink_increase: int, 43 | ) -> Tuple[Optional[AtkFlowSet], int, int]: 44 | isl_num = int(len([ed for ed in bw_data if -1 not in ed]) / 2) 45 | gamma = 1 / (isl_num ** 2) 46 | assert ( 47 | len(congest_edges) == 1 48 | ) # Otherwise, this strategy does not work yet. Needs some brain work to adapt. 49 | target = congest_edges[0] 50 | needed_hosts = bw_data[target].get_remaining_bw() 51 | 52 | # Prepare a list of sorted probabilities -> sort is for the greedy algorithm 53 | prob_map: PairData = {} 54 | for dire, pairs in direction_data.items(): 55 | for p in pairs: 56 | if p not in prob_map: 57 | prob_map[p] = PairInfo() 58 | prob_map[p].prob += 1 59 | prob_map[p].directions.add(dire) 60 | for p in prob_map: 61 | ord_p = get_ordered_idx(p)[0] 62 | prob_map[p].prob /= len(path_data[ord_p]) 63 | prob_map[p].tot = len(path_data[ord_p]) 64 | 65 | pair_list = list(prob_map.keys()) 66 | pair_list.sort(key=lambda k: prob_map[k].prob, reverse=True) 67 | 68 | # Compute the probabilistic structure 69 | mean, min_mean = ( 70 | 0.0, 71 | sqrt(log(self.beta) * (log(self.beta) - 2 * needed_hosts)) 72 | + needed_hosts 73 | - log(self.beta), 74 | ) 75 | link_means, link_max_means, link_caps = {}, {}, {} 76 | atk_flow_set = set() 77 | 78 | # For each pair, check how many sdpairs we can add to the attack set 79 | for pair in pair_list: 80 | # Count how many directions in the pair traverse each edge 81 | prob, tot, dirs = ( 82 | prob_map[pair].prob, 83 | prob_map[pair].tot, 84 | prob_map[pair].directions, 85 | ) 86 | link_counts = {} 87 | for di in dirs: 88 | for link in get_edges( 89 | di, excl_end=1 90 | ): # -1, because the last edge is the flooded one 91 | if link not in link_means: 92 | if link[0] == -1: 93 | cap = min( 94 | bw_data[link].get_remaining_bw(), max_uplink_increase 95 | ) 96 | else: 97 | cap = bw_data[link].get_remaining_bw() 98 | link_caps[link], link_means[link] = cap, 0.0 99 | link_max_means[link] = ( 100 | -sqrt(log(gamma) * (log(gamma) - 8 * cap)) 101 | - log(gamma) 102 | + 2 * cap 103 | ) / 2 104 | if link not in link_counts: 105 | link_counts[link] = 0 106 | link_counts[link] += 1 107 | 108 | # Compute minimum number of hosts needed to flood 109 | if prob == 1.0: 110 | fits = int(ceil(needed_hosts - mean)) 111 | else: 112 | fits = int(ceil((min_mean - mean) / prob)) 113 | 114 | # Enforce no self-bnecks: Check how many can fit in the links without having too large link mean 115 | for link in link_counts: 116 | link_prob = link_counts[link] / tot 117 | if link_prob == 1.0: 118 | fits_link = int(link_caps[link] - link_means[link]) 119 | else: 120 | fits_link = int( 121 | (link_max_means[link] - link_means[link]) / link_prob 122 | ) 123 | fits = min(fits, fits_link) 124 | if fits <= 0: 125 | break 126 | # If no host can fit, continue to next pair 127 | if fits <= 0: 128 | continue 129 | 130 | # Update min/max means if prob is 1, otw update mean 131 | if prob == 1.0: 132 | needed_hosts -= fits 133 | if needed_hosts <= 0: 134 | min_mean = 0.0 135 | else: 136 | min_mean = ( 137 | sqrt(log(self.beta) * (log(self.beta) - 2 * needed_hosts)) 138 | + needed_hosts 139 | - log(self.beta) 140 | ) 141 | else: 142 | mean += prob * fits 143 | 144 | atk_flow_set.add((pair, fits)) 145 | if mean >= min_mean: # Do not update link means if you can exit 146 | break 147 | 148 | for link in link_counts: 149 | link_prob = link_counts[link] / tot 150 | # If the prob is 1, allocation is deterministic: all hosts will be allocated to the current link 151 | # In this case, update the cap and the max_mean, without updating the mean 152 | # Otherwise, update the mean 153 | if link_prob == 1.0: 154 | link_caps[link] = max(link_caps[link] - fits, 0) 155 | cap = link_caps[link] 156 | # We do not need any if/else here, because the max with 0 takes care of it 157 | link_max_means[link] = max( 158 | ( 159 | -sqrt(log(gamma) * (log(gamma) - 8 * cap)) 160 | - log(gamma) 161 | + 2 * cap 162 | ) 163 | / 2, 164 | 0, 165 | ) 166 | else: 167 | link_means[link] += (link_counts[link] / tot) * fits 168 | 169 | if mean < min_mean: # If not successful, return None 170 | return None, -1, -1 171 | 172 | # Determine the probabilistic number of flows on target and the maximum increase 173 | on_trg = ceil( 174 | mean + bw_data[target].get_remaining_bw() - needed_hosts 175 | ) # mean covers needed_hosts, not rest 176 | detect = -1 177 | for link in link_caps: 178 | if link[0] == -1: 179 | incr = ceil( 180 | link_means[link] 181 | + min( # This covers the final capacity, the next line accounts for optim 182 | bw_data[link].get_remaining_bw(), max_uplink_increase 183 | ) 184 | - link_caps[link] 185 | ) 186 | if incr > detect: 187 | detect = incr 188 | 189 | return atk_flow_set, on_trg, detect 190 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_geo_constraint/__init__.py: -------------------------------------------------------------------------------- 1 | from .no_constr_strat import NoConstrStrat 2 | from .grid_constr_strat import GridConstrStrat 3 | from .geo_constr_strat import GeoConstrStrat 4 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_geo_constraint/base_geo_constraint_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import Set 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import GridPos 12 | 13 | 14 | class BaseGeoConstraintStrat(BaseStrat): 15 | @abstractmethod 16 | def compute(self, grid_pos: GridPos) -> Set[int]: 17 | raise NotImplementedError 18 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_geo_constraint/geo_constr_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import os 3 | import json 4 | import numpy as np 5 | 6 | from typing import Set, List 7 | from geopy.distance import great_circle 8 | from scipy.spatial.ckdtree import cKDTree 9 | from shapely.geometry import Polygon, shape, Point 10 | 11 | from icarus_simulator.sat_core.coordinate_util import geo2cart 12 | from icarus_simulator.strategies.atk_geo_constraint.base_geo_constraint_strat import ( 13 | BaseGeoConstraintStrat, 14 | ) 15 | from icarus_simulator.structure_definitions import GridPos 16 | 17 | dirname = os.path.dirname(__file__) 18 | strategies_dirname = os.path.split(dirname)[0] 19 | library_dirname = os.path.split(strategies_dirname)[0] 20 | data_dirname = os.path.join(library_dirname, "data") 21 | COUNTRIES_FILE: str = os.path.join(data_dirname, "natural_earth_world_small.geo.json") 22 | 23 | 24 | class GeoConstrStrat(BaseGeoConstraintStrat): 25 | def __init__(self, geo_names: List[str], **kwargs): 26 | super().__init__() 27 | self.geo_names = geo_names 28 | if len(kwargs) > 0: 29 | pass # Appease the unused param inspection 30 | 31 | @property 32 | def name(self) -> str: 33 | return "geo" 34 | 35 | @property 36 | def param_description(self) -> str: 37 | return ",".join(self.geo_names) 38 | 39 | def compute(self, grid_pos: GridPos) -> Set[int]: 40 | allowed = set() 41 | geo_data = load_country_geojson() 42 | for s in self.geo_names: 43 | allowed.update(get_allowed_gridpoints(s, grid_pos, geo_data)) 44 | return allowed 45 | 46 | 47 | # noinspection PyTypeChecker 48 | def get_allowed_gridpoints(geo_location: str, grid_pos: GridPos, geo_data) -> Set[int]: 49 | # Get a list of all possible source points 50 | if geo_location in geo_data["countries"]: 51 | indices = [geo_data["countries"][geo_location]] 52 | elif geo_location in geo_data["subregions"]: 53 | indices = geo_data["subregions"][geo_location] 54 | elif geo_location in geo_data["continents"]: 55 | indices = geo_data["continents"][geo_location] 56 | else: 57 | raise ValueError("Invalid geographic constraint") 58 | 59 | geometries = [geo_data["geometries"][index] for index in indices] 60 | allowed_points = set() 61 | # Create a unique shape, union of all shapes in the region, and take the points include within 62 | shp = Polygon() 63 | for idx, geo in enumerate(geometries): 64 | shp = shp.union(shape(geo)) 65 | for idx, pos in grid_pos.items(): 66 | if Point(pos.lat, pos.lon).within(shp): 67 | allowed_points.add(idx) 68 | 69 | # Extract the border points 70 | x, y = [], [] 71 | if shp.geom_type == "MultiPolygon": 72 | for idx, shap in enumerate(shp.geoms): 73 | if True: 74 | x1, y1 = shap.exterior.xy 75 | x.extend(x1) 76 | y.extend(y1) 77 | else: 78 | x1, y1 = shp.exterior.xy 79 | x.extend(x1) 80 | y.extend(y1) 81 | # plotter.plot_points({idx: GeodeticPosInfo({"lat": x[idx], "lon": y[idx], "elev": 0.0}) 82 | # for idx in range(len(x))}, "GRID", "TEST", "aa", "asas",) 83 | 84 | grid_cart = np.zeros((len(grid_pos), 3)) 85 | grid_map = {} 86 | i = 0 87 | for idx, pos in grid_pos.items(): 88 | grid_map[i] = idx 89 | grid_cart[i] = geo2cart({"elev": 0, "lon": pos.lon, "lat": pos.lat}) 90 | i += 1 91 | 92 | # Put the homogeneous grid into a KD-tree and query the border points to include also point slightly in the sea 93 | kd = cKDTree(grid_cart) 94 | for idx in range(len(x)): 95 | _, closest_grid_idx = kd.query( 96 | geo2cart({"elev": 0, "lon": y[idx], "lat": x[idx]}), k=1 97 | ) 98 | grid_id = grid_map[closest_grid_idx] 99 | if ( 100 | great_circle( 101 | (grid_pos[grid_id].lat, grid_pos[grid_id].lon), (x[idx], y[idx]) 102 | ).meters 103 | < 300000 104 | ): 105 | # 300000 -> number elaborated to keep the out-of-coast values without including wrong points 106 | allowed_points.add(grid_map[closest_grid_idx]) 107 | return allowed_points 108 | 109 | 110 | # noinspection PyTypeChecker 111 | def load_country_geojson(): 112 | new_data = {"geometries": [], "countries": {}, "continents": {}, "subregions": {}} 113 | with open(COUNTRIES_FILE, encoding="utf-8") as f: 114 | data = json.load(f) 115 | new_data["geometries"] = [""] * len(data["features"]) 116 | 117 | for idx, feature in enumerate(data["features"]): 118 | props = feature["properties"] 119 | code = props["iso_a3"] 120 | if code == "-99": 121 | continue 122 | continent = props["continent"] 123 | subregion = props["region_wb"] 124 | subregion2 = props["subregion"] 125 | if continent not in new_data["continents"]: 126 | new_data["continents"][continent] = [] 127 | if subregion not in new_data["subregions"]: 128 | new_data["subregions"][subregion] = [] 129 | if subregion2 not in new_data["subregions"]: 130 | new_data["subregions"][subregion2] = [] 131 | new_data["continents"][continent].append(idx) 132 | new_data["subregions"][subregion].append(idx) 133 | new_data["subregions"][subregion2].append(idx) 134 | new_data["countries"][code] = idx 135 | new_data["geometries"][idx] = feature["geometry"] 136 | geom = new_data["geometries"][idx] 137 | if geom["type"] == "MultiPolygon": 138 | for l1 in range(len(geom["coordinates"])): 139 | for l2 in range(len(geom["coordinates"][l1])): 140 | for l3 in range(len(geom["coordinates"][l1][l2])): 141 | geom["coordinates"][l1][l2][l3] = geom["coordinates"][l1][l2][ 142 | l3 143 | ][::-1] 144 | elif geom["type"] == "Polygon": 145 | for l1 in range(len(geom["coordinates"])): 146 | for l2 in range(len(geom["coordinates"][l1])): 147 | geom["coordinates"][l1][l2] = geom["coordinates"][l1][l2][::-1] 148 | print(f"Available subregions: {list(new_data['subregions'].keys())}") 149 | return new_data 150 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_geo_constraint/grid_constr_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import Set, List 3 | 4 | from icarus_simulator.strategies.atk_geo_constraint.base_geo_constraint_strat import ( 5 | BaseGeoConstraintStrat, 6 | ) 7 | from icarus_simulator.structure_definitions import GridPos 8 | 9 | 10 | class GridConstrStrat(BaseGeoConstraintStrat): 11 | def __init__(self, grid_points: List[int], **kwargs): 12 | super().__init__() 13 | self.grid_points = grid_points 14 | if len(kwargs) > 0: 15 | pass # Appease the unused param inspection 16 | 17 | @property 18 | def name(self) -> str: 19 | return "grd" 20 | 21 | @property 22 | def param_description(self) -> str: 23 | return ",".join([str(i) for i in self.grid_points]) 24 | 25 | def compute(self, grid_pos: GridPos) -> Set[int]: 26 | return set(self.grid_points) 27 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_geo_constraint/no_constr_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import Set 3 | 4 | from icarus_simulator.strategies.atk_geo_constraint.base_geo_constraint_strat import ( 5 | BaseGeoConstraintStrat, 6 | ) 7 | from icarus_simulator.structure_definitions import GridPos 8 | 9 | 10 | class NoConstrStrat(BaseGeoConstraintStrat): 11 | @property 12 | def name(self) -> str: 13 | return "no" 14 | 15 | @property 16 | def param_description(self) -> None: 17 | return None 18 | 19 | def compute(self, grid_pos: GridPos) -> Set[int]: 20 | return set(grid_pos.keys()) 21 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_path_filtering/__init__.py: -------------------------------------------------------------------------------- 1 | from .directional_filtering_strat import DirectionalFilteringStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_path_filtering/base_path_filtering_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import ( 12 | PathData, 13 | Edge, 14 | EdgeData, 15 | DirectionData, 16 | ) 17 | 18 | 19 | class BasePathFilteringStrat(BaseStrat): 20 | @abstractmethod 21 | def compute( 22 | self, 23 | edges: List[Edge], 24 | edge_data: EdgeData, 25 | path_data: PathData, 26 | allowed_sources: List[int], 27 | ) -> DirectionData: 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/atk_path_filtering/directional_filtering_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import List 3 | 4 | from icarus_simulator.strategies.atk_path_filtering.base_path_filtering_strat import ( 5 | BasePathFilteringStrat, 6 | ) 7 | from icarus_simulator.structure_definitions import ( 8 | Edge, 9 | PathData, 10 | EdgeData, 11 | DirectionData, 12 | ) 13 | 14 | 15 | class DirectionalFilteringStrat(BasePathFilteringStrat): 16 | @property 17 | def name(self) -> str: 18 | return "dir" 19 | 20 | @property 21 | def param_description(self) -> None: 22 | return None 23 | 24 | def compute( 25 | self, 26 | edges: List[Edge], 27 | edge_data: EdgeData, 28 | path_data: PathData, 29 | allowed_sources: List[int], 30 | ) -> DirectionData: 31 | direction_data = {} 32 | for edge in edges: 33 | inv_ed = (edge[1], edge[0]) 34 | idxs_in_order = edge_data[edge].paths_through 35 | idxs_in_rev = edge_data[inv_ed].paths_through 36 | 37 | # Avoid duplicate indices. It can occur that gnd-sat-gnd paths are in both lists 38 | set_in_order, set_in_rev = set(idxs_in_order), set(idxs_in_rev) 39 | set_in_rev.difference_update(set_in_order) 40 | idxs_in_rev = list(set_in_rev) 41 | 42 | # Extract the path in the correct order 43 | idxs = idxs_in_order + idxs_in_rev 44 | for i, idx in enumerate(idxs): 45 | in_order = i < len(idxs_in_order) 46 | base_path = path_data[(idx[0], idx[1])][idx[2]][0] 47 | pair = idx[0], idx[1] 48 | if not in_order: 49 | base_path = base_path[::-1] 50 | pair = idx[1], idx[0] 51 | # Skip if the source is not in the allowed sources 52 | if -base_path[0] not in allowed_sources: 53 | continue 54 | 55 | # Path truncation 56 | # Cut the path at the target if ed[1] != -1. In this case the whole path is kept. 57 | if edge[1] != -1: 58 | last_idx = base_path.index(edge[1]) 59 | truncated = tuple([-1] + base_path[1 : last_idx + 1]) 60 | else: 61 | truncated = tuple([-1] + base_path[1:-1] + [-1]) 62 | 63 | # Note that we are not adding ordered pairs! Sending data from a to b is different 64 | # than sending from b to a, the probability of hitting the target is different! 65 | if truncated not in direction_data: 66 | direction_data[truncated] = [] 67 | direction_data[truncated].append(pair) # Add multiple times if needed 68 | 69 | return direction_data 70 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/base_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | This class defines the abstraction for a strategy, passed to a phase to define a specific computation step. 4 | This class is open for custom extension, in order to create different execution strategies for specific steps. 5 | 6 | The methods name() and param_description() are used by the phase to manage naming. 7 | The compute() method, contains the computation logic, and must be specified according to the task. 8 | 9 | For extension examples, see the subdirectories. Every subdirectory contains strategies for a different task, and must 10 | have a level 2 base class specifying the constructor and compute arguments and returns for the task. All __init__() 11 | overrides must call the superclass init and must specify **kwargs in the parameters, to enable the configuration 12 | mechanism (see configuration.py in the main directory). 13 | """ 14 | from abc import ABC, abstractmethod 15 | from typing import Any 16 | 17 | 18 | class BaseStrat(ABC): 19 | def __init__(self, **kwargs): 20 | pass 21 | 22 | # Methods to override 23 | @property 24 | def name(self) -> str: 25 | raise NotImplementedError 26 | 27 | @property 28 | def param_description(self) -> str: 29 | raise NotImplementedError 30 | 31 | @abstractmethod 32 | def compute(self, *args) -> Any: 33 | raise NotImplementedError 34 | 35 | # No override 36 | @property 37 | def description(self) -> str: 38 | conf_desc = "-" 39 | if self.param_description is not None: 40 | conf_desc = f"-{self.param_description}-" 41 | return f"{self.name}{conf_desc}" 42 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_assignment/__init__.py: -------------------------------------------------------------------------------- 1 | from .bidir_bw_assign_strat import BidirBwAssignStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_assignment/base_bw_assig_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import PathData, EdgeData, BwData, PathId 12 | 13 | 14 | class BaseBwAssignStrat(BaseStrat): 15 | @abstractmethod 16 | def compute( 17 | self, path_data: PathData, path_list: List[PathId], edge_data: EdgeData 18 | ) -> BwData: 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_assignment/bidir_bw_assign_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import List 3 | 4 | from icarus_simulator.strategies.bw_assignment.base_bw_assig_strat import ( 5 | BaseBwAssignStrat, 6 | ) 7 | from icarus_simulator.structure_definitions import ( 8 | BwData, 9 | PathData, 10 | EdgeData, 11 | PathId, 12 | BwInfo, 13 | ) 14 | from icarus_simulator.utils import get_edges 15 | 16 | 17 | class BidirBwAssignStrat(BaseBwAssignStrat): 18 | def __init__(self, isl_bw: int, udl_bw: int, utilisation: float, **kwargs): 19 | super().__init__() 20 | self.isl_bw = isl_bw 21 | self.udl_bw = udl_bw 22 | self.utilisation = utilisation 23 | if len(kwargs) > 0: 24 | pass # Appease the unused param inspection 25 | 26 | @property 27 | def name(self) -> str: 28 | return "bidi" 29 | 30 | @property 31 | def param_description(self) -> str: 32 | return f"i{self.isl_bw}ud{self.udl_bw}u{self.utilisation}" 33 | 34 | def compute( 35 | self, path_data: PathData, path_list: List[PathId], edge_data: EdgeData 36 | ) -> BwData: 37 | allocated, dropped = 0, 0 38 | max_updown = int(self.udl_bw * self.utilisation) 39 | max_isl = int(self.isl_bw * self.utilisation) 40 | bw_data: BwData = { 41 | ed: (BwInfo(0, self.isl_bw) if -1 not in ed else BwInfo(0, self.udl_bw)) 42 | for ed in edge_data 43 | } 44 | 45 | # Allocate one quantum per path 46 | for path_id in path_list: 47 | path = path_data[(path_id[0], path_id[1])][path_id[2]][0] 48 | # Convert to -1 notation 49 | first, last = -path[0], -path[-1] 50 | path[0], path[-1], path_fits = -1, -1, True 51 | eds = list(get_edges(path)) 52 | # Swap last and second edges: optimisation, moved the downlink check at the beginning 53 | dlink, sec = eds[-1], eds[1] 54 | eds[1], eds[-1] = dlink, sec 55 | 56 | # Check if the addition of this communication would fit in the involved edges 57 | for ed in eds: 58 | max_link = max_isl 59 | if -1 in ed: 60 | max_link = max_updown 61 | if ( 62 | bw_data[ed].idle_bw + 1 > max_link 63 | ): # Can be equal, we have utilisation 64 | path_fits = False 65 | break 66 | # If the path fits, add it to the current bandwidths 67 | if path_fits: 68 | allocated += 1 69 | for ed in get_edges(path): 70 | inv_ed = (ed[1], ed[0]) 71 | bw_data[ed].idle_bw += 1 72 | bw_data[inv_ed].idle_bw += 1 73 | else: 74 | dropped += 1 75 | # Swap back the extremes 76 | path[0], path[-1] = first, last 77 | 78 | # Interesting data prints 79 | print(f"Alloc, drop, multi_drop: {allocated}, {dropped}") 80 | return bw_data 81 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_selection/__init__.py: -------------------------------------------------------------------------------- 1 | from .sampled_bw_select_strat import SampledBwSelectStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_selection/base_bw_select_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import GridPos, PathData, PathId 12 | 13 | 14 | class BaseBwSelectStrat(BaseStrat): 15 | @abstractmethod 16 | def compute(self, grid_pos: GridPos, path_data: PathData) -> List[PathId]: 17 | raise NotImplementedError 18 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/bw_selection/sampled_bw_select_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import random 3 | from typing import List 4 | 5 | from icarus_simulator.strategies.bw_selection.base_bw_select_strat import ( 6 | BaseBwSelectStrat, 7 | ) 8 | from icarus_simulator.structure_definitions import GridPos, PathData, PathId 9 | from icarus_simulator.utils import get_ordered_idx 10 | 11 | 12 | # Computes a sampled traffic matrix. IMPORTANT: this strategy assumes that all paths are symmetrical, and path_data 13 | # only stores the ordered pairs for space and performance reasons. 14 | class SampledBwSelectStrat(BaseBwSelectStrat): 15 | def __init__(self, sampled_quanta: int, **kwargs): 16 | super().__init__() 17 | self.sampled_quanta = sampled_quanta 18 | if len(kwargs) > 0: 19 | pass # Appease the unused param inspection 20 | 21 | @property 22 | def name(self) -> str: 23 | return "samp" 24 | 25 | @property 26 | def param_description(self) -> str: 27 | return f"{self.sampled_quanta}" 28 | 29 | def compute(self, grid_pos: GridPos, path_data: PathData) -> List[PathId]: 30 | random.seed("ETHZ") 31 | # Sample communication pairs 32 | samples = random.choices( 33 | list(grid_pos.keys()), 34 | [val.weight for val in grid_pos.values()], 35 | k=self.sampled_quanta * 2, 36 | ) 37 | # If the pair exists, sample a suitable path 38 | path_ids = [] 39 | for i in range(0, self.sampled_quanta * 2, 2): 40 | ord_sample = get_ordered_idx((samples[i], samples[i + 1]))[0] 41 | # If the sample is not in the paths, or there is no path between the pair, the sample is dropped 42 | if ord_sample not in path_data or len(path_data[ord_sample]) == 0: 43 | continue 44 | lbset_size = len(path_data[ord_sample]) 45 | id_in_lbset = random.randrange(0, lbset_size) 46 | path_ids.append((ord_sample[0], ord_sample[1], id_in_lbset)) 47 | return path_ids 48 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/coverage/__init__.py: -------------------------------------------------------------------------------- 1 | from .angle_cov_strat import AngleCovStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/coverage/angle_cov_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from icarus_simulator.sat_core.coverage import positions_satellite_coverage 3 | 4 | from icarus_simulator.strategies.coverage.base_coverage_strat import BaseCoverageStrat 5 | from icarus_simulator.structure_definitions import SatPos, GridPos, Coverage 6 | 7 | 8 | class AngleCovStrat(BaseCoverageStrat): 9 | def __init__(self, min_elev_angle: int, **kwargs): 10 | super().__init__() 11 | self.min_elev_angle = min_elev_angle 12 | if len(kwargs) > 0: 13 | pass # Appease the unused param inspection 14 | 15 | @property 16 | def name(self) -> str: 17 | return "ang" 18 | 19 | @property 20 | def param_description(self) -> str: 21 | return f"{self.min_elev_angle}°" 22 | 23 | def compute(self, grid_pos: GridPos, sat_pos: SatPos) -> Coverage: 24 | coverage = positions_satellite_coverage( 25 | {ka: v.to_geo_pos() for ka, v in grid_pos.items()}, 26 | {ka: v.to_geo_pos() for ka, v in sat_pos.items()}, 27 | self.min_elev_angle, 28 | ) 29 | return coverage 30 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/coverage/base_coverage_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | 9 | from icarus_simulator.strategies.base_strat import BaseStrat 10 | from icarus_simulator.structure_definitions import GridPos, SatPos, Coverage 11 | 12 | 13 | class BaseCoverageStrat(BaseStrat): 14 | @abstractmethod 15 | def compute(self, grid_pos: GridPos, sat_pos: SatPos) -> Coverage: 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/edge/__init__.py: -------------------------------------------------------------------------------- 1 | from .bidir_edge_strat import BidirEdgeStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/edge/base_edge_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import Dict 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import Edge, TempEdgeInfo, Path, PathId 12 | 13 | 14 | class BaseEdgeStrat(BaseStrat): 15 | @abstractmethod # Side effects on temp_edge_data are produced 16 | def compute( 17 | self, temp_edge_data: Dict[Edge, TempEdgeInfo], path: Path, path_id: PathId 18 | ) -> None: 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/edge/bidir_edge_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import Dict 3 | 4 | from icarus_simulator.strategies.edge.base_edge_strat import BaseEdgeStrat 5 | from icarus_simulator.structure_definitions import Path, PathId, Edge, TempEdgeInfo 6 | from icarus_simulator.utils import get_edges 7 | 8 | 9 | class BidirEdgeStrat(BaseEdgeStrat): 10 | @property 11 | def name(self) -> str: 12 | return "bidir" 13 | 14 | @property 15 | def param_description(self) -> None: 16 | return None 17 | 18 | # Side effects on temp_edge_data are produced 19 | def compute( 20 | self, temp_edge_data: Dict[Edge, TempEdgeInfo], path: Path, path_id: PathId 21 | ) -> None: 22 | # Convert the up and downlink to the -1 convention 23 | first, last = -path[0], -path[-1] 24 | path[0], path[-1] = -1, -1 25 | # For each edge in each path, compute stats 26 | for ed in get_edges(path): 27 | inv_ed = (ed[1], ed[0]) 28 | if ed not in temp_edge_data: 29 | # paths_through, centrality, start_points 30 | temp_edge_data[ed] = TempEdgeInfo([], 0, set()) 31 | temp_edge_data[inv_ed] = TempEdgeInfo([], 0, set()) 32 | # Update paths_through 33 | # IMPORTANT: paths_through will ONLY contain paths through with the edge in the same order. 34 | # Also the paths through the opposite edge are through the current edge, but must be taken in reversed form. 35 | temp_edge_data[ed].paths_through.append(path_id) 36 | # Update edge centrality 37 | temp_edge_data[ed].centrality += 1 38 | temp_edge_data[inv_ed].centrality += 1 39 | # Update start_points 40 | temp_edge_data[ed].source_gridpoints.add(first) 41 | temp_edge_data[inv_ed].source_gridpoints.add(last) 42 | # Restore the path first and last (gnd) hops 43 | path[0], path[-1] = -first, -last 44 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid/__init__.py: -------------------------------------------------------------------------------- 1 | from .geodesic_grid_strat import GeodesicGridStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid/base_grid_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | 9 | from icarus_simulator.strategies.base_strat import BaseStrat 10 | from icarus_simulator.structure_definitions import GridPos 11 | 12 | 13 | class BaseGridStrat(BaseStrat): 14 | @abstractmethod 15 | def compute(self) -> GridPos: 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid/geodesic_grid_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import math 3 | 4 | from anti_lib import Vec 5 | 6 | from icarus_simulator.strategies.grid.base_grid_strat import BaseGridStrat 7 | from icarus_simulator.sat_core.planetary_const import EARTH_SURFACE 8 | from icarus_simulator.structure_definitions import GridPos, GridPoint 9 | 10 | 11 | class GeodesicGridStrat(BaseGridStrat): 12 | def __init__(self, repeats: int, **kwargs): 13 | super().__init__() 14 | self.repeats = repeats 15 | if len(kwargs) > 0: 16 | pass # Appease the unused param inspection 17 | 18 | @property 19 | def name(self) -> str: 20 | return "geo" 21 | 22 | @property 23 | def param_description(self) -> str: 24 | return f"{self.repeats}" 25 | 26 | def compute(self) -> GridPos: 27 | verts, edges, faces = [], {}, [] 28 | get_poly(verts, edges, faces) 29 | 30 | # a and b have same meanings as in geodesic notation 31 | # with the hardcoded 1, 0, 1: the grid will generate an icosahedron of class 1 with no divisions 32 | # when a=0 or b=0, the one that is nonzero will determine the number of divisions per face 33 | # The parameter repeats sets the multiple of the chosen pattern. if a=6 and repeats=2, each edge will be divided 34 | # into 12 parts. Setting a value of 1 and 6 to repeats and a or to a and repeats yields the same result. 35 | a, b, reps = 1, 0, 1 36 | repeats = self.repeats * reps 37 | triangulation_number = ( 38 | a ** 2 + a * b + b ** 2 39 | ) # Total number of triangles in a face w/o repeats 40 | freq = repeats * triangulation_number 41 | 42 | single_face_grid = make_face_grid(freq, a, b) 43 | points = verts 44 | for face in faces: 45 | points[len(points) : len(points)] = grid_to_points( 46 | single_face_grid, freq, False, [verts[face[i]] for i in range(3)], face 47 | ) 48 | points = [p.unit().v for p in points] # Project onto sphere 49 | grid = { 50 | idx: GridPoint.from_geo_pos(cart2geo(p)) for idx, p in enumerate(points) 51 | } 52 | sur = EARTH_SURFACE / len(grid) 53 | for idx in grid: 54 | grid[idx].surface = sur 55 | return grid 56 | 57 | 58 | def cart2geo(point): 59 | # radius is fixed here to the Earth's radius 60 | x = point[0] 61 | y = point[1] 62 | z = point[2] 63 | 64 | latitude = math.asin(z) 65 | longitude = math.atan2(y, x) 66 | return {"lat": math.degrees(latitude), "lon": math.degrees(longitude), "elev": 0.0} 67 | 68 | 69 | def get_ico_coords(): 70 | """Return icosahedron coordinate values""" 71 | phi = (math.sqrt(5) + 1) / 2 72 | rad = math.sqrt(phi + 2) 73 | return 1 / rad, phi / rad 74 | 75 | 76 | def get_icosahedron(verts, faces): 77 | """Return an icosahedron""" 78 | x, z = get_ico_coords() 79 | verts.extend( 80 | [ 81 | Vec(-x, 0.0, z), 82 | Vec(x, 0.0, z), 83 | Vec(-x, 0.0, -z), 84 | Vec(x, 0.0, -z), 85 | Vec(0.0, z, x), 86 | Vec(0.0, z, -x), 87 | Vec(0.0, -z, x), 88 | Vec(0.0, -z, -x), 89 | Vec(z, x, 0.0), 90 | Vec(-z, x, 0.0), 91 | Vec(z, -x, 0.0), 92 | Vec(-z, -x, 0.0), 93 | ] 94 | ) 95 | 96 | faces.extend( 97 | [ 98 | (0, 4, 1), 99 | (0, 9, 4), 100 | (9, 5, 4), 101 | (4, 5, 8), 102 | (4, 8, 1), 103 | (8, 10, 1), 104 | (8, 3, 10), 105 | (5, 3, 8), 106 | (5, 2, 3), 107 | (2, 7, 3), 108 | (7, 10, 3), 109 | (7, 6, 10), 110 | (7, 11, 6), 111 | (11, 0, 6), 112 | (0, 1, 6), 113 | (6, 1, 10), 114 | (9, 0, 11), 115 | (9, 11, 2), 116 | (9, 2, 5), 117 | (7, 2, 11), 118 | ] 119 | ) 120 | 121 | 122 | def get_poly(verts, edges, faces): 123 | """Return the base polyhedron""" 124 | get_icosahedron(verts, faces) 125 | for face in faces: 126 | for i in range(0, len(face)): 127 | i2 = i + 1 128 | if i2 == len(face): 129 | i2 = 0 130 | if face[i] < face[i2]: 131 | edges[(face[i], face[i2])] = 0 132 | else: 133 | edges[(face[i2], face[i])] = 0 134 | return 1 135 | 136 | 137 | def grid_to_points(grid, freq, div_by_len, face_verts, face): 138 | """Convert grid coordinates to Cartesian coordinates""" 139 | points = [] 140 | v = [] 141 | for vtx in range(3): 142 | v.append([Vec(0.0, 0.0, 0.0)]) 143 | edge_vec = face_verts[(vtx + 1) % 3] - face_verts[vtx] 144 | if div_by_len: 145 | for i in range(1, freq + 1): 146 | v[vtx].append(edge_vec * float(i) / freq) 147 | else: 148 | ang = 2 * math.asin(edge_vec.mag() / 2.0) 149 | unit_edge_vec = edge_vec.unit() 150 | for i in range(1, freq + 1): 151 | len_var = math.sin(i * ang / freq) / math.sin( 152 | math.pi / 2 + ang / 2 - i * ang / freq 153 | ) 154 | v[vtx].append(unit_edge_vec * len_var) 155 | 156 | for (i, j) in grid.values(): 157 | 158 | if (i == 0) + (j == 0) + (i + j == freq) == 2: # skip vertex 159 | continue 160 | # skip edges in one direction 161 | if ( 162 | (i == 0 and face[2] > face[0]) 163 | or (j == 0 and face[0] > face[1]) 164 | or (i + j == freq and face[1] > face[2]) 165 | ): 166 | continue 167 | 168 | n = [i, j, freq - i - j] 169 | v_delta = ( 170 | v[0][n[0]] + v[(0 - 1) % 3][freq - n[(0 + 1) % 3]] - v[(0 - 1) % 3][freq] 171 | ) 172 | pt = face_verts[0] + v_delta 173 | if not div_by_len: 174 | for k in [1, 2]: 175 | v_delta = ( 176 | v[k][n[k]] 177 | + v[(k - 1) % 3][freq - n[(k + 1) % 3]] 178 | - v[(k - 1) % 3][freq] 179 | ) 180 | pt = pt + face_verts[k] + v_delta 181 | pt = pt / 3 182 | points.append(pt) 183 | 184 | return points 185 | 186 | 187 | def make_face_grid(freq, m, n): 188 | """Make the geodesic pattern grid""" 189 | grid = {} 190 | rng = (2 * freq) // (m + n) 191 | for i in range(rng): 192 | for j in range(rng): 193 | x = i * (-n) + j * (m + n) 194 | y = i * (m + n) + j * (-m) 195 | 196 | if x >= 0 and y >= 0 and x + y <= freq: 197 | grid[(i, j)] = (x, y) 198 | 199 | return grid 200 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid_weight/__init__.py: -------------------------------------------------------------------------------- 1 | from .gdp_weight_strat import GDPWeightStrat 2 | from .uniform_weight_strat import UniformWeightStrat 3 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid_weight/base_weight_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | 9 | from icarus_simulator.strategies.base_strat import BaseStrat 10 | from icarus_simulator.structure_definitions import GridPos 11 | 12 | 13 | class BaseWeightStrat(BaseStrat): 14 | @abstractmethod 15 | def compute(self, grid_pos: GridPos) -> GridPos: 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid_weight/gdp_weight_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import os 3 | 4 | from netCDF4 import Dataset 5 | from scipy.spatial.ckdtree import cKDTree 6 | import numpy as np 7 | 8 | from icarus_simulator.sat_core.coordinate_util import geo2cart 9 | from icarus_simulator.strategies.grid_weight.base_weight_strat import BaseWeightStrat 10 | from icarus_simulator.structure_definitions import GridPos 11 | 12 | dirname = os.path.dirname(__file__) 13 | strategies_dirname = os.path.split(dirname)[0] 14 | library_dirname = os.path.split(strategies_dirname)[0] 15 | data_dirname = os.path.join(library_dirname, "data") 16 | GDP_FILE: str = os.path.join(data_dirname, "GDP_PPP_1990_2015_5arcmin_v2.nc") 17 | 18 | 19 | class GDPWeightStrat(BaseWeightStrat): 20 | def __init__(self, dataset_file: str = None, **kwargs): 21 | super().__init__() 22 | if dataset_file is None: 23 | self.dataset = GDP_FILE 24 | else: 25 | self.dataset = dataset_file 26 | if len(kwargs) > 0: 27 | pass # Appease the unused param inspection 28 | 29 | @property 30 | def name(self) -> str: 31 | return "gdp" 32 | 33 | @property 34 | def param_description(self) -> None: 35 | return None 36 | 37 | def compute(self, grid_pos: GridPos) -> GridPos: 38 | # Add the default weight for this unweighted grid 39 | # Load the GDP data 40 | gdp_matrix = load_dataset(self.dataset) 41 | lat_size, lon_size = gdp_matrix.shape 42 | gdp_cart = [] 43 | gdp_values = [] 44 | idx = 0 45 | gdp_grid = {} 46 | for lat_id in range(0, lat_size): 47 | for lon_id in range(0, lon_size): 48 | value = gdp_matrix[lat_id, lon_id] 49 | if value > 0: 50 | lat = get_lat(lat_id) 51 | lon = get_lon(lon_id) 52 | gdp_cart.append(geo2cart({"elev": 0, "lon": lon, "lat": lat})) 53 | gdp_grid[idx] = {"lat": lat, "lon": lon, "value": value} 54 | gdp_values.append(value) 55 | idx += 1 56 | 57 | # Generate the homogeneous grid 58 | grid_cart = np.zeros((len(grid_pos), 3)) 59 | for i in grid_pos: 60 | grid_cart[i] = geo2cart( 61 | {"elev": 0, "lon": grid_pos[i].lon, "lat": grid_pos[i].lat} 62 | ) 63 | grid_pos[i].weight = 0.0 64 | 65 | # Put the homogeneous grid into a KD-tree and query all the points, summing values to the closest grid point 66 | kd = cKDTree(grid_cart) 67 | for gdp_idx in range(len(gdp_cart)): 68 | _, closest_grid_idx = kd.query(gdp_cart[gdp_idx], k=1) 69 | grid_pos[closest_grid_idx].weight += gdp_values[gdp_idx] 70 | 71 | # Remove the zero-weight points 72 | grid_pos = {idx: point for idx, point in grid_pos.items() if point.weight > 0.0} 73 | # for idx, point in grid_s.items(): 74 | # grid_s[idx].weight = math.log10(point.weight) 75 | max_single_gdp = max(grid_pos.values(), key=lambda ka: ka.weight).weight 76 | for gp in grid_pos: 77 | grid_pos[gp].weight = grid_pos[gp].weight / max_single_gdp 78 | return grid_pos 79 | 80 | 81 | # Downsample an ndarray to a new shape by summing 82 | def downsample_ndarray(ndarray: np.ndarray, new_shape: np.shape) -> np.ndarray: 83 | compression_pairs = [(d, c // d) for d, c in zip(new_shape, ndarray.shape)] 84 | flattened = [li for p in compression_pairs for li in p] 85 | ndarray = ndarray.reshape(flattened) 86 | for i in range(len(new_shape)): 87 | ndarray = ndarray.sum(-1 * (i + 1)) 88 | return ndarray 89 | 90 | 91 | # Load the GDP distribution data. 92 | def load_dataset(dataset_fname) -> np.ndarray: 93 | # Resolution is 5 arcmin -> 12 points per degree 94 | # lat: full globe, 0 is 90°; lon: full globe, 0 is -180 95 | # Unit: 2011 USD, Index for 2015 is 25 96 | # GDP_PPP indexed by (year_id, latitude_id, longitude_id) 97 | nc = Dataset(dataset_fname, "r") 98 | matrix = nc["GDP_PPP"][25, :, :] 99 | # matrix = nc["Population Count, v4.11 (2000, 2005, 2010, 2015, 2020): 2.5 arc-minutes"][4, :, :] 100 | matrix = matrix.filled(0.0) 101 | # Return indexed by lat, long 102 | return downsample_ndarray(matrix, (180, 360)) 103 | 104 | 105 | # Get latitude from index in gdp matrix 106 | def get_lat(idx: int): 107 | return 90 - idx - 0.5 108 | 109 | 110 | # Get longitude from index in gdp matrix 111 | def get_lon(idx: int): 112 | return -180 + idx + 0.5 113 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/grid_weight/uniform_weight_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from icarus_simulator.strategies.grid_weight.base_weight_strat import BaseWeightStrat 3 | from icarus_simulator.structure_definitions import GridPos 4 | 5 | 6 | class UniformWeightStrat(BaseWeightStrat): 7 | @property 8 | def name(self) -> str: 9 | return "uni" 10 | 11 | @property 12 | def param_description(self) -> None: 13 | return None 14 | 15 | def compute(self, grid_pos: GridPos) -> GridPos: 16 | # Add the default weight for this unweighted grid 17 | for idx in grid_pos: 18 | grid_pos[idx].weight = 1.0 19 | return grid_pos 20 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/lsn/__init__.py: -------------------------------------------------------------------------------- 1 | from .manh_lsn_strat import ManhLSNStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/lsn/base_lsn_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | import networkx as nx 8 | 9 | from abc import abstractmethod 10 | from typing import Tuple, List 11 | 12 | from icarus_simulator.strategies.base_strat import BaseStrat 13 | from icarus_simulator.structure_definitions import SatPos, IslInfo 14 | 15 | 16 | class BaseLSNStrat(BaseStrat): 17 | @abstractmethod 18 | def compute(self) -> Tuple[SatPos, nx.Graph, List[IslInfo]]: 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/lsn/manh_lsn_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | 3 | import networkx as nx 4 | 5 | from typing import Tuple, List 6 | 7 | from icarus_simulator.sat_core import WalkerConstellationNetwork 8 | from icarus_simulator.strategies.lsn.base_lsn_strat import BaseLSNStrat 9 | from icarus_simulator.structure_definitions import SatPos, GeoPoint, IslInfo 10 | 11 | 12 | class ManhLSNStrat(BaseLSNStrat): 13 | def __init__( 14 | self, 15 | inclination: int, 16 | sats_per_orbit: int, 17 | orbits: int, 18 | f: int, 19 | elevation: int, 20 | hrs: int, 21 | mins: int, 22 | secs: int, 23 | millis: int, 24 | epoch: str, 25 | **kwargs, 26 | ): 27 | super().__init__() 28 | self.inclination = inclination 29 | self.sats_per_orbit = sats_per_orbit 30 | self.orbits = orbits 31 | self.f = f 32 | self.elevation = elevation 33 | self.hrs = hrs 34 | self.mins = mins 35 | self.secs = secs 36 | self.millis = millis 37 | self.epoch = epoch 38 | if len(kwargs) > 0: 39 | pass # Appease the unused param inspection 40 | 41 | @property 42 | def name(self) -> str: 43 | return "reg" 44 | 45 | @property 46 | def param_description(self) -> str: 47 | return ( 48 | f"{self.inclination}°{self.sats_per_orbit}x{self.orbits}f{self.f}" 49 | f"e{int(self.elevation//1000)}km" 50 | f"{str(self.hrs).zfill(2)}h{str(self.mins).zfill(2)}m{str(self.secs).zfill(2)}s" 51 | f"{str(self.millis).zfill(4)}ms" 52 | f"{str(self.epoch).replace(' ' , '').replace(':', '').replace('/', '')}" 53 | ) 54 | 55 | def compute(self) -> Tuple[SatPos, nx.Graph, List[IslInfo]]: 56 | walker = WalkerConstellationNetwork( 57 | self.sats_per_orbit, 58 | self.orbits, 59 | self.inclination, 60 | self.epoch, 61 | self.f, 62 | elevation=self.elevation, 63 | ) 64 | walker.compute_network_at_epoch_offset(self.hrs, self.mins, self.secs) 65 | sat_pos = { 66 | key: GeoPoint.from_geo_pos(val) 67 | for key, val in walker.cnet.get_sats().items() 68 | } 69 | nw = walker.cnet.network 70 | isls = walker.cnet.get_isls() 71 | isls = [IslInfo(isl["sat1"], isl["sat2"], isl["length"]) for isl in isls] 72 | return sat_pos, nw, isls 73 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/__init__.py: -------------------------------------------------------------------------------- 1 | from .kdg_rout_strat import KDGRoutStrat 2 | from .kds_rout_strat import KDSRoutStrat 3 | from .klo_rout_strat import KLORoutStrat 4 | from .ksp_rout_strat import KSPRoutStrat 5 | from .ssp_rout_strat import SSPRoutStrat 6 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/base_routing_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | import networkx as nx 8 | 9 | from abc import abstractmethod 10 | 11 | from icarus_simulator.strategies.base_strat import BaseStrat 12 | from icarus_simulator.structure_definitions import GridPos, SdPair, Coverage, LbSet 13 | 14 | 15 | class BaseRoutingStrat(BaseStrat): 16 | @abstractmethod 17 | def compute( 18 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 19 | ) -> LbSet: 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/kdg_rout_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import networkx as nx 3 | from geopy.distance import great_circle 4 | 5 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 6 | from icarus_simulator.structure_definitions import GridPos, SdPair, Coverage, LbSet 7 | 8 | 9 | class KDGRoutStrat(BaseRoutingStrat): 10 | def __init__(self, desirability_stretch: float, k: int, **kwargs): 11 | super().__init__() 12 | self.desirability_stretch = desirability_stretch 13 | self.k = k 14 | if len(kwargs) > 0: 15 | pass # Appease the unused param inspection 16 | 17 | @property 18 | def name(self) -> str: 19 | return "kdg" 20 | 21 | @property 22 | def param_description(self) -> str: 23 | return f"{self.desirability_stretch}k{self.k}" 24 | 25 | def compute( 26 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 27 | ) -> LbSet: 28 | in_grid, out_grid = pair[0], pair[1] 29 | fiber_len = ( 30 | great_circle( 31 | (grid[in_grid].lat, grid[in_grid].lon), 32 | (grid[out_grid].lat, grid[out_grid].lon), 33 | ).meters 34 | * self.desirability_stretch 35 | ) 36 | # Add the gnd nodes 37 | for gnd in pair: 38 | for dst_sat in coverage[gnd]: 39 | network.add_edge(-gnd, dst_sat, length=coverage[gnd][dst_sat]) 40 | 41 | # Compute the possible paths using the chosen criterion 42 | lbset: LbSet = [] 43 | lengths = {} 44 | cnt = 0 45 | while True: 46 | try: 47 | length, path = nx.single_source_dijkstra( 48 | network, -pair[0], -pair[1], cutoff=fiber_len, weight="length" 49 | ) 50 | except nx.NetworkXNoPath: 51 | break 52 | 53 | lbset.append((path, length)) 54 | cnt += 1 55 | if cnt >= self.k: 56 | break 57 | # It makes no sense to find alternate paths in gnd-sat-gnd situation, as they will likely be too long 58 | if len(path) <= 3: 59 | break 60 | 61 | # Set used edges to INF length. 62 | for n in path[1:-1]: 63 | out_ed = network.edges(n) 64 | for ed in out_ed: 65 | # Exclude up and downlinks from the disjointness 66 | if ed not in lengths and (ed[1], ed[0]) not in lengths: 67 | lengths[ed] = network[ed[0]][ed[1]]["length"] 68 | network[ed[0]][ed[1]]["length"] = float("inf") 69 | # Restore the infinite lengths 70 | for ed in lengths: 71 | network[ed[0]][ed[1]]["length"] = lengths[ed] 72 | 73 | # Delete the added nodes 74 | for gnd in pair: 75 | network.remove_node(-gnd) 76 | return lbset 77 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/kds_rout_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import networkx as nx 3 | from geopy.distance import great_circle 4 | 5 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 6 | from icarus_simulator.structure_definitions import GridPos, SdPair, Coverage, LbSet 7 | 8 | 9 | class KDSRoutStrat(BaseRoutingStrat): 10 | def __init__(self, desirability_stretch: float, k: int, **kwargs): 11 | super().__init__() 12 | self.desirability_stretch = desirability_stretch 13 | self.k = k 14 | if len(kwargs) > 0: 15 | pass # Appease the unused param inspection 16 | 17 | @property 18 | def name(self) -> str: 19 | return "kds" 20 | 21 | @property 22 | def param_description(self) -> str: 23 | return f"{self.desirability_stretch}k{self.k}" 24 | 25 | def compute( 26 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 27 | ) -> LbSet: 28 | in_grid, out_grid = pair[0], pair[1] 29 | fiber_len = ( 30 | great_circle( 31 | (grid[in_grid].lat, grid[in_grid].lon), 32 | (grid[out_grid].lat, grid[out_grid].lon), 33 | ).meters 34 | * self.desirability_stretch 35 | ) 36 | # Add the gnd nodes 37 | for gnd in pair: 38 | for dst_sat in coverage[gnd]: 39 | network.add_edge(-gnd, dst_sat, length=coverage[gnd][dst_sat]) 40 | 41 | # Compute the possible paths using the chosen criterion 42 | lbset: LbSet = [] 43 | lengths = {} 44 | cnt = 0 45 | while True: 46 | try: 47 | length, path = nx.single_source_dijkstra( 48 | network, -pair[0], -pair[1], cutoff=fiber_len, weight="length" 49 | ) 50 | except nx.NetworkXNoPath: 51 | break 52 | 53 | lbset.append((path, length)) 54 | cnt += 1 55 | if cnt >= self.k: 56 | break 57 | # It makes no sense to find alternate paths in gnd-sat-gnd situation, as they will likely be too long 58 | if len(path) <= 3: 59 | break 60 | # Set used edges to INF length, and also the ones coming out of the nodes 61 | ed = path[1], path[2] 62 | lengths[ed] = network[ed[0]][ed[1]]["length"] 63 | network[ed[0]][ed[1]]["length"] = float("inf") 64 | for n in path[2:-2]: 65 | out_ed = network.edges(n) 66 | for ed in out_ed: 67 | # Exclude up and downlinks from the disjointness 68 | if ed not in lengths and (ed[1], ed[0]) not in lengths: 69 | lengths[ed] = network[ed[0]][ed[1]]["length"] 70 | network[ed[0]][ed[1]]["length"] = float("inf") 71 | 72 | # Restore the infinite lengths 73 | for ed in lengths: 74 | network[ed[0]][ed[1]]["length"] = lengths[ed] 75 | 76 | # Delete the added nodes 77 | for gnd in pair: 78 | network.remove_node(-gnd) 79 | return lbset 80 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/klo_rout_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import networkx as nx 3 | 4 | from heapq import heapify, heappop 5 | from geopy.distance import great_circle 6 | 7 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 8 | from icarus_simulator.structure_definitions import GridPos, SdPair, Coverage, LbSet 9 | from icarus_simulator.utils import ( 10 | get_edge_length, 11 | get_ordered_idx, 12 | get_edges, 13 | similarity, 14 | ) 15 | 16 | 17 | class KLORoutStrat(BaseRoutingStrat): 18 | def __init__(self, desirability_stretch: float, k: int, esx_theta: float, **kwargs): 19 | super().__init__() 20 | self.desirability_stretch = desirability_stretch 21 | self.k = k 22 | self.esx_theta = esx_theta 23 | if len(kwargs) > 0: 24 | pass # Appease the unused param inspection 25 | 26 | @property 27 | def name(self) -> str: 28 | return "klo" 29 | 30 | @property 31 | def param_description(self) -> str: 32 | return f"{self.desirability_stretch}k{self.k}th{self.esx_theta}" 33 | 34 | def compute( 35 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 36 | ) -> LbSet: 37 | in_grid, out_grid = pair[0], pair[1] 38 | fiber_len = ( 39 | great_circle( 40 | (grid[in_grid].lat, grid[in_grid].lon), 41 | (grid[out_grid].lat, grid[out_grid].lon), 42 | ).meters 43 | * self.desirability_stretch 44 | ) 45 | 46 | # Add the gnd nodes 47 | for gnd in pair: 48 | for dst_sat in coverage[gnd]: 49 | network.add_edge(-gnd, dst_sat, length=coverage[gnd][dst_sat]) 50 | 51 | # Run the ESX algorithm. Find the shortest path first 52 | try: 53 | length, path = nx.single_source_dijkstra( 54 | network, -pair[0], -pair[1], cutoff=fiber_len, weight="length" 55 | ) 56 | chosen_paths = [ 57 | { 58 | "path": path, 59 | "length": length, 60 | "heap": [ 61 | (get_edge_length(network, ed), ed) for ed in get_edges(path) 62 | ][1:-1], 63 | } 64 | ] 65 | heapify(chosen_paths[0]["heap"]) 66 | p_c = chosen_paths[0] 67 | except nx.NetworkXNoPath: 68 | chosen_paths = [] 69 | p_c = None 70 | 71 | excluded_eds, lengths = set(), {} 72 | while len(chosen_paths) < self.k and any( 73 | len(ch["heap"]) > 0 for ch in chosen_paths 74 | ): 75 | # Firstly, check if the current p_c can be added to the chosen paths -> all similarities < theta 76 | sim_values = [ 77 | ( 78 | similarity( 79 | p_c["path"], ch["path"], p_c["length"], ch["length"], network 80 | ), 81 | len(ch["heap"]), 82 | idx, 83 | ) 84 | for idx, ch in enumerate(chosen_paths) 85 | ] 86 | if all(sv[0] <= self.esx_theta for sv in sim_values): 87 | p_c["heap"] = [ 88 | (get_edge_length(network, ed), ed) for ed in get_edges(p_c["path"]) 89 | ][1:-1] 90 | heapify(p_c["heap"]) 91 | chosen_paths.append(p_c) 92 | sim_values.append( 93 | (1.0, len(p_c["heap"]), len(chosen_paths) - 1) 94 | ) # Append similarity p_c to p_c 95 | 96 | # We want to update p_c by making it more dissimilar from the most similar path available. 97 | # To do this, we find the most similar path that has edges left in its heap, and remove top edge from net 98 | sim_values.sort(key=lambda k: k[0], reverse=True) 99 | most_similar_idx = next(sv[2] for sv in sim_values if sv[1] > 0) 100 | most_similar = chosen_paths[most_similar_idx] 101 | 102 | top_ed = heappop(most_similar["heap"])[1] 103 | ord_top = get_ordered_idx(top_ed)[0] 104 | if ord_top in excluded_eds or ord_top in lengths: 105 | continue 106 | lengths[ord_top] = get_edge_length(network, top_ed) 107 | network[top_ed[0]][top_ed[1]]["length"] = float("inf") 108 | 109 | # Get the shortest path available. Optimisations are put into place: 110 | # * This code uses setting to float("inf") instead of deleting edges, much faster 111 | # * Heuristically, the dijkstra call can be done with the cutoff set to fiber_length: it is much easier for 112 | # a call to fail bc the path is too long rather than bc the graph is disconnected in this setting 113 | # * Cutoff reduces runtime considerably in case of no convenient available 114 | # * If that fails, check for disconnection with BFS 115 | try: 116 | length, path = nx.single_source_dijkstra( 117 | network, -pair[0], -pair[1], cutoff=fiber_len, weight="length" 118 | ) 119 | p_c = {"path": path, "length": length} 120 | except nx.NetworkXNoPath: 121 | cc = nx.node_connected_component(network, -pair[0]) 122 | if -pair[1] in cc: 123 | # Connected: current path is too long and so will be the future ones 124 | break 125 | # Disconnected: this edge must not be removed, or disconnection. Revert. 126 | network[top_ed[0]][top_ed[1]]["length"] = lengths[ord_top] 127 | del lengths[ord_top] 128 | excluded_eds.add(ord_top) 129 | 130 | # Rebuild the original network, then delete the added nodes 131 | for ed in lengths: 132 | network[ed[0]][ed[1]]["length"] = lengths[ed] 133 | for gnd in pair: 134 | network.remove_node(-gnd) 135 | return [(ch["path"], ch["length"]) for ch in chosen_paths] 136 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/ksp_rout_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import networkx as nx 3 | from geopy.distance import great_circle 4 | 5 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 6 | from icarus_simulator.structure_definitions import GridPos, SdPair, Coverage, LbSet 7 | 8 | 9 | class KSPRoutStrat(BaseRoutingStrat): 10 | def __init__(self, desirability_stretch: float, k: int, **kwargs): 11 | super().__init__() 12 | self.desirability_stretch = desirability_stretch 13 | self.k = k 14 | if len(kwargs) > 0: 15 | pass # Appease the unused param inspection 16 | 17 | @property 18 | def name(self) -> str: 19 | return "ksp" 20 | 21 | @property 22 | def param_description(self) -> str: 23 | return f"{self.desirability_stretch}k{self.k}" 24 | 25 | def compute( 26 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 27 | ) -> LbSet: 28 | in_grid, out_grid = pair[0], pair[1] 29 | fiber_len = ( 30 | great_circle( 31 | (grid[in_grid].lat, grid[in_grid].lon), 32 | (grid[out_grid].lat, grid[out_grid].lon), 33 | ).meters 34 | * self.desirability_stretch 35 | ) 36 | # Add the gnd nodes 37 | for gnd in pair: 38 | for dst_sat in coverage[gnd]: 39 | network.add_edge(-gnd, dst_sat, length=coverage[gnd][dst_sat]) 40 | 41 | # Compute the first n shortest paths 42 | lbset: LbSet = [] 43 | cnt = 0 44 | gen = nx.shortest_simple_paths(network, -pair[0], -pair[1], weight="length") 45 | for path in gen: 46 | path_length = 0.0 47 | for i in range(len(path) - 1): 48 | path_length += network[path[i]][path[i + 1]]["length"] 49 | if path_length > fiber_len: 50 | break 51 | lbset.append((path, path_length)) 52 | cnt += 1 53 | if cnt >= self.k: 54 | break 55 | # Remove the added nodes 56 | for gnd in pair: 57 | network.remove_node(-gnd) 58 | return lbset 59 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/routing/ssp_rout_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import networkx as nx 3 | from geopy.distance import great_circle 4 | 5 | from icarus_simulator.strategies.routing.base_routing_strat import BaseRoutingStrat 6 | from icarus_simulator.structure_definitions import ( 7 | GridPos, 8 | SdPair, 9 | Coverage, 10 | LbSet, 11 | PathInfo, 12 | ) 13 | 14 | 15 | class SSPRoutStrat(BaseRoutingStrat): 16 | def __init__(self, desirability_stretch: float, **kwargs): 17 | super().__init__() 18 | self.desirability_stretch = desirability_stretch 19 | if len(kwargs) > 0: 20 | pass # Appease the unused param inspection 21 | 22 | @property 23 | def name(self) -> str: 24 | return "ssp" 25 | 26 | @property 27 | def param_description(self) -> str: 28 | return f"{self.desirability_stretch}" 29 | 30 | def compute( 31 | self, pair: SdPair, grid: GridPos, network: nx.Graph, coverage: Coverage 32 | ) -> LbSet: 33 | in_grid, out_grid = pair[0], pair[1] 34 | fiber_len = ( 35 | great_circle( 36 | (grid[in_grid].lat, grid[in_grid].lon), 37 | (grid[out_grid].lat, grid[out_grid].lon), 38 | ).meters 39 | * self.desirability_stretch 40 | ) 41 | # Add the gnd nodes to the network, flipping the sign 42 | for gnd in pair: 43 | for dst_sat in coverage[gnd]: 44 | network.add_edge(-gnd, dst_sat, length=coverage[gnd][dst_sat]) 45 | 46 | # Compute the shortest path 47 | lbset: LbSet = [] 48 | try: 49 | length, path = nx.single_source_dijkstra( 50 | network, -pair[0], -pair[1], cutoff=fiber_len, weight="length" 51 | ) 52 | pi: PathInfo = (path, length) 53 | lbset.append(pi) 54 | except nx.NetworkXNoPath: 55 | pass 56 | 57 | # Remove the added nodes 58 | for gnd in pair: 59 | network.remove_node(-gnd) 60 | return lbset 61 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_bneck/__init__.py: -------------------------------------------------------------------------------- 1 | from .detect_bneck_strat import DetectBneckStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_bneck/base_zone_bneck_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import ( 12 | BwData, 13 | AttackData, 14 | PathEdgeData, 15 | Edge, 16 | ) 17 | 18 | 19 | class BaseZoneBneckStrat(BaseStrat): 20 | @abstractmethod 21 | def compute( 22 | self, 23 | bw_data: BwData, 24 | atk_data: AttackData, 25 | path_edges: PathEdgeData, 26 | tot_cross_zone_paths: int, 27 | ) -> List[List[Edge]]: 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_bneck/detect_bneck_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import copy 3 | 4 | from typing import List 5 | 6 | from icarus_simulator.strategies.zone_bneck.base_zone_bneck_strat import ( 7 | BaseZoneBneckStrat, 8 | ) 9 | from icarus_simulator.structure_definitions import ( 10 | PathEdgeData, 11 | Edge, 12 | AttackData, 13 | BwData, 14 | ) 15 | 16 | 17 | class DetectBneckStrat(BaseZoneBneckStrat): 18 | @property 19 | def name(self) -> str: 20 | return "dtct" 21 | 22 | @property 23 | def param_description(self) -> None: 24 | return None 25 | 26 | def compute( 27 | self, 28 | bw_data: BwData, 29 | atk_data: AttackData, 30 | path_edges: PathEdgeData, 31 | tot_cross_zone_paths: int, 32 | ) -> List[List[Edge]]: 33 | incrs = {ed: atk_data[ed].detectability for ed in path_edges} 34 | costs = {ed: (incrs[ed] / len(path_edges[ed])) for ed in path_edges} 35 | sorted_path_edges = sorted(costs.items(), key=lambda k: k[1]) 36 | 37 | # Then pick edges until full coverage is reached, with 3 specific attempts 38 | inf = 90000000000000 # High value given to the already chosen edges 39 | bnecks = [] 40 | for attempt_id in range(3): 41 | costs_copy = copy.deepcopy(costs) 42 | covered_paths, picked_edges, best_ed = set(), [], None 43 | 44 | # Force insertion of the 1st, 2nd and 3rd best elements in each attempt, to shake things up 45 | try: 46 | best_ed = sorted_path_edges[attempt_id][0] # Get the key of the edge 47 | except IndexError: # The edge cannot be inserted, not enough candidates 48 | continue 49 | picked_edges.append(best_ed) 50 | costs_copy[best_ed] = inf 51 | covered_paths.update(path_edges[best_ed]) 52 | 53 | # Insert the rest of the edges 54 | while len(covered_paths) < tot_cross_zone_paths: 55 | # First, update the cost function to only include the non-covered paths 56 | # costs[ed] = detectability / new_paths_covered 57 | for ed in costs_copy: 58 | if costs_copy[ed] < inf: 59 | diff_len = len(path_edges[ed].difference(covered_paths)) 60 | if diff_len == 0: 61 | costs_copy[ed] = inf # useless, does not cover anything new 62 | else: 63 | costs_copy[ed] = incrs[ed] / diff_len 64 | 65 | best_ed = min(costs_copy, key=lambda k: costs_copy[k]) 66 | if ( 67 | costs_copy[best_ed] == inf 68 | ): # No more edges to choose from, we chose all of them 69 | break 70 | picked_edges.append(best_ed) 71 | costs_copy[best_ed] = inf 72 | covered_paths.update(path_edges[best_ed]) 73 | 74 | # Greedily remove the redundant links 75 | # Criterion (minimised, maximised): (min_redundancy, atk_bw) 76 | redundancies = [0] * tot_cross_zone_paths 77 | for pe in picked_edges: 78 | for p in path_edges[pe]: 79 | redundancies[p] += 1 80 | # Pick the most redundant one each time and remove it 81 | while True: 82 | removable_edges = [] 83 | for pe in picked_edges: 84 | removable, min_redundancy = True, 999999 85 | for p in path_edges[pe]: 86 | min_redundancy = min(min_redundancy, redundancies[p]) 87 | if redundancies[p] - 1 == 0: 88 | removable = False 89 | break 90 | if removable: 91 | removable_edges.append( 92 | (min_redundancy, bw_data[pe].get_remaining_bw(), pe) 93 | ) 94 | if len(removable_edges) == 0: 95 | break 96 | chosen = min(removable_edges, key=lambda k: (k[0], -k[1]))[2] 97 | for p in path_edges[chosen]: 98 | redundancies[p] -= 1 99 | picked_edges.remove(chosen) 100 | 101 | # Check if the found bottleneck covers all the paths to be covered 102 | covered_paths = set() 103 | for link in picked_edges: 104 | covered_paths.update(path_edges[link]) 105 | if len(covered_paths) == tot_cross_zone_paths: 106 | bnecks.append(picked_edges) 107 | 108 | return bnecks 109 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_build/__init__.py: -------------------------------------------------------------------------------- 1 | from .k_closest_zone_strat import KclosestZoneStrat 2 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_build/base_zone_build_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import Tuple, List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import GridPos 12 | 13 | 14 | class BaseZoneBuildStrat(BaseStrat): 15 | @abstractmethod 16 | def compute( 17 | self, grid_pos: GridPos, center1: int, center2: int 18 | ) -> Tuple[List[int], List[int]]: 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_build/k_closest_zone_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import numpy as np 3 | 4 | from typing import Tuple, List 5 | from scipy.spatial.ckdtree import cKDTree 6 | 7 | from icarus_simulator.sat_core.coordinate_util import geo2cart 8 | from icarus_simulator.strategies.zone_build.base_zone_build_strat import ( 9 | BaseZoneBuildStrat, 10 | ) 11 | from icarus_simulator.structure_definitions import GridPos 12 | 13 | 14 | class KclosestZoneStrat(BaseZoneBuildStrat): 15 | def __init__(self, size: int, **kwargs): 16 | super().__init__() 17 | self.size = size 18 | if len(kwargs) > 0: 19 | pass # Appease the unused param inspection 20 | 21 | @property 22 | def name(self) -> str: 23 | return "kcl" 24 | 25 | @property 26 | def param_description(self) -> str: 27 | return f"{self.size}" 28 | 29 | def compute( 30 | self, grid_pos: GridPos, center1: int, center2: int 31 | ) -> Tuple[List[int], List[int]]: 32 | grid_cart = np.zeros((len(grid_pos), 3)) 33 | grid_map = {} 34 | for i, grid_id in enumerate(grid_pos): 35 | grid_map[i] = grid_id 36 | grid_cart[i] = geo2cart( 37 | {"elev": 0, "lon": grid_pos[grid_id].lon, "lat": grid_pos[grid_id].lat} 38 | ) 39 | 40 | # Put the homogeneous grid into a KD-tree and query all the points, summing values to the closest grid point 41 | closest = [] 42 | kd = cKDTree(grid_cart) 43 | for point in [grid_pos[center1], grid_pos[center2]]: 44 | _, closest_grid_indices = kd.query( 45 | geo2cart({"elev": 0, "lon": point.lon, "lat": point.lat}), k=self.size 46 | ) 47 | if type(closest_grid_indices) == int: 48 | closest_grid_indices = [closest_grid_indices] 49 | closest.append([grid_map[idx] for idx in closest_grid_indices]) 50 | return closest[0], closest[1] 51 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_edges/__init__.py: -------------------------------------------------------------------------------- 1 | from .dwl_zone_strat import DWLZoneStrat 2 | from .isl_zone_strat import ISLZoneStrat 3 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_edges/base_zone_edges_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | 9 | from icarus_simulator.strategies.base_strat import BaseStrat 10 | from icarus_simulator.structure_definitions import Edge 11 | 12 | 13 | class BaseZoneEdgesStrat(BaseStrat): 14 | @abstractmethod 15 | def compute(self, edge: Edge) -> bool: 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_edges/dwl_zone_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from icarus_simulator.strategies.zone_edges.base_zone_edges_strat import ( 3 | BaseZoneEdgesStrat, 4 | ) 5 | from icarus_simulator.structure_definitions import Edge 6 | 7 | 8 | class DWLZoneStrat(BaseZoneEdgesStrat): 9 | @property 10 | def name(self) -> str: 11 | return "dwl" 12 | 13 | @property 14 | def param_description(self) -> None: 15 | return None 16 | 17 | def compute(self, edge: Edge) -> bool: 18 | return edge[1] == -1 19 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_edges/isl_zone_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from icarus_simulator.strategies.zone_edges.base_zone_edges_strat import ( 3 | BaseZoneEdgesStrat, 4 | ) 5 | from icarus_simulator.structure_definitions import Edge 6 | 7 | 8 | class ISLZoneStrat(BaseZoneEdgesStrat): 9 | @property 10 | def name(self) -> str: 11 | return "isl" 12 | 13 | @property 14 | def param_description(self) -> None: 15 | return None 16 | 17 | def compute(self, edge: Edge) -> bool: 18 | return -1 not in edge 19 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_select/__init__.py: -------------------------------------------------------------------------------- 1 | from .list_zone_strat import ListZoneStrat 2 | from .rand_zone_strat import RandZoneStrat 3 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_select/base_zone_select_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Base strategy class for a specific task. 4 | This class is open for custom extension, in order to create different execution strategies for this task. 5 | See BaseStrategy for more details. 6 | """ 7 | from abc import abstractmethod 8 | from typing import Tuple, List 9 | 10 | from icarus_simulator.strategies.base_strat import BaseStrat 11 | from icarus_simulator.structure_definitions import GridPos 12 | 13 | 14 | class BaseZoneSelectStrat(BaseStrat): 15 | @abstractmethod 16 | def compute(self, grid_pos: GridPos) -> List[Tuple[int, int]]: 17 | raise NotImplementedError 18 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_select/list_zone_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from typing import Tuple, List 3 | 4 | from icarus_simulator.strategies.zone_select.base_zone_select_strat import ( 5 | BaseZoneSelectStrat, 6 | ) 7 | from icarus_simulator.structure_definitions import GridPos 8 | 9 | 10 | class ListZoneStrat(BaseZoneSelectStrat): 11 | def __init__(self, centers: List[Tuple[int, int]], **kwargs): 12 | super().__init__() 13 | self.centers = centers 14 | if len(kwargs) > 0: 15 | pass # Appease the unused param inspection 16 | 17 | @property 18 | def name(self) -> str: 19 | return "lst" 20 | 21 | @property 22 | def param_description(self) -> str: 23 | return f"{self.centers}" 24 | 25 | def compute(self, grid_pos: GridPos) -> List[Tuple[int, int]]: 26 | for tup in self.centers: 27 | for loc in tup: 28 | assert loc in grid_pos.keys() 29 | return self.centers 30 | -------------------------------------------------------------------------------- /icarus_simulator/strategies/zone_select/rand_zone_strat.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import random 3 | 4 | from typing import Tuple, List 5 | 6 | from icarus_simulator.strategies.zone_select.base_zone_select_strat import ( 7 | BaseZoneSelectStrat, 8 | ) 9 | from icarus_simulator.structure_definitions import GridPos 10 | 11 | 12 | class RandZoneStrat(BaseZoneSelectStrat): 13 | def __init__(self, samples: int, **kwargs): 14 | super().__init__() 15 | self.samples = samples 16 | if len(kwargs) > 0: 17 | pass # Appease the unused param inspection 18 | 19 | @property 20 | def name(self) -> str: 21 | return "ran" 22 | 23 | @property 24 | def param_description(self) -> str: 25 | return f"{self.samples}" 26 | 27 | def compute(self, grid_pos: GridPos) -> List[Tuple[int, int]]: 28 | random.seed("Icarus") 29 | indices, locs, grid_pts = [], set(), list(grid_pos.keys()) 30 | for i in range(self.samples): 31 | loc1, loc2 = tuple(random.sample(grid_pts, 2)) 32 | indices.append((loc1, loc2)) 33 | return indices 34 | -------------------------------------------------------------------------------- /icarus_simulator/structure_definitions.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | Definitions for aliases and custom data structures used in the predefined phases and strategies of the library. 4 | """ 5 | 6 | from dataclasses import dataclass, field 7 | from typing import List, Tuple, Dict, Any, Set, Optional 8 | 9 | from .sat_core.coordinate_util import GeodeticPosition 10 | 11 | Length = float 12 | Pname = str 13 | PropertyDict = Dict[Pname, Any] 14 | DependencyDict = Dict[Pname, Set[str]] 15 | 16 | 17 | # Satellite and Earth positions 18 | @dataclass 19 | class GeoPoint: 20 | # Extension of the geodetic position class defined in sat_core library 21 | lat: float 22 | lon: float 23 | elev: float 24 | 25 | @staticmethod 26 | def from_geo_pos(pos: GeodeticPosition) -> "GeoPoint": 27 | return GeoPoint(pos["lat"], pos["lon"], pos["elev"]) 28 | 29 | def to_geo_pos(self) -> GeodeticPosition: 30 | return {"lat": self.lat, "lon": self.lon, "elev": self.elev} 31 | 32 | 33 | @dataclass 34 | class GridPoint(GeoPoint): 35 | weight: float = 0.0 36 | surface: float = 0.0 37 | 38 | 39 | @dataclass 40 | class IslInfo: 41 | sat1: int 42 | sat2: int 43 | length: float 44 | 45 | 46 | # List definitions associate each position to an index 47 | SatPos = Dict[int, GeoPoint] 48 | GridPos = Dict[int, GridPoint] 49 | 50 | # Coverage -> first id is the gnd index, second is the satellite, float is the distance gnd-sat 51 | Coverage = Dict[int, Dict[int, Length]] 52 | 53 | # Routing 54 | SdPair = Tuple[int, int] 55 | Path = List[int] 56 | TuplePath = Tuple[int, ...] 57 | PathInfo = Tuple[Path, Length] 58 | LbSet = List[PathInfo] 59 | PathData = Dict[SdPair, LbSet] # The sdpair structure is always ordered numerically 60 | PathId = Tuple[int, int, int] 61 | 62 | # Edges 63 | Edge = Tuple[int, int] # An edge is a pair of sat indices, or gnd-sat indices 64 | # The set contains the indices of cross zone paths covered by the edge 65 | PathEdgeData = Dict[Edge, Set[int]] 66 | 67 | 68 | @dataclass 69 | class TempEdgeInfo: 70 | paths_through: List[PathId] 71 | centrality: float 72 | source_gridpoints: Set[int] 73 | 74 | 75 | @dataclass 76 | class EdgeInfo: 77 | paths_through: List[PathId] 78 | centrality: float = 0.0 79 | cov_centr: float = 0.0 80 | 81 | 82 | EdgeData = Dict[Edge, EdgeInfo] # The Edge here must be ordered numerically 83 | 84 | 85 | # Traffic matrix 86 | @dataclass 87 | class BwInfo: 88 | idle_bw: int = 0 89 | capacity: int = 0 90 | 91 | def get_remaining_bw(self) -> int: 92 | return self.capacity - self.idle_bw 93 | 94 | 95 | BwData = Dict[Edge, BwInfo] 96 | 97 | 98 | # SSingle-target attacks 99 | @dataclass 100 | class PairInfo: 101 | directions: Set[TuplePath] = field(default_factory=set) 102 | prob: float = 0.0 103 | tot: int = 0 104 | 105 | 106 | DirectionData = Dict[TuplePath, List[SdPair]] 107 | PairData = Dict[SdPair, PairInfo] 108 | AtkFlowSet = Set[Tuple[SdPair, int]] 109 | 110 | 111 | @dataclass 112 | class AttackInfo: 113 | cost: int 114 | detectability: int 115 | flows_on_trg: int 116 | atkflowset: AtkFlowSet 117 | 118 | 119 | AttackData = Dict[Edge, Optional[AttackInfo]] 120 | 121 | Zone = List[int] 122 | TupleZone = Tuple[int, ...] 123 | 124 | 125 | @dataclass 126 | class ZoneAttackInfo(AttackInfo): 127 | cross_zone_paths: List[Path] 128 | bottlenecks: List[Edge] 129 | distance: float 130 | 131 | 132 | ZoneAttackData = Dict[Tuple[TupleZone, TupleZone], Optional[ZoneAttackInfo]] 133 | -------------------------------------------------------------------------------- /icarus_simulator/utils.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | File containing utility functions 4 | """ 5 | import math 6 | from typing import Tuple, List 7 | 8 | 9 | def get_ordered_idx(idx: Tuple[int, int]): 10 | if idx[0] > idx[1]: 11 | return (idx[1], idx[0]), False 12 | return idx, True 13 | 14 | 15 | def get_edges(list_elements, excl_start=0, excl_end=0): 16 | minus = 1 + excl_end 17 | for i in range(excl_start, len(list_elements) - minus): 18 | yield list_elements[i], list_elements[i + 1] 19 | 20 | 21 | def similarity(path1, path2, len1, len2, network): 22 | p1_eds, p2_eds = set(get_edges(path1)), set(get_edges(path2)) 23 | common_eds = p1_eds.intersection(p2_eds) 24 | numer = sum([get_edge_length(network, ed) for ed in common_eds]) 25 | return numer / min(len1, len2) 26 | 27 | 28 | def get_edge_length(network, ed, prop="length"): 29 | return network[ed[0]][ed[1]][prop] 30 | 31 | 32 | def compute_intervals_uniform(length: int, cpus: int) -> List[Tuple[int, int]]: 33 | if length < cpus: 34 | cpus = length 35 | itvls = [] 36 | prev_e = 0 37 | uniform_measure = int(math.ceil(length / cpus)) 38 | for i in range(cpus - 1): 39 | s = prev_e 40 | if (length - s) / (cpus - i) <= uniform_measure - 1: 41 | e = s + uniform_measure - 1 42 | else: 43 | e = s + uniform_measure 44 | prev_e = e 45 | itvls.append((s, e)) 46 | itvls.append((prev_e, length)) 47 | return itvls 48 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Tommaso Ciussani and Giacomo Giuliari 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /sat_plotter/__init__.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | from .geo_plot_builder import GeoPlotBuilder 3 | -------------------------------------------------------------------------------- /sat_plotter/geo_plot_builder.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | This Builder object builds a geographic plot step by step. The order in which methods are called influences the plot. 4 | The class is self-explanatory, method names add what they promise on the globe map. 5 | """ 6 | import numpy as np 7 | import plotly.graph_objects as go 8 | 9 | from matplotlib import cm 10 | from typing import Dict, List 11 | 12 | from icarus_simulator.structure_definitions import ( 13 | GeoPoint, 14 | IslInfo, 15 | SatPos, 16 | GridPos, 17 | Path, 18 | Edge, 19 | ) 20 | 21 | 22 | class GeoPlotBuilder: 23 | viridis = cm.get_cmap("Spectral_r", 12) # Spectral_r 24 | 25 | LOW = 0 26 | MED = 1 27 | HI = 2 28 | 29 | def __init__(self): 30 | self.fig = go.Figure() 31 | self.transparency = True 32 | self.l_thick = [2, 6, 9] 33 | self.p_thick = [6, 12, 16] 34 | self.is_2d = True 35 | 36 | # Set if the less important links should have lower transparency 37 | def set_transparency(self, transparency: bool) -> "GeoPlotBuilder": 38 | self.transparency = transparency 39 | return self 40 | 41 | # Set the line thickness 42 | def set_line_thickness(self, low: int, med: int, high: int) -> "GeoPlotBuilder": 43 | self.l_thick = [low, med, high] 44 | return self 45 | 46 | def set_point_thickness(self, low: int, med: int, high: int) -> "GeoPlotBuilder": 47 | self.p_thick = [low, med, high] 48 | return self 49 | 50 | # Set if the less important links should have lower transparency 51 | def set_2d(self, is_2d: bool) -> "GeoPlotBuilder": 52 | self.is_2d = is_2d 53 | return self 54 | 55 | # Save the plot to file 56 | def save_to_file(self, fname: str) -> "GeoPlotBuilder": 57 | self._update_layout() 58 | self.fig.write_image(fname, width=3000, height=1800, scale=2.0) 59 | return self 60 | 61 | # Show plot on browser 62 | def show(self) -> "GeoPlotBuilder": 63 | self._update_layout() 64 | self.fig.show() 65 | return self 66 | 67 | # Ensure the map is plotted on an empty graph 68 | def empty(self) -> "GeoPlotBuilder": 69 | self.points( 70 | {0: GeoPoint(0.0, 0.0, 0.0)}, 71 | color="rgba(0,0,0,0.0)", 72 | size_id=self.LOW, 73 | word_hover="", 74 | ) 75 | return self 76 | 77 | # Plot the whole constellation in the background 78 | def constellation(self, sat_pos: SatPos, isls: List[IslInfo]) -> "GeoPlotBuilder": 79 | for isl in isls: 80 | self.path( 81 | [sat_pos[isl.sat1], sat_pos[isl.sat2]], 82 | "rgba(200, 200, 200, 0.5)", 83 | self.LOW, 84 | ) 85 | self.points(sat_pos, "rgba(200, 200, 200, 0.5)", self.LOW, "SAT") 86 | return self 87 | 88 | # Plots a heatmap on the isls 89 | def isl_heatmap( 90 | self, sat_pos: SatPos, isl_values: Dict[Edge, float], max_isl: float = None 91 | ) -> "GeoPlotBuilder": 92 | # Print highest at the top 93 | isl_values = list(sorted(isl_values.items(), key=lambda k: k[1])) 94 | if max_isl is None and len(isl_values) > 0: 95 | max_isl = isl_values[-1][1] 96 | for edge, val in isl_values: 97 | self.path( 98 | [sat_pos[idx] for idx in edge], 99 | self.get_color(float(val), float(max_isl), self.transparency), 100 | self.MED, 101 | "solid", 102 | f"{float(val):,.4f}", 103 | ) 104 | return self 105 | 106 | # Plots a heatmap of both points and isls 107 | def point_heatmap( 108 | self, 109 | point_pos: Dict[int, GeoPoint], 110 | point_values: Dict[int, float], 111 | max_point: float = None, 112 | ) -> "GeoPlotBuilder": 113 | point_values = list(sorted(point_values.items(), key=lambda k: k[1])) 114 | if max_point is None and len(point_values) > 0: 115 | max_point = point_values[-1][1] 116 | for idx, val in point_values: 117 | self.points( 118 | {idx: point_pos[idx]}, 119 | self.get_color(float(val), float(max_point), self.transparency), 120 | self.MED, 121 | "", 122 | ) 123 | return self 124 | 125 | # Plot a list of paths 126 | def path_list( 127 | self, 128 | sat_pos: SatPos, 129 | grid_pos: GridPos, 130 | paths: List[Path], 131 | color: str, 132 | size_id: int, 133 | dash: str = "solid", 134 | ) -> "GeoPlotBuilder": 135 | udls, sat_paths, gnds, ext_sats = [], [], {}, {} 136 | for path in paths: 137 | # Check if start has an uplink 138 | if path[0] < 0: 139 | st = 1 140 | ext_sats[path[1]] = sat_pos[path[1]] 141 | if path[0] != -1: 142 | gnds[-path[0]] = grid_pos[-path[0]] 143 | udls.append([grid_pos[-path[0]], sat_pos[path[1]]]) 144 | else: 145 | st = 0 146 | ext_sats[path[0]] = sat_pos[path[0]] 147 | # Check if the end has a downlink 148 | if path[-1] < 0: 149 | end = len(path) - 1 150 | ext_sats[path[-2]] = sat_pos[path[-2]] 151 | if path[-1] != -1: 152 | gnds[-path[-1]] = grid_pos[-path[-1]] 153 | udls.append([grid_pos[-path[-1]], sat_pos[path[-2]]]) 154 | else: 155 | end = len(path) 156 | ext_sats[path[-1]] = sat_pos[path[-1]] 157 | sat_paths.append([sat_pos[idx] for idx in path[st:end]]) 158 | 159 | # Print the gnd points and uplinks first 160 | low_sz = max(0, size_id - 1) 161 | self.points(gnds, color, low_sz, "GND") 162 | for udl in udls: 163 | self.path(udl, color, low_sz, dash) 164 | # Print the satellite portion 165 | self.points(ext_sats, color, size_id, "SAT") 166 | for sp in sat_paths: 167 | self.path(sp, color, size_id, dash) 168 | return self 169 | 170 | # Plot basics 171 | def points( 172 | self, point_dict: Dict[int, GeoPoint], color: str, size_id: int, word_hover: str 173 | ) -> "GeoPlotBuilder": 174 | lats, lons, texts = [], [], [] 175 | for idx, point in point_dict.items(): 176 | lats.append(point.lat) 177 | lons.append(point.lon) 178 | texts.append(f"{word_hover}{idx} - {point.lat}, {point.lon}") 179 | self.fig.add_trace( 180 | go.Scattergeo( 181 | lon=lons, 182 | lat=lats, 183 | hoverinfo="text", 184 | text=texts, 185 | mode="markers", 186 | marker=dict(size=self.p_thick[size_id], color=color), 187 | ) 188 | ) 189 | return self 190 | 191 | def path( 192 | self, 193 | path_points: List[GeoPoint], 194 | color: str, 195 | size_id: int, 196 | dash: str = "solid", 197 | hover: str = "", 198 | ) -> "GeoPlotBuilder": 199 | # [‘solid’, ‘dot’, ‘dash’, ‘longdash’, ‘dashdot’, ‘longdashdot’] 200 | lats, lons = [], [] 201 | for cur in path_points: 202 | lats.append(cur.lat) 203 | lons.append(cur.lon) 204 | self.fig.add_trace( 205 | go.Scattergeo( 206 | lon=lons, 207 | lat=lats, 208 | hoverinfo="text", 209 | text=hover, 210 | mode="lines", 211 | line=dict(width=self.l_thick[size_id], color=color, dash=dash), 212 | ) 213 | ) 214 | return self 215 | 216 | def arrow( 217 | self, 218 | src_point: GeoPoint, 219 | trg_point: GeoPoint, 220 | color: str, 221 | size_id: int, 222 | hover_info: str, 223 | ) -> "GeoPlotBuilder": 224 | # Draw the line 225 | lats, lons = [src_point.lat, trg_point.lat], [src_point.lon, trg_point.lon] 226 | self.fig.add_trace( 227 | go.Scattergeo( 228 | lon=lons, 229 | lat=lats, 230 | hoverinfo="text", 231 | text=hover_info, 232 | mode="lines", 233 | line=dict(width=self.l_thick[size_id], color=color), 234 | ) 235 | ) 236 | # Draw an arrow with some plotly magic 237 | arrow_len = 3 # the arrow length 238 | arrow_width = 0.2 # 2*arrow_width is the width of the arrow base as triangle 239 | src = np.array([src_point.lon, src_point.lat]) 240 | trg = np.array([trg_point.lon, trg_point.lat]) 241 | v = trg - src 242 | w = v / np.linalg.norm(v) 243 | u = np.array([-v[1], v[0]]) # u orthogonal on w 244 | p = trg - arrow_len * w 245 | s = p - arrow_width * u 246 | t = p + arrow_width * u 247 | self.fig.add_trace( 248 | go.Scattergeo( 249 | lon=[s[0], t[0], trg[0], s[0]], 250 | lat=[s[1], t[1], trg[1], s[1]], 251 | mode="lines", 252 | fill="toself", 253 | fillcolor=color, 254 | line_color=color, 255 | ) 256 | ) 257 | return self 258 | 259 | def _update_layout(self): 260 | self.fig.update_layout( 261 | showlegend=False, 262 | autosize=True, 263 | geo=dict( 264 | resolution=50, 265 | showcountries=False, 266 | countrywidth=0.5, 267 | showland=False, 268 | showocean=False, 269 | oceancolor="rgba(245,245,253, 0.0)", 270 | landcolor="rgba(232,232,232, 0.0)", 271 | countrycolor="rgb(200, 200, 200)", 272 | projection=dict( 273 | type="equirectangular" if self.is_2d else "orthographic" 274 | ), 275 | lonaxis=dict( 276 | showgrid=False, gridcolor="rgb(102, 102, 102)", gridwidth=0.5 277 | ), 278 | lataxis=dict( 279 | showgrid=False, gridcolor="rgb(102, 102, 102)", gridwidth=0.5 280 | ), 281 | ), 282 | ) 283 | 284 | # Method used internally to get colours from the colormap based on a value 285 | @classmethod 286 | def get_color(cls, value: float, max_val: float, transp_adjust=True) -> str: 287 | if max_val > 0.0: 288 | ratio = value / max_val 289 | else: 290 | ratio = 0.0 291 | color = cls.viridis(ratio) 292 | # Adjust transparency to improve aesthetics, if required 293 | if transp_adjust: 294 | transp = 1.02785 + (0.15 - 1.02785) / (1 + (ratio / 0.2377001) ** 2.379258) 295 | if transp < 0.15: 296 | transp = 0.15 297 | else: 298 | transp = 1.0 299 | 300 | return f"rgba({color[0]:.4f}, {color[1]:.4f}, {color[2]:.4f}, {transp:.4f})" 301 | -------------------------------------------------------------------------------- /sat_plotter/stat_plot_builder.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | """ 3 | This Builder object builds a statistical plot step by step. The order in which methods are called influences the plot. 4 | The class is self-explanatory, method names add what they promise on the graph. 5 | This class uses the amazing defaults from Amazingplots by Giacomo Giuliari for colour and dash. 6 | Refer to and edit the file amazingplots.mpstyle for changes in the default style. 7 | """ 8 | import os 9 | import itertools 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | 13 | from matplotlib import cm 14 | from typing import List, Optional 15 | 16 | dirname = os.path.dirname(__file__) 17 | filename = os.path.join(dirname, "amazingplots.mpstyle") 18 | plt.style.use(filename) 19 | 20 | 21 | class StatPlotBuilder: 22 | viridis = cm.get_cmap("Spectral_r", 12) # Spectral_r 23 | 24 | def __init__(self): 25 | self.fig, self.ax = plt.subplots() 26 | self.thick = 5 27 | self.bins = 1000 28 | self.fig.set_size_inches(14, 5) 29 | plt.xticks(fontsize=25) 30 | plt.yticks(fontsize=25) 31 | 32 | # Save the plot to file, jpeg not allowed as extension 33 | def save_to_file(self, fname: str) -> "StatPlotBuilder": 34 | plt.savefig(fname, dpi=300) 35 | return self 36 | 37 | # Show plot 38 | def show(self) -> "StatPlotBuilder": 39 | plt.show() 40 | return self 41 | 42 | # Set the axis labels 43 | def labels(self, x_label: str, y_label: str) -> "StatPlotBuilder": 44 | plt.xlabel(x_label, fontsize=32) 45 | plt.ylabel(y_label, fontsize=32) 46 | return self 47 | 48 | # Add the legend 49 | def legend(self) -> "StatPlotBuilder": 50 | plt.legend(loc="lower right") 51 | return self 52 | 53 | # Set the line width 54 | def set_thickness(self, thickness: int) -> "StatPlotBuilder": 55 | self.thick = thickness 56 | return self 57 | 58 | # Set the bin number for the cdf/pdf 59 | def set_bins(self, bins: int) -> "StatPlotBuilder": 60 | self.bins = bins 61 | return self 62 | 63 | # Set the scaling to zero 64 | def set_zero_y(self) -> "StatPlotBuilder": 65 | self.bins = self.ax.set_ylim(ymin=0) 66 | return self 67 | 68 | # Set y logarithmic scale 69 | def set_log_y(self) -> "StatPlotBuilder": 70 | plt.yscale("log") 71 | return self 72 | 73 | # Set the size 74 | def set_size(self, width: int, height: int) -> "StatPlotBuilder": 75 | self.fig.set_size_inches(width, height) 76 | return self 77 | 78 | # Add a CDF from data 79 | def cdf( 80 | self, data: List, label: str, weights: Optional[List] = None 81 | ) -> "StatPlotBuilder": 82 | # Weights None implies weights ignored 83 | _, _, patches = self.ax.hist( 84 | data, 85 | cumulative=True, 86 | density=True, 87 | bins=self.bins, 88 | histtype="step", 89 | label=label, 90 | linewidth=self.thick, 91 | joinstyle="round", 92 | capstyle="round", 93 | weights=weights, 94 | ) 95 | patches[0].set_xy(patches[0].get_xy()[:-1]) # Avoids vertical line at the end 96 | return self 97 | 98 | # Add a PDF from data 99 | def pdf( 100 | self, data: List, label: str, weights: Optional[List] = None 101 | ) -> "StatPlotBuilder": 102 | _, _, patches = self.ax.hist( 103 | data, 104 | bins=self.bins, 105 | histtype="step", 106 | label=label, 107 | weights=weights, 108 | linewidth=self.thick, 109 | joinstyle="round", 110 | capstyle="round", 111 | ) 112 | patches[0].set_xy(patches[0].get_xy()[:-1]) 113 | return self 114 | 115 | # Histogram 116 | def hist(self, data: List, label: str) -> "StatPlotBuilder": 117 | self.ax.hist(data, bins=self.bins, label=label) 118 | return self 119 | 120 | # Line plot xy 121 | def line_xy(self, x: List, y: List, label: str) -> "StatPlotBuilder": 122 | self.ax.plot("x", "y", data={"x": x, "y": y}, label=label) 123 | return self 124 | 125 | # Point plot xy 126 | def point_xy(self, x: List, y: List, label: str) -> "StatPlotBuilder": 127 | self.ax.scatter( 128 | "x", "y", s="s", data={"x": x, "y": y, "s": [20] * len(x)}, label=label 129 | ) 130 | return self 131 | 132 | # Plot a binned median line based on an xy dataset 133 | def binned_line_xy(self, x: List, y: List, label: str): 134 | min_x = min(x) 135 | quant_intvl = (max(x) - min_x) / self.bins 136 | quant_x = [ 137 | (quant_intvl * round(d / quant_intvl), orig_idx) 138 | for orig_idx, d in enumerate(x) 139 | ] 140 | quant_x.sort(key=lambda k: k[0]) 141 | temp, quant_y = [], [] 142 | for item, group in itertools.groupby(quant_x, key=lambda k: k[0]): 143 | temp.append(item) 144 | bin_costs = [] 145 | for g in group: 146 | bin_costs.append(y[g[1]]) 147 | perc_cost = np.percentile(bin_costs, [50]) 148 | quant_y.append(perc_cost[0]) 149 | quant_x = temp 150 | self.line_xy(quant_x, quant_y, f"Median {label}") 151 | return self 152 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2020 Tommaso Ciussani and Giacomo Giuliari 2 | import setuptools 3 | 4 | setuptools.setup( 5 | name="icarus_simulator", 6 | version="0.0.1", 7 | author="Tommaso Ciussani", 8 | author_email="tommasoc@student.ethz.ch", 9 | description="Simulator for the Icarus attack", 10 | packages=["icarus_simulator"], 11 | python_requires=">=3.6", 12 | ) 13 | 14 | setuptools.setup( 15 | name="sat_plotter", 16 | version="0.0.1", 17 | author="Tommaso Ciussani", 18 | author_email="tommasoc@student.ethz.ch", 19 | description="Plot builders for satellite networks", 20 | packages=["sat_plotter"], 21 | python_requires=">=3.6", 22 | ) 23 | --------------------------------------------------------------------------------