kashima 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kashima-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.2
2
+ Name: kashima
3
+ Version: 1.0.0
4
+ Summary: Machine Learning Tools for Geosciences.
5
+ Home-page: https://github.com/averriK/kashima
6
+ Author: Alejandro Verri Kozlowski
7
+ Author-email: averri@fi.uba.ar
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.6
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pandas
13
+ Requires-Dist: numpy
14
+ Requires-Dist: folium
15
+ Requires-Dist: geopandas
16
+ Requires-Dist: pyproj
17
+ Requires-Dist: requests
18
+ Requires-Dist: branca
19
+ Requires-Dist: geopy
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: dataclasses; python_version < "3.7"
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # kashimaPy
@@ -0,0 +1 @@
1
+ # kashimaPy
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.2
2
+ Name: kashima
3
+ Version: 1.0.0
4
+ Summary: Machine Learning Tools for Geosciences.
5
+ Home-page: https://github.com/averriK/kashima
6
+ Author: Alejandro Verri Kozlowski
7
+ Author-email: averri@fi.uba.ar
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.6
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pandas
13
+ Requires-Dist: numpy
14
+ Requires-Dist: folium
15
+ Requires-Dist: geopandas
16
+ Requires-Dist: pyproj
17
+ Requires-Dist: requests
18
+ Requires-Dist: branca
19
+ Requires-Dist: geopy
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: dataclasses; python_version < "3.7"
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # kashimaPy
@@ -0,0 +1,19 @@
1
+ README.md
2
+ setup.py
3
+ dsra/__init__.py
4
+ gmdb/__init__.py
5
+ gmdp/__init__.py
6
+ gmpe/__init__.py
7
+ gmsp/__init__.py
8
+ kashima.egg-info/PKG-INFO
9
+ kashima.egg-info/SOURCES.txt
10
+ kashima.egg-info/dependency_links.txt
11
+ kashima.egg-info/requires.txt
12
+ kashima.egg-info/top_level.txt
13
+ mapper/__init__.py
14
+ mapper/base_map.py
15
+ mapper/blast_catalog.py
16
+ mapper/config.py
17
+ mapper/event_map.py
18
+ mapper/usgs_catalog.py
19
+ mapper/utils.py
@@ -0,0 +1,12 @@
1
+ pandas
2
+ numpy
3
+ folium
4
+ geopandas
5
+ pyproj
6
+ requests
7
+ branca
8
+ geopy
9
+ matplotlib
10
+
11
+ [:python_version < "3.7"]
12
+ dataclasses
@@ -0,0 +1,6 @@
1
+ dsra
2
+ gmdb
3
+ gmdp
4
+ gmpe
5
+ gmsp
6
+ mapper
@@ -0,0 +1,19 @@
1
+ from .config import MapConfig, EventConfig, FaultConfig, StationConfig, BlastConfig
2
+ from .utils import calculate_zoom_level, EARTH_RADIUS_KM
3
+ from .base_map import BaseMap
4
+ from .usgs_catalog import USGSCatalog
5
+ from .blast_catalog import BlastCatalog
6
+ from .event_map import EventMap
7
+ __all__ = [
8
+ 'MapConfig',
9
+ 'EventConfig',
10
+ 'FaultConfig',
11
+ 'StationConfig',
12
+ 'BlastConfig',
13
+ 'calculate_zoom_level',
14
+ 'EARTH_RADIUS_KM',
15
+ 'BaseMap',
16
+ 'USGSCatalog',
17
+ 'BlastCatalog',
18
+ 'EventMap'
19
+ ]
@@ -0,0 +1,337 @@
1
+ import folium
2
+ import logging
3
+ import math
4
+ from .config import MapConfig, EventConfig, FaultConfig, StationConfig, TILE_LAYER_CONFIGS
5
+ from .utils import EARTH_RADIUS_KM
6
+ from pyproj import Transformer
7
+ from folium import plugins
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class BaseMap:
12
+ def __init__(
13
+ self,
14
+ map_config: MapConfig,
15
+ event_config: EventConfig = None,
16
+ fault_config: FaultConfig = None,
17
+ station_config: StationConfig = None,
18
+ log_level: int = logging.INFO
19
+ ):
20
+ """
21
+ Initialize the BaseMap object.
22
+
23
+ Parameters:
24
+ map_config (MapConfig): Configuration for the map.
25
+ event_config (EventConfig, optional): Configuration for seismic events.
26
+ fault_config (FaultConfig, optional): Configuration for fault lines.
27
+ station_config (StationConfig, optional): Configuration for seismic stations.
28
+ log_level (int, optional): Logging level. Defaults to logging.INFO.
29
+ """
30
+ logger.setLevel(log_level)
31
+ # Validate that map_config is provided and is of correct type
32
+ if not isinstance(map_config, MapConfig):
33
+ logger.error("map_config must be an instance of MapConfig.")
34
+ raise TypeError("map_config must be an instance of MapConfig.")
35
+ self.map_config = map_config
36
+
37
+ # Validate event_config if provided
38
+ if event_config is not None and not isinstance(event_config, EventConfig):
39
+ logger.error("event_config must be an instance of EventConfig.")
40
+ raise TypeError("event_config must be an instance of EventConfig.")
41
+ self.event_config = event_config
42
+
43
+ # Validate fault_config if provided
44
+ if fault_config is not None and not isinstance(fault_config, FaultConfig):
45
+ logger.error("fault_config must be an instance of FaultConfig.")
46
+ raise TypeError("fault_config must be an instance of FaultConfig.")
47
+ self.fault_config = fault_config
48
+
49
+ # Validate station_config if provided
50
+ if station_config is not None and not isinstance(station_config, StationConfig):
51
+ logger.error("station_config must be an instance of StationConfig.")
52
+ raise TypeError("station_config must be an instance of StationConfig.")
53
+ self.station_config = station_config
54
+
55
+ self.events = None
56
+ self.color_map = None
57
+ self.m = None
58
+ self.stations = None
59
+
60
+ # Cap the radius to half the Earth's circumference (~20,037 km)
61
+ max_radius = EARTH_RADIUS_KM * math.pi
62
+ if self.map_config.radius_km > max_radius:
63
+ logger.warning(f"radius_km exceeds half the Earth's circumference. Capping to {max_radius} km.")
64
+ self.map_config.radius_km = max_radius
65
+
66
+ # Ensure radius_km is positive
67
+ if self.map_config.radius_km <= 0:
68
+ logger.error("radius_km must be a positive value.")
69
+ raise ValueError("radius_km must be a positive value.")
70
+
71
+ def initialize_map(self):
72
+ """Initialize the folium map object."""
73
+ logger.info("Initializing folium map...")
74
+
75
+ self.m = folium.Map(
76
+ location=[self.map_config.latitude, self.map_config.longitude],
77
+ zoom_start=self.map_config.base_zoom_level,
78
+ min_zoom=self.map_config.min_zoom_level,
79
+ max_zoom=self.map_config.max_zoom_level,
80
+ tiles=None # Prevents adding the default OpenStreetMap layer
81
+ )
82
+
83
+ # Add tile layers
84
+ self.add_tile_layers()
85
+
86
+ logger.info("Folium map initialized.")
87
+
88
+ def add_tooltip_css(self) -> None:
89
+ """Add custom CSS for tooltip styling."""
90
+ tooltip_style = """
91
+ <style>
92
+ .folium-tooltip {
93
+ width: 200px;
94
+ white-space: normal;
95
+ }
96
+ </style>
97
+ """
98
+ self.m.get_root().header.add_child(folium.Element(tooltip_style))
99
+
100
+ def set_map_bounds(self):
101
+ """Set the map bounds based on the site location and radius."""
102
+ logger.info("Setting map bounds...")
103
+ bounds = self.get_combined_bounds()
104
+ self.m.fit_bounds(bounds)
105
+ logger.info("Map bounds set.")
106
+
107
+ def get_combined_bounds(self):
108
+ """Calculate and return the combined bounds for the map."""
109
+ logger.info("Calculating combined bounds...")
110
+
111
+ # Start with the bounding box around the site location
112
+ site_bounds = self.get_bounding_box()
113
+
114
+ # Initialize lists to collect all coordinates
115
+ all_lats = [site_bounds[0][0], site_bounds[1][0]] # min_lat, max_lat
116
+ all_lons = [site_bounds[0][1], site_bounds[1][1]] # min_lon, max_lon
117
+
118
+ # Include event coordinates if available
119
+ if self.events is not None:
120
+ all_lats.extend(self.events['latitude'].tolist())
121
+ all_lons.extend(self.events['longitude'].tolist())
122
+
123
+ # Include station coordinates if available
124
+ if self.stations is not None:
125
+ all_lats.extend(self.stations['latitude'].tolist())
126
+ all_lons.extend(self.stations['longitude'].tolist())
127
+
128
+ # Compute the min and max for latitude and longitude
129
+ min_lat, max_lat = min(all_lats), max(all_lats)
130
+ min_lon, max_lon = min(all_lons), max(all_lons)
131
+
132
+ bounds = [[min_lat, min_lon], [max_lat, max_lon]]
133
+ logger.info(f"Calculated map bounds: {bounds}")
134
+ return bounds
135
+
136
+ def get_bounding_box(self, padding_factor=1.1):
137
+ """Calculate the bounding box for a given point and radius with padding."""
138
+ lat_rad = math.radians(self.map_config.latitude)
139
+ lon_rad = math.radians(self.map_config.longitude)
140
+ angular_distance = (self.map_config.radius_km * padding_factor) / EARTH_RADIUS_KM
141
+
142
+ # Limit angular_distance to pi radians
143
+ angular_distance = min(angular_distance, math.pi)
144
+
145
+ min_lat = math.degrees(lat_rad - angular_distance)
146
+ max_lat = math.degrees(lat_rad + angular_distance)
147
+
148
+ # Ensure latitude bounds are within -90 and 90 degrees
149
+ min_lat = max(min_lat, -90)
150
+ max_lat = min(max_lat, 90)
151
+
152
+ cos_lat_rad = math.cos(lat_rad)
153
+
154
+ # Compute delta_lon with clamping to avoid math domain error
155
+ if abs(cos_lat_rad) < 1e-10:
156
+ # At the poles, longitude bounds are irrelevant
157
+ min_lon = -180
158
+ max_lon = 180
159
+ else:
160
+ sin_angular_distance = math.sin(angular_distance)
161
+ ratio = sin_angular_distance / cos_lat_rad
162
+
163
+ # Clamp the ratio between -1 and 1 to avoid math domain error
164
+ ratio = max(-1, min(1, ratio))
165
+
166
+ delta_lon = math.asin(ratio)
167
+ min_lon = math.degrees(lon_rad - delta_lon)
168
+ max_lon = math.degrees(lon_rad + delta_lon)
169
+
170
+ # Normalize longitude to be within -180 to 180 degrees
171
+ min_lon = (min_lon + 180) % 360 - 180
172
+ max_lon = (max_lon + 180) % 360 - 180
173
+
174
+ return [[min_lat, min_lon], [max_lat, max_lon]]
175
+
176
+ def create_marker_group(self, name='Events'):
177
+ """Create a feature group for markers."""
178
+ logger.info(f"Creating marker feature group: {name}...")
179
+ self.marker_group = folium.FeatureGroup(name=name)
180
+
181
+ def convert_xy_to_latlon(self, df, x_col='x', y_col='y', source_crs='EPSG:4326'):
182
+ """
183
+ Convert XY coordinates in a given DataFrame to latitude/longitude (WGS84).
184
+
185
+ Parameters
186
+ ----------
187
+ df : pd.DataFrame
188
+ The DataFrame containing columns for X and Y coordinates.
189
+ x_col : str, optional
190
+ The column name of the X (easting) coordinate. Default is 'x'.
191
+ y_col : str, optional
192
+ The column name of the Y (northing) coordinate. Default is 'y'.
193
+ source_crs : str, optional
194
+ The coordinate reference system (CRS) of the input coordinates, e.g., "EPSG:31982".
195
+ Default is 'EPSG:4326'.
196
+
197
+ Returns
198
+ -------
199
+ pd.DataFrame
200
+ The original DataFrame with two new columns: 'latitude' and 'longitude'.
201
+
202
+ Raises
203
+ ------
204
+ ValueError
205
+ If transformation fails or input columns are missing.
206
+ """
207
+ logger.info("Converting XY coordinates to latitude/longitude...")
208
+ if x_col not in df.columns or y_col not in df.columns:
209
+ logger.error(f"DataFrame must contain '{x_col}' and '{y_col}' columns.")
210
+ raise ValueError(f"DataFrame must contain '{x_col}' and '{y_col}' columns.")
211
+
212
+ try:
213
+ transformer = Transformer.from_crs(
214
+ source_crs, # Source CRS provided
215
+ 'EPSG:4326', # Target CRS (WGS84)
216
+ always_xy=True
217
+ )
218
+ x_coords = df[x_col].values
219
+ y_coords = df[y_col].values
220
+ lon, lat = transformer.transform(x_coords, y_coords)
221
+ df['latitude'] = lat
222
+ df['longitude'] = lon
223
+ logger.info("Coordinate conversion complete.")
224
+ return df
225
+ except Exception as e:
226
+ logger.error(f"Error converting station coordinates: {e}")
227
+ raise ValueError(f"Error converting coordinates: {e}")
228
+
229
+ def assign_color(self, value):
230
+ """Assign color based on magnitude value."""
231
+ return 'lightgrey' if value < self.event_config.vmin else self.color_map(value)
232
+
233
+ def calculate_radius(self, value):
234
+ """Calculate the radius of the circle marker based on a value."""
235
+ base_radius = 2
236
+ return base_radius + (value - self.event_config.vmin) * self.event_config.scaling_factor
237
+
238
+ def add_tile_layers(self):
239
+ """Add all tile layers to the map, ensuring the specified layer is the default."""
240
+ logger.info("Adding tile layers...")
241
+
242
+ # Add all layers except the default one
243
+ for layer_name, config in TILE_LAYER_CONFIGS.items():
244
+ if layer_name != self.map_config.default_tile_layer:
245
+ folium.TileLayer(
246
+ layer_name,
247
+ name=layer_name,
248
+ attr=config['attr'],
249
+ control=True,
250
+ max_zoom=self.map_config.max_zoom_level,
251
+ min_zoom=self.map_config.min_zoom_level
252
+ ).add_to(self.m)
253
+
254
+ # Add the default layer last
255
+ default_config = TILE_LAYER_CONFIGS[self.map_config.default_tile_layer]
256
+ folium.TileLayer(
257
+ self.map_config.default_tile_layer,
258
+ name=self.map_config.default_tile_layer,
259
+ attr=default_config['attr'],
260
+ control=True,
261
+ max_zoom=self.map_config.max_zoom_level,
262
+ min_zoom=self.map_config.min_zoom_level
263
+ ).add_to(self.m)
264
+
265
+ logger.info("Tile layers added.")
266
+
267
+ def add_layer_controls(self):
268
+ """Add layer controls to the map."""
269
+ logger.info("Adding layer control...")
270
+ folium.LayerControl().add_to(self.m)
271
+ logger.info("Layer control added.")
272
+
273
+ def add_fullscreen_option(self):
274
+ """Add a fullscreen button to the map."""
275
+ logger.info("Adding fullscreen option...")
276
+ plugins.Fullscreen(
277
+ position='topleft',
278
+ title='Full Screen',
279
+ title_cancel='Exit Full Screen',
280
+ force_separate_button=True
281
+ ).add_to(self.m)
282
+ logger.info("Fullscreen option added.")
283
+
284
+ def add_legends(self) -> None:
285
+ """Add legends to the map."""
286
+ if self.event_config is None:
287
+ logger.info("Event configuration is missing. Skipping legends.")
288
+ return
289
+
290
+ # Add the magnitude color legend using branca colormap
291
+ logger.info("Adding magnitude color legend...")
292
+ if hasattr(self, 'color_map') and self.color_map:
293
+ self.color_map.caption = self.event_config.legend_title
294
+ self.color_map.position = self.get_color_map_position()
295
+ self.color_map.add_to(self.m)
296
+ logger.info("Magnitude color legend added.")
297
+ else:
298
+ logger.warning("Color map not found. Magnitude color legend not added.")
299
+
300
+ def add_site_marker(self):
301
+ """Add a marker for the site location on the map."""
302
+ logger.info("Adding site location marker...")
303
+ # Create the tooltip and popup content
304
+ tooltip_content = self.map_config.project_name
305
+ popup_content = f"""
306
+ <b>Site Project:</b> {self.map_config.project_name}<br>
307
+ <b>Client:</b> {self.map_config.client}
308
+ """
309
+
310
+ # Add the marker with star icon, tooltip, and popup
311
+ folium.Marker(
312
+ location=[self.map_config.latitude, self.map_config.longitude],
313
+ icon=folium.Icon(color='red', icon='star', prefix='fa'),
314
+ tooltip=tooltip_content,
315
+ popup=folium.Popup(popup_content, max_width=300)
316
+ ).add_to(self.m)
317
+ logger.info("Site location marker added.")
318
+
319
+ def get_position_style(self, position):
320
+ positions = {
321
+ 'topright': 'top: 10px; right: 10px;',
322
+ 'topleft': 'top: 10px; left: 10px;',
323
+ 'bottomright': 'bottom: 10px; right: 10px;',
324
+ 'bottomleft': 'bottom: 10px; left: 10px;'
325
+ }
326
+ return positions.get(position.lower(), 'top: 10px; right: 10px;')
327
+
328
+ def get_color_map_position(self) -> str:
329
+ """Get the position for the color map legend."""
330
+ position = self.event_config.legend_position.lower()
331
+ positions = {
332
+ 'topright': 'topright',
333
+ 'topleft': 'topleft',
334
+ 'bottomright': 'bottomright',
335
+ 'bottomleft': 'bottomleft',
336
+ }
337
+ return positions.get(position, 'bottomright')
@@ -0,0 +1,111 @@
1
+ import logging
2
+ import pandas as pd
3
+ from .config import BlastConfig
4
+ from .utils import convert_xy_to_latlon, calculate_magnitude # Import functions from utils
5
+
6
+ logger = logging.getLogger(__name__)
7
+ logging.basicConfig(level=logging.INFO)
8
+
9
+ class BlastCatalog:
10
+ """Class to process blast data and build a catalog."""
11
+ def __init__(self, blast_config: BlastConfig):
12
+ self.blast_config = blast_config
13
+ self.dataframe = None
14
+
15
+ def read_blast_data(self):
16
+ """Read blast data from CSV file."""
17
+ try:
18
+ self.dataframe = pd.read_csv(self.blast_config.blast_file_path)
19
+ logger.info(f"Blast data loaded from {self.blast_config.blast_file_path}")
20
+ except Exception as e:
21
+ logger.error(f"Failed to read blast data: {e}")
22
+ self.dataframe = pd.DataFrame()
23
+
24
+ def build_catalog(self):
25
+ """Build catalog DataFrame with required columns."""
26
+ if self.dataframe is None or self.dataframe.empty:
27
+ logger.error("No data to build catalog.")
28
+ return pd.DataFrame()
29
+
30
+ # Check for essential columns x, y, Q
31
+ essential_columns = ['x', 'y', 'Q']
32
+ missing_essentials = [col for col in essential_columns if col not in self.dataframe.columns]
33
+ if missing_essentials:
34
+ logger.error(f"Missing essential columns: {missing_essentials}")
35
+ return pd.DataFrame()
36
+
37
+ # Convert coordinates if not already done
38
+ if 'latitude' not in self.dataframe.columns or 'longitude' not in self.dataframe.columns:
39
+ self.process_data()
40
+
41
+ # Calculate magnitude if not already done
42
+ if 'mag' not in self.dataframe.columns:
43
+ self.calculate_magnitude()
44
+
45
+ # Add or modify the 'magType' column
46
+ self.dataframe['magType'] = 'ML'
47
+
48
+ # Handle 'depth' column; if not present, create with default value 0
49
+ if 'depth' not in self.dataframe.columns:
50
+ if 'z' in self.dataframe.columns:
51
+ self.dataframe['depth'] = self.dataframe['z']
52
+ else:
53
+ self.dataframe['depth'] = 0
54
+
55
+ # Handle 'time' column; if not present, create with an arbitrary date
56
+ if 'date' in self.dataframe.columns:
57
+ self.dataframe['time'] = pd.to_datetime(self.dataframe['date'], format='%d/%m/%Y', errors='coerce')
58
+ else:
59
+ # Assign arbitrary date '1970-01-01'
60
+ self.dataframe['time'] = pd.Timestamp('1970-01-01')
61
+
62
+ # Handle 'place' column; if not present, assign 'Unknown'
63
+ self.dataframe['place'] = self.dataframe.get('place', 'Unknown')
64
+
65
+ # Handle 'id' column; if not present, assign sequence numbers
66
+ if 'id' not in self.dataframe.columns:
67
+ self.dataframe['id'] = range(1, len(self.dataframe) + 1)
68
+
69
+ # Select required columns
70
+ required_columns = ['latitude', 'longitude', 'mag', 'magType', 'depth', 'id', 'time', 'place']
71
+ catalog = self.dataframe[required_columns].copy()
72
+
73
+ # Drop rows with missing essential values
74
+ catalog.dropna(subset=['latitude', 'longitude', 'mag'], inplace=True)
75
+
76
+ logger.info(f"Catalog built with {len(catalog)} events.")
77
+ return catalog
78
+
79
+ def process_data(self):
80
+ """Process the blast data by converting coordinates and calculating magnitude."""
81
+ if self.dataframe is None or self.dataframe.empty:
82
+ logger.error("No data to process.")
83
+ return
84
+
85
+ # Convert coordinates using the utility function
86
+ x_coords = self.dataframe['x'].values
87
+ y_coords = self.dataframe['y'].values
88
+
89
+ lon, lat = convert_xy_to_latlon(
90
+ x_coords,
91
+ y_coords,
92
+ source_crs=self.blast_config.coordinate_system
93
+ )
94
+
95
+ self.dataframe['longitude'] = lon
96
+ self.dataframe['latitude'] = lat
97
+
98
+ # Calculate magnitude using the function from utils
99
+ Q_values = self.dataframe['Q'].values
100
+
101
+ mag = calculate_magnitude(
102
+ Q=Q_values,
103
+ f_TNT=self.blast_config.f_TNT,
104
+ a_ML=self.blast_config.a_ML,
105
+ b_ML=self.blast_config.b_ML
106
+ )
107
+
108
+ self.dataframe['mag'] = mag
109
+ logger.info("Magnitude calculation completed.")
110
+
111
+